Skip to content

Commit ce5be96

Browse files
authored
Merge pull request #667 from fwcd/fwcd/trailing-trivia
Add trailing trivia and `with{Leading,Trailing}Trivia` to buildable nodes
2 parents d64b4ce + 63a7ace commit ce5be96

File tree

7 files changed

+4616
-518
lines changed

7 files changed

+4616
-518
lines changed

Sources/SwiftSyntaxBuilder/generated/BuildableCollectionNodes.swift

Lines changed: 924 additions & 44 deletions
Large diffs are not rendered by default.

Sources/SwiftSyntaxBuilder/generated/BuildableNodes.swift

Lines changed: 3539 additions & 419 deletions
Large diffs are not rendered by default.

Sources/generate-swift-syntax-builder/String+Extensions.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ import Foundation
1414

1515
extension StringProtocol {
1616
var withFirstCharacterLowercased: String { prefix(1).lowercased() + dropFirst() }
17+
var withFirstCharacterUppercased: String { prefix(1).uppercased() + dropFirst() }
1718
var backticked: String { "`\(self)`" }
1819
}

Sources/generate-swift-syntax-builder/SyntaxUtilities.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,59 @@ func createDisambiguatingExpressibleAsCreateFunction(type: SyntaxBuildableType,
9999
"/// through `ExpressibleAs*` protocols. To resolve the ambiguity, provie a fixed implementation that doesn't perform any conversions.",
100100
])
101101
}
102+
103+
/// Generate a `withATrivia` function.
104+
func createWithTriviaFunction(trivia: String) -> FunctionDecl {
105+
FunctionDecl(
106+
modifiers: [Token.public],
107+
identifier: .identifier("with\(trivia.withFirstCharacterUppercased)"),
108+
signature: FunctionSignature(
109+
input: ParameterClause {
110+
FunctionParameter(
111+
firstName: .wildcard,
112+
secondName: .identifier(trivia),
113+
colon: .colon,
114+
type: "Trivia"
115+
)
116+
},
117+
output: "Self"
118+
)
119+
) {
120+
VariableDecl(.var, name: "result", initializer: "self")
121+
SequenceExpr {
122+
MemberAccessExpr(base: "result", name: trivia)
123+
AssignmentExpr()
124+
trivia
125+
}
126+
ReturnStmt(expression: "result")
127+
}
128+
}
129+
130+
func createTriviaAttachment(varName: String, triviaVarName: String, trivia: String) -> IfStmt {
131+
IfStmt(
132+
conditions: ExprList {
133+
PrefixOperatorExpr(
134+
operatorToken: .prefixOperator("!"),
135+
postfixExpression: MemberAccessExpr(base: triviaVarName, name: "isEmpty")
136+
)
137+
}
138+
) {
139+
SequenceExpr {
140+
varName
141+
AssignmentExpr()
142+
FunctionCallExpr(MemberAccessExpr(base: varName, name: "with\(trivia.withFirstCharacterUppercased)")) {
143+
TupleExprElement(expression: SequenceExpr {
144+
triviaVarName
145+
BinaryOperatorExpr("+")
146+
TupleExpr {
147+
SequenceExpr {
148+
MemberAccessExpr(base: varName, name: trivia)
149+
BinaryOperatorExpr("??")
150+
ArrayExpr()
151+
}
152+
}
153+
})
154+
}
155+
}
156+
}
157+
}

Sources/generate-swift-syntax-builder/Templates/BuildableCollectionNodesFile.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ let buildableCollectionNodesFile = SourceFile {
2020
path: "SwiftSyntax"
2121
)
2222

23+
let triviaSides = ["leading", "trailing"]
24+
let trivias = triviaSides.map { "\($0)Trivia" }
25+
2326
for node in SYNTAX_NODES where node.isSyntaxCollection {
2427
let type = node.type
2528
let elementType = node.collectionElementType
@@ -34,6 +37,16 @@ let buildableCollectionNodesFile = SourceFile {
3437
identifier: type.buildableBaseName,
3538
inheritanceClause: createTypeInheritanceClause(conformances: conformances)
3639
) {
40+
for (side, trivia) in zip(triviaSides, trivias) {
41+
VariableDecl(
42+
leadingTrivia: .docLineComment("/// The \(side) trivia attached to this syntax node once built.") + .newline,
43+
.var,
44+
name: trivia,
45+
type: "Trivia",
46+
initializer: ArrayExpr()
47+
)
48+
}
49+
3750
VariableDecl(.let, name: "elements", type: ArrayType(elementType: elementType.buildable))
3851

3952
// Generate initializers
@@ -42,10 +55,14 @@ let buildableCollectionNodesFile = SourceFile {
4255
createArrayLiteralInitializer(node: node)
4356

4457
// Generate function declarations
45-
createBuildFunction(node: node)
58+
createBuildFunction(node: node, trivias: trivias)
4659
createBuildSyntaxFunction(node: node)
4760
createExpressibleAsCreateFunction(type: type)
4861
createDisambiguatingExpressibleAsCreateFunction(type: type, baseType: .init(syntaxKind: "Syntax"))
62+
63+
for trivia in trivias {
64+
createWithTriviaFunction(trivia: trivia)
65+
}
4966
}
5067

5168
// For nodes without expressible-as conformances, conform Array to the corresponding expressible-as
@@ -170,7 +187,7 @@ private func createArrayLiteralInitializer(node: Node) -> InitializerDecl {
170187
}
171188

172189
/// Generate the function building the collection syntax.
173-
private func createBuildFunction(node: Node) -> FunctionDecl {
190+
private func createBuildFunction(node: Node, trivias: [String]) -> FunctionDecl {
174191
let type = node.type
175192
let elementType = node.collectionElementType
176193
return FunctionDecl(
@@ -182,7 +199,7 @@ private func createBuildFunction(node: Node) -> FunctionDecl {
182199
)
183200
) {
184201
VariableDecl(
185-
.let,
202+
.var,
186203
name: "result",
187204
initializer: FunctionCallExpr("\(type.syntaxBaseName)") {
188205
if elementType.isToken {
@@ -204,6 +221,9 @@ private func createBuildFunction(node: Node) -> FunctionDecl {
204221
}
205222
}
206223
)
224+
for trivia in trivias {
225+
createTriviaAttachment(varName: "result", triviaVarName: trivia, trivia: trivia)
226+
}
207227
ReturnStmt(expression: FunctionCallExpr(MemberAccessExpr(base: "format", name: "_format")) {
208228
TupleExprElement(
209229
label: "syntax",

Sources/generate-swift-syntax-builder/Templates/BuildableNodesFile.swift

Lines changed: 37 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ let buildableNodesFile = SourceFile {
2020
path: "SwiftSyntax"
2121
)
2222

23+
let triviaSides = ["leading", "trailing"]
24+
let trivias = triviaSides.map { "\($0)Trivia" }
25+
2326
for node in SYNTAX_NODES where node.isBuildable {
2427
let type = node.type
2528
let baseType = node.baseType
@@ -35,44 +38,46 @@ let buildableNodesFile = SourceFile {
3538
identifier: type.buildableBaseName,
3639
inheritanceClause: createTypeInheritanceClause(conformances: conformances)
3740
) {
38-
VariableDecl(
39-
leadingTrivia: [
40-
"/// The leading trivia attached to this syntax node once built.",
41-
"/// This is typically used to add comments (e.g. for documentation).",
42-
].map { .docLineComment($0) + .newline }.reduce([], +),
43-
.var,
44-
name: "leadingTrivia",
45-
type: "Trivia"
46-
)
41+
for (side, trivia) in zip(triviaSides, trivias) {
42+
VariableDecl(
43+
leadingTrivia: .docLineComment("/// The \(side) trivia attached to this syntax node once built.") + .newline,
44+
.var,
45+
name: trivia,
46+
type: "Trivia"
47+
)
48+
}
4749

4850
// Generate members
4951
for child in node.children {
5052
VariableDecl(.var, name: child.swiftName, type: child.type.buildable)
5153
}
5254

5355
// Generate initializers
54-
createDefaultInitializer(node: node)
56+
createDefaultInitializer(node: node, trivias: trivias)
5557
if let convenienceInit = createConvenienceInitializer(node: node) {
5658
convenienceInit
5759
}
5860

5961
// Generate function declarations
60-
createBuildFunction(node: node)
62+
createBuildFunction(node: node, trivias: trivias)
6163
createBuildBaseTypeFunction(node: node)
6264
createExpressibleAsCreateFunction(type: node.type)
6365
createDisambiguatingExpressibleAsCreateFunction(type: node.type, baseType: node.baseType)
6466
if baseType.baseName != "Syntax" {
6567
createDisambiguatingExpressibleAsCreateFunction(type: node.baseType, baseType: .init(syntaxKind: "Syntax"))
6668
}
6769
if hasTrailingComma {
68-
createWithTrailingCommaFunction(node: node)
70+
createWithTrailingCommaFunction()
71+
}
72+
for trivia in trivias {
73+
createWithTriviaFunction(trivia: trivia)
6974
}
7075
}
7176
}
7277
}
7378

7479
/// Create the default initializer for the given node.
75-
private func createDefaultInitializer(node: Node) -> InitializerDecl {
80+
private func createDefaultInitializer(node: Node, trivias: [String]) -> InitializerDecl {
7681
let type = node.type
7782
return InitializerDecl(
7883
leadingTrivia: ([
@@ -84,12 +89,14 @@ private func createDefaultInitializer(node: Node) -> InitializerDecl {
8489
modifiers: [Token.public],
8590
signature: FunctionSignature(
8691
input: ParameterClause {
87-
FunctionParameter(
88-
firstName: .identifier("leadingTrivia"),
89-
colon: .colon,
90-
type: "Trivia",
91-
defaultArgument: ArrayExpr()
92-
)
92+
for trivia in trivias {
93+
FunctionParameter(
94+
firstName: .identifier(trivia),
95+
colon: .colon,
96+
type: "Trivia",
97+
defaultArgument: ArrayExpr()
98+
)
99+
}
93100
for child in node.children {
94101
FunctionParameter(
95102
firstName: .identifier(child.swiftName),
@@ -101,10 +108,12 @@ private func createDefaultInitializer(node: Node) -> InitializerDecl {
101108
}
102109
)
103110
) {
104-
SequenceExpr {
105-
MemberAccessExpr(base: "self", name: "leadingTrivia")
106-
AssignmentExpr()
107-
"leadingTrivia"
111+
for trivia in trivias {
112+
SequenceExpr {
113+
MemberAccessExpr(base: "self", name: trivia)
114+
AssignmentExpr()
115+
trivia
116+
}
108117
}
109118
for child in node.children {
110119
SequenceExpr {
@@ -226,7 +235,7 @@ private func createConvenienceInitializer(node: Node) -> InitializerDecl? {
226235
}
227236

228237
/// Generate the function building the node syntax.
229-
private func createBuildFunction(node: Node) -> FunctionDecl {
238+
private func createBuildFunction(node: Node, trivias: [String]) -> FunctionDecl {
230239
let type = node.type
231240
let children = node.children
232241
return FunctionDecl(
@@ -254,31 +263,8 @@ private func createBuildFunction(node: Node) -> FunctionDecl {
254263
}
255264
}
256265
)
257-
IfStmt(
258-
conditions: ExprList {
259-
PrefixOperatorExpr(
260-
operatorToken: .prefixOperator("!"),
261-
postfixExpression: MemberAccessExpr(base: "leadingTrivia", name: "isEmpty")
262-
)
263-
}
264-
) {
265-
SequenceExpr {
266-
"result"
267-
AssignmentExpr()
268-
FunctionCallExpr(MemberAccessExpr(base: "result", name: "withLeadingTrivia")) {
269-
TupleExprElement(expression: SequenceExpr {
270-
"leadingTrivia"
271-
BinaryOperatorExpr("+")
272-
TupleExpr {
273-
SequenceExpr {
274-
MemberAccessExpr(base: "result", name: "leadingTrivia")
275-
BinaryOperatorExpr("??")
276-
ArrayExpr()
277-
}
278-
}
279-
})
280-
}
281-
}
266+
for trivia in trivias {
267+
createTriviaAttachment(varName: "result", triviaVarName: trivia, trivia: trivia)
282268
}
283269
ReturnStmt(expression: FunctionCallExpr(MemberAccessExpr(base: "format", name: "_format")) {
284270
TupleExprElement(
@@ -316,9 +302,8 @@ private func createBuildBaseTypeFunction(node: Node) -> FunctionDecl {
316302
}
317303

318304
/// Generate the `withTrailingComma` function.
319-
private func createWithTrailingCommaFunction(node: Node) -> FunctionDecl {
320-
let children = node.children
321-
return FunctionDecl(
305+
private func createWithTrailingCommaFunction() -> FunctionDecl {
306+
FunctionDecl(
322307
leadingTrivia: .docLineComment("/// Conformance to `HasTrailingComma`.") + .newline,
323308
modifiers: [Token.public],
324309
identifier: .identifier("withTrailingComma"),

Tests/SwiftSyntaxBuilderTest/TriviaTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,40 @@ final class TriviaTests: XCTestCase {
3232
XCTAssertEqual(y, x + .space)
3333
XCTAssertEqual(y, [.newlines(1), .spaces(1)])
3434
}
35+
36+
func testAttachedTrivia() {
37+
let testCases: [UInt: (VariableDecl, String)] = [
38+
#line: (
39+
VariableDecl(.let, name: "x", type: "Int").withLeadingTrivia(.space),
40+
" let x: Int"
41+
),
42+
#line: (
43+
VariableDecl(.let, name: "x", type: "Int").withTrailingTrivia(.space),
44+
"let x: Int "
45+
),
46+
]
47+
for (line, testCase) in testCases {
48+
let (decl, expected) = testCase
49+
let syntax = decl.buildSyntax(format: Format())
50+
XCTAssertEqual(syntax.description, expected, line: line)
51+
}
52+
}
53+
54+
func testAttachedListTrivia() {
55+
let testCases: [UInt: (AttributeList, String)] = [
56+
#line: (
57+
AttributeList([CustomAttribute("Test")]).withLeadingTrivia(.space),
58+
" @Test"
59+
),
60+
#line: (
61+
AttributeList([CustomAttribute("A").withTrailingTrivia(.space), CustomAttribute("B")]).withTrailingTrivia(.space),
62+
"@A @B "
63+
),
64+
]
65+
for (line, testCase) in testCases {
66+
let (decl, expected) = testCase
67+
let syntax = decl.buildSyntax(format: Format())
68+
XCTAssertEqual(syntax.description, expected, line: line)
69+
}
70+
}
3571
}

0 commit comments

Comments
 (0)