Skip to content

Make trivia expressible by string literal in SwiftSyntaxBuilder #1238

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
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
2 changes: 1 addition & 1 deletion Sources/SwiftSyntax/Trivia.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ extension RawTriviaPiece: CustomDebugStringConvertible {
}

extension TriviaPiece {
init(raw: RawTriviaPiece) {
@_spi(RawSyntax) public init(raw: RawTriviaPiece) {
switch raw {
% for trivia in TRIVIAS:
% if trivia.is_collection():
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftSyntax/gyb_generated/Trivia.swift
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ extension RawTriviaPiece: CustomDebugStringConvertible {
}

extension TriviaPiece {
init(raw: RawTriviaPiece) {
@_spi(RawSyntax) public init(raw: RawTriviaPiece) {
switch raw {
case let .spaces(count): self = .spaces(count)
case let .tabs(count): self = .tabs(count)
Expand Down
85 changes: 85 additions & 0 deletions Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import SwiftBasicFormat
import SwiftDiagnostics
@_spi(RawSyntax) import SwiftSyntax
@_spi(RawSyntax) import SwiftParser

/// An individual interpolated syntax node.
struct InterpolatedSyntaxNode {
Expand Down Expand Up @@ -447,3 +448,87 @@ extension TokenSyntax: SyntaxExpressibleByStringInterpolation {
self = .identifier(string)
}
}

// MARK: - Trivia expressible as string

extension TriviaPiece {
var isUnexpected: Bool {
switch self {
case .unexpectedText: return true
default: return false
}
}
}

struct UnexpectedTrivia: DiagnosticMessage {
let triviaContents: String

let diagnosticID = MessageID(domain: "SwiftSyntaxBuilder", id: "UnexpectedTrivia")
let severity = DiagnosticSeverity.error
var message: String {
"unexpected trivia '\(triviaContents)'"
}

}

extension Trivia: ExpressibleByStringInterpolation {
/// 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: String.StringInterpolation) {
do {
try self.init(stringInterpolationOrThrow: stringInterpolation)
} catch {
fatalError(String(describing: error))
}
}

public init(stringInterpolationOrThrow stringInterpolation: String.StringInterpolation) throws {
var text = String(stringInterpolation: stringInterpolation)
let pieces = text.withUTF8 { (buf) -> [TriviaPiece] in
// The leading trivia position is a little bit less restrictive (it allows a shebang), so let's use it.
let rawPieces = TriviaParser.parseTrivia(SyntaxText(buffer: buf), position: .leading)
return rawPieces.map { TriviaPiece.init(raw: $0) }
}

self.init(pieces: pieces)

if pieces.contains(where: { $0.isUnexpected }) {
var diagnostics: [Diagnostic] = []
let tree = SourceFileSyntax(statements: [], eofToken: .eof(leadingTrivia: self))
var offset = 0
for piece in pieces {
if case .unexpectedText(let contents) = piece {
diagnostics.append(
Diagnostic(
node: Syntax(tree),
position: tree.position.advanced(by: offset),
message: UnexpectedTrivia(triviaContents: contents)
)
)
}
offset += piece.sourceLength.utf8Length
}
throw SyntaxStringInterpolationError.diagnostics(diagnostics, tree: Syntax(tree))
}
}

@_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 = String.StringInterpolation(literalCapacity: 1, interpolationCount: 0)
interpolation.appendLiteral(value)
try self.init(stringInterpolationOrThrow: interpolation)
}
}
46 changes: 46 additions & 0 deletions Tests/SwiftSyntaxBuilderTest/StringInterpolation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -439,4 +439,50 @@ final class StringInterpolationTests: XCTestCase {
"""
)
}

func testTrivia() {
XCTAssertEqual(
"/// doc comment" as Trivia,
[
.docLineComment("/// doc comment")
]
)

XCTAssertEqual(
"""
/// doc comment
/// another doc comment
""" as Trivia,
[
.docLineComment("/// doc comment"),
.newlines(1),
.docLineComment("/// another doc comment"),
]
)

XCTAssertEqual(
"""
// 1 + 1 = \(1 + 1)
""" as Trivia,
[
.lineComment("// 1 + 1 = 2")
]
)
}

func testInvalidTrivia() {
var interpolation = String.StringInterpolation(literalCapacity: 1, interpolationCount: 0)
interpolation.appendLiteral("/*comment*/ invalid /*comm*/")
XCTAssertThrowsError(try Trivia(stringInterpolationOrThrow: interpolation)) { error in
AssertStringsEqualWithDiff(
String(describing: error),
"""
1 │ /*comment*/ invalid /*comm*/
∣ ╰─ unexpected trivia 'invalid'
"""
)
}
}
}
6 changes: 5 additions & 1 deletion Tests/SwiftSyntaxBuilderTest/TriviaTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import SwiftSyntaxBuilder
final class TriviaTests: XCTestCase {
func testLeadingTrivia() {
let decl = VariableDeclSyntax(
leadingTrivia: .docLineComment("/// A doc comment") + .newline + .blockComment("/* An inline comment */") + .space,
leadingTrivia: """
/// A doc comment
/* An inline comment */ \
""",
modifiers: [DeclModifierSyntax(name: .keyword(.static))],
letOrVarKeyword: .keyword(.var)
) {
Expand Down