Skip to content

Introduce new refactoring code actions based on the Swift syntax tree. #1179

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

Conversation

DougGregor
Copy link
Member

This change includes a number of new refactoring code actions that build on the syntax refactorings for the SwiftRefactor module of swift-syntax:

  • Add digit separators to an integer literal, e.g., 1000000 -> 1_000_000.
  • Remove digit separators from an integer literal, e.g., 1_000_000 -> 1000000.
  • Format a raw string literal, e.g., "Hello \#(world)" -> ##"Hello\#(world)"##
  • Migrate to new if let syntax, e.g., if let x = x { ... } -> if let x { ... }
  • Replace opaque parameters with generic parameters, e.g., func f(p: some P) --> func f<T1: P>(p: T1).

This is generally easy to do, requiring one conformance to provide a name for the refactoring:

extension AddSeparatorsToIntegerLiteral: SyntaxRefactoringCodeActionProvider {
  public static var title: String { "Add digit separators" }
}

This change includes a number of new refactoring code actions that
build on the syntax refactorings for the SwiftRefactor module of swift-syntax:

  * Add digit separators to an integer literal, e.g., `1000000` ->
    `1_000_000`.
  * Remove digit separators from an integer literal, e.g., 1_000_000 ->
    1000000.
  * Format a raw string literal, e.g., `"Hello \#(world)"` ->
    `##"Hello\#(world)"##`
  * Migrate to new if let syntax, e.g., `if let x = x { ... }` ->
    `if let x { ... }`
  * Replace opaque parameters with generic parameters, e.g.,
    `func f(p: some P)` --> `func f<T1: P>(p: T1)`.

This is generally easy to do, requiring one conformance to provide a name for the refactoring:

    extension AddSeparatorsToIntegerLiteral: SyntaxRefactoringCodeActionProvider {
      public static var title: String { "Add digit separators" }
    }
@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor DougGregor merged commit e13ce13 into swiftlang:main Apr 17, 2024
3 checks passed
@DougGregor DougGregor deleted the syntax-code-action-refactorings branch April 17, 2024 18:01
@DougGregor
Copy link
Member Author

Speculatively merging, and can address comments in subsequent PRs

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.

Thanks for bridging this gap, Doug! Great to see these refactorings in sourcekit-lsp. I think a few tests to check that the refactorings actually do show up would be good though.


/// Syntactic code action provider to convert integer literals between
/// different bases.
struct ConvertIntegerLiteral: SyntaxCodeActionProvider {
Copy link
Member

Choose a reason for hiding this comment

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

Could you add a test case for this, checking that the refactoring do actually show up when retrieving code actions? Ie. a test in https://github.com/apple/sourcekit-lsp/blob/main/Tests/SourceKitLSPTests/CodeActionTests.swift

Comment on lines +724 to +726
func retrieveCodeActions(_ req: CodeActionRequest, providers: [CodeActionProvider]) async throws
-> [CodeAction]
{
Copy link
Member

Choose a reason for hiding this comment

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

I’d prefer to format this as

  func retrieveCodeActions(
    _ req: CodeActionRequest, 
    providers: [CodeActionProvider]
  ) async throws -> [CodeAction]
  {

Comment on lines +1121 to +1122
let lowerBound = self.position(of: node.position)
let upperBound = self.position(of: node.endPosition)
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 you should pass callerFile and callerLine into position(of:) so that the logger fault message contains the file and line number that is the real originator if position conversion fails.

Comment on lines +54 to +57
let left = file.token(at: start)
let right = file.token(at: end)
let leftOff = left?.position ?? AbsolutePosition(utf8Offset: 0)
let rightOff = right?.endPosition ?? leftOff
Copy link
Member

Choose a reason for hiding this comment

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

I’d prefer to fail the creation of SyntaxCodeActionScope and emit a logger.fault if we can’t find a token at a specific position. It’s the kind of thing where things start failing and then it’s hard to track down what’s going wrong if there’s no logging.

static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] {
guard
let token = scope.firstToken,
let node = token.parent?.as(Input.self)
Copy link
Member

Choose a reason for hiding this comment

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

Should we traverse the parents until we find a node of type Input? That way you don’t need to start eg. a MigrateToNewIfLetSyntax refactoring on the if token but you could also start it from within the condition or the body (not sure if we want the latter though, maybe we should stop the traversal at some pre-defined nodes like CodeBlockItemListSyntax and MemberBlockItemListSyntax to prevent that).

Similarly, the OpaqueParameterToGeneric refactoring action can’t currently be invoked from one of the some types, which might be confusing.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm wondering if this is something that should be handled by the MigrateToNewIfLetSyntax, OpaqueParameterToGeneric, etc., refactorings themselves, because we don't necessarily know how far out in the tree we should go.

Copy link
Member

Choose a reason for hiding this comment

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

That seems reasonable.

ahoppen added a commit to ahoppen/sourcekit-lsp that referenced this pull request May 7, 2024
Addresses a few minor comments and the following major ones:
- Add test cases for the syntax refactorings
- Don’t report code actions for refactorings that don’t actually modify the source
- Instead of just looking at the parent of the token of the selected range, walk up the syntax tree to find the syntax node to refactor. This makes the refactorings available in a lot more locations.
ahoppen added a commit to ahoppen/sourcekit-lsp that referenced this pull request May 8, 2024
Addresses a few minor comments and the following major ones:
- Add test cases for the syntax refactorings
- Don’t report code actions for refactorings that don’t actually modify the source
- Instead of just looking at the parent of the token of the selected range, walk up the syntax tree to find the syntax node to refactor. This makes the refactorings available in a lot more locations.
ahoppen added a commit that referenced this pull request May 9, 2024
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.

2 participants