@@ -6,17 +6,18 @@ use clippy_utils::source::snippet_with_context;
6
6
use clippy_utils:: ty:: is_type_diagnostic_item;
7
7
use clippy_utils:: visitors:: { Descend , Visitable } ;
8
8
use if_chain:: if_chain;
9
- use rustc_data_structures:: fx:: FxHashSet ;
9
+ use rustc_data_structures:: fx:: { FxHashMap , FxHashSet } ;
10
10
use rustc_errors:: Applicability ;
11
11
use rustc_hir:: intravisit:: { walk_expr, Visitor } ;
12
12
use rustc_hir:: { Expr , ExprKind , HirId , ItemId , Local , MatchSource , Pat , PatKind , QPath , Stmt , StmtKind , Ty } ;
13
13
use rustc_lint:: { LateContext , LateLintPass , LintContext } ;
14
14
use rustc_middle:: lint:: in_external_macro;
15
15
use rustc_session:: { declare_tool_lint, impl_lint_pass} ;
16
- use rustc_span:: symbol:: sym;
16
+ use rustc_span:: symbol:: { sym, Symbol } ;
17
17
use rustc_span:: Span ;
18
18
use serde:: Deserialize ;
19
19
use std:: ops:: ControlFlow ;
20
+ use std:: slice;
20
21
21
22
declare_clippy_lint ! {
22
23
/// ### What it does
@@ -81,11 +82,11 @@ impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
81
82
{
82
83
match if_let_or_match {
83
84
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) ;
85
86
if let Some ( if_else) = if_else;
86
87
if expr_diverges( cx, if_else) ;
87
88
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) ;
89
90
}
90
91
} ,
91
92
IfLetOrMatch :: Match ( match_expr, arms, source) => {
@@ -118,11 +119,11 @@ impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
118
119
return ;
119
120
}
120
121
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
+ } ;
124
125
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 ) ;
126
127
} ,
127
128
}
128
129
} ;
@@ -135,7 +136,7 @@ fn emit_manual_let_else(
135
136
cx : & LateContext < ' _ > ,
136
137
span : Span ,
137
138
expr : & Expr < ' _ > ,
138
- local : & Pat < ' _ > ,
139
+ ident_map : & FxHashMap < Symbol , & Pat < ' _ > > ,
139
140
pat : & Pat < ' _ > ,
140
141
else_body : & Expr < ' _ > ,
141
142
) {
@@ -146,8 +147,8 @@ fn emit_manual_let_else(
146
147
"this could be rewritten as `let...else`" ,
147
148
|diag| {
148
149
// 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
151
152
// * unused binding collision detection with existing ones
152
153
// for this to be machine applicable.
153
154
let mut app = Applicability :: HasPlaceholders ;
@@ -159,57 +160,126 @@ fn emit_manual_let_else(
159
160
} else {
160
161
format ! ( "{{ {sn_else} }}" )
161
162
} ;
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 ) ;
163
164
let sugg = format ! ( "let {sn_bl} = {sn_expr} else {else_bl};" ) ;
164
165
diag. span_suggestion ( span, "consider writing" , sugg, app) ;
165
166
} ,
166
167
) ;
167
168
}
168
169
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
+ /// ```
170
190
fn replace_in_pattern (
171
191
cx : & LateContext < ' _ > ,
172
192
span : Span ,
173
- local : & Pat < ' _ > ,
193
+ ident_map : & FxHashMap < Symbol , & Pat < ' _ > > ,
174
194
pat : & Pat < ' _ > ,
175
195
app : & mut Applicability ,
196
+ top_level : bool ,
176
197
) -> 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
+ }
185
208
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
+ }
213
283
}
214
284
let ( sn_pat, _) = snippet_with_context ( cx, pat. span , span. ctxt ( ) , "" , app) ;
215
285
sn_pat. into_owned ( )
@@ -353,37 +423,74 @@ fn pat_allowed_for_else(cx: &LateContext<'_>, pat: &'_ Pat<'_>, check_types: boo
353
423
!has_disallowed
354
424
}
355
425
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 > > > {
360
452
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 ,
365
459
} ;
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
+
366
468
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| {
368
470
pat_bindings. insert ( ident) ;
369
471
} ) ;
370
472
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 ;
372
478
}
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 ;
383
487
}
488
+ ident_map. insert ( ident. name , sub_pat) ;
489
+ } else {
490
+ return None ;
384
491
}
385
492
}
386
- true
493
+ Some ( ident_map )
387
494
}
388
495
389
496
#[ derive( Clone , Copy , Debug , PartialEq , Eq , Hash , Deserialize ) ]
0 commit comments