Skip to content

Commit 09e7d57

Browse files
committed
WIP: Make convenience initializers with CodeGeneration
1 parent 86f50c2 commit 09e7d57

File tree

5 files changed

+191
-20
lines changed

5 files changed

+191
-20
lines changed

CodeGeneration/Sources/SyntaxSupport/AttributeNodes.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ public let ATTRIBUTE_NODES: [Node] = [
4949
nameForDiagnostics: "attribute",
5050
documentation: "An `@` attribute.",
5151
parserFunction: "parseAttribute",
52+
rules: [
53+
NodeInitRule(
54+
nonOptionalChildName: "arguments",
55+
childDefaultValues: [
56+
"leftParen": .leftParen,
57+
"rightParen": .rightParen
58+
])
59+
],
5260
children: [
5361
Child(
5462
name: "atSign",

CodeGeneration/Sources/SyntaxSupport/Node.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ public class Node {
6363
return kind.varOrCaseName
6464
}
6565

66+
/// List of convenience initializer rules for this node.
67+
public let rules: [NodeInitRule]
68+
6669
/// If this is a layout node, return a view of the node that provides access
6770
/// to the layout-node specific properties.
6871
public var layoutNode: LayoutNode? {
@@ -112,6 +115,7 @@ public class Node {
112115
documentation: String? = nil,
113116
parserFunction: TokenSyntax? = nil,
114117
traits: [String] = [],
118+
rules: [NodeInitRule] = [],
115119
children: [Child] = []
116120
) {
117121
precondition(base != .syntaxCollection)
@@ -124,6 +128,11 @@ public class Node {
124128
self.documentation = docCommentTrivia(from: documentation)
125129
self.parserFunction = parserFunction
126130

131+
132+
// FIXME: We should validate rules and check that all referenced children
133+
// elements in fact exist on that node.
134+
self.rules = rules
135+
127136
let childrenWithUnexpected: [Child]
128137
if children.isEmpty {
129138
childrenWithUnexpected = [
@@ -229,6 +238,7 @@ public class Node {
229238
isExperimental: Bool = false,
230239
nameForDiagnostics: String?,
231240
documentation: String? = nil,
241+
rules: [NodeInitRule] = [],
232242
parserFunction: TokenSyntax? = nil,
233243
elementChoices: [SyntaxNodeKind]
234244
) {
@@ -239,6 +249,7 @@ public class Node {
239249
self.nameForDiagnostics = nameForDiagnostics
240250
self.documentation = docCommentTrivia(from: documentation)
241251
self.parserFunction = parserFunction
252+
self.rules = rules
242253

243254
assert(!elementChoices.isEmpty)
244255
self.data = .collection(choices: elementChoices)
@@ -380,3 +391,8 @@ fileprivate extension Child {
380391
fileprivate extension Node {
381392

382393
}
394+
395+
public struct NodeInitRule {
396+
public let nonOptionalChildName: String
397+
public let childDefaultValues: [String: Token]
398+
}

CodeGeneration/Sources/generate-swift-syntax/LayoutNode+Extensions.swift

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,52 +16,114 @@ import SyntaxSupport
1616
import Utils
1717

1818
extension LayoutNode {
19-
func generateInitializerDeclHeader(useDeprecatedChildName: Bool = false) -> SyntaxNodeString {
19+
20+
func makeChildParamType(for child: Child, isOptional: Bool = false) -> TypeSyntax {
21+
var paramType: TypeSyntax
22+
23+
if !child.kind.isNodeChoicesEmpty {
24+
paramType = "\(child.syntaxChoicesType)"
25+
} else if child.hasBaseType {
26+
paramType = "some \(child.syntaxNodeKind.protocolType)"
27+
} else {
28+
paramType = child.syntaxNodeKind.syntaxType
29+
}
30+
31+
if isOptional {
32+
if paramType.is(SomeOrAnyTypeSyntax.self) {
33+
paramType = "(\(paramType))?"
34+
} else {
35+
paramType = "\(paramType)?"
36+
}
37+
}
38+
39+
return paramType
40+
}
41+
42+
/// Generates a memberwise SyntaxNode initializer `SyntaxNodeString`.
43+
///
44+
/// - parameters:
45+
/// - rule: The ``NodeInitRule`` to use for generating the initializer. Applying a rule will make some children non-optional, and set default values for other children.
46+
/// - useDeprecatedChildName: Whether to use the deprecated child name for the initializer parameter.
47+
func generateInitializerDeclHeader(for rule: NodeInitRule? = nil, useDeprecatedChildName: Bool = false) -> SyntaxNodeString {
2048
if children.isEmpty {
2149
return "public init()"
2250
}
2351

24-
func createFunctionParameterSyntax(for child: Child) -> FunctionParameterSyntax {
25-
var paramType: TypeSyntax
26-
if !child.kind.isNodeChoicesEmpty {
27-
paramType = "\(child.syntaxChoicesType)"
28-
} else if child.hasBaseType {
29-
paramType = "some \(child.syntaxNodeKind.protocolType)"
52+
func childParameterName(for child: Child) -> TokenSyntax {
53+
let parameterName: TokenSyntax
54+
55+
if useDeprecatedChildName, let deprecatedVarName = child.deprecatedVarName {
56+
parameterName = deprecatedVarName
3057
} else {
31-
paramType = child.syntaxNodeKind.syntaxType
58+
parameterName = child.varOrCaseName
3259
}
60+
return parameterName
61+
}
3362

34-
if child.isOptional {
35-
if paramType.is(SomeOrAnyTypeSyntax.self) {
36-
paramType = "(\(paramType))?"
63+
func ruleBasedChildIsOptional(for child: Child, with rule: NodeInitRule?) -> Bool? {
64+
if let rule = rule {
65+
if rule.nonOptionalChildName == child.name {
66+
return false
3767
} else {
38-
paramType = "\(paramType)?"
68+
return child.isOptional
3969
}
70+
} else {
71+
return nil
4072
}
73+
}
4174

42-
let parameterName: TokenSyntax
75+
func ruleBasedChildDefaultValue(for child: Child, with rule: NodeInitRule?) -> InitializerClauseSyntax? {
76+
if let rule, let defaultValue = rule.childDefaultValues[child.name] {
77+
return InitializerClauseSyntax(
78+
equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space),
79+
value: ExprSyntax(".\(defaultValue.spec.varOrCaseName)Token()")
80+
)
81+
} else {
82+
return nil
83+
}
84+
}
4385

44-
if useDeprecatedChildName, let deprecatedVarName = child.deprecatedVarName {
45-
parameterName = deprecatedVarName
86+
func ruleBasedShouldOverrideDefault(for child: Child, with rule: NodeInitRule?) -> Bool {
87+
if let rule {
88+
// If the rule provides a default for this child, override it and set the rule-based default.
89+
if rule.childDefaultValues[child.name] != nil {
90+
return true
91+
}
92+
93+
// For the non-optional rule-based parameter, strip the default value (override, but there will be no default)
94+
return rule.nonOptionalChildName == child.name
4695
} else {
47-
parameterName = child.varOrCaseName
96+
return false
4897
}
98+
}
99+
100+
func createFunctionParameterSyntax(for child: Child, overrideOptional: Bool? = nil, shouldOverrideDefault: Bool = false, overrideDefaultValue: InitializerClauseSyntax? = nil) -> FunctionParameterSyntax {
101+
102+
let parameterName = childParameterName(for: child)
49103

50104
return FunctionParameterSyntax(
51105
leadingTrivia: .newline,
52106
firstName: child.isUnexpectedNodes ? .wildcardToken(trailingTrivia: .space) : parameterName,
53107
secondName: child.isUnexpectedNodes ? parameterName : nil,
54108
colon: .colonToken(),
55-
type: paramType,
56-
defaultValue: child.defaultInitialization
109+
type: makeChildParamType(for: child, isOptional: overrideOptional ?? child.isOptional),
110+
defaultValue: shouldOverrideDefault ? overrideDefaultValue : child.defaultInitialization
57111
)
58112
}
59113

114+
// For convenience initializers, we don't need unexpected tokens in the arguments list
115+
// because convenience initializers are meant to be used bo developers manually
116+
// hence there should be no unexpected tokens
117+
let childrenToIterate = rule != nil ? nonUnexpectedChildren : children
118+
60119
let params = FunctionParameterListSyntax {
61120
FunctionParameterSyntax("leadingTrivia: Trivia? = nil")
62121

63-
for child in children {
64-
createFunctionParameterSyntax(for: child)
122+
for child in childrenToIterate {
123+
createFunctionParameterSyntax(for: child,
124+
overrideOptional: ruleBasedChildIsOptional(for: child, with: rule),
125+
shouldOverrideDefault: ruleBasedShouldOverrideDefault(for: child, with: rule),
126+
overrideDefaultValue: ruleBasedChildDefaultValue(for: child, with: rule))
65127
}
66128

67129
FunctionParameterSyntax("trailingTrivia: Trivia? = nil")
@@ -75,6 +137,14 @@ extension LayoutNode {
75137
"""
76138
}
77139

140+
func generateRuleBasedDefaultValuesDocComment(for rule: NodeInitRule) -> SwiftSyntax.Trivia {
141+
var params = ""
142+
for (childName, defaultValue) in rule.childDefaultValues {
143+
params += " - `\(childName)`: `TokenSyntax.\(defaultValue.spec.varOrCaseName)Token()`\n"
144+
}
145+
return docCommentTrivia(from: params)
146+
}
147+
78148
func generateInitializerDocComment() -> SwiftSyntax.Trivia {
79149
func generateParamDocComment(for child: Child) -> String? {
80150
if child.documentationAbstract.isEmpty {

CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxNodesFile.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,55 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax {
7878
"""
7979
)
8080

81+
for rule in node.rules {
82+
try! InitializerDeclSyntax(
83+
"""
84+
/// A convenience initializer for ``\(node.kind.syntaxType)``
85+
/// that takes a non-optional value for `\(raw: rule.nonOptionalChildName)` parameter,
86+
/// and adds the following default values:
87+
\(node.generateRuleBasedDefaultValuesDocComment(for: rule))
88+
\(node.generateInitializerDeclHeader(for: rule))
89+
"""
90+
) {
91+
// Convenience initializer just calls the full initializer
92+
// with certain child parameters specified as optional types
93+
// and providing the rule-based default value for the affected
94+
// parameters.
95+
FunctionCallExprSyntax(
96+
calledExpression: ExprSyntax("self.init"),
97+
leftParen: .leftParenToken(),
98+
arguments: LabeledExprListSyntax {
99+
LabeledExprSyntax(
100+
label: TokenSyntax("leadingTrivia"),
101+
colon: .colonToken(),
102+
expression: ExprSyntax("leadingTrivia"),
103+
trailingComma: .commaToken()
104+
)
105+
106+
for child in node.nonUnexpectedChildren {
107+
LabeledExprSyntax(
108+
label: child.varOrCaseName,
109+
colon: .colonToken(),
110+
expression: rule.nonOptionalChildName == child.name ? ExprSyntax("\(child.varOrCaseName.backtickedIfNeeded) as \(node.makeChildParamType(for: child, isOptional: true))") : ExprSyntax("\(child.varOrCaseName.backtickedIfNeeded)"),
111+
trailingComma: .commaToken()
112+
)
113+
}
114+
115+
LabeledExprSyntax(
116+
label: TokenSyntax("trailingTrivia"),
117+
colon: .colonToken(),
118+
expression: ExprSyntax("trailingTrivia")
119+
)
120+
},
121+
rightParen: .rightParenToken()
122+
)
123+
}
124+
}
125+
126+
// The main member-wise initializer
127+
// generateInitializerDocComment renders DocC comment
128+
// generateInitializerDeclHeader renders the actual init declaration
129+
// and lists out all it's parameters and their default values
81130
try! InitializerDeclSyntax(
82131
"""
83132
\(node.generateInitializerDocComment())
@@ -100,6 +149,7 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax {
100149
)
101150
)
102151
)
152+
103153
let layoutList = ArrayExprSyntax {
104154
for child in node.children {
105155
ArrayElementSyntax(

Sources/SwiftSyntax/generated/syntaxNodes/SyntaxNodesAB.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2575,6 +2575,33 @@ public struct AttributeSyntax: SyntaxProtocol, SyntaxHashable, _LeafSyntaxNodePr
25752575
self._syntaxNode = Syntax(data)
25762576
}
25772577

2578+
/// A convenience initializer for ``AttributeSyntax``
2579+
/// that takes a non-optional value for `arguments` parameter,
2580+
/// and adds the following default values:
2581+
/// - `rightParen`: `TokenSyntax.rightParenToken()`
2582+
/// - `leftParen`: `TokenSyntax.leftParenToken()`
2583+
///
2584+
public init(
2585+
leadingTrivia: Trivia? = nil,
2586+
atSign: TokenSyntax = .atSignToken(),
2587+
attributeName: some TypeSyntaxProtocol,
2588+
leftParen: TokenSyntax? = .leftParenToken(),
2589+
arguments: Arguments,
2590+
rightParen: TokenSyntax? = .rightParenToken(),
2591+
trailingTrivia: Trivia? = nil
2592+
2593+
) {
2594+
self.init(
2595+
leadingTrivia: leadingTrivia,
2596+
atSign: atSign,
2597+
attributeName: attributeName,
2598+
leftParen: leftParen,
2599+
arguments: arguments as Arguments?,
2600+
rightParen: rightParen,
2601+
trailingTrivia: trailingTrivia
2602+
)
2603+
}
2604+
25782605
/// - Parameters:
25792606
/// - leadingTrivia: Trivia to be prepended to the leading trivia of the node’s first token. If the node is empty, there is no token to attach the trivia to and the parameter is ignored.
25802607
/// - atSign: The `@` sign.

0 commit comments

Comments
 (0)