Skip to content

Commit

Permalink
Introduce leaf protocols to prevent leaf nodes from being casted
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Matejkob committed Sep 1, 2023
1 parent acf23c8 commit b7ce72f
Show file tree
Hide file tree
Showing 22 changed files with 880 additions and 372 deletions.
13 changes: 13 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,19 @@ public enum SyntaxNodeKind: String, CaseIterable {
}
}

/// For base node types, generates the name of the protocol to which all
/// concrete leaf nodes that derive from this base kind should conform.
///
/// - Warning: This property can only be accessed for base node kinds; attempting to
/// access it for a non-base kind will result in a runtime error.
public var leafProtocolType: TypeSyntax {
if isBase {
return "Leaf\(syntaxType)NodeProtocol"
} else {
fatalError("Only base kind can define leaf protocol")
}
}

/// If the syntax kind has been renamed, the previous raw value that is now
/// deprecated.
public var deprecatedRawValue: String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,68 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
"""
)

DeclSyntax(
"""
/// Extension of ``\(node.kind.protocolType)`` to provide casting methods.
///
/// These methods enable casting between syntax node types within the same
/// base node protocol hierarchy (e.g., ``DeclSyntaxProtocol``).
///
/// While ``SyntaxProtocol`` offers general casting methods (``SyntaxProtocol.as(_:)``,
/// ``SyntaxProtocol.is(_:)``, and ``SyntaxProtocol.cast(_:)``), these often aren't
/// appropriate for use on types conforming to a specific base node protocol
/// like ``\(node.kind.protocolType)``. That's because at this level,
/// we know that the cast to another base node type (e.g., ``DeclSyntaxProtocol``
/// when working with ``ExprSyntaxProtocol``) is guaranteed to fail.
///
/// To guide developers toward correct usage, this extension provides overloads
/// of these casting methods that are restricted to the same base node type.
/// Furthermore, it marks the inherited casting methods from ``SyntaxProtocol`` as
/// deprecated, indicating that they will always fail when used in this context.
extension \(node.kind.protocolType) {
/// Checks if the syntax node can be cast to the specified type.
///
/// - Returns: A boolean indicating whether the cast would succeed.
public func `is`<S: \(node.kind.protocolType)>(_ syntaxType: S.Type) -> Bool {
return self.as(syntaxType) != nil
}
/// Attempts to cast the syntax node to the specified type.
///
/// - Returns: An optional containing the casted syntax node, or `nil` if the cast fails.
public func `as`<S: \(node.kind.protocolType)>(_ syntaxType: S.Type) -> S? {
return S.init(self)
}
/// Force-casts the syntax node to the specified type.
///
/// - Returns: The casted syntax node.
/// - Warning: This will crash if the cast fails. Use with caution.
public func cast<S: \(node.kind.protocolType)>(_ syntaxType: S.Type) -> S {
return self.as(S.self)!
}
/// Deprecated cast check that's inherited from ``SyntaxProtocol``.
@available(*, deprecated, message: "This cast will always fail")
public func `is`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> Bool {
return self.as(syntaxType) != nil
}
/// Deprecated casting method that's inherited from ``SyntaxProtocol``.
@available(*, deprecated, message: "This cast will always fail")
public func `as`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S? {
return S.init(self)
}
/// Deprecated force-cast method that's inherited from ``SyntaxProtocol``.
@available(*, deprecated, message: "This cast will always fail")
public func cast<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S {
return self.as(S.self)!
}
}
"""
)

try! ExtensionDeclSyntax("public extension Syntax") {
DeclSyntax(
"""
Expand Down Expand Up @@ -171,30 +233,6 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
ExprSyntax("self._syntaxNode = Syntax(data)")
}

DeclSyntax(
"""
public func `is`<S: \(node.kind.protocolType)>(_ syntaxType: S.Type) -> Bool {
return self.as(syntaxType) != nil
}
"""
)

DeclSyntax(
"""
public func `as`<S: \(node.kind.protocolType)>(_ syntaxType: S.Type) -> S? {
return S.init(self)
}
"""
)

DeclSyntax(
"""
public func cast<S: \(node.kind.protocolType)>(_ syntaxType: S.Type) -> S {
return self.as(S.self)!
}
"""
)

DeclSyntax(
"""
/// Syntax nodes always conform to `\(node.kind.protocolType)`. This API is just
Expand Down Expand Up @@ -232,9 +270,17 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
StmtSyntax("return .choices(\(choices))")
}
}

leafProtocolDecl(type: node.kind.leafProtocolType, inheritedType: node.kind.protocolType)
}

try! ExtensionDeclSyntax("extension Syntax") {
try! ExtensionDeclSyntax(
"""
// MARK: - Syntax
extension Syntax
"""
) {
try VariableDeclSyntax("public static var structure: SyntaxNodeStructure") {
let choices = ArrayExprSyntax {
ArrayElementSyntax(
Expand All @@ -254,4 +300,38 @@ let syntaxBaseNodesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
}
}

leafProtocolDecl(type: "LeafSyntaxNodeProtocol", inheritedType: "SyntaxProtocol")
}

private func leafProtocolDecl(type: TypeSyntax, inheritedType: TypeSyntax) -> DeclSyntax {
DeclSyntax(
"""
/// Protocol that syntax nodes conform to if they don't have any semantic subtypes.
/// These are syntax nodes that are not considered base nodes for other syntax types.
///
/// Syntax nodes conforming to this protocol have their inherited casting methods
/// deprecated to prevent incorrect casting.
public protocol \(type): \(inheritedType) {}
public extension \(type) {
/// Deprecated cast check that's inherited from ``\(inheritedType)``.
@available(*, deprecated, message: "This cast will always fail")
func `is`<S: \(inheritedType)>(_ syntaxType: S.Type) -> Bool {
return self.as(syntaxType) != nil
}
/// Deprecated casting method that's inherited from ``\(inheritedType)``.
@available(*, deprecated, message: "This cast will always fail")
func `as`<S: \(inheritedType)>(_ syntaxType: S.Type) -> S? {
return S.init(self)
}
/// Deprecated force-cast method that's inherited from ``\(inheritedType)``.
@available(*, deprecated, message: "This cast will always fail")
func cast<S: \(inheritedType)>(_ syntaxType: S.Type) -> S {
return self.as(S.self)!
}
}
"""
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func syntaxNode(nodesStartingWith: [Character]) -> SourceFileSyntax {
\(documentation)
\(node.node.apiAttributes())\
public struct \(node.kind.syntaxType): \(node.baseType.syntaxBaseName)Protocol, SyntaxHashable
public struct \(node.kind.syntaxType): \(node.baseType.syntaxBaseName)Protocol, \(node.base.leafProtocolType), SyntaxHashable
"""
) {
for child in node.children {
Expand Down
5 changes: 5 additions & 0 deletions Release Notes/510.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@

## API Behavior Changes

- Leaf Node Protocols
- Description: Syntax nodes that do not act as base nodes for other syntax types now conform to a new protocols that deprecates inherited casting methods (e.g. `AccessorBlockSyntax` to `LeafSyntaxNodeProtocol` or `AccessorDeclSyntax` to `LeafDeclSyntaxNodeProtocol`). This change aims to prevent unsafe type-casting by issuing deprecation warnings for methods that will result in failed casts.
- Issue: https://github.com/apple/swift-syntax/issues/2092
- Pull Request: https://github.com/apple/swift-syntax/pull/2108

## Deprecations

## API-Incompatible Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1146,8 +1146,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
return .skipChildren
}

if node.conditions.count == 1,
node.conditions.first?.as(ConditionElementSyntax.self)?.condition.is(MissingExprSyntax.self) == true,
if node.conditions.only?.condition.is(MissingExprSyntax.self) == true,
!node.body.leftBrace.isMissingAllTokens
{
addDiagnostic(node.conditions, MissingConditionInStatement(node: node), handledNodes: [node.conditions.id])
Expand Down Expand Up @@ -2009,8 +2008,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
return .skipChildren
}

if node.conditions.count == 1,
node.conditions.first?.as(ConditionElementSyntax.self)?.condition.is(MissingExprSyntax.self) == true,
if node.conditions.only?.condition.is(MissingExprSyntax.self) == true,
!node.body.leftBrace.isMissingAllTokens
{
addDiagnostic(node.conditions, MissingConditionInStatement(node: node), handledNodes: [node.conditions.id])
Expand Down
15 changes: 13 additions & 2 deletions Sources/SwiftSyntax/Syntax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,23 +196,34 @@ extension SyntaxProtocol {

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

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

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

/// Casts the current syntax node to a given specialized syntax type.
///
/// - Returns: An instance of the specialized type.
/// - Warning: This function will crash if the cast is not possible. Use `as` to safely attempt a cast.
func cast<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S {
return self.as(S.self)!
}
Expand Down
Loading

0 comments on commit b7ce72f

Please sign in to comment.