Skip to content

Commit 675cc5a

Browse files
committed
Support multi-binding situations as well as tuple patterns
1 parent 0a73668 commit 675cc5a

File tree

3 files changed

+173
-77
lines changed

3 files changed

+173
-77
lines changed

clippy_lints/src/manual_let_else.rs

Lines changed: 145 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ use clippy_utils::source::snippet_with_context;
66
use clippy_utils::ty::is_type_diagnostic_item;
77
use clippy_utils::visitors::{Descend, Visitable};
88
use if_chain::if_chain;
9-
use rustc_data_structures::fx::FxHashSet;
9+
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
1010
use rustc_errors::Applicability;
1111
use rustc_hir::intravisit::{walk_expr, Visitor};
1212
use rustc_hir::{Expr, ExprKind, HirId, ItemId, Local, MatchSource, Pat, PatKind, QPath, Stmt, StmtKind, Ty};
1313
use rustc_lint::{LateContext, LateLintPass, LintContext};
1414
use rustc_middle::lint::in_external_macro;
1515
use rustc_session::{declare_tool_lint, impl_lint_pass};
16-
use rustc_span::symbol::sym;
16+
use rustc_span::symbol::{sym, Symbol};
1717
use rustc_span::Span;
1818
use serde::Deserialize;
1919
use std::ops::ControlFlow;
20+
use std::slice;
2021

2122
declare_clippy_lint! {
2223
/// ### What it does
@@ -81,11 +82,11 @@ impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
8182
{
8283
match if_let_or_match {
8384
IfLetOrMatch::IfLet(if_let_expr, let_pat, if_then, if_else) => if_chain! {
84-
if expr_is_simple_identity(let_pat, if_then);
85+
if let Some(ident_map) = expr_simple_identity_map(local.pat, let_pat, if_then);
8586
if let Some(if_else) = if_else;
8687
if expr_diverges(cx, if_else);
8788
then {
88-
emit_manual_let_else(cx, stmt.span, if_let_expr, local.pat, let_pat, if_else);
89+
emit_manual_let_else(cx, stmt.span, if_let_expr, &ident_map, let_pat, if_else);
8990
}
9091
},
9192
IfLetOrMatch::Match(match_expr, arms, source) => {
@@ -118,11 +119,11 @@ impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
118119
return;
119120
}
120121
let pat_arm = &arms[1 - idx];
121-
if !expr_is_simple_identity(pat_arm.pat, pat_arm.body) {
122-
return;
123-
}
122+
let Some(ident_map) = expr_simple_identity_map(local.pat, pat_arm.pat, pat_arm.body) else {
123+
return
124+
};
124125

125-
emit_manual_let_else(cx, stmt.span, match_expr, local.pat, pat_arm.pat, diverging_arm.body);
126+
emit_manual_let_else(cx, stmt.span, match_expr, &ident_map, pat_arm.pat, diverging_arm.body);
126127
},
127128
}
128129
};
@@ -135,7 +136,7 @@ fn emit_manual_let_else(
135136
cx: &LateContext<'_>,
136137
span: Span,
137138
expr: &Expr<'_>,
138-
local: &Pat<'_>,
139+
ident_map: &FxHashMap<Symbol, &Pat<'_>>,
139140
pat: &Pat<'_>,
140141
else_body: &Expr<'_>,
141142
) {
@@ -159,62 +160,99 @@ fn emit_manual_let_else(
159160
} else {
160161
format!("{{ {sn_else} }}")
161162
};
162-
let sn_bl = replace_in_pattern(cx, span, local, pat, &mut app, true);
163+
let sn_bl = replace_in_pattern(cx, span, ident_map, pat, &mut app, true);
163164
let sugg = format!("let {sn_bl} = {sn_expr} else {else_bl};");
164165
diag.span_suggestion(span, "consider writing", sugg, app);
165166
},
166167
);
167168
}
168169

169-
// replaces the locals in the pattern
170+
/// Replaces the locals in the pattern
171+
///
172+
/// For this example:
173+
///
174+
/// ```
175+
/// let (a, FooBar { b, c }) = if let Bar { Some(a_i), b_i } = ex { (a_i, b_i) } else { return };
176+
/// ```
177+
///
178+
/// We have:
179+
///
180+
/// ```
181+
/// pat: Bar { Some(a_i), b_i }
182+
/// ident_map: (a_i) -> (a), (b_i) -> (FooBar { b, c })
183+
/// ```
184+
///
185+
/// We return:
186+
///
187+
/// ```
188+
/// Bar { Some(a), b_i: FooBar { b, c } }
189+
/// ```
170190
fn replace_in_pattern(
171191
cx: &LateContext<'_>,
172192
span: Span,
173-
local: &Pat<'_>,
193+
ident_map: &FxHashMap<Symbol, &Pat<'_>>,
174194
pat: &Pat<'_>,
175195
app: &mut Applicability,
176196
top_level: bool,
177197
) -> String {
178-
let mut bindings_count = 0;
179-
pat.each_binding_or_first(&mut |_, _, _, _| bindings_count += 1);
180-
// If the pattern creates multiple bindings, exit early,
181-
// as otherwise we might paste the pattern to the positions of multiple bindings.
182-
if bindings_count > 1 {
183-
let (sn_pat, _) = snippet_with_context(cx, pat.span, span.ctxt(), "", app);
184-
return sn_pat.into_owned();
185-
}
198+
// We put a labeled block here so that we can implement the fallback in this function.
199+
// As the function has multiple call sites, implementing the fallback via an Option<T>
200+
// return type and unwrap_or_else would cause repetition. Similarly, the function also
201+
// invokes the fall back multiple times.
202+
'a: {
203+
// If the ident map is empty, there is no replacement to do.
204+
// The code following this if assumes a non-empty ident_map.
205+
if ident_map.is_empty() {
206+
break 'a;
207+
}
186208

187-
match pat.kind {
188-
PatKind::Binding(..) => {
189-
let (sn_bdg, _) = snippet_with_context(cx, local.span, span.ctxt(), "", app);
190-
return sn_bdg.to_string();
191-
},
192-
PatKind::Or(pats) => {
193-
let patterns = pats
194-
.iter()
195-
.map(|pat| replace_in_pattern(cx, span, local, pat, app, false))
196-
.collect::<Vec<_>>();
197-
let or_pat = patterns.join(" | ");
198-
if top_level {
199-
return format!("({or_pat})");
200-
} else {
209+
match pat.kind {
210+
PatKind::Binding(_ann, _id, binding_name, opt_subpt) => {
211+
let Some(pat_to_put) = ident_map.get(&binding_name.name) else { break 'a };
212+
let (sn_ptp, _) = snippet_with_context(cx, pat_to_put.span, span.ctxt(), "", app);
213+
if let Some(subpt) = opt_subpt {
214+
let subpt = replace_in_pattern(cx, span, ident_map, subpt, app, false);
215+
return format!("{sn_ptp} @ {subpt}");
216+
}
217+
return sn_ptp.to_string();
218+
},
219+
PatKind::Or(pats) => {
220+
let patterns = pats
221+
.iter()
222+
.map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false))
223+
.collect::<Vec<_>>();
224+
let or_pat = patterns.join(" | ");
225+
if top_level {
226+
return format!("({or_pat})");
227+
}
201228
return or_pat;
202-
}
203-
},
204-
// Replace the variable name iff `TupleStruct` has one argument like `Variant(v)`.
205-
PatKind::TupleStruct(ref w, args, dot_dot_pos) => {
206-
let mut args = args
207-
.iter()
208-
.map(|pat| replace_in_pattern(cx, span, local, pat, app, false))
209-
.collect::<Vec<_>>();
210-
if let Some(pos) = dot_dot_pos.as_opt_usize() {
211-
args.insert(pos, "..".to_owned());
212-
}
213-
let args = args.join(", ");
214-
let sn_wrapper = cx.sess().source_map().span_to_snippet(w.span()).unwrap_or_default();
215-
return format!("{sn_wrapper}({args})");
216-
},
217-
_ => {},
229+
},
230+
// Replace the variable name iff `TupleStruct` has one argument like `Variant(v)`.
231+
PatKind::TupleStruct(ref w, args, dot_dot_pos) => {
232+
let mut args = args
233+
.iter()
234+
.map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false))
235+
.collect::<Vec<_>>();
236+
if let Some(pos) = dot_dot_pos.as_opt_usize() {
237+
args.insert(pos, "..".to_owned());
238+
}
239+
let args = args.join(", ");
240+
let sn_wrapper = cx.sess().source_map().span_to_snippet(w.span()).unwrap_or_default();
241+
return format!("{sn_wrapper}({args})");
242+
},
243+
PatKind::Tuple(args, dot_dot_pos) => {
244+
let mut args = args
245+
.iter()
246+
.map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false))
247+
.collect::<Vec<_>>();
248+
if let Some(pos) = dot_dot_pos.as_opt_usize() {
249+
args.insert(pos, "..".to_owned());
250+
}
251+
let args = args.join(", ");
252+
return format!("({args})");
253+
},
254+
_ => {},
255+
}
218256
}
219257
let (sn_pat, _) = snippet_with_context(cx, pat.span, span.ctxt(), "", app);
220258
sn_pat.into_owned()
@@ -358,37 +396,73 @@ fn pat_allowed_for_else(cx: &LateContext<'_>, pat: &'_ Pat<'_>, check_types: boo
358396
!has_disallowed
359397
}
360398

361-
/// Checks if the passed block is a simple identity referring to bindings created by the pattern
362-
fn expr_is_simple_identity(pat: &'_ Pat<'_>, expr: &'_ Expr<'_>) -> bool {
363-
// We support patterns with multiple bindings and tuples, like:
364-
// let ... = if let (Some(foo), bar) = g() { (foo, bar) } else { ... }
399+
/// Checks if the passed block is a simple identity referring to bindings created by the pattern,
400+
/// and if yes, returns a mapping between the relevant sub-pattern and the identifier it corresponds
401+
/// to.
402+
///
403+
/// We support patterns with multiple bindings and tuples, e.g.:
404+
///
405+
/// ```
406+
/// let (foo_o, bar_o) = if let (Some(foo), bar) = g() { (foo, bar) } else { ... }
407+
/// ```
408+
///
409+
/// The expected params would be:
410+
///
411+
/// ```
412+
/// local_pat: (foo_o, bar_o)
413+
/// let_pat: (Some(foo), bar)
414+
/// expr: (foo, bar)
415+
/// ```
416+
///
417+
/// We build internal `sub_pats` so that it looks like `[foo_o, bar_o]` and `paths` so that it looks like
418+
/// `[foo, bar]`. Then we turn that into `FxHashMap [(foo) -> (foo_o), (bar) -> (bar_o)]` which we return.
419+
fn expr_simple_identity_map<'a, 'hir>(
420+
local_pat: &'a Pat<'hir>,
421+
let_pat: &'_ Pat<'hir>,
422+
expr: &'_ Expr<'hir>,
423+
) -> Option<FxHashMap<Symbol, &'a Pat<'hir>>> {
365424
let peeled = peel_blocks(expr);
366-
let paths = match peeled.kind {
367-
ExprKind::Tup(exprs) | ExprKind::Array(exprs) => exprs,
368-
ExprKind::Path(_) => std::slice::from_ref(peeled),
369-
_ => return false,
425+
let (sub_pats, paths) = match (local_pat.kind, peeled.kind) {
426+
(PatKind::Tuple(pats, _), ExprKind::Tup(exprs)) | (PatKind::Slice(pats, ..), ExprKind::Array(exprs)) => {
427+
(pats, exprs)
428+
},
429+
(_, ExprKind::Path(_)) => (slice::from_ref(local_pat), slice::from_ref(peeled)),
430+
_ => return None,
370431
};
432+
433+
// There is some length mismatch, which indicates usage of .. in the patterns above e.g.:
434+
// let (a, ..) = if let [a, b, _c] = ex { (a, b) } else { ... };
435+
// We bail in these cases as they should be rare.
436+
if paths.len() != sub_pats.len() {
437+
return None;
438+
}
439+
371440
let mut pat_bindings = FxHashSet::default();
372-
pat.each_binding_or_first(&mut |_ann, _hir_id, _sp, ident| {
441+
let_pat.each_binding_or_first(&mut |_ann, _hir_id, _sp, ident| {
373442
pat_bindings.insert(ident);
374443
});
375444
if pat_bindings.len() < paths.len() {
376-
return false;
445+
// This rebinds some bindings from the outer scope, or it repeats some copy-able bindings multiple
446+
// times. We don't support these cases so we bail here. E.g.:
447+
// let foo = 0;
448+
// let (new_foo, bar, bar_copied) = if let Some(bar) = Some(0) { (foo, bar, bar) } else { .. };
449+
return None;
377450
}
378-
for path in paths {
379-
if_chain! {
380-
if let ExprKind::Path(QPath::Resolved(_ty, path)) = path.kind;
381-
if let [path_seg] = path.segments;
382-
then {
383-
if !pat_bindings.remove(&path_seg.ident) {
384-
return false;
385-
}
386-
} else {
387-
return false;
451+
let mut ident_map = FxHashMap::default();
452+
for (sub_pat, path) in sub_pats.iter().zip(paths.iter()) {
453+
if let ExprKind::Path(QPath::Resolved(_ty, path)) = path.kind &&
454+
let [path_seg] = path.segments
455+
{
456+
let ident = path_seg.ident;
457+
if !pat_bindings.remove(&ident) {
458+
return None;
388459
}
460+
ident_map.insert(ident.name, sub_pat);
461+
} else {
462+
return None;
389463
}
390464
}
391-
true
465+
Some(ident_map)
392466
}
393467

394468
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize)]

tests/ui/manual_let_else.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ fn fire() {
127127
return;
128128
};
129129

130-
// Tuples supported for the identity block and pattern
131-
let v = if let (Some(v_some), w_some) = (g(), 0) {
130+
// Tuples supported with multiple bindings
131+
let (w, S { v }) = if let (Some(v_some), w_some) = (g().map(|_| S { v: 0 }), 0) {
132132
(w_some, v_some)
133133
} else {
134134
return;
@@ -160,6 +160,9 @@ fn fire() {
160160
};
161161
// dot dot works
162162
let v = if let Variant::A(.., a) = e() { a } else { return };
163+
164+
// () is preserved: a bit of an edge case but make sure it stays around
165+
let w = if let (Some(v), ()) = (g(), ()) { v } else { return };
163166
}
164167

165168
fn not_fire() {
@@ -284,4 +287,17 @@ fn not_fire() {
284287
};
285288
1
286289
};
290+
291+
// This would require creation of a suggestion of the form
292+
// let v @ (Some(_), _) = (...) else { return };
293+
// Which is too advanced for our code, so we just bail.
294+
let v = if let (Some(v_some), w_some) = (g(), 0) {
295+
(w_some, v_some)
296+
} else {
297+
return;
298+
};
299+
}
300+
301+
struct S {
302+
v: u32,
287303
}

tests/ui/manual_let_else.stderr

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ LL + };
234234
error: this could be rewritten as `let...else`
235235
--> $DIR/manual_let_else.rs:131:5
236236
|
237-
LL | / let v = if let (Some(v_some), w_some) = (g(), 0) {
237+
LL | / let (w, S { v }) = if let (Some(v_some), w_some) = (g().map(|_| S { v: 0 }), 0) {
238238
LL | | (w_some, v_some)
239239
LL | | } else {
240240
LL | | return;
@@ -243,7 +243,7 @@ LL | | };
243243
|
244244
help: consider writing
245245
|
246-
LL ~ let (Some(v_some), w_some) = (g(), 0) else {
246+
LL ~ let (Some(S { v }), w) = (g().map(|_| S { v: 0 }), 0) else {
247247
LL + return;
248248
LL + };
249249
|
@@ -295,13 +295,19 @@ LL | let v = if let Variant::A(.., a) = e() { a } else { return };
295295
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let Variant::A(.., v) = e() else { return };`
296296

297297
error: this could be rewritten as `let...else`
298-
--> $DIR/manual_let_else.rs:272:5
298+
--> $DIR/manual_let_else.rs:165:5
299+
|
300+
LL | let w = if let (Some(v), ()) = (g(), ()) { v } else { return };
301+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let (Some(w), ()) = (g(), ()) else { return };`
302+
303+
error: this could be rewritten as `let...else`
304+
--> $DIR/manual_let_else.rs:275:5
299305
|
300306
LL | / let _ = match ff {
301307
LL | | Some(value) => value,
302308
LL | | _ => macro_call!(),
303309
LL | | };
304310
| |______^ help: consider writing: `let Some(_) = ff else { macro_call!() };`
305311

306-
error: aborting due to 22 previous errors
312+
error: aborting due to 23 previous errors
307313

0 commit comments

Comments
 (0)