-
Notifications
You must be signed in to change notification settings - Fork 404
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
Introduce leaf protocols to prevent leaf nodes from being casted #2108
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach looks good to me. I have one suggestion of how to avoid all the generated code.
Out of curiosity: How many warnings does this create in swift-syntax?
|
||
@available(*, deprecated, message: "This cast will always fail") | ||
public func `is`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> Bool { | ||
return self.as(syntaxType) != nil | ||
} | ||
|
||
@available(*, deprecated, message: "This cast will always fail") | ||
public func `as`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S? { | ||
return S.init(self) | ||
} | ||
|
||
@available(*, deprecated, message: "This cast will always fail") | ||
public func cast<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S { | ||
return self.as(S.self)! | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should be able to avoid generating all these methods by creating the following protocol and then conforming all the non-base nodes to it.
/// Protocol that syntax nodes conform to which don’t have any semantic subtypes, i.e. syntax nodes that aren't base nodes.
public protocol LeafSyntaxNodeProtocol: SyntaxProtocol {}
public extension LeafSyntaxNodeProtocol {
@available(*, deprecated, message: "This cast will always fail")
public func `is`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> Bool {
return self.as(syntaxType) != nil
}
@available(*, deprecated, message: "This cast will always fail")
public func `as`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S? {
return S.init(self)
}
@available(*, deprecated, message: "This cast will always fail")
public func cast<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S {
return self.as(S.self)!
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, I believe we should be able to do that. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cast will always fail
Isn't necessarily true right 😅? They could be casting to the same type. I suspect that's usually not the case though, so probably makes sense still.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In our codebase, there are a few instances where we're casting leaf nodes to their own types. I've assumed this is redundant and unnecessary. Have I overlooked something? 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I've just realized you probably meant the content of the message, not the mechanism. 😄 I need coffee 🙈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, to be clear I was referring to deprecation message "This cast will always fail". When we're casting to the same type it won't fail, it's just pointless. The message is fine, I was just being pedantic :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Introduced 🎸
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good comment, @bnbarham.
I think we should introduce the as
/is
/cast
methods as follows on SyntaxProtocol
that will catch same-type conversions an emit a warning the the cast will always succeed, rather than fail.
@available(*, deprecated, message: "This cast will always succeed")
func `as`(_ syntaxType: Self.Type) -> Self? {
return self
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really good idea! Implemented in 9986d57
b33c407
to
b7ce72f
Compare
With changes to also choice nodes something around 25 warnings. Speaking about choice nodes I'll introduce changes to those nodes in separate PR. |
b7ce72f
to
1e8a59c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. Looks very good to me. Just a couple comments.
/// access it for a non-base kind will result in a runtime error. | ||
public var leafProtocolType: TypeSyntax { | ||
if isBase { | ||
return "Leaf\(syntaxType)NodeProtocol" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I should have noted this earlier. Since this protocol needs to be public so that that methods defined within it are visible publicly but since we don’t want to offer any guarantees about supporting it in the future, we should prefix it with an underscore.
return "Leaf\(syntaxType)NodeProtocol" | |
return "_Leaf\(syntaxType)NodeProtocol" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Release Notes/510.md
Outdated
@@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would try to avoid the introduction of the protocols since it’s just a technical detail.
- 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. | |
- 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, done
|
||
@available(*, deprecated, message: "This cast will always fail") | ||
public func `is`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> Bool { | ||
return self.as(syntaxType) != nil | ||
} | ||
|
||
@available(*, deprecated, message: "This cast will always fail") | ||
public func `as`<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S? { | ||
return S.init(self) | ||
} | ||
|
||
@available(*, deprecated, message: "This cast will always fail") | ||
public func cast<S: SyntaxProtocol>(_ syntaxType: S.Type) -> S { | ||
return self.as(S.self)! | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good comment, @bnbarham.
I think we should introduce the as
/is
/cast
methods as follows on SyntaxProtocol
that will catch same-type conversions an emit a warning the the cast will always succeed, rather than fail.
@available(*, deprecated, message: "This cast will always succeed")
func `as`(_ syntaxType: Self.Type) -> Self? {
return self
}
#line: (BreakStmtSyntax().as(StmtSyntax.self)!, "break"), | ||
#line: (StmtSyntax(BreakStmtSyntax()), "break"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, we should support BreakStmtSyntax().as(StmtSyntax.self)
. It definitely shouldn’t emit a warning that the cast will always fail because that’s not true, it will always succeed.
I think in LeafDeclSyntaxNodeProtocol
, we need the following (same for is
and cast
).
func `as`(_ syntaxType: DeclSyntax.Type) -> Self? {
return self
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been thinking more about producing a warning when the user is trying to case to the base node with a message like:
`as` method should be used to downcasting for upcasting to base node use `BaseNode.init`
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about making it slightly more action-focused?
Use `BaseNode.init` for upcasting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added such a feature for all base nodes and Syntax
node here: 9986d57
1e8a59c
to
9986d57
Compare
In a separate PR, I'm going to address casting issues related to syntax traits and syntax choice nodes. I'm thinking about changing the approach here a bit: Rather than scattering bits of casting logic across multiple files, it might be more efficient to consolidate all the casting functionality for each node and case into a single file. This could make it easier to grasp the overall logic and any edge cases. How does that sound? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One minor comment on the release notes, otherwise LGTM.
I'm thinking about changing the approach here a bit: Rather than scattering bits of casting logic across multiple files, it might be more efficient to consolidate all the casting functionality for each node and case into a single file. This could make it easier to grasp the overall logic and any edge cases.
How does that sound?
I think that could be a good idea. Let’s do it in a follow-up PR. That way we can get this behavior change merged and the discussion on the follow-up PR can be purely around the implementation structuring.
- 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 | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@swift-ci Please test |
9986d57
to
b757ea2
Compare
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.
- Implement overloads for `is`, `as`, and `cast` methods in SyntaxProtocol to handle same-type conversions. - Mark these overloaded methods as deprecated with a warning message that the cast will always succeed. - Update the codebase to eliminate a redundant casts.
b757ea2
to
d2945f8
Compare
@swift-ci Please test |
@swift-ci Please test Windows |
To-Do after concept approval:
I'm opening this PR as a draft to initiate a discussion about the proposed changes.
In issue #2092, @ahoppen mentioned:
Best to my understanding, we cannot remove the
as
method fromSyntaxProtocol
. This function is necessary due to entities likeSyntax
orExprSyntax
. Without these entities, we could use theis
/as
Swift keywords. To illustrate, consider the example below:The following argument can be passed to this function:
In this scenario, using the
is
Swift keyword won't correctly identify that the argument is actually of typeClassExprSyntax
.Consequently, I've tried to enhance the downcasting API in a slightly more intricate manner by adding casting methods for lower types.
If any decisions are unclear, please let me know. I'm also eager to hear your general feedback!
This PR resolves part of #2092.