Skip to content

Commit 0611679

Browse files
committed
Make trivia expressible by string literal in SwiftSyntaxBuilder
rdar://104109903
1 parent fcc2b00 commit 0611679

File tree

5 files changed

+138
-3
lines changed

5 files changed

+138
-3
lines changed

Sources/SwiftSyntax/Trivia.swift.gyb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ extension RawTriviaPiece: CustomDebugStringConvertible {
282282
}
283283

284284
extension TriviaPiece {
285-
init(raw: RawTriviaPiece) {
285+
@_spi(RawSyntax) public init(raw: RawTriviaPiece) {
286286
switch raw {
287287
% for trivia in TRIVIAS:
288288
% if trivia.is_collection():

Sources/SwiftSyntax/gyb_generated/Trivia.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ extension RawTriviaPiece: CustomDebugStringConvertible {
434434
}
435435

436436
extension TriviaPiece {
437-
init(raw: RawTriviaPiece) {
437+
@_spi(RawSyntax) public init(raw: RawTriviaPiece) {
438438
switch raw {
439439
case let .spaces(count): self = .spaces(count)
440440
case let .tabs(count): self = .tabs(count)

Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import SwiftBasicFormat
1414
import SwiftDiagnostics
1515
@_spi(RawSyntax) import SwiftSyntax
16+
@_spi(RawSyntax) import SwiftParser
1617

1718
/// An individual interpolated syntax node.
1819
struct InterpolatedSyntaxNode {
@@ -447,3 +448,87 @@ extension TokenSyntax: SyntaxExpressibleByStringInterpolation {
447448
self = .identifier(string)
448449
}
449450
}
451+
452+
// MARK: - Trivia expressible as string
453+
454+
extension TriviaPiece {
455+
var isUnexpected: Bool {
456+
switch self {
457+
case .unexpectedText: return true
458+
default: return false
459+
}
460+
}
461+
}
462+
463+
struct UnexpectedTrivia: DiagnosticMessage {
464+
let triviaContents: String
465+
466+
let diagnosticID = MessageID(domain: "SwiftSyntaxBuilder", id: "UnexpectedTrivia")
467+
let severity = DiagnosticSeverity.error
468+
var message: String {
469+
"unexpected trivia '\(triviaContents)'"
470+
}
471+
472+
}
473+
474+
extension Trivia: ExpressibleByStringInterpolation {
475+
/// Initialize a syntax node by parsing the contents of the interpolation.
476+
/// This function is marked `@_transparent` so that fatalErrors raised here
477+
/// are reported at the string literal itself.
478+
/// This makes debugging easier because Xcode will jump to the string literal
479+
/// that had a parsing error instead of the initializer that raised the `fatalError`
480+
@_transparent
481+
public init(stringInterpolation: String.StringInterpolation) {
482+
do {
483+
try self.init(stringInterpolationOrThrow: stringInterpolation)
484+
} catch {
485+
fatalError(String(describing: error))
486+
}
487+
}
488+
489+
public init(stringInterpolationOrThrow stringInterpolation: String.StringInterpolation) throws {
490+
var text = String(stringInterpolation: stringInterpolation)
491+
let pieces = text.withUTF8 { (buf) -> [TriviaPiece] in
492+
// The leading trivia position is a little bit less restrictive (it allows a shebang), so let's use it.
493+
let rawPieces = TriviaParser.parseTrivia(SyntaxText(buffer: buf), position: .leading)
494+
return rawPieces.map { TriviaPiece.init(raw: $0) }
495+
}
496+
497+
self.init(pieces: pieces)
498+
499+
if pieces.contains(where: { $0.isUnexpected }) {
500+
var diagnostics: [Diagnostic] = []
501+
let tree = SourceFileSyntax(statements: [], eofToken: .eof(leadingTrivia: self))
502+
var offset = 0
503+
for piece in pieces {
504+
if case .unexpectedText(let contents) = piece {
505+
diagnostics.append(
506+
Diagnostic(
507+
node: Syntax(tree),
508+
position: tree.position.advanced(by: offset),
509+
message: UnexpectedTrivia(triviaContents: contents)
510+
)
511+
)
512+
}
513+
offset += piece.sourceLength.utf8Length
514+
}
515+
throw SyntaxStringInterpolationError.diagnostics(diagnostics, tree: Syntax(tree))
516+
}
517+
}
518+
519+
@_transparent
520+
public init(stringLiteral value: String) {
521+
do {
522+
try self.init(stringLiteralOrThrow: value)
523+
} catch {
524+
fatalError(String(describing: error))
525+
}
526+
}
527+
528+
/// Initialize a syntax node from a string literal.
529+
public init(stringLiteralOrThrow value: String) throws {
530+
var interpolation = String.StringInterpolation(literalCapacity: 1, interpolationCount: 0)
531+
interpolation.appendLiteral(value)
532+
try self.init(stringInterpolationOrThrow: interpolation)
533+
}
534+
}

Tests/SwiftSyntaxBuilderTest/StringInterpolation.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,4 +439,50 @@ final class StringInterpolationTests: XCTestCase {
439439
"""
440440
)
441441
}
442+
443+
func testTrivia() {
444+
XCTAssertEqual(
445+
"/// doc comment" as Trivia,
446+
[
447+
.docLineComment("/// doc comment")
448+
]
449+
)
450+
451+
XCTAssertEqual(
452+
"""
453+
/// doc comment
454+
/// another doc comment
455+
""" as Trivia,
456+
[
457+
.docLineComment("/// doc comment"),
458+
.newlines(1),
459+
.docLineComment("/// another doc comment"),
460+
]
461+
)
462+
463+
XCTAssertEqual(
464+
"""
465+
// 1 + 1 = \(1 + 1)
466+
""" as Trivia,
467+
[
468+
.lineComment("// 1 + 1 = 2")
469+
]
470+
)
471+
}
472+
473+
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
477+
AssertStringsEqualWithDiff(
478+
String(describing: error),
479+
"""
480+
481+
1 │ /*comment*/ invalid /*comm*/
482+
∣ ╰─ unexpected trivia 'invalid'
483+
484+
"""
485+
)
486+
}
487+
}
442488
}

Tests/SwiftSyntaxBuilderTest/TriviaTests.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import SwiftSyntaxBuilder
1717
final class TriviaTests: XCTestCase {
1818
func testLeadingTrivia() {
1919
let decl = VariableDeclSyntax(
20-
leadingTrivia: .docLineComment("/// A doc comment") + .newline + .blockComment("/* An inline comment */") + .space,
20+
leadingTrivia: """
21+
/// A doc comment
22+
/* An inline comment */ \
23+
24+
""",
2125
modifiers: [DeclModifierSyntax(name: .keyword(.static))],
2226
letOrVarKeyword: .keyword(.var)
2327
) {

0 commit comments

Comments
 (0)