Skip to content

Commit 4575970

Browse files
committed
Port SyntaxBuildableWrappers to Swift
Port SyntaxBuildable{Node,Child,Type} to Swift for use in SwiftSyntaxBuilderGeneration. The former two types are modeled as extensions to Node and Child for simplicity and clarity.
1 parent f495286 commit 4575970

File tree

5 files changed

+383
-12
lines changed

5 files changed

+383
-12
lines changed

Sources/SwiftSyntaxBuilderGeneration/Node.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,6 @@ class Node {
2828
let elementsSeparatedByNewline: Bool
2929
let collectionElement: String
3030

31-
var baseType: String {
32-
if self.baseKind == "SyntaxCollection" {
33-
return "Syntax"
34-
} else {
35-
return kindToType(kind: baseKind)
36-
}
37-
}
38-
39-
var collectionElementType: String {
40-
return kindToType(kind: collectionElement)
41-
}
42-
4331
/// Returns `True` if this node declares one of the base syntax kinds.
4432
var isBase: Bool {
4533
return SyntaxBaseKinds.contains(syntaxKind)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
/// Extension to the `Child` type to provide functionality specific to
14+
/// SwiftSyntaxBuilder.
15+
extension Child {
16+
/// The type of this child, represented by a `SyntaxBuildableType`, which can
17+
/// be used to create the corresponding `Buildable` and `ExpressibleAs` types.
18+
var type: SyntaxBuildableType {
19+
SyntaxBuildableType(
20+
syntaxKind: syntaxKind,
21+
isOptional: isOptional
22+
)
23+
}
24+
25+
/// If the child node has documentation associated with it, return it as single
26+
/// line string. Otherwise return an empty string.
27+
var documentation: String {
28+
flattened(indentedDocumentation: description ?? "")
29+
}
30+
31+
/// Generate a Swift expression that creates a proper SwiftSyntax node of type
32+
/// `type.syntax` from a variable named `varName` of type `type.buildable` that
33+
/// represents this child node.
34+
func generateExprBuildSyntaxNode(varName: String, formatName: String) -> String {
35+
if type.isToken {
36+
if requiresLeadingNewline {
37+
return "\(varName).withLeadingTrivia(.newline + \(formatName)._makeIndent() + (\(varName).leadingTrivia ?? []))"
38+
} else {
39+
return varName
40+
}
41+
} else {
42+
let format = formatName + (isIndented ? "._indented()" : "")
43+
let expr = "\(varName)\(type.optionalQuestionMark)"
44+
return "\(expr).build\(type.baseName)(format: \(format), leadingTrivia: nil)"
45+
}
46+
}
47+
48+
/// If this node is a token that can't contain arbitrary text, generate a Swift
49+
/// `assert` statement that verifies the variable with name var_name and of type
50+
/// `TokenSyntax` contains one of the supported text options. Otherwise return `nil`.
51+
func generateAssertStmtTextChoices(varName: String) -> String? {
52+
guard type.isToken else {
53+
return nil
54+
}
55+
56+
let choices: [String]
57+
if !textChoices.isEmpty {
58+
choices = textChoices
59+
} else if !tokenChoices.isEmpty {
60+
let optionalChoices = tokenChoices.map { SYNTAX_TOKEN_MAP["\($0.name)Token"]?.text }
61+
choices = optionalChoices.compactMap { $0 }
62+
guard choices.count == optionalChoices.count else {
63+
// If `nil` is in the text choices, one of the token options can contain arbitrary
64+
// text. Don't generate an assert statement.
65+
return nil
66+
}
67+
} else {
68+
return nil
69+
}
70+
71+
var assertChoices: [String] = []
72+
if type.isOptional {
73+
assertChoices.append("\(varName) == nil")
74+
}
75+
let unwrap = type.isOptional ? "!" : ""
76+
for textChoice in choices {
77+
assertChoices.append("\(varName)\(unwrap) == \"\(textChoice)\"")
78+
}
79+
return "assert(\(assertChoices.joined(separator: " || ")))"
80+
}
81+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
/// Extension to the `Node` type to provide functionality specific to
14+
/// SwiftSyntaxBuilder.
15+
extension Node {
16+
/// The node's syntax kind as `SyntaxBuildableType`.
17+
var type: SyntaxBuildableType {
18+
SyntaxBuildableType(syntaxKind: syntaxKind)
19+
}
20+
21+
/// The node's syntax kind as `SyntaxBuildableType`.
22+
var baseType: SyntaxBuildableType {
23+
SyntaxBuildableType(syntaxKind: baseKind)
24+
}
25+
26+
/// If documentation exists for this node, return it as a single-line string.
27+
/// Otherwise return an empty string.
28+
var documentation: String {
29+
guard let description = description,
30+
!description.isEmpty else {
31+
return ""
32+
}
33+
if isSyntaxCollection {
34+
return "`\(syntaxKind)` represents a collection of `\(description)`"
35+
} else {
36+
return flattened(indentedDocumentation: description)
37+
}
38+
}
39+
40+
/// Assuming this node is a syntax collection, the type of its elements.
41+
var collectionElementType: SyntaxBuildableType {
42+
assert(isSyntaxCollection)
43+
return SyntaxBuildableType(syntaxKind: collectionElement)
44+
}
45+
46+
/// Assuming this node has a single child without a default value, that child.
47+
var singleNonDefaultedChild: Child {
48+
let nonDefaultedParams = children.filter(\.type.defaultInitialization.isEmpty)
49+
assert(nonDefaultedParams.count == 1)
50+
return nonDefaultedParams[0]
51+
}
52+
53+
static func from(type: SyntaxBuildableType) -> Node {
54+
let baseName = type.baseName
55+
guard let node = SYNTAX_NODE_MAP[baseName] else {
56+
fatalError("Base name \(baseName) does not have a syntax node")
57+
}
58+
return node
59+
}
60+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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+
/// Wrapper around the syntax kind strings to provide functionality specific to
14+
/// SwiftSyntaxBuilder. In particular, this includes the functionality to create
15+
/// the `*Buildable`, `ExpressibleAs*` and `*Syntax` Swift types from the syntax
16+
/// kind.
17+
struct SyntaxBuildableType: Hashable {
18+
let syntaxKind: String
19+
let tokenKind: String?
20+
let isOptional: Bool
21+
22+
/// If optional `?`, otherwise the empty string.
23+
var optionalQuestionMark: String {
24+
isOptional ? "?" : ""
25+
}
26+
27+
/// Whether this is a token.
28+
var isToken: Bool {
29+
syntaxKind == "Token"
30+
}
31+
32+
/// This type with `isOptional` set to false.
33+
var nonOptional: Self {
34+
tokenKind.map { Self(syntaxKind: $0) } ?? Self(syntaxKind: syntaxKind)
35+
}
36+
37+
/// The token if this is a token.
38+
var token: Token? {
39+
tokenKind.flatMap { SYNTAX_TOKEN_MAP[$0] }
40+
}
41+
42+
/// If the type has a default value (because it is optional or a token
43+
/// with fixed test), return an expression of the form ` = defaultValue`
44+
/// that can be used as the default value for a function parameter.
45+
/// Otherwise, return the empty string.
46+
var defaultInitialization: String {
47+
if isOptional {
48+
return " = nil"
49+
} else if isToken {
50+
if let token = token, token.text != nil {
51+
return " = TokenSyntax.`\(lowercaseFirstWord(name: token.name))`"
52+
} else if tokenKind == "EOFToken" {
53+
return " = TokenSyntax.eof"
54+
}
55+
}
56+
return ""
57+
}
58+
59+
/// Whether the type is a syntax collection.
60+
var isSyntaxCollection: Bool {
61+
syntaxKind == "SyntaxCollection"
62+
|| (baseType.map(\.isSyntaxCollection) ?? false)
63+
}
64+
65+
/// The raw base name of this kind. Used for the `build*` methods in the
66+
/// defined in the buildable types.
67+
var baseName: String {
68+
syntaxKind
69+
}
70+
71+
/// Return the name of the `Buildable` type that is the main entry point for building
72+
/// SwiftSyntax trees using `SwiftSyntaxBuilder`.
73+
///
74+
/// These names look as follows:
75+
/// - For nodes: The node name, e.g. `IdentifierExpr` (these are implemented as structs)
76+
/// - For base kinds: `<BaseKind>Buildable`, e.g. `ExprBuildable` (these are implemented as protocols)
77+
/// - For token: `TokenSyntax` (tokens don't have a dedicated type in SwiftSyntaxBuilder)
78+
/// If the type is optional, this terminates with a '?'.
79+
var buildable: String {
80+
if isToken {
81+
// Tokens don't have a dedicated buildable type.
82+
return "TokenSyntax\(optionalQuestionMark)"
83+
} else if SYNTAX_BASE_KINDS.contains(syntaxKind) {
84+
return "\(syntaxKind)Buildable\(optionalQuestionMark)"
85+
} else {
86+
return "\(syntaxKind)\(optionalQuestionMark)"
87+
}
88+
}
89+
90+
/// Whether parameters of this type should be initializable by a result builder.
91+
/// Used for certain syntax collections and block-like structures (e.g. `CodeBlock`,
92+
/// `MemberDeclList`).
93+
var isBuilderInitializable: Bool {
94+
BUILDER_INITIALIZABLE_TYPES.keys.contains(nonOptional.buildable)
95+
}
96+
97+
/// A type suitable for initializing this type through a result builder (e.g.
98+
/// returns `CodeBlockItemList` for `CodeBlock`) and otherwise itself.
99+
var builderInitializableType: Self {
100+
let buildable = nonOptional.buildable
101+
return Self(
102+
syntaxKind: BUILDER_INITIALIZABLE_TYPES[buildable].flatMap { $0 } ?? buildable,
103+
isOptional: isOptional
104+
)
105+
}
106+
107+
/// The type from `buildable()` without any question marks attached.
108+
/// This is used for the `create*` methods defined in the `ExpressibleAs*` protocols.
109+
var buildableBaseName: String {
110+
nonOptional.buildable
111+
}
112+
113+
/// The `ExpressibleAs*` Swift type for this syntax kind. Tokens don't
114+
/// have an `ExpressibleAs*` type, so for those this method just returns
115+
/// `TokenSyntax`. If the type is optional, this terminates with a `?`.
116+
var expressibleAs: String {
117+
if isToken {
118+
// Tokens don't have a dedicated ExpressibleAs type.
119+
return buildable
120+
} else {
121+
return "ExpressibleAs\(buildable)"
122+
}
123+
}
124+
125+
/// The corresponding `*Syntax` type defined in the `SwiftSyntax` module,
126+
/// which will eventually get built from `SwiftSyntaxBuilder`. If the type
127+
/// is optional, this terminates with a `?`.
128+
var syntax: String {
129+
if syntaxKind == "Syntax" {
130+
return "Syntax\(optionalQuestionMark)"
131+
} else {
132+
return "\(syntaxKind)Syntax\(optionalQuestionMark)"
133+
}
134+
}
135+
136+
/// Assuming that this is a collection type, the type of the result builder
137+
/// that can be used to build the collection.
138+
var resultBuilder: String {
139+
"\(syntaxKind)Builder\(optionalQuestionMark)"
140+
}
141+
142+
/// The collection types in which this type occurs as an element.
143+
/// We automatically make the `ExpressibleAs*` protocols conform to the
144+
/// `ExpressibleAs*` protocol of the collection they occur in.
145+
var elementInCollections: [Self] {
146+
SYNTAX_NODES
147+
.filter { $0.isSyntaxCollection && $0.collectionElementType == self }
148+
.map { $0.type }
149+
}
150+
151+
/// Whether this type has the `WithTrailingComma` trait.
152+
var hasWithTrailingCommaTrait: Bool {
153+
SYNTAX_NODES.contains { $0.type == self && $0.traits.contains("WithTrailingComma") }
154+
}
155+
156+
/// Types that take a single non-optional parameter of this types
157+
/// and to which this type is thus convertible. We automatically
158+
/// make the `ExpressibleAs*` of this type conform to the `ExpressibleAs*`
159+
/// protocol of the convertible types.
160+
var convertibleToTypes: [Self] {
161+
(SYNTAX_BUILDABLE_EXPRESSIBLE_AS_CONFORMANCES[buildable] ?? [])
162+
.map { Self(syntaxKind: $0) }
163+
}
164+
165+
/// If this type is not a base kind, its base type (see `SyntaxBuildableNode.base_type()`),
166+
/// otherwise `nil`.
167+
var baseType: Self? {
168+
if !SYNTAX_BASE_KINDS.contains(syntaxKind) && !isToken {
169+
return Node.from(type: self).baseType
170+
} else {
171+
return nil
172+
}
173+
}
174+
175+
/// The types to which this `ExpressibleAs*` type conforms to via
176+
/// automatically generated conformances.
177+
var generatedExpressibleAsConformances: [Self] {
178+
var conformances = elementInCollections + convertibleToTypes
179+
if let baseType = baseType, baseType.baseName != "SyntaxCollection" {
180+
conformances.append(baseType)
181+
}
182+
return conformances
183+
}
184+
185+
/// The types to which this `ExpressibleAs*` type conforms to via
186+
/// automatically generated conformances, including transitive conformances.
187+
var transitiveExpressibleAsConformances: [Self] {
188+
generatedExpressibleAsConformances.flatMap { conformance in
189+
[conformance] + conformance.transitiveExpressibleAsConformances
190+
}
191+
}
192+
193+
/// The types to which this `ExpressibleAs*` type implicitly conforms
194+
/// to via transitive conformances. These conformances don't need to be
195+
/// spelled out explicitly in the source code.
196+
var impliedExpressibleAsConformances: [Self] {
197+
generatedExpressibleAsConformances.flatMap { conformance in
198+
conformance.transitiveExpressibleAsConformances
199+
}
200+
}
201+
202+
init(syntaxKind: String, isOptional: Bool = false) {
203+
self.isOptional = isOptional
204+
if syntaxKind.hasSuffix("Token") {
205+
// There are different token kinds but all of them are represented by `Token` in the Swift source (see `kind_to_type` in `gyb_syntax_support`).
206+
self.syntaxKind = "Token"
207+
self.tokenKind = syntaxKind
208+
} else {
209+
self.syntaxKind = syntaxKind
210+
self.tokenKind = nil
211+
}
212+
}
213+
214+
/// Generate an expression that converts a variable named `varName`
215+
/// which is of `expressibleAs` type to an object of type `buildable`.
216+
func generateExprConvertParamTypeToStorageType(varName: String) -> String {
217+
if isToken {
218+
return varName
219+
} else {
220+
return "\(varName)\(optionalQuestionMark).create\(buildableBaseName)()"
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)