@@ -6,17 +6,18 @@ use clippy_utils::source::snippet_with_context;
66use clippy_utils:: ty:: is_type_diagnostic_item;
77use clippy_utils:: visitors:: { Descend , Visitable } ;
88use if_chain:: if_chain;
9- use rustc_data_structures:: fx:: FxHashSet ;
9+ use rustc_data_structures:: fx:: { FxHashMap , FxHashSet } ;
1010use rustc_errors:: Applicability ;
1111use rustc_hir:: intravisit:: { walk_expr, Visitor } ;
1212use rustc_hir:: { Expr , ExprKind , HirId , ItemId , Local , MatchSource , Pat , PatKind , QPath , Stmt , StmtKind , Ty } ;
1313use rustc_lint:: { LateContext , LateLintPass , LintContext } ;
1414use rustc_middle:: lint:: in_external_macro;
1515use rustc_session:: { declare_tool_lint, impl_lint_pass} ;
16- use rustc_span:: symbol:: sym;
16+ use rustc_span:: symbol:: { sym, Symbol } ;
1717use rustc_span:: Span ;
1818use serde:: Deserialize ;
1919use std:: ops:: ControlFlow ;
20+ use std:: slice;
2021
2122declare_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+ /// ```
170190fn 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 ) ]
0 commit comments