Description
This is the counterpart to #11908 (assist for turning let statements + match expression into let-else statements).
Read about the unstable feature let_else
. Rust Analyzer has basic support for it.
Motivation
Many times I have found myself manually turning let-else statements handling Result
s into let statements + match expression in cases where I previously did not need to inspect the Err
variant but now needed to. E.g:
// context: we are inside a loop
// code before: the `Err` payload can be ignored
let Ok((first, second)) = calculation(param0, transform(param1)) else {
log_something_or_change_state();
continue;
};
// code after implementing more features: the `Err` payload needs to be handled
let (first, second) = match calculation(param0, transform(param1)) {
Ok(calculated) => calculated,
Err(Error::Fatal) => return Err(PubError::Something),
Err(Error::NonFatal) => {
log_something_or_change_state();
continue;
}
};
It's quite a hassle to manually shift around the pattern and adding match
, a lot of curly braces, removing the else
etc.
It would be nice if Rust Analyzer could automate this refactoring.
Desugaring / Transformation
Read about the desugaring of arbitrary let-else statements.
let $pat = $expr else { $( $stmt );* $diverging_expr };
// ---->
let $all_binders_in_pat = match $expr { // those binders adopt the `mut`s from `$pat`
$pat_ => $all_binders_in_pat_, // `$pat_` is `$pat` without any `mut`s (excluding `ref mut` here)
_ => { $( $stmt );+ $diverging_expr }
// if there are no statements before the diverging expression:
// _ => $diverging_expr, // nicer to look at
};
// in general, `$all_binders_in_pat` is a tuple unless n=1, then it's the binder itself
// note: `$all_binders_in_pat_` (w/ trailing underscore) does not
// contain any `mut`s unlike `$tuple_of_all_binders_in_pat`
// (again excluding `ref mut` here)
If there is only one binder in $pat
, use the binder itself instead of a one-element tuple (i.e. $x
instead of ($x,)
) for $all_binders_in_pat
/$all_binders_in_pat_
. It looks better.
If there is no binder in $pat
(e.g. _
, 3
, Ok(..)
), remove the redundant leading let () =
, the trailing ;
and use {}
instead of (),
as the match arm. See example (D).
Regarding the Motivational Example
To achieve the “same” (very similar) output as the manual refactoring of the example given in the section motivation, we'd first apply the assist Replace let else with let match, then we'd move our cursor to the generated _
(the last branch) and apply the assist Fill match arms to obtain Err(_)
(this currently does not remove the existing _
but I think that's a bug). In a future Rust Analyzer we could then potentially even offer an assist to turn Err(_) => …
into Err(Error::Fatal) => …, Err(Error::NonFatal) => …
(if I remember correctly this feature was discussed on this issue tracker but I don't have a link to it right now).
Alternatively, instead of generating the _ => …
unconditionally we could immediately try to fill the match arms. Not sure what's best here.
NB: Compared to the manual desugaring, the proposed automatic transformation generates Ok((first, second)) => (first, second)
instead of Ok(calculated) => calculated
. Theoretically, we could special-case this in RA if we wanted to. The identifier (here: calculated
) would need to be generated (using heuristics for a sensible name).
Examples
(A) Single binder & mutability:
let Ok(mut x) = f() else { continue };
// ---->
let mut x = match f() {
Ok(x) => x, // no `mut` here!
_ => continue,
};
(B) Multiple binders & string literal patterns which don't introduce any binders:
let ControlFlow::Break((x, "tag", y, ..)) = f() else { g(); return };
// ---->
let (x, y) = match f() {
ControlFlow::Break((x, "tag", y, ..)) => (x, y),
_ => { g(); return },
};
(C) Slice & struct patterns:
let [Struct { inner: Some(it) }, 1001, other] = f() else { break };
// ---->
let (it, other) = match f() {
[Struct { inner: Some(it) }, 1001, other] => (it, other),
_ => break,
};
(D) Or-patterns & no binders at all:
let (8 | 9) = f() else { panic!() }; // parentheses around the or-pattern are mandatory
// ---->
match f() { // omitting the `let () =`
(8 | 9) => {} // `{}` more conventional than `(),`
// in this case, we could also automatically remove the now redundant parens:
// 8 | 9 => {}
_ => panic!(),
} // `;` not necessary
(E) ref mut
(just to show it's not treated special):
let Ok(ref mut x) = f() else { return };
// ---->
let x = match f() {
Ok(ref mut x) => x,
_ => return,
};