Skip to content

Commit 3983a8c

Browse files
committed
Generate BuildableNodes with SwiftSyntaxBuilderGeneration
- Set up BuildableNodesFile - Generate BuildableNodes members - Use ...BaseName where needed in BuildableNodesFile - Generate default initializer in BuildableNodes fully - Disable assert stmt generation until #549 is fixed - Generate convenience initializer parameters - Generate convenience initializer declaration - Use ParameterClause builder initializer - Generate initializer delegations - Generate build method in BuildableNodes - Generate build base type method in BuildableNodes - Generate expressible-as conformance in BuildableNodes - Generate base type ExpressibleAs conformance - Generate disambiguating conformances in BuildableNodes - Generate withTrailingComma function in BuildableNodes
1 parent b116451 commit 3983a8c

File tree

2 files changed

+398
-0
lines changed

2 files changed

+398
-0
lines changed
Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 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 Foundation
14+
import SwiftSyntax
15+
import SwiftSyntaxBuilder
16+
17+
let buildableNodesFile = SourceFile {
18+
ImportDecl(
19+
leadingTrivia: .docLineComment(copyrightHeader),
20+
path: "SwiftSyntax"
21+
)
22+
23+
for node in SYNTAX_NODES where node.isBuildable {
24+
let type = node.type
25+
let baseType = node.baseType
26+
let hasTrailingComma = node.traits.contains("WithTrailingComma")
27+
let conformances = [baseType.buildableBaseName, type.expressibleAsBaseName] + (hasTrailingComma ? ["HasTrailingComma"] : [])
28+
29+
// Generate node struct
30+
StructDecl(
31+
leadingTrivia: node.documentation.isEmpty
32+
? []
33+
: .docLineComment("/// \(node.documentation)") + .newline,
34+
modifiers: [TokenSyntax.public],
35+
identifier: type.buildableBaseName,
36+
inheritanceClause: createTypeInheritanceClause(conformances: conformances)
37+
) {
38+
let children = node.children
39+
40+
// Generate members
41+
for child in children {
42+
VariableDecl(.let, name: child.swiftName, type: child.type.buildable)
43+
}
44+
45+
VariableDecl(
46+
leadingTrivia: [
47+
"/// The leading trivia attached to this syntax node once built.",
48+
"/// This is typically used to add comments (e.g. for documentation).",
49+
].map { .docLineComment($0) + .newline }.reduce([], +),
50+
.let,
51+
name: "leadingTrivia",
52+
type: "Trivia"
53+
)
54+
55+
// Generate initializers
56+
createDefaultInitializer(node: node)
57+
if let convenienceInit = createConvenienceInitializer(node: node) {
58+
convenienceInit
59+
}
60+
61+
// Generate function declarations
62+
createBuildFunction(node: node)
63+
createBuildBaseTypeFunction(node: node)
64+
createExpressibleAsCreateFunction(type: node.type)
65+
createDisambiguatingExpressibleAsCreateFunction(type: node.type, baseType: node.baseType)
66+
if baseType.baseName != "Syntax" {
67+
createDisambiguatingExpressibleAsCreateFunction(type: node.baseType, baseType: .init(syntaxKind: "Syntax"))
68+
}
69+
if hasTrailingComma {
70+
createWithTrailingCommaFunction(node: node)
71+
}
72+
}
73+
}
74+
}
75+
76+
/// Create the default initializer for the given node.
77+
private func createDefaultInitializer(node: Node) -> InitializerDecl {
78+
let type = node.type
79+
let children = node.children
80+
return InitializerDecl(
81+
leadingTrivia: ([
82+
"/// Creates a `\(type.buildableBaseName)` using the provided parameters.",
83+
"/// - Parameters:",
84+
] + children.map { child in
85+
"/// - \(child.swiftName): \(child.documentation)"
86+
}).map { .docLineComment($0) + .newline }.reduce([], +),
87+
modifiers: [TokenSyntax.public],
88+
signature: FunctionSignature(
89+
input: ParameterClause {
90+
FunctionParameter(
91+
firstName: .identifier("leadingTrivia"),
92+
colon: .colon,
93+
type: "Trivia",
94+
defaultArgument: ArrayExpr()
95+
)
96+
for child in children {
97+
FunctionParameter(
98+
firstName: .identifier(child.swiftName),
99+
colon: .colon,
100+
type: child.type.expressibleAs,
101+
defaultArgument: child.type.defaultInitialization
102+
)
103+
}
104+
}
105+
)
106+
) {
107+
SequenceExpr {
108+
MemberAccessExpr(base: "self", name: "leadingTrivia")
109+
AssignmentExpr()
110+
"leadingTrivia"
111+
}
112+
for child in children {
113+
SequenceExpr {
114+
MemberAccessExpr(base: "self", name: child.swiftName)
115+
AssignmentExpr()
116+
child.type.generateExprConvertParamTypeToStorageType(varName: child.swiftName)
117+
}
118+
if let assertStmt = child.generateAssertStmtTextChoices(varName: child.swiftName) {
119+
assertStmt
120+
}
121+
}
122+
}
123+
}
124+
125+
/// Create a builder-based convenience initializer, if needed.
126+
private func createConvenienceInitializer(node: Node) -> InitializerDecl? {
127+
// Only create the convenience initializer if at least one parameter
128+
// is different than in the default initializer generated above.
129+
var shouldCreateInitializer = false
130+
131+
// Keep track of init parameters and result builder parameters in different
132+
// lists to make sure result builder params occur at the end, so that
133+
// they can use trailing closure syntax.
134+
var normalParameters: [FunctionParameter] = []
135+
var builderParameters: [FunctionParameter] = []
136+
var delegatedInitArgs: [TupleExprElement] = []
137+
138+
for child in node.children {
139+
/// The expression that is used to call the default initializer defined above.
140+
let produceExpr: ExpressibleAsExprBuildable
141+
if child.type.isBuilderInitializable {
142+
// Allow initializing certain syntax collections with result builders
143+
shouldCreateInitializer = true
144+
let builderInitializableType = child.type.builderInitializableType
145+
produceExpr = FunctionCallExpr("\(child.swiftName)Builder")
146+
builderParameters.append(FunctionParameter(
147+
attributes: [CustomAttribute(attributeName: builderInitializableType.resultBuilderBaseName, argumentList: nil)],
148+
firstName: .identifier("\(child.swiftName)Builder").withLeadingTrivia(.space),
149+
colon: .colon,
150+
type: FunctionType(
151+
arguments: [],
152+
returnType: builderInitializableType.expressibleAs
153+
),
154+
defaultArgument: ClosureExpr {
155+
if child.type.isOptional {
156+
NilLiteralExpr()
157+
} else {
158+
FunctionCallExpr(builderInitializableType.buildableBaseName) {
159+
TupleExprElement(expression: ArrayExpr())
160+
}
161+
}
162+
}
163+
))
164+
} else if let token = child.type.token, token.text == nil {
165+
// Allow initializing identifiers and other tokens without default text with a String
166+
shouldCreateInitializer = true
167+
let paramType = child.type.optionalWrapped(type: "String")
168+
let tokenExpr = MemberAccessExpr(base: "TokenSyntax", name: token.swiftKind.withFirstCharacterLowercased)
169+
if child.type.isOptional {
170+
produceExpr = FunctionCallExpr(MemberAccessExpr(base: child.swiftName, name: "map")) {
171+
TupleExprElement(expression: tokenExpr)
172+
}
173+
} else {
174+
produceExpr = FunctionCallExpr(tokenExpr) {
175+
TupleExprElement(expression: child.swiftName)
176+
}
177+
}
178+
normalParameters.append(FunctionParameter(
179+
firstName: .identifier(child.swiftName),
180+
colon: .colon,
181+
type: paramType
182+
))
183+
} else {
184+
produceExpr = child.swiftName
185+
normalParameters.append(FunctionParameter(
186+
firstName: .identifier(child.swiftName),
187+
colon: .colon,
188+
type: child.type.expressibleAs,
189+
defaultArgument: child.type.defaultInitialization
190+
))
191+
}
192+
delegatedInitArgs.append(TupleExprElement(label: child.swiftName, expression: produceExpr))
193+
}
194+
195+
guard shouldCreateInitializer else {
196+
return nil
197+
}
198+
199+
return InitializerDecl(
200+
leadingTrivia: [
201+
"/// A convenience initializer that allows:",
202+
"/// - Initializing syntax collections using result builders",
203+
"/// - Initializing tokens without default text using strings",
204+
].map { .docLineComment($0) + .newline }.reduce([], +),
205+
modifiers: [TokenSyntax.public],
206+
signature: FunctionSignature(
207+
input: ParameterClause {
208+
FunctionParameter(
209+
firstName: .identifier("leadingTrivia"),
210+
colon: .colon,
211+
type: "Trivia",
212+
defaultArgument: ArrayExpr()
213+
)
214+
for param in normalParameters + builderParameters {
215+
param
216+
}
217+
}
218+
)
219+
) {
220+
FunctionCallExpr(MemberAccessExpr(base: "self", name: "init")) {
221+
TupleExprElement(label: "leadingTrivia", expression: "leadingTrivia")
222+
for arg in delegatedInitArgs {
223+
arg
224+
}
225+
}
226+
}
227+
}
228+
229+
/// Generate the function building the node syntax.
230+
private func createBuildFunction(node: Node) -> FunctionDecl {
231+
let type = node.type
232+
let children = node.children
233+
return FunctionDecl(
234+
leadingTrivia: [
235+
"/// Builds a `\(type.syntaxBaseName)`.",
236+
"/// - Parameter format: The `Format` to use.",
237+
"/// - Parameter leadingTrivia: Additional leading trivia to attach, typically used for indentation.",
238+
"/// - Returns: The built `\(type.syntaxBaseName)`.",
239+
].map { .docLineComment($0) + .newline }.reduce([], +),
240+
identifier: .identifier("build\(type.baseName)"),
241+
signature: FunctionSignature(
242+
input: createFormatAdditionalLeadingTriviaParams(),
243+
output: type.syntax
244+
)
245+
) {
246+
VariableDecl(
247+
.let,
248+
name: "result",
249+
initializer: FunctionCallExpr(MemberAccessExpr(base: "SyntaxFactory", name: "make\(type.baseName)")) {
250+
for child in children {
251+
TupleExprElement(
252+
label: child.isGarbageNodes ? nil : child.swiftName,
253+
expression: child.generateExprBuildSyntaxNode(varName: child.swiftName, formatName: "format")
254+
)
255+
}
256+
}
257+
)
258+
VariableDecl(
259+
.let,
260+
name: "combinedLeadingTrivia",
261+
initializer: SequenceExpr {
262+
"leadingTrivia"
263+
BinaryOperatorExpr("+")
264+
TupleExpr {
265+
SequenceExpr {
266+
"additionalLeadingTrivia"
267+
BinaryOperatorExpr("??")
268+
ArrayExpr()
269+
}
270+
}
271+
BinaryOperatorExpr("+")
272+
TupleExpr {
273+
SequenceExpr {
274+
MemberAccessExpr(base: "result", name: "leadingTrivia")
275+
BinaryOperatorExpr("??")
276+
ArrayExpr()
277+
}
278+
}
279+
}
280+
)
281+
ReturnStmt(expression: FunctionCallExpr(MemberAccessExpr(base: "result", name: "withLeadingTrivia")) {
282+
TupleExprElement(expression: FunctionCallExpr(MemberAccessExpr(
283+
base: "combinedLeadingTrivia",
284+
name: "addingSpacingAfterNewlinesIfNeeded"
285+
)))
286+
})
287+
}
288+
}
289+
290+
/// Generate the function building the base type.
291+
private func createBuildBaseTypeFunction(node: Node) -> FunctionDecl {
292+
let type = node.type
293+
let baseType = node.baseType
294+
return FunctionDecl(
295+
leadingTrivia: .docLineComment("/// Conformance to `\(baseType.buildableBaseName)`.") + .newline,
296+
modifiers: [TokenSyntax.public],
297+
identifier: .identifier("build\(baseType.baseName)"),
298+
signature: FunctionSignature(
299+
input: createFormatAdditionalLeadingTriviaParams(),
300+
output: baseType.syntax
301+
)
302+
) {
303+
VariableDecl(
304+
.let,
305+
name: "result",
306+
initializer: FunctionCallExpr("build\(type.baseName)") {
307+
TupleExprElement(label: "format", expression: "format")
308+
TupleExprElement(label: "leadingTrivia", expression: "additionalLeadingTrivia")
309+
}
310+
)
311+
ReturnStmt(expression: FunctionCallExpr(baseType.syntaxBaseName) {
312+
TupleExprElement(expression: "result")
313+
})
314+
}
315+
}
316+
317+
private func createFormatAdditionalLeadingTriviaParams() -> ParameterClause {
318+
ParameterClause {
319+
FunctionParameter(
320+
firstName: .identifier("format"),
321+
colon: .colon,
322+
type: "Format"
323+
)
324+
FunctionParameter(
325+
firstName: .identifier("leadingTrivia").withTrailingTrivia(.space),
326+
secondName: .identifier("additionalLeadingTrivia"),
327+
colon: .colon,
328+
type: OptionalType(wrappedType: "Trivia"),
329+
defaultArgument: NilLiteralExpr()
330+
)
331+
}
332+
}
333+
334+
/// Generate the `create...` function for an `ExpressibleAs...` conformance.
335+
private func createExpressibleAsCreateFunction(type: SyntaxBuildableType, additionalDocComments: [String] = []) -> FunctionDecl {
336+
FunctionDecl(
337+
leadingTrivia: ([
338+
"/// Conformance to `\(type.expressibleAsBaseName)`.",
339+
] + additionalDocComments).map { .docLineComment($0) + .newline }.reduce([], +),
340+
modifiers: [TokenSyntax.public],
341+
identifier: .identifier("create\(type.buildableBaseName)"),
342+
signature: FunctionSignature(
343+
input: ParameterClause(),
344+
output: type.buildable
345+
)
346+
) {
347+
ReturnStmt(expression: "self")
348+
}
349+
}
350+
351+
/// Generate the `create...` function for an `ExpressibleAs...` conformance
352+
/// that includes an explanation as to how the function disambiguates a conformance.
353+
private func createDisambiguatingExpressibleAsCreateFunction(type: SyntaxBuildableType, baseType: SyntaxBuildableType) -> FunctionDecl {
354+
createExpressibleAsCreateFunction(type: baseType, additionalDocComments: [
355+
"/// `\(type.buildableBaseName)` may conform to `\(baseType.expressibleAsBaseName)` via different `ExpressibleAs*` paths.",
356+
"/// Thus, there are multiple default implementations of `create\(baseType.buildableBaseName)`, some of which perform conversions",
357+
"/// through `ExpressibleAs*` protocols. To resolve the ambiguity, provie a fixed implementation that doesn't perform any conversions.",
358+
])
359+
}
360+
361+
/// Generate the `withTrailingComma` function.
362+
private func createWithTrailingCommaFunction(node: Node) -> FunctionDecl {
363+
let children = node.children
364+
return FunctionDecl(
365+
leadingTrivia: .docLineComment("/// Conformance to `HasTrailingComma`.") + .newline,
366+
modifiers: [TokenSyntax.public],
367+
identifier: .identifier("withTrailingComma"),
368+
signature: FunctionSignature(
369+
input: ParameterClause {
370+
FunctionParameter(
371+
firstName: .wildcard,
372+
secondName: .identifier("withComma"),
373+
colon: .colon,
374+
type: "Bool"
375+
)
376+
},
377+
output: "Self"
378+
)
379+
) {
380+
FunctionCallExpr("Self") {
381+
for child in children {
382+
TupleExprElement(
383+
label: child.swiftName,
384+
expression: child.name == "TrailingComma" ? SequenceExpr {
385+
TernaryExpr(
386+
conditionExpression: "withComma",
387+
questionMark: .infixQuestionMark.withLeadingTrivia(.space).withTrailingTrivia(.space),
388+
firstChoice: MemberAccessExpr(name: "comma"),
389+
colonMark: .colon.withLeadingTrivia(.space).withTrailingTrivia(.space),
390+
secondChoice: NilLiteralExpr()
391+
)
392+
} : child.swiftName
393+
)
394+
}
395+
}
396+
}
397+
}

0 commit comments

Comments
 (0)