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

Add local refactoring code actions based on swift-syntax #1169

Closed
wants to merge 6 commits into from

Conversation

DougGregor
Copy link
Member

@DougGregor DougGregor commented Apr 5, 2024

Introduce new local refactoring code actions based on the Swift syntax tree, without going through SourceKit.

This change includes a number of new refactoring code actions. Most of them adapt the syntax refactorings from the SwiftRefactor module of swift-syntax:

  • Add digit separators to an integer literal, e.g., 100000 -> 1_000_000.
  • Remove digit separators from an integer literal, e.g., 1_000_000 -> 1000000. (from SwiftRefactor)
  • 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" }
}

There are also a few new refactoring code actions implemented here. These could be sunk down into SwiftRefactor to be made more widely available, or stay here if they're too IDE-centric to live in swift-refactor:

  • Convert an integer literal from one base to another (e.g., 172387 -> 0x2a163).
  • Convert JSON into a Codable struct.
  • Apply Demorgan's law, e.g., !(a || b) --> !a && !b.

There is a small amount of overlap between the refactoring code actions added here and those SourceKit provides. For example, SourceKit provides a "Simplify long number literal" operation that is similar to "Add digit separators". We have several directions we could go with those:

  1. Don't add the new refactoring code actions if they conflict. This would be sad.
  2. Filter out the SourceKit ones so the user doesn't see them. We can keep the list in SourceKit-LSP.
  3. Remove the redundant actions from SourceKit. Clients of SourceKit (not -LSP) would lose access to them.
  4. Replace the SourceKit implementation with the ones from SwiftRefactor to server SourceKit clients, and filter them out (like option 2) for LSP clients.

Most of the code here is actually from @CodaFi, and I've given it a light dusting to bring it up to modern times.

CodaFi and others added 3 commits April 5, 2024 12:24
…oringProviders

For swift-syntax SwiftRefactoringProviders that have no context information,
we only need to supply a title.
@DougGregor
Copy link
Member Author

@swift-ci please test

Copy link
Member

@CodaFi CodaFi left a comment

Choose a reason for hiding this comment

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

❤️

/// ## After
/// ```swift
///
/// ```
Copy link
Member

Choose a reason for hiding this comment

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

Heh the fact I didn't finish documenting this likely means it may not be ready for prime time. I dunno, tho, the implementation looks mostly fine.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah.... we should sink it down into SwiftRefactor and put some proper testing in there.

Comment on lines 65 to 71
switch unexpected {
case let .closure(closure):
closure.write(to: &text)
case let .tail(closure, unexpected):
closure.write(to: &text)
unexpected.write(to: &text)
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
switch unexpected {
case let .closure(closure):
closure.write(to: &text)
case let .tail(closure, unexpected):
closure.write(to: &text)
unexpected.write(to: &text)
}
switch unexpected {
case let .closure(closure):
closure.trimmed.write(to: &text)
case let .tail(closure, unexpected):
closure.trimmed.write(to: &text)
unexpected.write(to: &text)
}

Copy link
Member

Choose a reason for hiding this comment

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

Otherwise we'll pick up comments!

// Foo Bar Baz

{
"abc": "xyz"
}

@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test windows

@DougGregor
Copy link
Member Author

@swift-ci please test Windows

@fwcd
Copy link
Collaborator

fwcd commented Apr 6, 2024

Love the JSON-to-Codable conversion, that looks super useful!

@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

@swift-ci please test Windows

1 similar comment
@DougGregor
Copy link
Member Author

@swift-ci please test Windows

import SwiftRefactor
import SwiftSyntax

public protocol CodeActionProvider {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you add a doc comment describing what this type does?

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay!

Comment on lines +216 to +220
guard let firstValue = (value as! [Any]).first else {
return .array(.null)
}
let innerType = self.jsonType(of: firstValue, suggestion: name)
return .array(innerType)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, I think it’s unfortunate that we don’t allow merging the keys from multiple objects in the array. I don’t think it’s uncommon for JSON to omit keys for null values. We should be able to take the union of all the keys in the array. And I think it would also be good to infer optionality of a value eg. from

[
  {
    "value": 1
  },
  {
    "value": null
  }
]

var values: [Any] = self
for i in 0..<depth {
if i + 1 == depth {
return values[0] as? Dictionary<String, Any>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we know that self is not empty here?

}

extension Array where Element == Any {
fileprivate func unwrap(_ depth: Int) -> Dictionary<String, Any>? {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you add a comment of what unwrap does?

}

if !nestedTypes.isEmpty {
members.append("")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Having to append an empty string to members to (presumably) bet an empty line between the members seems wrong. I think we should add an empty line between all the members instead.

case null
case object(String)

var outermostObject: (Int, String)? {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of returning unwraps here and then calling unwrap with it, wouldn’t it be easier to just return the name and JSONType.object here?

@DougGregor
Copy link
Member Author

I'm tackling the first part of this, exposing swift-syntax-based actions, via the PR at #1179. It drops Demorgans and the JSON-to-structs refactoring actions and addresses most of the feedback here. (Not all of it, yet)

@DougGregor
Copy link
Member Author

Everything but DeMorgan's has merged via other pull requests, so I'm going to close this out. DeMorgan's needs a some work that I'm not up for just now.

@DougGregor DougGregor closed this May 7, 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.

None yet

4 participants