Skip to content

Commit 2490de4

Browse files
committed
Auto merge of #10866 - est31:manual_let_else_pattern, r=Manishearth
manual_let_else: support struct patterns This adds upon the improvements of #10797 and: * Only prints `()` around `Or` patterns at the top level (fixing a regression of #10797) * Supports multi-binding patterns: `let (u, v) = if let (Some(u_i), Ok(v_i)) = ex { (u_i, v_i) } else ...` * Traverses through tuple patterns: `let v = if let (Some(v), None) = ex { v } else ...` * Supports struct patterns: `let v = if let S { v, w, } = ex { (v, w) } else ...` ``` changelog: [`manual_let_else`]: improve pattern printing to support struct patterns ``` fixes #10708 fixes #10424
2 parents 52c2353 + f538402 commit 2490de4

File tree

5 files changed

+299
-78
lines changed

5 files changed

+299
-78
lines changed

clippy_lints/src/manual_let_else.rs

Lines changed: 177 additions & 70 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
) {
@@ -146,8 +147,8 @@ fn emit_manual_let_else(
146147
"this could be rewritten as `let...else`",
147148
|diag| {
148149
// This is far from perfect, for example there needs to be:
149-
// * tracking for multi-binding cases: let (foo, bar) = if let (Some(foo), Ok(bar)) = ...
150-
// * renamings of the bindings for many `PatKind`s like structs, slices, etc.
150+
// * renamings of the bindings for many `PatKind`s like slices, etc.
151+
// * limitations in the existing replacement algorithms
151152
// * unused binding collision detection with existing ones
152153
// for this to be machine applicable.
153154
let mut app = Applicability::HasPlaceholders;
@@ -159,57 +160,126 @@ 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);
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+
/// ```ignore
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+
/// ```ignore
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+
/// ```ignore
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,
196+
top_level: bool,
176197
) -> String {
177-
let mut bindings_count = 0;
178-
pat.each_binding_or_first(&mut |_, _, _, _| bindings_count += 1);
179-
// If the pattern creates multiple bindings, exit early,
180-
// as otherwise we might paste the pattern to the positions of multiple bindings.
181-
if bindings_count > 1 {
182-
let (sn_pat, _) = snippet_with_context(cx, pat.span, span.ctxt(), "", app);
183-
return sn_pat.into_owned();
184-
}
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+
}
185208

186-
match pat.kind {
187-
PatKind::Binding(..) => {
188-
let (sn_bdg, _) = snippet_with_context(cx, local.span, span.ctxt(), "", app);
189-
return sn_bdg.to_string();
190-
},
191-
PatKind::Or(pats) => {
192-
let patterns = pats
193-
.iter()
194-
.map(|pat| replace_in_pattern(cx, span, local, pat, app))
195-
.collect::<Vec<_>>();
196-
let or_pat = patterns.join(" | ");
197-
return format!("({or_pat})");
198-
},
199-
// Replace the variable name iff `TupleStruct` has one argument like `Variant(v)`.
200-
PatKind::TupleStruct(ref w, args, dot_dot_pos) => {
201-
let mut args = args
202-
.iter()
203-
.map(|pat| replace_in_pattern(cx, span, local, pat, app))
204-
.collect::<Vec<_>>();
205-
if let Some(pos) = dot_dot_pos.as_opt_usize() {
206-
args.insert(pos, "..".to_owned());
207-
}
208-
let args = args.join(", ");
209-
let sn_wrapper = cx.sess().source_map().span_to_snippet(w.span()).unwrap_or_default();
210-
return format!("{sn_wrapper}({args})");
211-
},
212-
_ => {},
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+
}
228+
return or_pat;
229+
},
230+
PatKind::Struct(path, fields, has_dot_dot) => {
231+
let fields = fields
232+
.iter()
233+
.map(|fld| {
234+
if let PatKind::Binding(_, _, name, None) = fld.pat.kind &&
235+
let Some(pat_to_put) = ident_map.get(&name.name)
236+
{
237+
let (sn_fld_name, _) = snippet_with_context(cx, fld.ident.span, span.ctxt(), "", app);
238+
let (sn_ptp, _) = snippet_with_context(cx, pat_to_put.span, span.ctxt(), "", app);
239+
// TODO: this is a bit of a hack, but it does its job. Ideally, we'd check if pat_to_put is
240+
// a PatKind::Binding but that is also hard to get right.
241+
if sn_fld_name == sn_ptp {
242+
// Field init shorthand
243+
return format!("{sn_fld_name}");
244+
}
245+
return format!("{sn_fld_name}: {sn_ptp}");
246+
}
247+
let (sn_fld, _) = snippet_with_context(cx, fld.span, span.ctxt(), "", app);
248+
sn_fld.into_owned()
249+
})
250+
.collect::<Vec<_>>();
251+
let fields_string = fields.join(", ");
252+
253+
let dot_dot_str = if has_dot_dot { " .." } else { "" };
254+
let (sn_pth, _) = snippet_with_context(cx, path.span(), span.ctxt(), "", app);
255+
return format!("{sn_pth} {{ {fields_string}{dot_dot_str} }}");
256+
},
257+
// Replace the variable name iff `TupleStruct` has one argument like `Variant(v)`.
258+
PatKind::TupleStruct(ref w, args, dot_dot_pos) => {
259+
let mut args = args
260+
.iter()
261+
.map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false))
262+
.collect::<Vec<_>>();
263+
if let Some(pos) = dot_dot_pos.as_opt_usize() {
264+
args.insert(pos, "..".to_owned());
265+
}
266+
let args = args.join(", ");
267+
let sn_wrapper = cx.sess().source_map().span_to_snippet(w.span()).unwrap_or_default();
268+
return format!("{sn_wrapper}({args})");
269+
},
270+
PatKind::Tuple(args, dot_dot_pos) => {
271+
let mut args = args
272+
.iter()
273+
.map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false))
274+
.collect::<Vec<_>>();
275+
if let Some(pos) = dot_dot_pos.as_opt_usize() {
276+
args.insert(pos, "..".to_owned());
277+
}
278+
let args = args.join(", ");
279+
return format!("({args})");
280+
},
281+
_ => {},
282+
}
213283
}
214284
let (sn_pat, _) = snippet_with_context(cx, pat.span, span.ctxt(), "", app);
215285
sn_pat.into_owned()
@@ -353,37 +423,74 @@ fn pat_allowed_for_else(cx: &LateContext<'_>, pat: &'_ Pat<'_>, check_types: boo
353423
!has_disallowed
354424
}
355425

356-
/// Checks if the passed block is a simple identity referring to bindings created by the pattern
357-
fn expr_is_simple_identity(pat: &'_ Pat<'_>, expr: &'_ Expr<'_>) -> bool {
358-
// We support patterns with multiple bindings and tuples, like:
359-
// let ... = if let (Some(foo), bar) = g() { (foo, bar) } else { ... }
426+
/// Checks if the passed block is a simple identity referring to bindings created by the pattern,
427+
/// and if yes, returns a mapping between the relevant sub-pattern and the identifier it corresponds
428+
/// to.
429+
///
430+
/// We support patterns with multiple bindings and tuples, e.g.:
431+
///
432+
/// ```ignore
433+
/// let (foo_o, bar_o) = if let (Some(foo), bar) = g() { (foo, bar) } else { ... }
434+
/// ```
435+
///
436+
/// The expected params would be:
437+
///
438+
/// ```ignore
439+
/// local_pat: (foo_o, bar_o)
440+
/// let_pat: (Some(foo), bar)
441+
/// expr: (foo, bar)
442+
/// ```
443+
///
444+
/// We build internal `sub_pats` so that it looks like `[foo_o, bar_o]` and `paths` so that it looks
445+
/// like `[foo, bar]`. Then we turn that into `FxHashMap [(foo) -> (foo_o), (bar) -> (bar_o)]` which
446+
/// we return.
447+
fn expr_simple_identity_map<'a, 'hir>(
448+
local_pat: &'a Pat<'hir>,
449+
let_pat: &'_ Pat<'hir>,
450+
expr: &'_ Expr<'hir>,
451+
) -> Option<FxHashMap<Symbol, &'a Pat<'hir>>> {
360452
let peeled = peel_blocks(expr);
361-
let paths = match peeled.kind {
362-
ExprKind::Tup(exprs) | ExprKind::Array(exprs) => exprs,
363-
ExprKind::Path(_) => std::slice::from_ref(peeled),
364-
_ => return false,
453+
let (sub_pats, paths) = match (local_pat.kind, peeled.kind) {
454+
(PatKind::Tuple(pats, _), ExprKind::Tup(exprs)) | (PatKind::Slice(pats, ..), ExprKind::Array(exprs)) => {
455+
(pats, exprs)
456+
},
457+
(_, ExprKind::Path(_)) => (slice::from_ref(local_pat), slice::from_ref(peeled)),
458+
_ => return None,
365459
};
460+
461+
// There is some length mismatch, which indicates usage of .. in the patterns above e.g.:
462+
// let (a, ..) = if let [a, b, _c] = ex { (a, b) } else { ... };
463+
// We bail in these cases as they should be rare.
464+
if paths.len() != sub_pats.len() {
465+
return None;
466+
}
467+
366468
let mut pat_bindings = FxHashSet::default();
367-
pat.each_binding_or_first(&mut |_ann, _hir_id, _sp, ident| {
469+
let_pat.each_binding_or_first(&mut |_ann, _hir_id, _sp, ident| {
368470
pat_bindings.insert(ident);
369471
});
370472
if pat_bindings.len() < paths.len() {
371-
return false;
473+
// This rebinds some bindings from the outer scope, or it repeats some copy-able bindings multiple
474+
// times. We don't support these cases so we bail here. E.g.:
475+
// let foo = 0;
476+
// let (new_foo, bar, bar_copied) = if let Some(bar) = Some(0) { (foo, bar, bar) } else { .. };
477+
return None;
372478
}
373-
for path in paths {
374-
if_chain! {
375-
if let ExprKind::Path(QPath::Resolved(_ty, path)) = path.kind;
376-
if let [path_seg] = path.segments;
377-
then {
378-
if !pat_bindings.remove(&path_seg.ident) {
379-
return false;
380-
}
381-
} else {
382-
return false;
479+
let mut ident_map = FxHashMap::default();
480+
for (sub_pat, path) in sub_pats.iter().zip(paths.iter()) {
481+
if let ExprKind::Path(QPath::Resolved(_ty, path)) = path.kind &&
482+
let [path_seg] = path.segments
483+
{
484+
let ident = path_seg.ident;
485+
if !pat_bindings.remove(&ident) {
486+
return None;
383487
}
488+
ident_map.insert(ident.name, sub_pat);
489+
} else {
490+
return None;
384491
}
385492
}
386-
true
493+
Some(ident_map)
387494
}
388495

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

tests/ui/manual_let_else.rs

Lines changed: 45 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,30 @@ 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 };
166+
167+
// Tuple structs work
168+
let w = if let Some(S { v: x }) = Some(S { v: 0 }) {
169+
x
170+
} else {
171+
return;
172+
};
173+
174+
// Field init shorthand is suggested
175+
let v = if let Some(S { v: x }) = Some(S { v: 0 }) {
176+
x
177+
} else {
178+
return;
179+
};
180+
181+
// Multi-field structs also work
182+
let (x, S { v }, w) = if let Some(U { v, w, x }) = None::<U<S<()>>> {
183+
(x, v, w)
184+
} else {
185+
return;
186+
};
163187
}
164188

165189
fn not_fire() {
@@ -284,4 +308,23 @@ fn not_fire() {
284308
};
285309
1
286310
};
311+
312+
// This would require creation of a suggestion of the form
313+
// let v @ (Some(_), _) = (...) else { return };
314+
// Which is too advanced for our code, so we just bail.
315+
let v = if let (Some(v_some), w_some) = (g(), 0) {
316+
(w_some, v_some)
317+
} else {
318+
return;
319+
};
320+
}
321+
322+
struct S<T> {
323+
v: T,
324+
}
325+
326+
struct U<T> {
327+
v: T,
328+
w: T,
329+
x: T,
287330
}

0 commit comments

Comments
 (0)