Skip to content

Commit 64cabae

Browse files
authored
Merge pull request #1290 from ahoppen/ahoppen/string-interpolation-no-fatalerror
Don’t fatalError when expressing syntax nodes by string literals
2 parents c1bd0c9 + 21e5eaa commit 64cabae

File tree

9 files changed

+107
-102
lines changed

9 files changed

+107
-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
DeclSyntax("import SwiftParserDiagnostics")
2626

2727
try! ExtensionDeclSyntax("extension SyntaxParseable") {
28+
DeclSyntax("public typealias StringInterpolation = SyntaxStringInterpolation")
29+
2830
DeclSyntax(
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
DeclSyntax(
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: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ 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
22+
public typealias StringInterpolation = SyntaxStringInterpolation
23+
24+
public init(stringInterpolation: SyntaxStringInterpolation) {
25+
self = performParse(source: stringInterpolation.sourceText, parse: { parser in
2426
return Self.parse(from: &parser)
2527
})
2628
}
@@ -62,19 +64,15 @@ extension SwitchCaseSyntax: SyntaxExpressibleByStringInterpolation {
6264
extension TypeSyntax: SyntaxExpressibleByStringInterpolation {
6365
}
6466

65-
// TODO: This should be fileprivate, but is currently used in
66-
// `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
67+
// TODO: This should be inlined in SyntaxParseable.init(stringInterpolation:),
68+
// but is currently used in `ConvenienceInitializers.swift`.
69+
// See the corresponding TODO there.
70+
func performParse<SyntaxType: SyntaxProtocol>(source: [UInt8], parse: (inout Parser) -> SyntaxType) -> SyntaxType {
71+
return source.withUnsafeBufferPointer { buffer in
6972
var parser = Parser(buffer)
7073
// FIXME: When the parser supports incremental parsing, put the
7174
// 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-
}
75+
let result = parse(&parser)
7876
return result
7977
}
8078
}

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)