Skip to content

Commit a3ef07d

Browse files
authored
Merge pull request #2108 from Matejkob/prevent-unreleated-casts
Introduce leaf protocols to prevent leaf nodes from being casted
2 parents 452e07d + d2945f8 commit a3ef07d

File tree

23 files changed

+1413
-374
lines changed

23 files changed

+1413
-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: 170 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,117 @@ 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 upcast to its base node type (``\#(node.kind.syntaxType)``).
76+
///
77+
/// - Returns: `true` since the node can always be upcast to its base node.
78+
///
79+
/// - Note: This method overloads the general `is` method and is marked deprecated to produce a warning
80+
/// informing the user that the upcast will always succeed.
81+
@available(*, deprecated, message: "This cast will always succeed")
82+
public func `is`(_ syntaxType: \#(node.kind.syntaxType).Type) -> Bool {
83+
return true
84+
}
85+
86+
/// Attempts to upcast the current syntax node to its base node type (``\#(node.kind.syntaxType)``).
87+
///
88+
/// - Returns: The base node created from the current syntax node, as the node can always be upcast to its base type.
89+
///
90+
/// - Note: This method overloads the general `as` method and is marked deprecated to produce a warning
91+
/// informing the user the upcast should be performed using the target base node's initializer.
92+
@available(*, deprecated, message: "Use `\#(node.kind.syntaxType).init` for upcasting")
93+
public func `as`(_ syntaxType: \#(node.kind.syntaxType).Type) -> \#(node.kind.syntaxType)? {
94+
return \#(node.kind.syntaxType)(self)
95+
}
96+
97+
/// Force-upcast the current syntax node to its base node type (``\#(node.kind.syntaxType)``).
98+
///
99+
/// - Returns: The base node created from the current syntax node, as the node can always be upcast to its base type.
100+
///
101+
/// - Note: This method overloads the general `as` method and is marked deprecated to produce a warning
102+
/// informing the user the upcast should be performed using the target base node's initializer.
103+
@available(*, deprecated, message: "Use `\#(node.kind.syntaxType).init` for upcasting")
104+
public func cast(_ syntaxType: \#(node.kind.syntaxType).Type) -> \#(node.kind.syntaxType) {
105+
return \#(node.kind.syntaxType)(self)
106+
}
107+
108+
/// 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)``.
109+
///
110+
/// - Returns: `false` since the node can not be cast to the node type from different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
111+
///
112+
/// - Note: This method overloads the general `is` method and is marked as deprecated to produce a warning,
113+
/// informing the user that the cast will always fail.
114+
@available(*, deprecated, message: "This cast will always fail")
115+
public func `is`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> Bool {
116+
return false
117+
}
118+
119+
/// Attempts to cast the current syntax node to a given node type from the different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
120+
///
121+
/// - Returns: `nil` since the node can not be cast to the node type from different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
122+
///
123+
/// - Note: This method overloads the general `as` method and is marked as deprecated to produce a warning,
124+
/// informing the user that the cast will always fail.
125+
@available(*, deprecated, message: "This cast will always fail")
126+
public func `as`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S? {
127+
return nil
128+
}
129+
130+
/// Force-casts the current syntax node to a given node type from the different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
131+
///
132+
/// - Returns: This method will always trigger a runtime crash and never return.
133+
///
134+
/// - Note: This method overloads the general `cast` method and is marked as deprecated to produce a warning,
135+
/// informing the user that the cast will always fail.
136+
/// - Warning: Invoking this method will lead to a fatal error.
137+
@available(*, deprecated, message: "This cast will always fail")
138+
public func cast<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S {
139+
fatalError("\(Self.self) cannot be cast to \(S.self)")
140+
}
141+
}
142+
"""#
143+
)
144+
34145
try! ExtensionDeclSyntax("public extension Syntax") {
35146
DeclSyntax(
36147
"""
@@ -171,30 +282,6 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
171282
ExprSyntax("self._syntaxNode = Syntax(data)")
172283
}
173284

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-
198285
DeclSyntax(
199286
"""
200287
/// Syntax nodes always conform to `\(node.kind.protocolType)`. This API is just
@@ -232,9 +319,17 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
232319
StmtSyntax("return .choices(\(choices))")
233320
}
234321
}
322+
323+
leafProtocolDecl(type: node.kind.leafProtocolType, inheritedType: node.kind.protocolType)
235324
}
236325

237-
try! ExtensionDeclSyntax("extension Syntax") {
326+
try! ExtensionDeclSyntax(
327+
"""
328+
// MARK: - Syntax
329+
330+
extension Syntax
331+
"""
332+
) {
238333
try VariableDeclSyntax("public static var structure: SyntaxNodeStructure") {
239334
let choices = ArrayExprSyntax {
240335
ArrayElementSyntax(
@@ -254,4 +349,54 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
254349
}
255350
}
256351

352+
leafProtocolDecl(type: "_LeafSyntaxNodeProtocol", inheritedType: "SyntaxProtocol")
353+
}
354+
355+
private func leafProtocolDecl(type: TypeSyntax, inheritedType: TypeSyntax) -> DeclSyntax {
356+
DeclSyntax(
357+
#"""
358+
/// Protocol that syntax nodes conform to if they don't have any semantic subtypes.
359+
/// These are syntax nodes that are not considered base nodes for other syntax types.
360+
///
361+
/// Syntax nodes conforming to this protocol have their inherited casting methods
362+
/// deprecated to prevent incorrect casting.
363+
public protocol \#(type): \#(inheritedType) {}
364+
365+
public extension \#(type) {
366+
/// Checks if the current leaf syntax node can be cast to a different specified type.
367+
///
368+
/// - Returns: `false` since the leaf node cannot be cast to a different specified type.
369+
///
370+
/// - Note: This method overloads the general `is` method and is marked as deprecated to produce a warning,
371+
/// informing the user that the cast will always fail.
372+
@available(*, deprecated, message: "This cast will always fail")
373+
func `is`<S: \#(inheritedType)>(_ syntaxType: S.Type) -> Bool {
374+
return false
375+
}
376+
377+
/// Attempts to cast the current leaf syntax node to a different specified type.
378+
///
379+
/// - Returns: `nil` since the leaf node cannot be cast to a different specified type.
380+
///
381+
/// - Note: This method overloads the general `as` method and is marked as deprecated to produce a warning,
382+
/// informing the user that the cast will always fail.
383+
@available(*, deprecated, message: "This cast will always fail")
384+
func `as`<S: \#(inheritedType)>(_ syntaxType: S.Type) -> S? {
385+
return nil
386+
}
387+
388+
/// Force-casts the current leaf syntax node to a different specified type.
389+
///
390+
/// - Returns: This method will always trigger a runtime crash and never return.
391+
///
392+
/// - Note: This method overloads the general `cast` method and is marked as deprecated to produce a warning,
393+
/// informing the user that the cast will always fail.
394+
/// - Warning: Invoking this method will lead to a fatal error.
395+
@available(*, deprecated, message: "This cast will always fail")
396+
func cast<S: \#(inheritedType)>(_ syntaxType: S.Type) -> S {
397+
fatalError("\(Self.self) cannot be cast to \(S.self)")
398+
}
399+
}
400+
"""#
401+
)
257402
}

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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@
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+
33+
- Same-Type Casts
34+
- Description: `is`, `as`, and `cast` overloads on `SyntaxProtocol` with same-type conversions are marked as deprecated. The deprecated methods emit a warning indicating the cast will always succeed.
35+
- Issue: https://github.com/apple/swift-syntax/issues/2092
36+
- Pull Request: https://github.com/apple/swift-syntax/pull/2108
37+
38+
- Base Node Casts
39+
- Description: `is`, `as`, and `cast` methods on base node protocols with base-type conversions are marked as deprecated. The deprecated methods emit a warning that informs the developer that the cast will always succeed and should be done using the base node's initializer.
40+
- Issue: https://github.com/apple/swift-syntax/issues/2092
41+
- Pull Request: https://github.com/apple/swift-syntax/pull/2108
42+
2843
## API-Incompatible Changes
2944

3045

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])

0 commit comments

Comments
 (0)