-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
feature: Destructure Tuple Assist #9855
Conversation
Awesome, I've been wanting this for a long time 🎉 Addressing some of the points you have raised in the PR description for now:
This seems fine to me for now, but I don't see a reason why we shouldn't insert a deref for these cases in the end.
I don't think we have any feature checking mechanisms and since we currently only target latest stable rust it should be fine to just have this unconditionally, especially given 1.56 being there pretty soon.
Oh I dig this, I didn't even think of that problem being a possibility. Tracking down possible logic errors introduced by this could be pretty annoying so forcing a compile error of some sort seems reasonable for these cases. OTOH given we have Macros causing problems here is to be expected, I don't think there is much we can do in most cases here for the time being so its fine to not have those working for now, but we should have tests showing this incorrect behaviour for them so they won't be forgotten.
As you've noticed, we don't really handle this in any assist currently, definitely not somethign that needs to be tackled in this PR as it's something that can be done for assist in general instead at some later point. |
that results in some ugly code: struct S;
let t = &(S, 2);
// ^
// cannot use just `t.0` because: `cannot move`
let v = &t.0; -> struct S;
let (_0, _1) = &(S, 2);
let v = &*_0; which of course can be recognized and removed.... Another issue: precedence of deref operator struct S;
impl S {
fn r(&self) -> Self { S }
}
let t = &(S,2);
// ^
let s = t.0.ref(); -> let (_0, _1) = &(S,2);
let s = *_0.ref(); Method call has higher precedence than deref -> compiler error:
-> must be recognized too and put in parentheses ( At least how I use such refactoring assists: -> I think in the end it's about how the assist is used: Refactoring in an existing, large code area and probably lots of places a tuple is used. Or during writing the code and thinking "hey, it would be easier/cleaner to use the tuple items directly". And therefore just very little code and invalid stuff is ok. Considering uncommenting tuples (without index usage) is already done and results in invalid code, I think it's reasonable to allow invalid code here too. The question is then just: where does that error emerge (how close to the tuple usage location)
I think especially while rewriting code One easy thing to favour |
All of these are fine to recognize and special case so that they are emitted as better/simpler but working code. A lot of assists have special cases for some things so that they are emitted more idiomatically/correctly. I personally expect an assist when it is able to generate correct code to do so. I find it odd(and a bug even in most cases) if an assist generates non compiling code even though it would be able to generate code that compiles and works as intended. Basically my stance is, if the assist can generate code that compiles and does logically the correct thing it should do so, if it can generate compiling code that may introduce a logic error/semantic differences it would be better to bail out, that is either not apply or somehow force a compilation error at the problematic code side(as done here with the commenting).
Fair point, I agree that we should at least swap the order in that case since |
Ok, I try to catch at least some of the special cases. But this can get quite complex quite quickly, for example because of auto-ref/deref: trait T {
fn do_stuff(self);
}
#[derive(Clone, Copy)]
struct S;
impl T for S {
fn do_stuff(self) { println!("-"); }
}
impl T for &S {
fn do_stuff(self) { println!("&"); }
}
let t = &(S,2);
t.0.do_stuff(); // output: -
let (_0, _1) = &(S,2);
_0.do_stuff(); // output: &
(*_0).do_stuff(); // output: - But if there's only one trait impl (either one), we want to use Is there some way to lookup what method would be called if Ah well ... that are further improvements. First I'm looking at parentheses and refs-derefs that cancel each other out (`&*`) |
Ye its fine not to catch all corner cases for now, that should be gradually improved on afterwards. |
deref handling for usages of ref tuples added. ( let t = &(1,2); -> let (_0, _1) = &(1,2); (-> ) Currently handled cases:
Edit: |
Only tuple name is handled (uncommented), not tuple index
Note: 2nd Assist description is moved down: generated doc tests extracts now all tests (previously only the first one). But it uses the first `Assist` name -- which is the wrong one for the 2nd test. And 2nd assist is currently disabled -> would fail anyway.
Destructure in sub-pattern before Destructure in place to favor the first one
17881a9
to
b1ebb82
Compare
Looks got to me overall with 2 small nits. I would say we keep the Regarding the method resolution problems with traits, I'm not sure myself, I'd say we can keep that out of this initial PR and look at that in a follow up. And great work on the tests by the way! Thanks a lot for the work on this! |
Add node about uncommenting tuple in macro call
Thanks! |
changelog feature (first contribution) add "Destructure tuple" assist: |
Part of #8673. This PR only handles tuples, not TupleStruct and RecordStruct.
Code Assist to destructure a tuple into its items:
if let Some($0t) = Some((1,2))
)-> everywhere
IdentPat
is allowed@
):t
must be aName
;TuplePat
((_0, _1)
) isn't allowed(might be useful especially when it creates a sub-pattern (after
@
) and only changes the usage under cursor -- but not part of this PR).References
References can be destructured:
->
BUT:
t.0
and_0
have different types (i32
vs.&i32
) ->v
has now a different type.I think that's acceptable: I think the destructure assist is mostly used in simple, immediate scopes and not huge existing code.
Additional Notes:
ref
has same behaviour (->ref
is kept for items)&
orref
at allmutable
->
and
->
Again: with reference (
&mut
),t.0
and_0
have different types (i32
vs&mut i32
).And there's an additional issue with
&mut
and assignment:->
But I think that's quite a niche use case, so I don't catch that (
*_0 = 9;
)Additional Notes:
mut
(let mut t = ...
->let (_0, _1) = ...
), doesn't trigger with&mut
Binding after
@
Destructure tuple in sub-pattern is implemented:
->
BUT: Bindings after
@
aren't currently in stable and require#![feature(bindings_after_at)]
(though should be generally available quite soon (with1.56.0
)).But I don't know how to check for an enabled feature -> Destructure tuple in sub-pattern isn't enabled yet.
Destructure tuple in place
:Destructure tuple in sub-pattern
:Destructure tuple
Caveats
Unlike in Destructure assist #8673 or IntelliJ rust plugin, I'm not leaving the previous tuple name at function calls.
Reasoning: It's not too unlikely the tuple variable shadows another variable. Destructuring the tuple while leaving the function call untouched, results in still a valid function call -- but now with another variable:
=> Destructure Tuple
t.into()
is still valid -- using the first tuple.Instead I comment out the tuple usage, which results in invalid code -> must be handled by user:
t
s and quite ofen in lines next to each other...)-> not sure that's the best way....
Additional the tuple name surrounded by comment is more difficult to edit than just the name.
Code Assists don't support snippet placeholder, and rust analyzer just the first
$0
-> unfortunately no editing of generated tuple item variables. Cursor ($0
) is placed on first generated item.Issues
Issue is:
name.syntax() in each usage of a tuple is syntax & text_range in its file.
EXCEPT when tuple usage is in a macro call (
m!(t.0)
), the macro is expanded and syntax (and range) is based on that expanded macro, not in actual file.That leads to several things:
t
usages are resolved as tuple index usage-> don't know where to replace index usage
-> tuple items passed into a macro are ignored, and only the tuple name itself is handled (uncommented)
process_all_names
. But that doesn't find all local names for declarations (let t = (1,2)
) (for usages it does)extract into variable
orextract into function
). But here a name conflict is more likely (when destructuring multiple tuples, for examples nested ones (let t = ((1,2),3)
->let (_0, _1) = ...
->let ((_0, _1), _1) = ...
-> error))_00
)