Skip to content
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

Merged
merged 3 commits into from
Sep 6, 2023

Conversation

Matejkob
Copy link
Contributor

@Matejkob Matejkob commented Aug 26, 2023

To-Do after concept approval:

  • Fix all warnings related to API changes across the entire repo
  • Add documentation for Code Generation and the new API

I'm opening this PR as a draft to initiate a discussion about the proposed changes.

In issue #2092, @ahoppen mentioned:

What we should do instead, is to only have a as(_: ExprSyntaxProtocol.Type) function on ExprSyntax (analogous for StmtSyntax and the other base nodes).

The as function on SyntaxProtocol can then be marked as deprecated and produce a warning that the cast will always fail.

Best to my understanding, we cannot remove the as method from SyntaxProtocol. This function is necessary due to entities like Syntax or ExprSyntax. Without these entities, we could use the is/as Swift keywords. To illustrate, consider the example below:

func visit(node: some SyntaxProtocol) {
  if node is ClassExprSyntax { ... }
}

The following argument can be passed to this function:

let node = Syntax(ClassExprSyntax("class Foo {}"))

visit(node)

In this scenario, using the is Swift keyword won't correctly identify that the argument is actually of type ClassExprSyntax.

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.

Copy link
Member

@ahoppen ahoppen left a 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?

Comment on lines 210 to 224

@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)!
}
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 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)!
  }
}

Copy link
Contributor Author

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!

Copy link
Contributor

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.

Copy link
Contributor Author

@Matejkob Matejkob Aug 31, 2023

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? 😅

Copy link
Contributor Author

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 🙈

Copy link
Contributor

@bnbarham bnbarham Aug 31, 2023

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Introduced 🎸

Copy link
Member

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
  }

Copy link
Contributor Author

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

@Matejkob Matejkob changed the title [POC] Improve nodes downcasting API Introduce leaf protocols to prevent leaf nodes from being casted Sep 1, 2023
@Matejkob
Copy link
Contributor Author

Matejkob commented Sep 1, 2023

Out of curiosity: How many warnings does this create in swift-syntax?

With changes to also choice nodes something around 25 warnings. Speaking about choice nodes I'll introduce changes to those nodes in separate PR.

@Matejkob Matejkob marked this pull request as ready for review September 1, 2023 17:56
Copy link
Member

@ahoppen ahoppen left a 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"
Copy link
Member

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.

Suggested change
return "Leaf\(syntaxType)NodeProtocol"
return "_Leaf\(syntaxType)NodeProtocol"

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

@@ -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.
Copy link
Member

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.

Suggested change
- 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, done

Comment on lines 210 to 224

@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)!
}
Copy link
Member

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"),
Copy link
Member

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
  }

Copy link
Contributor Author

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?

Copy link
Member

@ahoppen ahoppen Sep 1, 2023

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.

Copy link
Contributor Author

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

@Matejkob
Copy link
Contributor Author

Matejkob commented Sep 3, 2023

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?

Copy link
Member

@ahoppen ahoppen left a 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.

Comment on lines +22 to +42
- 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

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

@ahoppen
Copy link
Member

ahoppen commented Sep 5, 2023

@swift-ci Please test

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.
@ahoppen
Copy link
Member

ahoppen commented Sep 6, 2023

@swift-ci Please test

@ahoppen
Copy link
Member

ahoppen commented Sep 6, 2023

@swift-ci Please test Windows

@ahoppen ahoppen merged commit a3ef07d into swiftlang:main Sep 6, 2023
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants