Skip to content

Commit b394a84

Browse files
committed
Improve error messages when using SyntaxStringInterpolation with invalid syntax code
If parsing source text of a `SyntaxStringInterpolation` produced a tree with errors or did not consume all characters in the string literal (and would thus drop text), raise a fatal error. To make sure parsing errors are displayed at the string literal itself and don’t navigate Xcode to the `SyntaxExpressibleByStringInterpolation` initializer that `fatalError`ed, split the initializer into a throwing variant and a `@_transparent` one that `fatalError`s when the throwing initializer encounters an error. Because `@_transparent` is also transparent in terms of source locations, this will make the crash appear at the start of the string literal.
1 parent 2a65025 commit b394a84

7 files changed

+83
-31
lines changed

Sources/SwiftDiagnostics/Diagnostic.swift

+3-7
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,9 @@ public struct Diagnostic: CustomDebugStringConvertible {
5555
}
5656

5757
public var debugDescription: String {
58-
if let root = node.root.as(SourceFileSyntax.self) {
59-
let locationConverter = SourceLocationConverter(file: "", tree: root)
60-
let location = location(converter: locationConverter)
61-
return "\(location): \(message)"
62-
} else {
63-
return "<unknown>: \(message)"
64-
}
58+
let locationConverter = SourceLocationConverter(file: "", tree: node.root)
59+
let location = location(converter: locationConverter)
60+
return "\(location): \(message)"
6561
}
6662
}
6763

Sources/SwiftSyntax/Raw/RawSyntaxNodeProtocol.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ extension RawSyntax: RawSyntaxNodeProtocol {
6565
}
6666

6767
@_spi(RawSyntax)
68-
public struct RawTokenSyntax: RawSyntaxNodeProtocol {
68+
public struct RawTokenSyntax: RawSyntaxToSyntax, RawSyntaxNodeProtocol {
69+
public typealias SyntaxType = TokenSyntax
70+
6971
var tokenView: RawSyntaxTokenView {
7072
return raw.tokenView!
7173
}

Sources/SwiftSyntax/SourceLocation.swift

+5-4
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,11 @@ public final class SourceLocationConverter {
121121

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

@@ -335,7 +336,7 @@ public extension SyntaxProtocol {
335336
/// Returns array of lines with the position at the start of the line and
336337
/// the end-of-file position.
337338
fileprivate func computeLines(
338-
tree: SourceFileSyntax
339+
tree: Syntax
339340
) -> ([AbsolutePosition], AbsolutePosition) {
340341
var lines: [AbsolutePosition] = []
341342
// First line starts from the beginning.

Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift

+59-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@_spi(RawSyntax) import SwiftSyntax
22
@_spi(RawSyntax) import SwiftParser
3+
import SwiftDiagnostics
34

45
/// An individual interpolated syntax node.
56
struct InterpolatedSyntaxNode {
@@ -90,26 +91,78 @@ public protocol SyntaxExpressibleByStringInterpolation:
9091
where Self.StringInterpolation == SyntaxStringInterpolation {
9192
/// Create an instance of this syntax node by parsing it from the given
9293
/// parser.
93-
static func parse(from parser: inout Parser) -> Self
94+
static func parse(from parser: inout Parser) throws -> Self
95+
}
96+
97+
enum SyntaxStringInterpolationError: Error, CustomStringConvertible {
98+
case didNotConsumeAllTokens(remainingTokens: [TokenSyntax])
99+
case producedInvalidNodeType(expectedType: SyntaxProtocol.Type, actualType: SyntaxProtocol.Type)
100+
case diagnostics([Diagnostic])
101+
102+
var description: String {
103+
switch self {
104+
case .didNotConsumeAllTokens(remainingTokens: let tokens):
105+
return "Extraneous text in snippet: '\(tokens.map(\.description).joined())'"
106+
case .producedInvalidNodeType(expectedType: let expectedType, actualType: let actualType):
107+
return "Parsing the code snippet was expected to produce a \(expectedType) but produced a \(actualType)"
108+
case .diagnostics(let diagnostics):
109+
return diagnostics.map(\.debugDescription).joined(separator: "\n")
110+
}
111+
}
94112
}
95113

96114
extension SyntaxExpressibleByStringInterpolation {
97115
/// Initialize a syntax node by parsing the contents of the interpolation.
116+
/// This function is marked `@_transparent` so that fatalErrors raised here
117+
/// are reported at the string literal itself.
118+
/// This makes debugging easier because Xcode will jump to the string literal
119+
/// that had a parsing error instead of the initializer that raised the `fatalError`
120+
@_transparent
98121
public init(stringInterpolation: SyntaxStringInterpolation) {
99-
self = stringInterpolation.sourceText.withUnsafeBufferPointer { buffer in
122+
do {
123+
try self.init(stringInterpolationOrThrow: stringInterpolation)
124+
} catch {
125+
fatalError(String(describing: error))
126+
}
127+
}
128+
129+
public init(stringInterpolationOrThrow stringInterpolation: SyntaxStringInterpolation) throws {
130+
self = try stringInterpolation.sourceText.withUnsafeBufferPointer { buffer in
100131
var parser = Parser(buffer)
101132
// FIXME: When the parser supports incremental parsing, put the
102133
// interpolatedSyntaxNodes in so we don't have to parse them again.
103-
return parser.arena.assumingSingleThread {
104-
return Self.parse(from: &parser)
134+
return try parser.arena.assumingSingleThread {
135+
let result = try Self.parse(from: &parser)
136+
if !parser.at(.eof) {
137+
var remainingTokens: [TokenSyntax] = []
138+
while !parser.at(.eof) {
139+
remainingTokens.append(parser.consumeAnyToken().syntax)
140+
}
141+
throw SyntaxStringInterpolationError.didNotConsumeAllTokens(remainingTokens: remainingTokens)
142+
}
143+
if result.hasError {
144+
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: result)
145+
assert(!diagnostics.isEmpty)
146+
throw SyntaxStringInterpolationError.diagnostics(diagnostics)
147+
}
148+
return result
105149
}
106150
}
107151
}
108152

109-
/// Initialize a syntax node from a string literal.
153+
@_transparent
110154
public init(stringLiteral value: String) {
155+
do {
156+
try self.init(stringLiteralOrThrow: value)
157+
} catch {
158+
fatalError(String(describing: error))
159+
}
160+
}
161+
162+
/// Initialize a syntax node from a string literal.
163+
public init(stringLiteralOrThrow value: String) throws {
111164
var interpolation = SyntaxStringInterpolation()
112165
interpolation.appendLiteral(value)
113-
self.init(stringInterpolation: interpolation)
166+
try self.init(stringInterpolationOrThrow: interpolation)
114167
}
115168
}

Sources/SwiftSyntaxBuilder/SyntaxExpressibleByStringInterpolationConformances.swift.gyb

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
% for base_kind in STRING_INTERPOLATION_BASE_KINDS:
2626
% node = NODE_MAP[base_kind]
2727
extension ${base_kind}SyntaxProtocol {
28-
public static func parse(from parser: inout Parser) -> Self {
28+
public static func parse(from parser: inout Parser) throws -> Self {
2929
let node = parser.${node.parser_function}().syntax
3030
guard let result = node.as(Self.self) else {
31-
fatalError("Parsing was expected to produce a \(Self.self) but produced \(type(of: node.asProtocol(${base_kind}SyntaxProtocol.self)))")
31+
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualType: type(of: node.asProtocol(${base_kind}SyntaxProtocol.self)))
3232
}
3333
return result
3434
}

Sources/SwiftSyntaxBuilder/gyb_generated/SyntaxExpressibleByStringInterpolationConformances.swift

+10-10
Original file line numberDiff line numberDiff line change
@@ -16,50 +16,50 @@
1616
@_spi(RawSyntax) import SwiftParser
1717

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

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

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

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

5858
extension TypeSyntaxProtocol {
59-
public static func parse(from parser: inout Parser) -> Self {
59+
public static func parse(from parser: inout Parser) throws -> Self {
6060
let node = parser.parseType().syntax
6161
guard let result = node.as(Self.self) else {
62-
fatalError("Parsing was expected to produce a \(Self.self) but produced \(type(of: node.asProtocol(TypeSyntaxProtocol.self)))")
62+
throw SyntaxStringInterpolationError.producedInvalidNodeType(expectedType: Self.self, actualType: type(of: node.asProtocol(TypeSyntaxProtocol.self)))
6363
}
6464
return result
6565
}

Tests/SwiftSyntaxBuilderTest/StringInterpolation.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ final class StringInterpolationTests: XCTestCase {
4949
}
5050

5151
func testPatternInterpolation() throws {
52-
let letPattern: PatternSyntax = "let x: Int"
52+
let letPattern: PatternSyntax = "let x"
5353
XCTAssertTrue(letPattern.is(ValueBindingPatternSyntax.self))
5454
}
5555

0 commit comments

Comments
 (0)