@@ -3,7 +3,10 @@ use clippy_utils::{eq_expr_value, in_macro, search_same, SpanlessEq, SpanlessHas
33use clippy_utils:: { get_parent_expr, if_sequence} ;
44use rustc_hir:: { Block , Expr , ExprKind } ;
55use rustc_lint:: { LateContext , LateLintPass } ;
6+ use rustc_middle:: hir:: map:: Map ;
67use rustc_session:: { declare_lint_pass, declare_tool_lint} ;
8+ use rustc_span:: source_map:: Span ;
9+ use std:: borrow:: Cow ;
710
811declare_clippy_lint ! {
912 /// **What it does:** Checks for consecutive `if`s with the same condition.
@@ -104,7 +107,45 @@ declare_clippy_lint! {
104107 "`if` with the same `then` and `else` blocks"
105108}
106109
107- declare_lint_pass ! ( CopyAndPaste => [ IFS_SAME_COND , SAME_FUNCTIONS_IN_IF_CONDITION , IF_SAME_THEN_ELSE ] ) ;
110+ declare_clippy_lint ! {
111+ /// **What it does:** Checks if the `if` and `else` block contain shared code that can be
112+ /// moved out of the blocks.
113+ ///
114+ /// **Why is this bad?** Duplicate code is less maintainable.
115+ ///
116+ /// **Known problems:** Hopefully none.
117+ ///
118+ /// **Example:**
119+ /// ```ignore
120+ /// let foo = if … {
121+ /// println!("Hello World");
122+ /// 13
123+ /// } else {
124+ /// println!("Hello World");
125+ /// 42
126+ /// };
127+ /// ```
128+ ///
129+ /// Could be written as:
130+ /// ```ignore
131+ /// println!("Hello World");
132+ /// let foo = if … {
133+ /// 13
134+ /// } else {
135+ /// 42
136+ /// };
137+ /// ```
138+ pub SHARED_CODE_IN_IF_BLOCKS ,
139+ pedantic,
140+ "`if` statement with shared code in all blocks"
141+ }
142+
143+ declare_lint_pass ! ( CopyAndPaste => [
144+ IFS_SAME_COND ,
145+ SAME_FUNCTIONS_IN_IF_CONDITION ,
146+ IF_SAME_THEN_ELSE ,
147+ SHARED_CODE_IN_IF_BLOCKS
148+ ] ) ;
108149
109150impl < ' tcx > LateLintPass < ' tcx > for CopyAndPaste {
110151 fn check_expr ( & mut self , cx : & LateContext < ' tcx > , expr : & ' tcx Expr < ' _ > ) {
@@ -121,30 +162,256 @@ impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
121162 }
122163
123164 let ( conds, blocks) = if_sequence ( expr) ;
124- lint_same_then_else ( cx , & blocks ) ;
165+ // Conditions
125166 lint_same_cond ( cx, & conds) ;
126167 lint_same_fns_in_if_cond ( cx, & conds) ;
168+ // Block duplication
169+ lint_same_then_else ( cx, & blocks, conds. len ( ) != blocks. len ( ) , expr) ;
127170 }
128171 }
129172}
130173
131- /// Implementation of `IF_SAME_THEN_ELSE`.
132- fn lint_same_then_else ( cx : & LateContext < ' _ > , blocks : & [ & Block < ' _ > ] ) {
133- let eq: & dyn Fn ( & & Block < ' _ > , & & Block < ' _ > ) -> bool =
134- & |& lhs, & rhs| -> bool { SpanlessEq :: new ( cx) . eq_block ( lhs, rhs) } ;
174+ /// Implementation of `SHARED_CODE_IN_IF_BLOCKS` and `IF_SAME_THEN_ELSE` if the blocks are equal.
175+ fn lint_same_then_else < ' tcx > (
176+ cx : & LateContext < ' tcx > ,
177+ blocks : & [ & Block < ' tcx > ] ,
178+ has_unconditional_else : bool ,
179+ expr : & ' tcx Expr < ' _ > ,
180+ ) {
181+ // We only lint ifs with multiple blocks
182+ // TODO xFrednet 2021-01-01: Check if it's an else if block
183+ if blocks. len ( ) < 2 {
184+ return ;
185+ }
135186
136- if let Some ( ( i, j) ) = search_same_sequenced ( blocks, eq) {
137- span_lint_and_note (
187+ let has_expr = blocks[ 0 ] . expr . is_some ( ) ;
188+
189+ // Check if each block has shared code
190+ let mut start_eq = usize:: MAX ;
191+ let mut end_eq = usize:: MAX ;
192+ let mut expr_eq = true ;
193+ for ( index, win) in blocks. windows ( 2 ) . enumerate ( ) {
194+ let l_stmts = win[ 0 ] . stmts ;
195+ let r_stmts = win[ 1 ] . stmts ;
196+
197+ let mut evaluator = SpanlessEq :: new ( cx) ;
198+ let current_start_eq = count_eq ( & mut l_stmts. iter ( ) , & mut r_stmts. iter ( ) , |l, r| evaluator. eq_stmt ( l, r) ) ;
199+ let current_end_eq = count_eq ( & mut l_stmts. iter ( ) . rev ( ) , & mut r_stmts. iter ( ) . rev ( ) , |l, r| {
200+ evaluator. eq_stmt ( l, r)
201+ } ) ;
202+ let block_expr_eq = both ( & win[ 0 ] . expr , & win[ 1 ] . expr , |l, r| evaluator. eq_expr ( l, r) ) ;
203+
204+ // IF_SAME_THEN_ELSE
205+ // We only lint the first two blocks (index == 0). Further blocks will be linted when that if
206+ // statement is checked
207+ if index == 0 && block_expr_eq && l_stmts. len ( ) == r_stmts. len ( ) && l_stmts. len ( ) == current_start_eq {
208+ span_lint_and_note (
209+ cx,
210+ IF_SAME_THEN_ELSE ,
211+ win[ 0 ] . span ,
212+ "this `if` has identical blocks" ,
213+ Some ( win[ 1 ] . span ) ,
214+ "same as this" ,
215+ ) ;
216+
217+ return ;
218+ }
219+
220+ start_eq = start_eq. min ( current_start_eq) ;
221+ end_eq = end_eq. min ( current_end_eq) ;
222+ expr_eq &= block_expr_eq;
223+
224+ // We can return if the eq count is 0 from both sides or if it has no unconditional else case
225+ if !has_unconditional_else || ( start_eq == 0 && end_eq == 0 && ( has_expr && !expr_eq) ) {
226+ return ;
227+ }
228+ }
229+
230+ if has_expr && !expr_eq {
231+ end_eq = 0 ;
232+ }
233+
234+ // Check if the regions are overlapping. Set `end_eq` to prevent the overlap
235+ let min_block_size = blocks. iter ( ) . map ( |x| x. stmts . len ( ) ) . min ( ) . unwrap ( ) ;
236+ if ( start_eq + end_eq) > min_block_size {
237+ end_eq = min_block_size - start_eq;
238+ }
239+
240+ // Only the start is the same
241+ if start_eq != 0 && end_eq == 0 && ( !has_expr || !expr_eq) {
242+ emit_shared_code_in_if_blocks_lint ( cx, start_eq, 0 , false , blocks, expr) ;
243+ } else if end_eq != 0 && ( !has_expr || !expr_eq) {
244+ let block = blocks[ blocks. len ( ) - 1 ] ;
245+ let stmts = block. stmts . split_at ( start_eq) . 1 ;
246+ let ( block_stmts, moved_stmts) = stmts. split_at ( stmts. len ( ) - end_eq) ;
247+
248+ // Scan block
249+ let mut walker = SymbolFinderVisitor :: new ( cx) ;
250+ for stmt in block_stmts {
251+ intravisit:: walk_stmt ( & mut walker, stmt) ;
252+ }
253+ let mut block_defs = walker. defs ;
254+
255+ // Scan moved stmts
256+ let mut moved_start: Option < usize > = None ;
257+ let mut walker = SymbolFinderVisitor :: new ( cx) ;
258+ for ( index, stmt) in moved_stmts. iter ( ) . enumerate ( ) {
259+ intravisit:: walk_stmt ( & mut walker, stmt) ;
260+
261+ for value in & walker. uses {
262+ // Well we can't move this and all prev statements. So reset
263+ if block_defs. contains ( & value) {
264+ moved_start = Some ( index + 1 ) ;
265+ walker. defs . drain ( ) . for_each ( |x| {
266+ block_defs. insert ( x) ;
267+ } ) ;
268+ }
269+ }
270+
271+ walker. uses . clear ( ) ;
272+ }
273+
274+ if let Some ( moved_start) = moved_start {
275+ end_eq -= moved_start;
276+ }
277+
278+ let mut end_linable = true ;
279+ if let Some ( expr) = block. expr {
280+ intravisit:: walk_expr ( & mut walker, expr) ;
281+ end_linable = walker. uses . iter ( ) . any ( |x| !block_defs. contains ( x) ) ;
282+ }
283+
284+ emit_shared_code_in_if_blocks_lint ( cx, start_eq, end_eq, end_linable, blocks, expr) ;
285+ }
286+ }
287+
288+ fn emit_shared_code_in_if_blocks_lint (
289+ cx : & LateContext < ' tcx > ,
290+ start_stmts : usize ,
291+ end_stmts : usize ,
292+ lint_end : bool ,
293+ blocks : & [ & Block < ' tcx > ] ,
294+ if_expr : & ' tcx Expr < ' _ > ,
295+ ) {
296+ if start_stmts == 0 && !lint_end {
297+ return ;
298+ }
299+
300+ // (help, span, suggestion)
301+ let mut suggestions: Vec < ( & str , Span , String ) > = vec ! [ ] ;
302+
303+ if start_stmts > 0 {
304+ let block = blocks[ 0 ] ;
305+ let span_start = first_line_of_span ( cx, if_expr. span ) . shrink_to_lo ( ) ;
306+ let span_end = block. stmts [ start_stmts - 1 ] . span . source_callsite ( ) ;
307+
308+ let cond_span = first_line_of_span ( cx, if_expr. span ) . until ( block. span ) ;
309+ let cond_snippet = reindent_multiline ( snippet ( cx, cond_span, "_" ) , false , None ) ;
310+ let cond_indent = indent_of ( cx, cond_span) ;
311+ let moved_span = block. stmts [ 0 ] . span . source_callsite ( ) . to ( span_end) ;
312+ let moved_snippet = reindent_multiline ( snippet ( cx, moved_span, "_" ) , true , None ) ;
313+ let suggestion = moved_snippet. to_string ( ) + "\n " + & cond_snippet + "{" ;
314+ let suggestion = reindent_multiline ( Cow :: Borrowed ( & suggestion) , true , cond_indent) ;
315+
316+ let span = span_start. to ( span_end) ;
317+ suggestions. push ( ( "START HELP" , span, suggestion. to_string ( ) ) ) ;
318+ }
319+
320+ if lint_end {
321+ let block = blocks[ blocks. len ( ) - 1 ] ;
322+ let span_end = block. span . shrink_to_hi ( ) ;
323+
324+ let moved_start = if end_stmts == 0 && block. expr . is_some ( ) {
325+ block. expr . unwrap ( ) . span
326+ } else {
327+ block. stmts [ block. stmts . len ( ) - end_stmts] . span
328+ }
329+ . source_callsite ( ) ;
330+ let moved_end = if let Some ( expr) = block. expr {
331+ expr. span
332+ } else {
333+ block. stmts [ block. stmts . len ( ) - 1 ] . span
334+ }
335+ . source_callsite ( ) ;
336+
337+ let moved_span = moved_start. to ( moved_end) ;
338+ let moved_snipped = reindent_multiline ( snippet ( cx, moved_span, "_" ) , true , None ) ;
339+ let indent = indent_of ( cx, if_expr. span . shrink_to_hi ( ) ) ;
340+ let suggestion = "}\n " . to_string ( ) + & moved_snipped;
341+ let suggestion = reindent_multiline ( Cow :: Borrowed ( & suggestion) , true , indent) ;
342+
343+ let span = moved_start. to ( span_end) ;
344+ suggestions. push ( ( "END_RANGE" , span, suggestion. to_string ( ) ) ) ;
345+ }
346+
347+ if suggestions. len ( ) == 1 {
348+ let ( _, span, sugg) = & suggestions[ 0 ] ;
349+ span_lint_and_sugg (
138350 cx,
139- IF_SAME_THEN_ELSE ,
140- j. span ,
141- "this `if` has identical blocks" ,
142- Some ( i. span ) ,
143- "same as this" ,
351+ SHARED_CODE_IN_IF_BLOCKS ,
352+ * span,
353+ "All code blocks contain the same code" ,
354+ "Consider moving the code out like this" ,
355+ sugg. clone ( ) ,
356+ Applicability :: Unspecified ,
357+ ) ;
358+ } else {
359+ span_lint_and_then (
360+ cx,
361+ SHARED_CODE_IN_IF_BLOCKS ,
362+ if_expr. span ,
363+ "All if blocks contain the same code" ,
364+ move |diag| {
365+ for ( help, span, sugg) in suggestions {
366+ diag. span_suggestion ( span, help, sugg, Applicability :: Unspecified ) ;
367+ }
368+ } ,
144369 ) ;
145370 }
146371}
147372
373+ pub struct SymbolFinderVisitor < ' a , ' tcx > {
374+ cx : & ' a LateContext < ' tcx > ,
375+ defs : FxHashSet < HirId > ,
376+ uses : FxHashSet < HirId > ,
377+ }
378+
379+ impl < ' a , ' tcx > SymbolFinderVisitor < ' a , ' tcx > {
380+ fn new ( cx : & ' a LateContext < ' tcx > ) -> Self {
381+ SymbolFinderVisitor {
382+ cx,
383+ defs : FxHashSet :: default ( ) ,
384+ uses : FxHashSet :: default ( ) ,
385+ }
386+ }
387+ }
388+
389+ impl < ' a , ' tcx > Visitor < ' tcx > for SymbolFinderVisitor < ' a , ' tcx > {
390+ type Map = Map < ' tcx > ;
391+
392+ fn nested_visit_map ( & mut self ) -> NestedVisitorMap < Self :: Map > {
393+ NestedVisitorMap :: All ( self . cx . tcx . hir ( ) )
394+ }
395+
396+ fn visit_local ( & mut self , l : & ' tcx rustc_hir:: Local < ' tcx > ) {
397+ let local_id = l. pat . hir_id ;
398+ self . defs . insert ( local_id) ;
399+ if let Some ( expr) = l. init {
400+ intravisit:: walk_expr ( self , expr) ;
401+ }
402+ }
403+
404+ fn visit_qpath ( & mut self , qpath : & ' tcx rustc_hir:: QPath < ' tcx > , id : HirId , _span : rustc_span:: Span ) {
405+ if let rustc_hir:: QPath :: Resolved ( _, ref path) = * qpath {
406+ if path. segments . len ( ) == 1 {
407+ if let rustc_hir:: def:: Res :: Local ( var) = self . cx . qpath_res ( qpath, id) {
408+ self . uses . insert ( var) ;
409+ }
410+ }
411+ }
412+ }
413+ }
414+
148415/// Implementation of `IFS_SAME_COND`.
149416fn lint_same_cond ( cx : & LateContext < ' _ > , conds : & [ & Expr < ' _ > ] ) {
150417 let hash: & dyn Fn ( & & Expr < ' _ > ) -> u64 = & |expr| -> u64 {
@@ -198,15 +465,3 @@ fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
198465 ) ;
199466 }
200467}
201-
202- fn search_same_sequenced < T , Eq > ( exprs : & [ T ] , eq : Eq ) -> Option < ( & T , & T ) >
203- where
204- Eq : Fn ( & T , & T ) -> bool ,
205- {
206- for win in exprs. windows ( 2 ) {
207- if eq ( & win[ 0 ] , & win[ 1 ] ) {
208- return Some ( ( & win[ 0 ] , & win[ 1 ] ) ) ;
209- }
210- }
211- None
212- }
0 commit comments