Skip to content

Introduce leaf protocols to prevent leaf nodes from being casted #2108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,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,117 @@ 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 current syntax node can be cast to a given specialized syntax type.
///
/// - Returns: `true` if the node can be cast, `false` otherwise.
public func `is`<S: \#(node.kind.protocolType)>(_ 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.
public func `as`<S: \#(node.kind.protocolType)>(_ syntaxType: S.Type) -> S? {
return S.init(self)
}

/// Force-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.
public func cast<S: \#(node.kind.protocolType)>(_ syntaxType: S.Type) -> S {
return self.as(S.self)!
}

/// Checks if the current syntax node can be upcast to its base node type (``\#(node.kind.syntaxType)``).
///
/// - Returns: `true` since the node can always be upcast to its base node.
///
/// - Note: This method overloads the general `is` method and is marked deprecated to produce a warning
/// informing the user that the upcast will always succeed.
@available(*, deprecated, message: "This cast will always succeed")
public func `is`(_ syntaxType: \#(node.kind.syntaxType).Type) -> Bool {
return true
}

/// Attempts to upcast the current syntax node to its base node type (``\#(node.kind.syntaxType)``).
///
/// - Returns: The base node created from the current syntax node, as the node can always be upcast to its base type.
///
/// - Note: This method overloads the general `as` method and is marked deprecated to produce a warning
/// informing the user the upcast should be performed using the target base node's initializer.
@available(*, deprecated, message: "Use `\#(node.kind.syntaxType).init` for upcasting")
public func `as`(_ syntaxType: \#(node.kind.syntaxType).Type) -> \#(node.kind.syntaxType)? {
return \#(node.kind.syntaxType)(self)
}

/// Force-upcast the current syntax node to its base node type (``\#(node.kind.syntaxType)``).
///
/// - Returns: The base node created from the current syntax node, as the node can always be upcast to its base type.
///
/// - Note: This method overloads the general `as` method and is marked deprecated to produce a warning
/// informing the user the upcast should be performed using the target base node's initializer.
@available(*, deprecated, message: "Use `\#(node.kind.syntaxType).init` for upcasting")
public func cast(_ syntaxType: \#(node.kind.syntaxType).Type) -> \#(node.kind.syntaxType) {
return \#(node.kind.syntaxType)(self)
}

/// 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)``.
///
/// - Returns: `false` since the node can not be cast to the node type from different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
///
/// - Note: This method overloads the general `is` method and is marked as deprecated to produce a warning,
/// informing the user that the cast will always fail.
@available(*, deprecated, message: "This cast will always fail")
public func `is`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> Bool {
return false
}

/// Attempts to cast the current syntax node to a given node type from the different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
///
/// - Returns: `nil` since the node can not be cast to the node type from different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
///
/// - Note: This method overloads the general `as` method and is marked as deprecated to produce a warning,
/// informing the user that the cast will always fail.
@available(*, deprecated, message: "This cast will always fail")
public func `as`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S? {
return nil
}

/// Force-casts the current syntax node to a given node type from the different base node protocol hierarchy than ``\#(node.kind.protocolType)``.
///
/// - Returns: This method will always trigger a runtime crash and never return.
///
/// - Note: This method overloads the general `cast` method and is marked as deprecated to produce a warning,
/// informing the user that the cast will always fail.
/// - Warning: Invoking this method will lead to a fatal error.
@available(*, deprecated, message: "This cast will always fail")
public func cast<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S {
fatalError("\(Self.self) cannot be cast to \(S.self)")
}
}
"""#
)

try! ExtensionDeclSyntax("public extension Syntax") {
DeclSyntax(
"""
Expand Down Expand Up @@ -171,30 +282,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 +319,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 +349,54 @@ 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) {
/// Checks if the current leaf syntax node can be cast to a different specified type.
///
/// - Returns: `false` since the leaf node cannot be cast to a different specified type.
///
/// - Note: This method overloads the general `is` method and is marked as deprecated to produce a warning,
/// informing the user that the cast will always fail.
@available(*, deprecated, message: "This cast will always fail")
func `is`<S: \#(inheritedType)>(_ syntaxType: S.Type) -> Bool {
return false
}

/// Attempts to cast the current leaf syntax node to a different specified type.
///
/// - Returns: `nil` since the leaf node cannot be cast to a different specified type.
///
/// - Note: This method overloads the general `as` method and is marked as deprecated to produce a warning,
/// informing the user that the cast will always fail.
@available(*, deprecated, message: "This cast will always fail")
func `as`<S: \#(inheritedType)>(_ syntaxType: S.Type) -> S? {
return nil
}

/// Force-casts the current leaf syntax node to a different specified type.
///
/// - Returns: This method will always trigger a runtime crash and never return.
///
/// - Note: This method overloads the general `cast` method and is marked as deprecated to produce a warning,
/// informing the user that the cast will always fail.
/// - Warning: Invoking this method will lead to a fatal error.
@available(*, deprecated, message: "This cast will always fail")
func cast<S: \#(inheritedType)>(_ syntaxType: S.Type) -> S {
fatalError("\(Self.self) cannot be cast to \(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, SyntaxHashable, \(node.base.leafProtocolType)
"""
) {
for child in node.children {
Expand Down
15 changes: 15 additions & 0 deletions Release Notes/510.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@

## Deprecations

- Leaf Node Casts
- 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.
- Issue: https://github.com/apple/swift-syntax/issues/2092
- Pull Request: https://github.com/apple/swift-syntax/pull/2108

- Same-Type Casts
- 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.
- Issue: https://github.com/apple/swift-syntax/issues/2092
- Pull Request: https://github.com/apple/swift-syntax/pull/2108

- Base Node Casts
- 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.
- Issue: https://github.com/apple/swift-syntax/issues/2092
- Pull Request: https://github.com/apple/swift-syntax/pull/2108

Comment on lines +28 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should go in the Deprecations sections since the runtime behavior of the API doesn’t change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

## API-Incompatible Changes


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
return .skipChildren
}
if let unexpected = node.unexpectedBetweenDeinitKeywordAndEffectSpecifiers,
let name = unexpected.presentTokens(satisfying: { $0.tokenKind.isIdentifier == true }).only?.as(TokenSyntax.self)
let name = unexpected.presentTokens(satisfying: { $0.tokenKind.isIdentifier == true }).only
{
addDiagnostic(
name,
Expand Down 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 @@ -2024,8 +2023,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
Loading