Skip to content

Improve error messages when using SyntaxStringInterpolation with invalid syntax code #855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions Sources/SwiftDiagnostics/Diagnostic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,9 @@ public struct Diagnostic: CustomDebugStringConvertible {
}

public var debugDescription: String {
if let root = node.root.as(SourceFileSyntax.self) {
let locationConverter = SourceLocationConverter(file: "", tree: root)
let location = location(converter: locationConverter)
return "\(location): \(message)"
} else {
return "<unknown>: \(message)"
}
let locationConverter = SourceLocationConverter(file: "", tree: node.root)
let location = location(converter: locationConverter)
return "\(location): \(message)"
}
}

4 changes: 3 additions & 1 deletion Sources/SwiftSyntax/Raw/RawSyntaxNodeProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ extension RawSyntax: RawSyntaxNodeProtocol {
}

@_spi(RawSyntax)
public struct RawTokenSyntax: RawSyntaxNodeProtocol {
public struct RawTokenSyntax: RawSyntaxToSyntax, RawSyntaxNodeProtocol {
public typealias SyntaxType = TokenSyntax

var tokenView: RawSyntaxTokenView {
return raw.tokenView!
}
Expand Down
9 changes: 5 additions & 4 deletions Sources/SwiftSyntax/SourceLocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,11 @@ public final class SourceLocationConverter {

/// - Parameters:
/// - file: The file path associated with the syntax tree.
/// - tree: The syntax tree to convert positions to line/columns for.
public init(file: String, tree: SourceFileSyntax) {
/// - tree: The root of the syntax tree to convert positions to line/columns for.
public init<SyntaxType: SyntaxProtocol>(file: String, tree: SyntaxType) {
assert(tree.parent == nil, "SourceLocationConverter must be passed the root of the syntax tree")
self.file = file
(self.lines, endOfFile) = computeLines(tree: tree)
(self.lines, endOfFile) = computeLines(tree: Syntax(tree))
assert(tree.byteSize == endOfFile.utf8Offset)
}

Expand Down Expand Up @@ -335,7 +336,7 @@ public extension SyntaxProtocol {
/// Returns array of lines with the position at the start of the line and
/// the end-of-file position.
fileprivate func computeLines(
tree: SourceFileSyntax
tree: Syntax
) -> ([AbsolutePosition], AbsolutePosition) {
var lines: [AbsolutePosition] = []
// First line starts from the beginning.
Expand Down
65 changes: 59 additions & 6 deletions Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@_spi(RawSyntax) import SwiftSyntax
@_spi(RawSyntax) import SwiftParser
import SwiftDiagnostics

/// An individual interpolated syntax node.
struct InterpolatedSyntaxNode {
Expand Down Expand Up @@ -90,26 +91,78 @@ public protocol SyntaxExpressibleByStringInterpolation:
where Self.StringInterpolation == SyntaxStringInterpolation {
/// Create an instance of this syntax node by parsing it from the given
/// parser.
static func parse(from parser: inout Parser) -> Self
static func parse(from parser: inout Parser) throws -> Self
}

enum SyntaxStringInterpolationError: Error, CustomStringConvertible {
case didNotConsumeAllTokens(remainingTokens: [TokenSyntax])
case producedInvalidNodeType(expectedType: SyntaxProtocol.Type, actualType: SyntaxProtocol.Type)
case diagnostics([Diagnostic])

var description: String {
switch self {
case .didNotConsumeAllTokens(remainingTokens: let tokens):
return "Extraneous text in snippet: '\(tokens.map(\.description).joined())'"
case .producedInvalidNodeType(expectedType: let expectedType, actualType: let actualType):
return "Parsing the code snippet was expected to produce a \(expectedType) but produced a \(actualType)"
case .diagnostics(let diagnostics):
return diagnostics.map(\.debugDescription).joined(separator: "\n")
}
}
}

extension SyntaxExpressibleByStringInterpolation {
/// Initialize a syntax node by parsing the contents of the interpolation.
/// This function is marked `@_transparent` so that fatalErrors raised here
/// are reported at the string literal itself.
/// This makes debugging easier because Xcode will jump to the string literal
/// that had a parsing error instead of the initializer that raised the `fatalError`
@_transparent
public init(stringInterpolation: SyntaxStringInterpolation) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does anyone have any concerns about this @_transparent hack? @DougGregor

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this using default arguments with #file and #line instead? @_transparent is a messy feature that we'd prefer not spread much farther.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because Xcode’s debugger will still jump to the position where the fatalError was called without @_transparent. 😬

self = stringInterpolation.sourceText.withUnsafeBufferPointer { buffer in
do {
try self.init(stringInterpolationOrThrow: stringInterpolation)
} catch {
fatalError(String(describing: error))
}
}

public init(stringInterpolationOrThrow stringInterpolation: SyntaxStringInterpolation) throws {
self = try stringInterpolation.sourceText.withUnsafeBufferPointer { buffer in
var parser = Parser(buffer)
// FIXME: When the parser supports incremental parsing, put the
// interpolatedSyntaxNodes in so we don't have to parse them again.
return parser.arena.assumingSingleThread {
return Self.parse(from: &parser)
return try parser.arena.assumingSingleThread {
let result = try Self.parse(from: &parser)
if !parser.at(.eof) {
var remainingTokens: [TokenSyntax] = []
while !parser.at(.eof) {
remainingTokens.append(parser.consumeAnyToken().syntax)
}
throw SyntaxStringInterpolationError.didNotConsumeAllTokens(remainingTokens: remainingTokens)
}
if result.hasError {
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: result)
assert(!diagnostics.isEmpty)
throw SyntaxStringInterpolationError.diagnostics(diagnostics)
}
return result
}
}
}

/// Initialize a syntax node from a string literal.
@_transparent
public init(stringLiteral value: String) {
do {
try self.init(stringLiteralOrThrow: value)
} catch {
fatalError(String(describing: error))
}
}

/// Initialize a syntax node from a string literal.
public init(stringLiteralOrThrow value: String) throws {
var interpolation = SyntaxStringInterpolation()
interpolation.appendLiteral(value)
self.init(stringInterpolation: interpolation)
try self.init(stringInterpolationOrThrow: interpolation)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
% for base_kind in STRING_INTERPOLATION_BASE_KINDS:
% node = NODE_MAP[base_kind]
extension ${base_kind}SyntaxProtocol {
public static func parse(from parser: inout Parser) -> Self {
public static func parse(from parser: inout Parser) throws -> Self {
let node = parser.${node.parser_function}().syntax
guard let result = node.as(Self.self) else {
fatalError("Parsing was expected to produce a \(Self.self) but produced \(type(of: node.asProtocol(${base_kind}SyntaxProtocol.self)))")
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualType: type(of: node.asProtocol(${base_kind}SyntaxProtocol.self)))
}
return result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,50 +16,50 @@
@_spi(RawSyntax) import SwiftParser

extension DeclSyntaxProtocol {
public static func parse(from parser: inout Parser) -> Self {
public static func parse(from parser: inout Parser) throws -> Self {
let node = parser.parseDeclaration().syntax
guard let result = node.as(Self.self) else {
fatalError("Parsing was expected to produce a \(Self.self) but produced \(type(of: node.asProtocol(DeclSyntaxProtocol.self)))")
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualType: type(of: node.asProtocol(DeclSyntaxProtocol.self)))
}
return result
}
}

extension ExprSyntaxProtocol {
public static func parse(from parser: inout Parser) -> Self {
public static func parse(from parser: inout Parser) throws -> Self {
let node = parser.parseExpression().syntax
guard let result = node.as(Self.self) else {
fatalError("Parsing was expected to produce a \(Self.self) but produced \(type(of: node.asProtocol(ExprSyntaxProtocol.self)))")
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualType: type(of: node.asProtocol(ExprSyntaxProtocol.self)))
}
return result
}
}

extension PatternSyntaxProtocol {
public static func parse(from parser: inout Parser) -> Self {
public static func parse(from parser: inout Parser) throws -> Self {
let node = parser.parsePattern().syntax
guard let result = node.as(Self.self) else {
fatalError("Parsing was expected to produce a \(Self.self) but produced \(type(of: node.asProtocol(PatternSyntaxProtocol.self)))")
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualType: type(of: node.asProtocol(PatternSyntaxProtocol.self)))
}
return result
}
}

extension StmtSyntaxProtocol {
public static func parse(from parser: inout Parser) -> Self {
public static func parse(from parser: inout Parser) throws -> Self {
let node = parser.parseStatement().syntax
guard let result = node.as(Self.self) else {
fatalError("Parsing was expected to produce a \(Self.self) but produced \(type(of: node.asProtocol(StmtSyntaxProtocol.self)))")
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualType: type(of: node.asProtocol(StmtSyntaxProtocol.self)))
}
return result
}
}

extension TypeSyntaxProtocol {
public static func parse(from parser: inout Parser) -> Self {
public static func parse(from parser: inout Parser) throws -> Self {
let node = parser.parseType().syntax
guard let result = node.as(Self.self) else {
fatalError("Parsing was expected to produce a \(Self.self) but produced \(type(of: node.asProtocol(TypeSyntaxProtocol.self)))")
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualType: type(of: node.asProtocol(TypeSyntaxProtocol.self)))
}
return result
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/SwiftSyntaxBuilderTest/StringInterpolation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ final class StringInterpolationTests: XCTestCase {
}

func testPatternInterpolation() throws {
let letPattern: PatternSyntax = "let x: Int"
let letPattern: PatternSyntax = "let x"
XCTAssertTrue(letPattern.is(ValueBindingPatternSyntax.self))
}

Expand Down