Skip to content

feat: Assist to replace generic with impl trait #14816

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 4 commits into from
May 26, 2023
Merged

feat: Assist to replace generic with impl trait #14816

merged 4 commits into from
May 26, 2023

Conversation

justahero
Copy link
Contributor

This adds a new assist named "Replace named generic with impl". It is the inverse operation to the existing "Replace impl trait with generic" assist.

It allows to refactor the following statement:

//      👇 cursor
fn new<T$0: ToString>(input: T) -> Self {}

to be transformed into:

fn new(input: impl ToString) -> Self {}
  • adds new helper function impl_trait_type to create AST node
  • add method to remove an existing generic param type from param list

Closes #14626

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label May 16, 2023
Copy link
Member

@Veykril Veykril left a comment

Choose a reason for hiding this comment

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

One thing we need to make sure is that the param is not used inside the function body or return type, as replacing it would make those uses invalid. You can do so by resolving the type_param to its hir definition via sema.to_def and then searching for usages on that via hir_def.usages()/ (You can also use that to collect the parameters then I believe)

justahero added 2 commits May 26, 2023 13:13
This adds a new assist named "replace named generic with impl" to move
the generic param type from the generic param list into the function
signature.

```rust
fn new<T: ToString>(input: T) -> Self {}
```

becomes

```rust
fn new(input: impl ToString) -> Self {}
```

The first step is to determine if the assist can be applied, there has
to be a match between generic trait param & function paramter types.

* replace function parameter type(s) with impl
* add new `impl_trait_type` function to generate the new trait bounds with `impl` keyword  for use in the
  function signature
This removes an existing generic param from the `GenericParamList`. It
also considers to remove the extra colon & whitespace to the previous
sibling.

* change order to get all param types first and mark them as mutable
  before the first edit happens
* add helper function to remove a generic parameter
* fix test output
justahero added 2 commits May 26, 2023 13:24
This checks the type param is referenced neither in the function body
nor as a return type.

* add tests
* pass in existing `Semantics` object to function
* pass in `Definition` for param type
* refactor iterator to use `flatten`
@Veykril Veykril changed the title Assist to replace generic with impl trait feat: Assist to replace generic with impl trait May 26, 2023
@Veykril
Copy link
Member

Veykril commented May 26, 2023

@bors r+

@bors
Copy link
Contributor

bors commented May 26, 2023

📌 Commit e78df83 has been approved by Veykril

It is now in the queue for this repository.

@bors
Copy link
Contributor

bors commented May 26, 2023

⌛ Testing commit e78df83 with merge e963846...

Copy link
Contributor

@lowr lowr left a comment

Choose a reason for hiding this comment

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

Sorry for the abrupt r-, current implementation may create dangling params unless I'm missing something.

@bors r-

Comment on lines +111 to +114
let search_ranges = vec![
fn_.body().map(|body| body.syntax().text_range()),
fn_.ret_type().map(|ret_type| ret_type.syntax().text_range()),
];
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's avoid allocation since this is always fixed length. Something like

Suggested change
let search_ranges = vec![
fn_.body().map(|body| body.syntax().text_range()),
fn_.ret_type().map(|ret_type| ret_type.syntax().text_range()),
];
let search_ranges = [
fn_.body().map(|body| body.syntax().text_range()),
fn_.ret_type().map(|ret_type| ret_type.syntax().text_range()),
];

Comment on lines +39 to +56
let params = fn_
.param_list()?
.params()
.filter_map(|param| {
// function parameter type needs to match generic type name
if let ast::Type::PathType(path_type) = param.ty()? {
let left = path_type.path()?.segment()?.name_ref()?.ident_token()?.to_string();
let right = type_param_name.to_string();
if left == right {
Some(param)
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
Copy link
Contributor

Choose a reason for hiding this comment

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

If I'm not mistaken this fails to consider params whose type contains P in their generic arguments, like:

fn foo<P: Trait>(
    _: P, // this will be replaced
    _: Option<P>, // this and following `P` would dangle after this assist
    _: impl Iterator<Item = P>,
    _: &dyn Iterator<Item = P>,
) {}

Either also collect them and replace the generic params in them, or we can just skip doing this (by not providing this assist if there are such params) for this initial implementation.

If I've missed things and they are actually supported, could you add some tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is a good catch, will add tests & try to replace them as well. If that takes too much time & effort I may skip the assist in these cases, because I'm still learning how to navigate all the constructs.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks! Take your time :)

I'll add one more thing just so that I don't forget, but don't take it too hard; it's rather hard to implement these perfectly correctly and we don't require assists to always produce compiling code.

  • Ideally we shouldn't provide the assist when P is used in the following positions because they'd produce invalid code:

    fn foo<P: Trait>(
        _: <P as Trait>::Assoc,          // within path type qualifier
        _: <() as OtherTrait<P>>::Assoc, // same as above
        _: P::Assoc,                     // associated type shorthand
        _: impl OtherTrait<P>            // generic arg in impl trait (note that associated type bindings are fine)
        _: &dyn Fn(P)                    // param type and/or return type for Fn* traits
    ) {}

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 for the list, this is really helpful. I hope to find a way to evaluate them so they are covered too.


acc.add(
AssistId("replace_named_generic_with_impl", AssistKind::RefactorRewrite),
"Replace named generic with impl",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"Replace named generic with impl",
"Replace named generic with impl trait",

@bors
Copy link
Contributor

bors commented May 26, 2023

☀️ Test successful - checks-actions
Approved by: Veykril
Pushing e963846 to master...

1 similar comment
@bors
Copy link
Contributor

bors commented May 26, 2023

☀️ Test successful - checks-actions
Approved by: Veykril
Pushing e963846 to master...

@bors
Copy link
Contributor

bors commented May 26, 2023

👀 Test was successful, but fast-forwarding failed: 422 Changes must be made through a pull request.

@bors bors merged commit e963846 into rust-lang:master May 26, 2023
@lowr
Copy link
Contributor

lowr commented May 26, 2023

Whoa bors doesn't see r- in review comments? Well, let's do this in a follow-up PR then!

@justahero Would you be willing to address my comments? I can follow up in case you're busy.

@Veykril
Copy link
Member

Veykril commented May 26, 2023

Right, I think bors does not scan review comments unfortunately (the old one we used did I think)

@lowr
Copy link
Contributor

lowr commented May 26, 2023

Heh, I see. At least I learned a new thing so I'm one step closer to bors master 🤦‍♂️

@lnicola
Copy link
Member

lnicola commented May 29, 2023

replace-generic.mp4

bors added a commit that referenced this pull request Jun 2, 2023
Fix Assist "replace named generic type with impl trait"

This is a follow-up PR to fix the assist "replace named generic type with impl trait" described in #14626 to filter invalid param types. It integrates the feedback given in PR #14816 .

The change updates the logic to determine when a function parameter is safe to replace a type param with its trait implementation. Some parameter definitions are invalid & should not be replaced by their traits, therefore skipping the assist completely.

First, all usages of the generic type under the cursor are determined. These usage references are checked to see if they occur outside the function parameter list. If an outside reference is found, e.g. in body, return type or where clause, the assist is skipped. All remaining usages need to appear only in the function param list. For each usage the param type is further inspected to see if it's valid. The logic to determine if a function parameter is valid, follows a heuristic and may not cover all possible parameter definitions.

With this change the following param types (as given in [this comment](#14816 (comment))) are not replaced & therefore skip the assist.

```rust
fn foo<P: Trait>(
    _: <P as Trait>::Assoc,          // within path type qualifier
    _: <() as OtherTrait<P>>::Assoc, // same as above
    _: P::Assoc,                     // associated type shorthand
    _: impl OtherTrait<P>            // generic arg in impl trait (note that associated type bindings are fine)
    _: &dyn Fn(P)                    // param type and/or return type for Fn* traits
) {}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

assist: replace generic with impl trait
6 participants