Skip to content

Commit 82e5258

Browse files
committed
Don’t fatalError when expressing syntax nodes by string literals
We should not crash the process if the source code that you use to create a syntax node using string interpolation is invalid. Instead we should: - Not check for errors in the normal string interpolation case and just return a syntax tree that has the `hasError` flag set when there are syntax errors - In either SwiftSyntaxBuilder offer `init(validating node: Self) throws` that check that `node` has no sytnax errors and throws otherwise. This allows us write e.g. `try wDeclSyntax(validating: "struct Foo {}")` to create a `DeclSyntax` that’s guaranteed to not have any syntax errors rdar://104423126
1 parent 5c98b24 commit 82e5258

File tree

9 files changed

+106
-102
lines changed

9 files changed

+106
-102
lines changed

CodeGeneration/Sources/generate-swiftsyntax/templates/swiftsyntaxbuilder/SyntaxExpressibleByStringInterpolationConformancesFile.swift

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ let syntaxExpressibleByStringInterpolationConformancesFile = SourceFileSyntax {
2525
ImportDeclSyntax("import SwiftParserDiagnostics")
2626

2727
ExtensionDeclSyntax("extension SyntaxParseable") {
28+
DeclSyntax("public typealias StringInterpolation = SyntaxStringInterpolation")
29+
2830
InitializerDeclSyntax(
2931
"""
30-
public init(stringInterpolationOrThrow stringInterpolation: SyntaxStringInterpolation) throws {
31-
self = try performParse(source: stringInterpolation.sourceText, parse: { parser in
32+
public init(stringInterpolation: SyntaxStringInterpolation) {
33+
self = performParse(source: stringInterpolation.sourceText, parse: { parser in
3234
return Self.parse(from: &parser)
3335
})
3436
}
@@ -41,19 +43,15 @@ let syntaxExpressibleByStringInterpolationConformancesFile = SourceFileSyntax {
4143

4244
FunctionDeclSyntax(
4345
"""
44-
// TODO: This should be fileprivate, but is currently used in
45-
// `ConvenienceInitializers.swift`. See the corresponding TODO there.
46-
func performParse<SyntaxType: SyntaxProtocol>(source: [UInt8], parse: (inout Parser) throws -> SyntaxType) throws -> SyntaxType {
47-
return try source.withUnsafeBufferPointer { buffer in
46+
// TODO: This should be inlined in SyntaxParseable.init(stringInterpolation:),
47+
// but is currently used in `ConvenienceInitializers.swift`.
48+
// See the corresponding TODO there.
49+
func performParse<SyntaxType: SyntaxProtocol>(source: [UInt8], parse: (inout Parser) -> SyntaxType) -> SyntaxType {
50+
return source.withUnsafeBufferPointer { buffer in
4851
var parser = Parser(buffer)
4952
// FIXME: When the parser supports incremental parsing, put the
5053
// interpolatedSyntaxNodes in so we don't have to parse them again.
51-
let result = try parse(&parser)
52-
if result.hasError {
53-
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: result)
54-
assert(!diagnostics.isEmpty)
55-
throw SyntaxStringInterpolationError.diagnostics(diagnostics, tree: Syntax(result))
56-
}
54+
let result = parse(&parser)
5755
return result
5856
}
5957
}

Sources/SwiftParserDiagnostics/MissingNodesError.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,6 @@ extension ParseDiagnosticsGenerator {
334334
break
335335
}
336336
}
337-
} else {
338-
missingNodes = []
339337
}
340338

341339
let changes = missingNodes.enumerated().map { (index, missingNode) -> FixIt.Changes in

Sources/SwiftSyntaxBuilder/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ add_swift_host_library(SwiftSyntaxBuilder
1212
ResultBuilderExtensions.swift
1313
Syntax+StringInterpolation.swift
1414
SyntaxNodeWithBody.swift
15+
ValidatingSyntaxNodes.swift
1516
WithTrailingCommaSyntax+EnsuringTrailingComma.swift
1617

1718

Sources/SwiftSyntaxBuilder/ConvenienceInitializers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ extension FunctionParameterSyntax {
176176
_ source: String,
177177
for subject: Parser.ParameterSubject
178178
) {
179-
self = try! performParse(
179+
self = performParse(
180180
source: Array(source.utf8),
181181
parse: {
182182
let raw = RawSyntax($0.parseFunctionParameter(for: subject))

Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift

Lines changed: 4 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ extension SyntaxStringInterpolation: StringInterpolationProtocol {
143143
public protocol SyntaxExpressibleByStringInterpolation:
144144
ExpressibleByStringInterpolation
145145
where Self.StringInterpolation == SyntaxStringInterpolation {
146-
init(stringInterpolationOrThrow stringInterpolation: SyntaxStringInterpolation) throws
146+
init(stringInterpolation: SyntaxStringInterpolation)
147147
}
148148

149149
enum SyntaxStringInterpolationError: Error, CustomStringConvertible {
@@ -221,34 +221,10 @@ public protocol ExpressibleByLiteralSyntax {
221221
}
222222

223223
extension SyntaxExpressibleByStringInterpolation {
224-
/// Initialize a syntax node by parsing the contents of the interpolation.
225-
/// This function is marked `@_transparent` so that fatalErrors raised here
226-
/// are reported at the string literal itself.
227-
/// This makes debugging easier because Xcode will jump to the string literal
228-
/// that had a parsing error instead of the initializer that raised the `fatalError`
229-
@_transparent
230-
public init(stringInterpolation: SyntaxStringInterpolation) {
231-
do {
232-
try self.init(stringInterpolationOrThrow: stringInterpolation)
233-
} catch {
234-
fatalError(String(describing: error))
235-
}
236-
}
237-
238-
@_transparent
239224
public init(stringLiteral value: String) {
240-
do {
241-
try self.init(stringLiteralOrThrow: value)
242-
} catch {
243-
fatalError(String(describing: error))
244-
}
245-
}
246-
247-
/// Initialize a syntax node from a string literal.
248-
public init(stringLiteralOrThrow value: String) throws {
249225
var interpolation = SyntaxStringInterpolation()
250226
interpolation.appendLiteral(value)
251-
try self.init(stringInterpolationOrThrow: interpolation)
227+
self.init(stringInterpolation: interpolation)
252228
}
253229
}
254230

@@ -445,7 +421,7 @@ extension Optional: ExpressibleByLiteralSyntax where Wrapped: ExpressibleByLiter
445421
}
446422

447423
extension TokenSyntax: SyntaxExpressibleByStringInterpolation {
448-
public init(stringInterpolationOrThrow stringInterpolation: SyntaxStringInterpolation) throws {
424+
public init(stringInterpolation: SyntaxStringInterpolation) {
449425
let string = stringInterpolation.sourceText.withUnsafeBufferPointer { buf in
450426
return String(syntaxText: SyntaxText(buffer: buf))
451427
}
@@ -476,21 +452,7 @@ struct UnexpectedTrivia: DiagnosticMessage {
476452
}
477453

478454
extension Trivia: ExpressibleByStringInterpolation {
479-
/// Initialize a syntax node by parsing the contents of the interpolation.
480-
/// This function is marked `@_transparent` so that fatalErrors raised here
481-
/// are reported at the string literal itself.
482-
/// This makes debugging easier because Xcode will jump to the string literal
483-
/// that had a parsing error instead of the initializer that raised the `fatalError`
484-
@_transparent
485455
public init(stringInterpolation: String.StringInterpolation) {
486-
do {
487-
try self.init(stringInterpolationOrThrow: stringInterpolation)
488-
} catch {
489-
fatalError(String(describing: error))
490-
}
491-
}
492-
493-
public init(stringInterpolationOrThrow stringInterpolation: String.StringInterpolation) throws {
494456
var text = String(stringInterpolation: stringInterpolation)
495457
let pieces = text.withUTF8 { (buf) -> [TriviaPiece] in
496458
// The leading trivia position is a little bit less restrictive (it allows a shebang), so let's use it.
@@ -499,40 +461,11 @@ extension Trivia: ExpressibleByStringInterpolation {
499461
}
500462

501463
self.init(pieces: pieces)
502-
503-
if pieces.contains(where: { $0.isUnexpected }) {
504-
var diagnostics: [Diagnostic] = []
505-
let tree = SourceFileSyntax(statements: [], eofToken: .eof(leadingTrivia: self))
506-
var offset = 0
507-
for piece in pieces {
508-
if case .unexpectedText(let contents) = piece {
509-
diagnostics.append(
510-
Diagnostic(
511-
node: Syntax(tree),
512-
position: tree.position.advanced(by: offset),
513-
message: UnexpectedTrivia(triviaContents: contents)
514-
)
515-
)
516-
}
517-
offset += piece.sourceLength.utf8Length
518-
}
519-
throw SyntaxStringInterpolationError.diagnostics(diagnostics, tree: Syntax(tree))
520-
}
521464
}
522465

523-
@_transparent
524466
public init(stringLiteral value: String) {
525-
do {
526-
try self.init(stringLiteralOrThrow: value)
527-
} catch {
528-
fatalError(String(describing: error))
529-
}
530-
}
531-
532-
/// Initialize a syntax node from a string literal.
533-
public init(stringLiteralOrThrow value: String) throws {
534467
var interpolation = String.StringInterpolation(literalCapacity: 1, interpolationCount: 0)
535468
interpolation.appendLiteral(value)
536-
try self.init(stringInterpolationOrThrow: interpolation)
469+
self.init(stringInterpolation: interpolation)
537470
}
538471
}

Sources/SwiftSyntaxBuilder/SyntaxNodeWithBody.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import SwiftSyntax
2323
public struct PartialSyntaxNodeString: SyntaxExpressibleByStringInterpolation {
2424
let sourceText: [UInt8]
2525

26-
public init(stringInterpolationOrThrow stringInterpolation: SyntaxStringInterpolation) throws {
26+
public init(stringInterpolation: SyntaxStringInterpolation) {
2727
self.sourceText = stringInterpolation.sourceText
2828
}
2929
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
import SwiftDiagnostics
15+
import SwiftParserDiagnostics
16+
17+
extension SyntaxProtocol {
18+
/// If `node` has contains no syntax errors, return `node`, otherwise
19+
/// throw an error with diagnostics describing the syntax errors.
20+
///
21+
/// This allows clients to e.g. write `try DeclSyntax(validating: "struct Foo {}")`
22+
/// which constructs a `DeclSyntax` that's guaranteed to be free of syntax
23+
/// errors.
24+
public init(validating node: Self) throws {
25+
if node.hasError {
26+
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: node)
27+
assert(!diagnostics.isEmpty)
28+
throw SyntaxStringInterpolationError.diagnostics(diagnostics, tree: Syntax(node))
29+
}
30+
self = node
31+
}
32+
}
33+
34+
extension Trivia {
35+
/// If `trivia` has contains no unexpected trivia, return `trivia`, otherwise
36+
/// throw an error with diagnostics describing the unexpected trivia.
37+
public init(validating trivia: Trivia) throws {
38+
self = trivia
39+
if pieces.contains(where: { $0.isUnexpected }) {
40+
var diagnostics: [Diagnostic] = []
41+
let tree = SourceFileSyntax(statements: [], eofToken: .eof(leadingTrivia: self))
42+
var offset = 0
43+
for piece in pieces {
44+
if case .unexpectedText(let contents) = piece {
45+
diagnostics.append(
46+
Diagnostic(
47+
node: Syntax(tree),
48+
position: tree.position.advanced(by: offset),
49+
message: UnexpectedTrivia(triviaContents: contents)
50+
)
51+
)
52+
}
53+
offset += piece.sourceLength.utf8Length
54+
}
55+
throw SyntaxStringInterpolationError.diagnostics(diagnostics, tree: Syntax(tree))
56+
}
57+
}
58+
}

Sources/SwiftSyntaxBuilder/generated/SyntaxExpressibleByStringInterpolationConformances.swift

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ import SwiftParser
1919
import SwiftParserDiagnostics
2020

2121
extension SyntaxParseable {
22-
public init(stringInterpolationOrThrow stringInterpolation: SyntaxStringInterpolation) throws {
23-
self = try performParse(source: stringInterpolation.sourceText, parse: { parser in
24-
return Self.parse(from: &parser)
25-
})
22+
public typealias StringInterpolation = SyntaxStringInterpolation
23+
24+
public init(stringInterpolation: SyntaxStringInterpolation) {
25+
self = performParse(source: stringInterpolation.sourceText, parse: { parser in
26+
return Self.parse(from: &parser)
27+
})
2628
}
2729
}
2830

@@ -64,17 +66,12 @@ extension TypeSyntax: SyntaxExpressibleByStringInterpolation {
6466

6567
// TODO: This should be fileprivate, but is currently used in
6668
// `ConvenienceInitializers.swift`. See the corresponding TODO there.
67-
func performParse<SyntaxType: SyntaxProtocol>(source: [UInt8], parse: (inout Parser) throws -> SyntaxType) throws -> SyntaxType {
68-
return try source.withUnsafeBufferPointer { buffer in
69+
func performParse<SyntaxType: SyntaxProtocol>(source: [UInt8], parse: (inout Parser) -> SyntaxType) -> SyntaxType {
70+
return source.withUnsafeBufferPointer { buffer in
6971
var parser = Parser(buffer)
7072
// FIXME: When the parser supports incremental parsing, put the
7173
// interpolatedSyntaxNodes in so we don't have to parse them again.
72-
let result = try parse(&parser)
73-
if result.hasError {
74-
let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: result)
75-
assert(!diagnostics.isEmpty)
76-
throw SyntaxStringInterpolationError.diagnostics(diagnostics, tree: Syntax(result))
77-
}
74+
let result = parse(&parser)
7875
return result
7976
}
8077
}

Tests/SwiftSyntaxBuilderTest/StringInterpolation.swift renamed to Tests/SwiftSyntaxBuilderTest/StringInterpolationTests.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -471,9 +471,10 @@ final class StringInterpolationTests: XCTestCase {
471471
}
472472

473473
func testInvalidTrivia() {
474-
var interpolation = String.StringInterpolation(literalCapacity: 1, interpolationCount: 0)
475-
interpolation.appendLiteral("/*comment*/ invalid /*comm*/")
476-
XCTAssertThrowsError(try Trivia(stringInterpolationOrThrow: interpolation)) { error in
474+
let invalid = Trivia("/*comment*/ invalid /*comm*/")
475+
XCTAssertEqual(invalid, [.blockComment("/*comment*/"), .spaces(1), .unexpectedText("invalid"), .spaces(1), .blockComment("/*comm*/")])
476+
477+
XCTAssertThrowsError(try Trivia(validating: "/*comment*/ invalid /*comm*/")) { error in
477478
AssertStringsEqualWithDiff(
478479
String(describing: error),
479480
"""
@@ -485,4 +486,22 @@ final class StringInterpolationTests: XCTestCase {
485486
)
486487
}
487488
}
489+
490+
func testInvalidSyntax() {
491+
let invalid = DeclSyntax("return 1")
492+
XCTAssert(invalid.hasError)
493+
494+
XCTAssertThrowsError(try DeclSyntax(validating: "return 1")) { error in
495+
AssertStringsEqualWithDiff(
496+
String(describing: error),
497+
"""
498+
499+
1 │ return 1
500+
∣ │ ╰─ expected declaration
501+
∣ ╰─ unexpected code 'return 1' before declaration
502+
503+
"""
504+
)
505+
}
506+
}
488507
}

0 commit comments

Comments
 (0)