Skip to content

Commit 2caa68e

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 9124cd1 commit 2caa68e

File tree

2 files changed

+399
-0
lines changed

2 files changed

+399
-0
lines changed
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
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+
// FIXME: This is currently broken due to https://github.com/apple/swift-syntax/issues/549
119+
// if let assertStmt = child.generateAssertStmtTextChoices(varName: child.swiftName) {
120+
// assertStmt
121+
// }
122+
}
123+
}
124+
}
125+
126+
/// Create a builder-based convenience initializer, if needed.
127+
private func createConvenienceInitializer(node: Node) -> InitializerDecl? {
128+
// Only create the convenience initializer if at least one parameter
129+
// is different than in the default initializer generated above.
130+
var shouldCreateInitializer = false
131+
132+
// Keep track of init parameters and result builder parameters in different
133+
// lists to make sure result builder params occur at the end, so that
134+
// they can use trailing closure syntax.
135+
var normalParameters: [FunctionParameter] = []
136+
var builderParameters: [FunctionParameter] = []
137+
var delegatedInitArgs: [TupleExprElement] = []
138+
139+
for child in node.children {
140+
/// The expression that is used to call the default initializer defined above.
141+
let produceExpr: ExpressibleAsExprBuildable
142+
if child.type.isBuilderInitializable {
143+
// Allow initializing certain syntax collections with result builders
144+
shouldCreateInitializer = true
145+
let builderInitializableType = child.type.builderInitializableType
146+
produceExpr = FunctionCallExpr("\(child.swiftName)Builder")
147+
builderParameters.append(FunctionParameter(
148+
attributes: [CustomAttribute(attributeName: builderInitializableType.resultBuilderBaseName, argumentList: nil)],
149+
firstName: .identifier("\(child.swiftName)Builder").withLeadingTrivia(.space),
150+
colon: .colon,
151+
type: FunctionType(
152+
arguments: [],
153+
returnType: builderInitializableType.expressibleAs
154+
),
155+
defaultArgument: ClosureExpr {
156+
if child.type.isOptional {
157+
NilLiteralExpr()
158+
} else {
159+
FunctionCallExpr(builderInitializableType.buildableBaseName) {
160+
TupleExprElement(expression: ArrayExpr())
161+
}
162+
}
163+
}
164+
))
165+
} else if let token = child.type.token, token.text == nil {
166+
// Allow initializing identifiers and other tokens without default text with a String
167+
shouldCreateInitializer = true
168+
let paramType = child.type.optionalWrapped(type: "String")
169+
let tokenExpr = MemberAccessExpr(base: "TokenSyntax", name: token.swiftKind.withFirstCharacterLowercased)
170+
if child.type.isOptional {
171+
produceExpr = FunctionCallExpr(MemberAccessExpr(base: child.swiftName, name: "map")) {
172+
TupleExprElement(expression: tokenExpr)
173+
}
174+
} else {
175+
produceExpr = FunctionCallExpr(tokenExpr) {
176+
TupleExprElement(expression: child.swiftName)
177+
}
178+
}
179+
normalParameters.append(FunctionParameter(
180+
firstName: .identifier(child.swiftName),
181+
colon: .colon,
182+
type: paramType
183+
))
184+
} else {
185+
produceExpr = child.swiftName
186+
normalParameters.append(FunctionParameter(
187+
firstName: .identifier(child.swiftName),
188+
colon: .colon,
189+
type: child.type.expressibleAs,
190+
defaultArgument: child.type.defaultInitialization
191+
))
192+
}
193+
delegatedInitArgs.append(TupleExprElement(label: child.swiftName, expression: produceExpr))
194+
}
195+
196+
guard shouldCreateInitializer else {
197+
return nil
198+
}
199+
200+
return InitializerDecl(
201+
leadingTrivia: [
202+
"/// A convenience initializer that allows:",
203+
"/// - Initializing syntax collections using result builders",
204+
"/// - Initializing tokens without default text using strings",
205+
].map { .docLineComment($0) + .newline }.reduce([], +),
206+
modifiers: [TokenSyntax.public],
207+
signature: FunctionSignature(
208+
input: ParameterClause {
209+
FunctionParameter(
210+
firstName: .identifier("leadingTrivia"),
211+
colon: .colon,
212+
type: "Trivia",
213+
defaultArgument: ArrayExpr()
214+
)
215+
for param in normalParameters + builderParameters {
216+
param
217+
}
218+
}
219+
)
220+
) {
221+
FunctionCallExpr(MemberAccessExpr(base: "self", name: "init")) {
222+
TupleExprElement(label: "leadingTrivia", expression: "leadingTrivia")
223+
for arg in delegatedInitArgs {
224+
arg
225+
}
226+
}
227+
}
228+
}
229+
230+
/// Generate the function building the node syntax.
231+
private func createBuildFunction(node: Node) -> FunctionDecl {
232+
let type = node.type
233+
let children = node.children
234+
return FunctionDecl(
235+
leadingTrivia: [
236+
"/// Builds a `\(type.syntaxBaseName)`.",
237+
"/// - Parameter format: The `Format` to use.",
238+
"/// - Parameter leadingTrivia: Additional leading trivia to attach, typically used for indentation.",
239+
"/// - Returns: The built `\(type.syntaxBaseName)`.",
240+
].map { .docLineComment($0) + .newline }.reduce([], +),
241+
identifier: .identifier("build\(type.baseName)"),
242+
signature: FunctionSignature(
243+
input: createFormatAdditionalLeadingTriviaParams(),
244+
output: type.syntax
245+
)
246+
) {
247+
VariableDecl(
248+
.let,
249+
name: "result",
250+
initializer: FunctionCallExpr(MemberAccessExpr(base: "SyntaxFactory", name: "make\(type.baseName)")) {
251+
for child in children {
252+
TupleExprElement(
253+
label: child.isGarbageNodes ? nil : child.swiftName,
254+
expression: child.generateExprBuildSyntaxNode(varName: child.swiftName, formatName: "format")
255+
)
256+
}
257+
}
258+
)
259+
VariableDecl(
260+
.let,
261+
name: "combinedLeadingTrivia",
262+
initializer: SequenceExpr {
263+
"leadingTrivia"
264+
BinaryOperatorExpr("+")
265+
TupleExpr {
266+
SequenceExpr {
267+
"additionalLeadingTrivia"
268+
BinaryOperatorExpr("??")
269+
ArrayExpr()
270+
}
271+
}
272+
BinaryOperatorExpr("+")
273+
TupleExpr {
274+
SequenceExpr {
275+
MemberAccessExpr(base: "result", name: "leadingTrivia")
276+
BinaryOperatorExpr("??")
277+
ArrayExpr()
278+
}
279+
}
280+
}
281+
)
282+
ReturnStmt(expression: FunctionCallExpr(MemberAccessExpr(base: "result", name: "withLeadingTrivia")) {
283+
TupleExprElement(expression: FunctionCallExpr(MemberAccessExpr(
284+
base: "combinedLeadingTrivia",
285+
name: "addingSpacingAfterNewlinesIfNeeded"
286+
)))
287+
})
288+
}
289+
}
290+
291+
/// Generate the function building the base type.
292+
private func createBuildBaseTypeFunction(node: Node) -> FunctionDecl {
293+
let type = node.type
294+
let baseType = node.baseType
295+
return FunctionDecl(
296+
leadingTrivia: .docLineComment("/// Conformance to `\(baseType.buildableBaseName)`.") + .newline,
297+
modifiers: [TokenSyntax.public],
298+
identifier: .identifier("build\(baseType.baseName)"),
299+
signature: FunctionSignature(
300+
input: createFormatAdditionalLeadingTriviaParams(),
301+
output: baseType.syntax
302+
)
303+
) {
304+
VariableDecl(
305+
.let,
306+
name: "result",
307+
initializer: FunctionCallExpr("build\(type.baseName)") {
308+
TupleExprElement(label: "format", expression: "format")
309+
TupleExprElement(label: "leadingTrivia", expression: "additionalLeadingTrivia")
310+
}
311+
)
312+
ReturnStmt(expression: FunctionCallExpr(baseType.syntaxBaseName) {
313+
TupleExprElement(expression: "result")
314+
})
315+
}
316+
}
317+
318+
private func createFormatAdditionalLeadingTriviaParams() -> ParameterClause {
319+
ParameterClause {
320+
FunctionParameter(
321+
firstName: .identifier("format"),
322+
colon: .colon,
323+
type: "Format"
324+
)
325+
FunctionParameter(
326+
firstName: .identifier("leadingTrivia").withTrailingTrivia(.space),
327+
secondName: .identifier("additionalLeadingTrivia"),
328+
colon: .colon,
329+
type: OptionalType(wrappedType: "Trivia"),
330+
defaultArgument: NilLiteralExpr()
331+
)
332+
}
333+
}
334+
335+
/// Generate the `create...` function for an `ExpressibleAs...` conformance.
336+
private func createExpressibleAsCreateFunction(type: SyntaxBuildableType, additionalDocComments: [String] = []) -> FunctionDecl {
337+
FunctionDecl(
338+
leadingTrivia: ([
339+
"/// Conformance to `\(type.expressibleAsBaseName)`.",
340+
] + additionalDocComments).map { .docLineComment($0) + .newline }.reduce([], +),
341+
modifiers: [TokenSyntax.public],
342+
identifier: .identifier("create\(type.buildableBaseName)"),
343+
signature: FunctionSignature(
344+
input: ParameterClause(),
345+
output: type.buildable
346+
)
347+
) {
348+
ReturnStmt(expression: "self")
349+
}
350+
}
351+
352+
/// Generate the `create...` function for an `ExpressibleAs...` conformance
353+
/// that includes an explanation as to how the function disambiguates a conformance.
354+
private func createDisambiguatingExpressibleAsCreateFunction(type: SyntaxBuildableType, baseType: SyntaxBuildableType) -> FunctionDecl {
355+
createExpressibleAsCreateFunction(type: baseType, additionalDocComments: [
356+
"/// `\(type.buildableBaseName)` may conform to `\(baseType.expressibleAsBaseName)` via different `ExpressibleAs*` paths.",
357+
"/// Thus, there are multiple default implementations of `create\(baseType.buildableBaseName)`, some of which perform conversions",
358+
"/// through `ExpressibleAs*` protocols. To resolve the ambiguity, provie a fixed implementation that doesn't perform any conversions.",
359+
])
360+
}
361+
362+
/// Generate the `withTrailingComma` function.
363+
private func createWithTrailingCommaFunction(node: Node) -> FunctionDecl {
364+
let children = node.children
365+
return FunctionDecl(
366+
leadingTrivia: .docLineComment("/// Conformance to `HasTrailingComma`.") + .newline,
367+
modifiers: [TokenSyntax.public],
368+
identifier: .identifier("withTrailingComma"),
369+
signature: FunctionSignature(
370+
input: ParameterClause {
371+
FunctionParameter(
372+
firstName: .wildcard,
373+
secondName: .identifier("withComma"),
374+
colon: .colon,
375+
type: "Bool"
376+
)
377+
},
378+
output: "Self"
379+
)
380+
) {
381+
FunctionCallExpr("Self") {
382+
for child in children {
383+
TupleExprElement(
384+
label: child.swiftName,
385+
expression: child.name == "TrailingComma" ? SequenceExpr {
386+
TernaryExpr(
387+
conditionExpression: "withComma",
388+
questionMark: .infixQuestionMark.withLeadingTrivia(.space).withTrailingTrivia(.space),
389+
firstChoice: MemberAccessExpr(name: "comma"),
390+
colonMark: .colon.withLeadingTrivia(.space).withTrailingTrivia(.space),
391+
secondChoice: NilLiteralExpr()
392+
)
393+
} : child.swiftName
394+
)
395+
}
396+
}
397+
}
398+
}

0 commit comments

Comments
 (0)