Skip to content

Commit f71d212

Browse files
committed
[NFC] Make DeclGroupSyntax.memberBlock an IUO
The special decls nested inside `@abi` attributes don’t have bodies, so it’s necessary to allow `DeclGroupSyntax.memberBlock` (and the nodes that conform to it, which include the nodes for extensions and nominal types) to be `nil`. The plan is that `nil`s will *only* occur in new syntax, not elsewhere in Swift code—even if a DeclGroupSyntax node is written without a body, the parser will still create a MemberBlockSyntax for it with missing tokens. Since `nil` will never occur in pre-existing nodes or code, I’ve chosen to make `memberBlock` an implicitly unwrapped optional. Alternatives would be to make it a normal optional (breaking source compatibility) or to make `memberBlock` a backwards compatibility shim for an optional property with a new name. I have nevertheless chosen to write all use sites for `memberBlock` as though the property was a normal Optional, except for ones in tests.
1 parent d145cb2 commit f71d212

31 files changed

+224
-143
lines changed

CodeGeneration/Sources/SyntaxSupport/Child.swift

+38-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ public enum TokenChoice: Equatable, IdentifierConvertible {
3636
}
3737
}
3838

39+
/// If a child can be optional, what flavor of optional is it? Used to make
40+
/// some children optional without breaking source compatibility.
41+
public enum Optionality: String {
42+
/// An ordinary `Optional` which must be explicitly unwrapped by users.
43+
case normal = "?"
44+
/// An implicitly-unwrapped `Optional` which maintains source compatibility
45+
/// if the child's optionality has changed.
46+
case implicitlyUnwrapped = "!"
47+
}
48+
3949
public enum ChildKind {
4050
/// The child always contains a node of the given `kind`.
4151
case node(kind: SyntaxNodeKind)
@@ -95,7 +105,7 @@ public class Child: NodeChoiceConvertible {
95105
public let kind: ChildKind
96106

97107
/// Whether this child is optional and can be `nil`.
98-
public let isOptional: Bool
108+
public let optionality: Optionality?
99109

100110
public let experimentalFeature: ExperimentalFeature?
101111

@@ -256,7 +266,7 @@ public class Child: NodeChoiceConvertible {
256266
experimentalFeature: ExperimentalFeature? = nil,
257267
nameForDiagnostics: String? = nil,
258268
documentation: String? = nil,
259-
isOptional: Bool = false
269+
optionality: Optionality? = nil
260270
) {
261271
precondition(name.first?.isLowercase ?? true, "The first letter of a child’s name should be lowercase")
262272
precondition(
@@ -270,6 +280,31 @@ public class Child: NodeChoiceConvertible {
270280
self.nameForDiagnostics = nameForDiagnostics
271281
self.documentationSummary = SwiftSyntax.Trivia.docCommentTrivia(from: documentation)
272282
self.documentationAbstract = String(documentation?.split(whereSeparator: \.isNewline).first ?? "")
273-
self.isOptional = isOptional
283+
self.optionality = optionality
284+
}
285+
286+
/// If a classification is passed, it specifies the color identifiers in
287+
/// that subtree should inherit for syntax coloring. Must be a member of
288+
/// ``SyntaxClassification``.
289+
/// If `forceClassification` is also set to true, all child nodes (not only
290+
/// identifiers) inherit the syntax classification.
291+
convenience init(
292+
name: String,
293+
deprecatedName: String? = nil,
294+
kind: ChildKind,
295+
experimentalFeature: ExperimentalFeature? = nil,
296+
nameForDiagnostics: String? = nil,
297+
documentation: String? = nil,
298+
isOptional: Bool
299+
) {
300+
self.init(
301+
name: name,
302+
deprecatedName: deprecatedName,
303+
kind: kind,
304+
experimentalFeature: experimentalFeature,
305+
nameForDiagnostics: nameForDiagnostics,
306+
documentation: documentation,
307+
optionality: isOptional ? .normal : nil
308+
)
274309
}
275310
}

CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift

+12-6
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,8 @@ public let DECL_NODES: [Node] = [
227227
),
228228
Child(
229229
name: "memberBlock",
230-
kind: .node(kind: .memberBlock)
230+
kind: .node(kind: .memberBlock),
231+
optionality: .implicitlyUnwrapped
231232
),
232233
]
233234
),
@@ -401,7 +402,8 @@ public let DECL_NODES: [Node] = [
401402
name: "memberBlock",
402403
kind: .node(kind: .memberBlock),
403404
documentation:
404-
"The members of the class declaration. As class extension declarations may declare additional members, the contents of this member block isn't guaranteed to be a complete list of members for this type."
405+
"The members of the class declaration. As class extension declarations may declare additional members, the contents of this member block isn't guaranteed to be a complete list of members for this type.",
406+
optionality: .implicitlyUnwrapped
405407
),
406408
]
407409
),
@@ -827,7 +829,8 @@ public let DECL_NODES: [Node] = [
827829
name: "memberBlock",
828830
kind: .node(kind: .memberBlock),
829831
documentation:
830-
"The cases and other members associated with this enum declaration. Because enum extension declarations may declare additional members the contents of this member block isn't guaranteed to be a complete list of members for this type."
832+
"The cases and other members associated with this enum declaration. Because enum extension declarations may declare additional members the contents of this member block isn't guaranteed to be a complete list of members for this type.",
833+
optionality: .implicitlyUnwrapped
831834
),
832835
]
833836
),
@@ -906,7 +909,8 @@ public let DECL_NODES: [Node] = [
906909
name: "memberBlock",
907910
kind: .node(kind: .memberBlock),
908911
documentation:
909-
"The members of the extension declaration. As this is an extension, the contents of this member block isn't guaranteed to be a complete list of members for this type."
912+
"The members of the extension declaration. As this is an extension, the contents of this member block isn't guaranteed to be a complete list of members for this type.",
913+
optionality: .implicitlyUnwrapped
910914
),
911915
]
912916
),
@@ -2011,7 +2015,8 @@ public let DECL_NODES: [Node] = [
20112015
Child(
20122016
name: "memberBlock",
20132017
kind: .node(kind: .memberBlock),
2014-
documentation: "The members of the protocol declaration."
2018+
documentation: "The members of the protocol declaration.",
2019+
optionality: .implicitlyUnwrapped
20152020
),
20162021
]
20172022
),
@@ -2182,7 +2187,8 @@ public let DECL_NODES: [Node] = [
21822187
name: "memberBlock",
21832188
kind: .node(kind: .memberBlock),
21842189
documentation:
2185-
"The members of the struct declaration. Because struct extension declarations may declare additional members the contents of this member block isn't guaranteed to be a complete list of members for this type."
2190+
"The members of the struct declaration. Because struct extension declarations may declare additional members the contents of this member block isn't guaranteed to be a complete list of members for this type.",
2191+
optionality: .implicitlyUnwrapped
21862192
),
21872193
]
21882194
),

CodeGeneration/Sources/SyntaxSupport/GrammarGenerator.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ struct GrammarGenerator {
3434
}
3535

3636
private func grammar(for child: Child) -> String {
37-
let optionality = child.isOptional ? "?" : ""
37+
let optionality = child.optionality?.rawValue ?? ""
3838
switch child.kind {
3939
case .node(let kind):
4040
return "\(kind.doccLink)\(optionality)"

CodeGeneration/Sources/SyntaxSupport/Traits.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public let TRAITS: [Trait] = [
6363
"A `where` clause that places additional constraints on generic parameters like `where Element: Hashable`.",
6464
isOptional: true
6565
),
66-
Child(name: "memberBlock", kind: .node(kind: .memberBlock)),
66+
Child(name: "memberBlock", kind: .node(kind: .memberBlock), optionality: .implicitlyUnwrapped),
6767
]
6868
),
6969
Trait(

CodeGeneration/Sources/Utils/SyntaxBuildableChild.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ extension Child {
4040
}
4141
return SyntaxBuildableType(
4242
kind: buildableKind,
43-
isOptional: isOptional
43+
optionality: optionality
4444
)
4545
}
4646

@@ -58,7 +58,7 @@ extension Child {
5858
}
5959

6060
public var defaultValue: ExprSyntax? {
61-
if isOptional || isUnexpectedNodes {
61+
if optionality != nil || isUnexpectedNodes {
6262
if buildableType.isBaseType && kind.isNodeChoicesEmpty {
6363
return ExprSyntax("\(buildableType.buildable).none")
6464
} else {
@@ -131,7 +131,7 @@ extension Child {
131131
}
132132

133133
var preconditionChoices: [ExprSyntax] = []
134-
if buildableType.isOptional {
134+
if buildableType.optionality != nil {
135135
preconditionChoices.append(
136136
ExprSyntax(
137137
SequenceExprSyntax {

CodeGeneration/Sources/Utils/SyntaxBuildableType.swift

+21-15
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ import SyntaxSupport
2020
/// kind.
2121
public struct SyntaxBuildableType: Hashable {
2222
public let kind: SyntaxOrTokenNodeKind
23-
public let isOptional: Bool
23+
public let optionality: Optionality?
2424

25-
public init(kind: SyntaxOrTokenNodeKind, isOptional: Bool = false) {
26-
self.isOptional = isOptional
25+
public init(kind: SyntaxOrTokenNodeKind, optionality: Optionality? = nil) {
26+
self.optionality = optionality
2727
self.kind = kind
2828
}
2929

@@ -60,7 +60,7 @@ public struct SyntaxBuildableType: Hashable {
6060
/// with fixed test), return an expression that can be used as the
6161
/// default value for a function parameter. Otherwise, return `nil`.
6262
public var defaultValue: ExprSyntax? {
63-
if isOptional {
63+
if optionality != nil {
6464
return ExprSyntax(NilLiteralExprSyntax())
6565
} else if let token = token {
6666
if token.text != nil {
@@ -108,7 +108,7 @@ public struct SyntaxBuildableType: Hashable {
108108
if case .some(.some(let builderInitializableType)) = BUILDER_INITIALIZABLE_TYPES[kind] {
109109
return Self(
110110
kind: .node(kind: builderInitializableType),
111-
isOptional: isOptional
111+
optionality: optionality
112112
)
113113
} else {
114114
return self
@@ -132,8 +132,8 @@ public struct SyntaxBuildableType: Hashable {
132132
/// The corresponding `*Syntax` type defined in the `SwiftSyntax` module,
133133
/// which will eventually get built from `SwiftSyntaxBuilder`. If the type
134134
/// is optional, this terminates with a `?`.
135-
public var syntax: TypeSyntax {
136-
return optionalWrapped(type: syntaxBaseName)
135+
public func syntax(canUseIUO: Bool = true) -> TypeSyntax {
136+
return optionalWrapped(type: syntaxBaseName, canUseIUO: canUseIUO)
137137
}
138138

139139
/// The type that is used for parameters in SwiftSyntaxBuilder that take this
@@ -167,27 +167,33 @@ public struct SyntaxBuildableType: Hashable {
167167
}
168168
}
169169

170-
/// Wraps a type in an optional depending on whether `isOptional` is true.
171-
public func optionalWrapped(type: some TypeSyntaxProtocol) -> TypeSyntax {
172-
if isOptional {
170+
/// Wraps a type in an optional depending on whether `optionality` is true.
171+
public func optionalWrapped(type: some TypeSyntaxProtocol, canUseIUO: Bool = true) -> TypeSyntax {
172+
switch optionality {
173+
case .some(.implicitlyUnwrapped):
174+
if canUseIUO {
175+
return TypeSyntax(ImplicitlyUnwrappedOptionalTypeSyntax(wrappedType: type))
176+
}
177+
fallthrough
178+
case .some(.normal):
173179
return TypeSyntax(OptionalTypeSyntax(wrappedType: type))
174-
} else {
180+
case nil:
175181
return TypeSyntax(type)
176182
}
177183
}
178184

179-
/// Wraps a type in an optional chaining depending on whether `isOptional` is true.
185+
/// Wraps a type in an optional chaining depending on whether `optionality` is true.
180186
public func optionalChained(expr: some ExprSyntaxProtocol) -> ExprSyntax {
181-
if isOptional {
187+
if optionality != nil {
182188
return ExprSyntax(OptionalChainingExprSyntax(expression: expr))
183189
} else {
184190
return ExprSyntax(expr)
185191
}
186192
}
187193

188-
/// Wraps a type in a force unwrap expression depending on whether `isOptional` is true.
194+
/// Wraps a type in a force unwrap expression depending on whether `optionality` is true.
189195
public func forceUnwrappedIfNeeded(expr: some ExprSyntaxProtocol) -> ExprSyntax {
190-
if isOptional {
196+
if optionality != nil {
191197
return ExprSyntax(ForceUnwrapExprSyntax(expression: expr))
192198
} else {
193199
return ExprSyntax(expr)

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ extension LayoutNode {
3131
paramType = child.syntaxNodeKind.syntaxType
3232
}
3333

34-
if child.isOptional {
34+
if let optionality = child.optionality {
3535
if paramType.is(SomeOrAnyTypeSyntax.self) {
36-
paramType = "(\(paramType))?"
36+
paramType = "(\(paramType))\(raw: optionality.rawValue)"
3737
} else {
38-
paramType = "\(paramType)?"
38+
paramType = "\(paramType)\(raw: optionality.rawValue)"
3939
}
4040
}
4141

@@ -125,7 +125,7 @@ extension LayoutNode {
125125
let builderInitializableType = child.buildableType.builderInitializableType
126126
if child.buildableType.builderInitializableType != child.buildableType {
127127
let param = Node.from(type: child.buildableType).layoutNode!.singleNonDefaultedChild
128-
if child.isOptional {
128+
if child.optionality != nil {
129129
produceExpr = ExprSyntax(
130130
"\(childName)Builder().map { \(child.buildableType.syntaxBaseName)(\(param.labelDeclName): $0) }"
131131
)
@@ -139,7 +139,7 @@ extension LayoutNode {
139139
}
140140
builderParameters.append(
141141
FunctionParameterSyntax(
142-
"@\(builderInitializableType.resultBuilderType) \(childName)Builder: () throws-> \(builderInitializableType.syntax)"
142+
"@\(builderInitializableType.resultBuilderType) \(childName)Builder: () throws -> \(builderInitializableType.syntax(canUseIUO: false))"
143143
)
144144
)
145145
} else {

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func rawSyntaxNodesFile(nodesStartingWith: [Character]) -> SourceFileSyntax {
161161
let list = ExprListSyntax {
162162
ExprSyntax("layout.initialize(repeating: nil)")
163163
for (index, child) in node.children.enumerated() {
164-
let optionalMark = child.isOptional ? "?" : ""
164+
let optionalMark = (child.optionality != nil) ? "?" : ""
165165

166166
ExprSyntax(
167167
"layout[\(raw: index)] = \(child.baseCallName)\(raw: optionalMark).raw"
@@ -188,7 +188,7 @@ func rawSyntaxNodesFile(nodesStartingWith: [Character]) -> SourceFileSyntax {
188188
try VariableDeclSyntax(
189189
"public var \(child.varDeclName): Raw\(child.buildableType.buildable)"
190190
) {
191-
let exclamationMark = child.isOptional ? "" : "!"
191+
let exclamationMark = child.optionality != nil ? "" : "!"
192192

193193
if child.syntaxNodeKind == .syntax {
194194
ExprSyntax("layoutView.children[\(raw: index)]\(raw: exclamationMark)")
@@ -232,7 +232,7 @@ fileprivate extension Child {
232232
var paramType: TypeSyntax
233233
if !kind.isNodeChoicesEmpty {
234234
paramType = "\(syntaxChoicesType)"
235-
} else if hasBaseType && !isOptional {
235+
} else if hasBaseType && optionality == nil {
236236
// we restrict the use of generic type to non-optional parameter types, otherwise call sites would no longer be
237237
// able to just pass `nil` to this parameter without specializing `(some Raw<Kind>SyntaxNodeProtocol)?`
238238
//

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ let renamedChildrenCompatibilityFile = try! SourceFileSyntax(leadingTrivia: copy
2222
if let deprecatedVarName = child.deprecatedVarName {
2323
let childType: TypeSyntax =
2424
child.kind.isNodeChoicesEmpty ? child.syntaxNodeKind.syntaxType : child.syntaxChoicesType
25-
let type = child.isOptional ? TypeSyntax("\(childType)?") : childType
25+
let type = switch child.optionality {
26+
case .none:
27+
childType
28+
case .normal:
29+
TypeSyntax("\(childType)?")
30+
case .implicitlyUnwrapped:
31+
TypeSyntax("\(childType)!")
32+
}
2633

2734
DeclSyntax(
2835
"""

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax {
135135

136136
let childType: TypeSyntax =
137137
child.kind.isNodeChoicesEmpty ? child.syntaxNodeKind.syntaxType : child.syntaxChoicesType
138-
let type = child.isOptional ? TypeSyntax("\(childType)?") : TypeSyntax("\(childType)")
138+
let type = TypeSyntax("\(childType)\(raw: child.optionality?.rawValue ?? "")")
139139

140140
try! VariableDeclSyntax(
141141
"""
@@ -145,7 +145,7 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax {
145145
) {
146146
AccessorDeclSyntax(accessorSpecifier: .keyword(.get)) {
147147
let optionalityMarker: TokenSyntax =
148-
child.isOptional ? .infixQuestionMarkToken() : .exclamationMarkToken()
148+
child.optionality != nil ? .infixQuestionMarkToken() : .exclamationMarkToken()
149149
StmtSyntax("return Syntax(self).child(at: \(raw: index))\(optionalityMarker).cast(\(childType).self)")
150150
}
151151

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ let syntaxTraitsFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
2828
"""
2929
) {
3030
for child in trait.children {
31-
let questionMark = child.isOptional ? "?" : ""
31+
let questionMark = child.optionality?.rawValue ?? ""
3232

3333
DeclSyntax(
3434
"""

CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ fileprivate extension ChildKind {
8181
fileprivate extension Child {
8282
func hasSameType(as other: Child) -> Bool {
8383
return identifier.description == other.identifier.description && kind.hasSameType(as: other.kind)
84-
&& isOptional == other.isOptional
84+
&& optionality == other.optionality
8585
}
8686

8787
func isFollowedByColonToken(in node: LayoutNode) -> Bool {
@@ -816,7 +816,7 @@ class ValidateSyntaxNodes: XCTestCase {
816816

817817
for node in SYNTAX_NODES.compactMap(\.layoutNode) {
818818
for child in node.children {
819-
if case .collection = child.kind, child.isOptional, !child.isUnexpectedNodes {
819+
if case .collection = child.kind, child.optionality != nil, !child.isUnexpectedNodes {
820820
failures.append(
821821
ValidationFailure(
822822
node: node.kind,

Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,8 @@ import SwiftSyntax
645645
var results: [LookupResult] = []
646646

647647
if let primaryAssociatedTypeClause,
648-
primaryAssociatedTypeClause.range.contains(lookUpPosition)
648+
primaryAssociatedTypeClause.range.contains(lookUpPosition),
649+
let memberBlock
649650
{
650651
results = memberBlock.lookupAssociatedTypeDeclarations(
651652
identifier,

0 commit comments

Comments
 (0)