-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
fix: Support generics in extract_function assist #12556
fix: Support generics in extract_function assist #12556
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.
I'd like to argue that this simple change is not actually an improvement over the status quo. If we are in a generic function and we extract something that does not rely on the generics at all we now copy all the generics and bounds needlessly, requiring the user to remove things after invoking the assist which is usually a pretty bad user experience (I'd rather have to manually add more things, than having to remove unnecessary things, as the IDE might have more tools that make the addition of whats missing still simpler).
Fixing this is probably non trivial, we'd basically have to check several things, are the types of the parameters actually using the generics, and is the extracted snippet using them. Part of that can be done syntactically probably, though I am unsure whether a pure syntax apparoch would work here (it should get us far enough at least).
Yeah, after reflecting on this, I'm inclined to agree with you that as-is this change doesn't represent an unambiguous improvement. I've started trying to filter the generic param list by usages, first by inspecting the types of the params. I haven't started trying to filter the where clauses, as that will probably be even more challenging. Definitely possible I'm out of my depth here, but this is a fun learning project for me. No worries if we don't get to a point where this is shippable, but I'm also very open to advice/direction as to how to move further in that direction. |
f36d076
to
1237f53
Compare
Update: I've made some further progress here. Both the generic param list and the where clause are now being filtered to only include items that match type params that appear in the new function's params list or body. Caveat: For the moment, I'm only matching type params, not constants or lifetimes. |
used_type_params: &[TypeParam], | ||
) -> bool { | ||
match param { | ||
ast::GenericParam::ConstParam(_) | ast::GenericParam::LifetimeParam(_) => true, |
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.
@Veykril Interested in your thoughts here. I could imagine three potential approaches:
- Always copy
ConstParam
andLifetimeParam
(as implemented here) - Never copy
ConstParam
orLifetimeParam
(essentially a more constrained version of the status quo before this change) - "Do the right thing" ie filter out unused
ConstParam
andLifetimeParam
, only include the ones referenced within the function
I feel like "Always Copy" is probably the wrong approach, for basically the same reasons you gave on my initial change.
Never copying these params would be trivial to implement, but potentially frustrating for users.
In order to "do the right thing", I'll need to do a similar kind of filtration as I've already done for TypeParam
s, for both consts and lifetimes. I think lifetimes should be relatively straightforward, but I'm not exactly sure how I'd go about this for const
params. For instance, unclear to me when/if they should be passed down as params, or via the turbo fish at the call site.
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 I'm currently leaning towards the "never copy" approach, ie changing the true
above to a false
, and then handling lifetime and const params in separate future changes.
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.
Especially since generic lifetime param support would be limited in usefulness until #9945 is fixed
1237f53
to
2e33349
Compare
8af51b5
to
14b3a23
Compare
I've updated this change to account for nested generics. Instead of only looking for generics and where clauses on the immediate parent, |
da4aec8
to
56b8e89
Compare
☔ The latest upstream changes (presumably #12676) made this pull request unmergeable. Please resolve the merge conflicts. |
56b8e89
to
5f94f60
Compare
☔ The latest upstream changes (presumably #12706) made this pull request unmergeable. Please resolve the merge conflicts. |
5f94f60
to
24a4b34
Compare
@Veykril I think this is ready for review again. Is there anything else you're hoping to see here? |
24a4b34
to
139b02b
Compare
fn parent_where_clauses(parent: &SyntaxNode) -> Vec<ast::WhereClause> { | ||
let mut where_clause: Vec<ast::WhereClause> = parent | ||
.ancestors() | ||
.filter_map(ast::AnyHasGenericParams::cast) | ||
.filter_map(|it| it.where_clause()) | ||
.collect(); | ||
where_clause.reverse(); | ||
where_clause | ||
} | ||
|
||
fn parent_generic_param_lists(parent: &SyntaxNode) -> Vec<ast::GenericParamList> { | ||
let mut generic_param_list: Vec<ast::GenericParamList> = parent | ||
.ancestors() | ||
.filter_map(ast::AnyHasGenericParams::cast) | ||
.filter_map(|it| it.generic_param_list()) | ||
.collect(); | ||
generic_param_list.reverse(); | ||
generic_param_list | ||
} |
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.
These are a bit too "weakly typed". There are only really up to two things that can offer use generics, the function this is an expression of (or const/static but those don't currently have generics) and then a trait/impl that contains that. So we should check for those two only.
So parent.ancestors().find_map(ast::Item::cast)
, then check that this is either an ast::Fn
, ast::Static
or ast::Const
since those are the only valid ones (nothing else should contain expressions atm).
Then with that item at hand we can do item.syntax().parent().and_then(ast::Item::cast)
and check if thats an ast::Impl
or ast::Trait
.
With those two things we can then fetch the generics and where clauses instead. Thats a bit nicer and that way we also dont grab unrelated generics and where clauses
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.
Okay, I'll try that out! It wasn't clear to me how specific or general I should be when looking up through the ancestors, I'll try to be more specific in the future.
Can you think up an example of an "unrelated" generic that this approach would capture? If so, I'll add it as a unit test.
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.
fn foo<T>() {
fn bar<T>() {
$0
}
}
It would probably be fine either way, but its better to be a bit more precise here.
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.
Ah, I see! I had been thinking that was a case we wanted to catch, but I just tried it out locally and learned about a new compiler error: https://doc.rust-lang.org/error-index.html#E0401
I'll make this change to be more precise now, thanks for the help!
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.
Okay, I've tried this now, what do you think?
139b02b
to
50b79a9
Compare
This change attempts to resolve issue rust-lang#7636: Extract into Function does not create a generic function with constraints when extracting generic code. In `FunctionBody::analyze_container`, we now traverse the `ancestors` in search of `AnyHasGenericParams`, and attach any `GenericParamList`s and `WhereClause`s we find to the `ContainerInfo`. Later, in `format_function`, we collect all the `GenericParam`s and `WherePred`s from the container, and filter them to keep only types matching `TypeParam`s used within the newly extracted function body or param list. We can then include the new `GenericParamList` and `WhereClause` in the new function definition. This change only impacts `TypeParam`s. `LifetimeParam`s and `ConstParam`s are out of scope for this change.
50b79a9
to
796641b
Compare
Thanks! |
☀️ Test successful - checks-actions |
This change attempts to resolve issue #7636: Extract into Function does not
create a generic function with constraints when extracting generic code.
In
FunctionBody::analyze_container
, we now traverse theancestors
in searchof
AnyHasGenericParams
, and attach anyGenericParamList
s andWhereClause
swe find to the
ContainerInfo
.Later, in
format_function
, we collect all theGenericParam
s andWherePred
s from the container, and filter them to keep only types matchingTypeParam
s used within the newly extracted function body or param list. Wecan then include the new
GenericParamList
andWhereClause
in the newfunction definition.
This change only impacts
TypeParam
s.LifetimeParam
s andConstParam
s areout of scope for this change.
I've never contributed to this project before, but I did try to follow the style guide. I believe that this change represents an improvement over the status quo, but I think it's also fair to argue that it doesn't fully "fix" the linked issue. I'm totally open to merging this as is, or going further to try to make a more complete solution. Also: if there are other unit or integration tests I should add, please let me know where to look!