Skip to content

Commit c2bdd2e

Browse files
committed
Introduce leaf protocols to prevent leaf nodes from being casted
This commit addresses an issue in the implementation of `SyntaxProtocol` where the 'as' method allows casting to any other syntax node type. This leads to problematic scenarios, such as casting a 'FunctionDeclSyntax' to an 'IdentifierExprSyntax' without compiler warnings, despite the cast being destined to fail at runtime. To solve this, specialized leaf protocols have been introduced. These restrict casting options to only those that are meaningful within the same base node type, thereby enhancing type safety and reducing the risk of runtime errors.
1 parent 452e07d commit c2bdd2e

File tree

23 files changed

+1124
-374
lines changed

23 files changed

+1124
-374
lines changed

CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,19 @@ public enum SyntaxNodeKind: String, CaseIterable {
368368
}
369369
}
370370

371+
/// For base node types, generates the name of the protocol to which all
372+
/// concrete leaf nodes that derive from this base kind should conform.
373+
///
374+
/// - Warning: This property can only be accessed for base node kinds; attempting to
375+
/// access it for a non-base kind will result in a runtime error.
376+
public var leafProtocolType: TypeSyntax {
377+
if isBase {
378+
return "_Leaf\(syntaxType)NodeProtocol"
379+
} else {
380+
fatalError("Only base kind can define leaf protocol")
381+
}
382+
}
383+
371384
/// If the syntax kind has been renamed, the previous raw value that is now
372385
/// deprecated.
373386
public var deprecatedRawValue: String? {

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

Lines changed: 137 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,84 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
3131
"""
3232
)
3333

34+
DeclSyntax(
35+
#"""
36+
/// Extension of ``\#(node.kind.protocolType)`` to provide casting methods.
37+
///
38+
/// These methods enable casting between syntax node types within the same
39+
/// base node protocol hierarchy (e.g., ``DeclSyntaxProtocol``).
40+
///
41+
/// While ``SyntaxProtocol`` offers general casting methods (``SyntaxProtocol.as(_:)``,
42+
/// ``SyntaxProtocol.is(_:)``, and ``SyntaxProtocol.cast(_:)``), these often aren't
43+
/// appropriate for use on types conforming to a specific base node protocol
44+
/// like ``\#(node.kind.protocolType)``. That's because at this level,
45+
/// we know that the cast to another base node type (e.g., ``DeclSyntaxProtocol``
46+
/// when working with ``ExprSyntaxProtocol``) is guaranteed to fail.
47+
///
48+
/// To guide developers toward correct usage, this extension provides overloads
49+
/// of these casting methods that are restricted to the same base node type.
50+
/// Furthermore, it marks the inherited casting methods from ``SyntaxProtocol`` as
51+
/// deprecated, indicating that they will always fail when used in this context.
52+
extension \#(node.kind.protocolType) {
53+
/// Checks if the current syntax node can be cast to a given specialized syntax type.
54+
///
55+
/// - Returns: `true` if the node can be cast, `false` otherwise.
56+
public func `is`<S: \#(node.kind.protocolType)>(_ syntaxType: S.Type) -> Bool {
57+
return self.as(syntaxType) != nil
58+
}
59+
60+
/// Attempts to cast the current syntax node to a given specialized syntax type.
61+
///
62+
/// - Returns: An instance of the specialized type, or `nil` if the cast fails.
63+
public func `as`<S: \#(node.kind.protocolType)>(_ syntaxType: S.Type) -> S? {
64+
return S.init(self)
65+
}
66+
67+
/// Force-casts the current syntax node to a given specialized syntax type.
68+
///
69+
/// - Returns: An instance of the specialized type.
70+
/// - Warning: This function will crash if the cast is not possible. Use `as` to safely attempt a cast.
71+
public func cast<S: \#(node.kind.protocolType)>(_ syntaxType: S.Type) -> S {
72+
return self.as(S.self)!
73+
}
74+
75+
/// Checks if the current syntax node can be cast to a given node type from the different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
76+
///
77+
/// - Returns: `false` since the node can not be cast to the node type from different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
78+
///
79+
/// - Note: This method overloads the general `is` method and is marked as deprecated to produce a warning,
80+
/// informing the user that the cast will always fail.
81+
@available(*, deprecated, message: "This cast will always fail")
82+
public func `is`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> Bool {
83+
return false
84+
}
85+
86+
/// Attempts to cast the current syntax node to a given node type from the different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
87+
///
88+
/// - Returns: `nil` since the node can not be cast to the node type from different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
89+
///
90+
/// - Note: This method overloads the general `as` method and is marked as deprecated to produce a warning,
91+
/// informing the user that the cast will always fail.
92+
@available(*, deprecated, message: "This cast will always fail")
93+
public func `as`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S? {
94+
return nil
95+
}
96+
97+
/// Force-casts the current syntax node to a given node type from the different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
98+
///
99+
/// - Returns: This method will always trigger a runtime crash and never return.
100+
///
101+
/// - Note: This method overloads the general `cast` method and is marked as deprecated to produce a warning,
102+
/// informing the user that the cast will always fail.
103+
/// - Warning: Invoking this method will lead to a fatal error.
104+
@available(*, deprecated, message: "This cast will always fail")
105+
public func cast<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S {
106+
fatalError("\(Self.self) cannot be cast to \(S.self)")
107+
}
108+
}
109+
"""#
110+
)
111+
34112
try! ExtensionDeclSyntax("public extension Syntax") {
35113
DeclSyntax(
36114
"""
@@ -171,30 +249,6 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
171249
ExprSyntax("self._syntaxNode = Syntax(data)")
172250
}
173251

174-
DeclSyntax(
175-
"""
176-
public func `is`<S: \(node.kind.protocolType)>(_ syntaxType: S.Type) -> Bool {
177-
return self.as(syntaxType) != nil
178-
}
179-
"""
180-
)
181-
182-
DeclSyntax(
183-
"""
184-
public func `as`<S: \(node.kind.protocolType)>(_ syntaxType: S.Type) -> S? {
185-
return S.init(self)
186-
}
187-
"""
188-
)
189-
190-
DeclSyntax(
191-
"""
192-
public func cast<S: \(node.kind.protocolType)>(_ syntaxType: S.Type) -> S {
193-
return self.as(S.self)!
194-
}
195-
"""
196-
)
197-
198252
DeclSyntax(
199253
"""
200254
/// Syntax nodes always conform to `\(node.kind.protocolType)`. This API is just
@@ -232,9 +286,17 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
232286
StmtSyntax("return .choices(\(choices))")
233287
}
234288
}
289+
290+
leafProtocolDecl(type: node.kind.leafProtocolType, inheritedType: node.kind.protocolType)
235291
}
236292

237-
try! ExtensionDeclSyntax("extension Syntax") {
293+
try! ExtensionDeclSyntax(
294+
"""
295+
// MARK: - Syntax
296+
297+
extension Syntax
298+
"""
299+
) {
238300
try VariableDeclSyntax("public static var structure: SyntaxNodeStructure") {
239301
let choices = ArrayExprSyntax {
240302
ArrayElementSyntax(
@@ -254,4 +316,54 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
254316
}
255317
}
256318

319+
leafProtocolDecl(type: "_LeafSyntaxNodeProtocol", inheritedType: "SyntaxProtocol")
320+
}
321+
322+
private func leafProtocolDecl(type: TypeSyntax, inheritedType: TypeSyntax) -> DeclSyntax {
323+
DeclSyntax(
324+
#"""
325+
/// Protocol that syntax nodes conform to if they don't have any semantic subtypes.
326+
/// These are syntax nodes that are not considered base nodes for other syntax types.
327+
///
328+
/// Syntax nodes conforming to this protocol have their inherited casting methods
329+
/// deprecated to prevent incorrect casting.
330+
public protocol \#(type): \#(inheritedType) {}
331+
332+
public extension \#(type) {
333+
/// Checks if the current leaf syntax node can be cast to a different specified type.
334+
///
335+
/// - Returns: `false` since the leaf node cannot be cast to a different specified type.
336+
///
337+
/// - Note: This method overloads the general `is` method and is marked as deprecated to produce a warning,
338+
/// informing the user that the cast will always fail.
339+
@available(*, deprecated, message: "This cast will always fail")
340+
func `is`<S: \#(inheritedType)>(_ syntaxType: S.Type) -> Bool {
341+
return false
342+
}
343+
344+
/// Attempts to cast the current leaf syntax node to a different specified type.
345+
///
346+
/// - Returns: `nil` since the leaf node cannot be cast to a different specified type.
347+
///
348+
/// - Note: This method overloads the general `as` method and is marked as deprecated to produce a warning,
349+
/// informing the user that the cast will always fail.
350+
@available(*, deprecated, message: "This cast will always fail")
351+
func `as`<S: \#(inheritedType)>(_ syntaxType: S.Type) -> S? {
352+
return nil
353+
}
354+
355+
/// Force-casts the current leaf syntax node to a different specified type.
356+
///
357+
/// - Returns: This method will always trigger a runtime crash and never return.
358+
///
359+
/// - Note: This method overloads the general `cast` method and is marked as deprecated to produce a warning,
360+
/// informing the user that the cast will always fail.
361+
/// - Warning: Invoking this method will lead to a fatal error.
362+
@available(*, deprecated, message: "This cast will always fail")
363+
func cast<S: \#(inheritedType)>(_ syntaxType: S.Type) -> S {
364+
fatalError("\(Self.self) cannot be cast to \(S.self)")
365+
}
366+
}
367+
"""#
368+
)
257369
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax {
4141
4242
\(documentation)
4343
\(node.node.apiAttributes())\
44-
public struct \(node.kind.syntaxType): \(node.baseType.syntaxBaseName)Protocol, SyntaxHashable
44+
public struct \(node.kind.syntaxType): \(node.baseType.syntaxBaseName)Protocol, SyntaxHashable, \(node.base.leafProtocolType)
4545
"""
4646
) {
4747
for child in node.children {

Release Notes/510.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525

2626
## Deprecations
2727

28+
- Leaf Node Casts
29+
- Description: Syntax nodes that do not act as base nodes for other syntax types have the casting methods marked as deprecated. This prevents unsafe type-casting by issuing deprecation warnings for methods that will always result in failed casts.
30+
- Issue: https://github.com/apple/swift-syntax/issues/2092
31+
- Pull Request: https://github.com/apple/swift-syntax/pull/2108
32+
2833
## API-Incompatible Changes
2934

3035

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
789789
return .skipChildren
790790
}
791791
if let unexpected = node.unexpectedBetweenDeinitKeywordAndEffectSpecifiers,
792-
let name = unexpected.presentTokens(satisfying: { $0.tokenKind.isIdentifier == true }).only?.as(TokenSyntax.self)
792+
let name = unexpected.presentTokens(satisfying: { $0.tokenKind.isIdentifier == true }).only
793793
{
794794
addDiagnostic(
795795
name,
@@ -1146,8 +1146,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
11461146
return .skipChildren
11471147
}
11481148

1149-
if node.conditions.count == 1,
1150-
node.conditions.first?.as(ConditionElementSyntax.self)?.condition.is(MissingExprSyntax.self) == true,
1149+
if node.conditions.only?.condition.is(MissingExprSyntax.self) == true,
11511150
!node.body.leftBrace.isMissingAllTokens
11521151
{
11531152
addDiagnostic(node.conditions, MissingConditionInStatement(node: node), handledNodes: [node.conditions.id])
@@ -2024,8 +2023,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
20242023
return .skipChildren
20252024
}
20262025

2027-
if node.conditions.count == 1,
2028-
node.conditions.first?.as(ConditionElementSyntax.self)?.condition.is(MissingExprSyntax.self) == true,
2026+
if node.conditions.only?.condition.is(MissingExprSyntax.self) == true,
20292027
!node.body.leftBrace.isMissingAllTokens
20302028
{
20312029
addDiagnostic(node.conditions, MissingConditionInStatement(node: node), handledNodes: [node.conditions.id])

Sources/SwiftSyntax/Syntax.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,23 +196,34 @@ extension SyntaxProtocol {
196196

197197
// Casting functions to specialized syntax nodes.
198198
public extension SyntaxProtocol {
199-
/// Converts the given specialized node to this type. Returns `nil` if the
200-
/// conversion is not possible or the given node was `nil`.
199+
/// Initializes a new instance of the conforming type from a given specialized syntax node.
200+
///
201+
/// Returns `nil` if the conversion isn't possible, or if the provided node is `nil`.
201202
init?<S: SyntaxProtocol>(_ node: S?) {
202203
guard let node = node else {
203204
return nil
204205
}
205206
self.init(node)
206207
}
207208

209+
/// Checks if the current syntax node can be cast to a given specialized syntax type.
210+
///
211+
/// - Returns: `true` if the node can be cast, `false` otherwise.
208212
func `is`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> Bool {
209213
return self.as(syntaxType) != nil
210214
}
211215

216+
/// Attempts to cast the current syntax node to a given specialized syntax type.
217+
///
218+
/// - Returns: An instance of the specialized type, or `nil` if the cast fails.
212219
func `as`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S? {
213220
return S.init(self)
214221
}
215222

223+
/// Force-casts the current syntax node to a given specialized syntax type.
224+
///
225+
/// - Returns: An instance of the specialized type.
226+
/// - Warning: This function will crash if the cast is not possible. Use `as` to safely attempt a cast.
216227
func cast<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S {
217228
return self.as(S.self)!
218229
}

Sources/SwiftSyntax/TokenSyntax.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,40 @@ public struct TokenSyntax: SyntaxProtocol, SyntaxHashable {
154154
public var tokenDiagnostic: TokenDiagnostic? {
155155
return tokenView.tokenDiagnostic
156156
}
157+
158+
/// Checks if the current leaf syntax node can be cast to a different specified type.
159+
///
160+
/// - Returns: `false` since the leaf node cannot be cast to a different specified type.
161+
///
162+
/// - Note: This method overloads the general `is` method and is marked as deprecated to produce a warning,
163+
/// informing the user that the cast will always fail.
164+
@available(*, deprecated, message: "This cast will always fail")
165+
public func `is`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> Bool {
166+
return false
167+
}
168+
169+
/// Attempts to cast the current leaf syntax node to a different specified type.
170+
///
171+
/// - Returns: `nil` since the leaf node cannot be cast to a different specified type.
172+
///
173+
/// - Note: This method overloads the general `as` method and is marked as deprecated to produce a warning,
174+
/// informing the user that the cast will always fail.
175+
@available(*, deprecated, message: "This cast will always fail")
176+
public func `as`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S? {
177+
return nil
178+
}
179+
180+
/// Force-casts the current leaf syntax node to a different specified type.
181+
///
182+
/// - Returns: This method will always trigger a runtime crash and never return.
183+
///
184+
/// - Note: This method overloads the general `cast` method and is marked as deprecated to produce a warning,
185+
/// informing the user that the cast will always fail.
186+
/// - Warning: Invoking this method will lead to a fatal error.
187+
@available(*, deprecated, message: "This cast will always fail")
188+
public func cast<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S {
189+
fatalError("\(Self.self) cannot be cast to \(S.self)")
190+
}
157191
}
158192

159193
extension TokenSyntax: CustomReflectable {

0 commit comments

Comments
 (0)