]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/copies.rs
Added inferred local type comparion to SpanlessEq
[rust.git] / clippy_lints / src / copies.rs
1 use clippy_utils::diagnostics::{span_lint_and_note, span_lint_and_then};
2 use clippy_utils::source::{first_line_of_span, indent_of, reindent_multiline, snippet, snippet_opt};
3 use clippy_utils::{
4     both, count_eq, eq_expr_value, get_enclosing_block, get_parent_expr, if_sequence, in_macro, parent_node_is_if_expr,
5     run_lints, search_same, ContainsName, SpanlessEq, SpanlessHash,
6 };
7 use if_chain::if_chain;
8 use rustc_data_structures::fx::FxHashSet;
9 use rustc_errors::{Applicability, DiagnosticBuilder};
10 use rustc_hir::intravisit::{self, NestedVisitorMap, Visitor};
11 use rustc_hir::{Block, Expr, ExprKind, HirId};
12 use rustc_lint::{LateContext, LateLintPass};
13 use rustc_middle::hir::map::Map;
14 use rustc_session::{declare_lint_pass, declare_tool_lint};
15 use rustc_span::{source_map::Span, symbol::Symbol, BytePos};
16 use std::borrow::Cow;
17
18 declare_clippy_lint! {
19     /// **What it does:** Checks for consecutive `if`s with the same condition.
20     ///
21     /// **Why is this bad?** This is probably a copy & paste error.
22     ///
23     /// **Known problems:** Hopefully none.
24     ///
25     /// **Example:**
26     /// ```ignore
27     /// if a == b {
28     ///     …
29     /// } else if a == b {
30     ///     …
31     /// }
32     /// ```
33     ///
34     /// Note that this lint ignores all conditions with a function call as it could
35     /// have side effects:
36     ///
37     /// ```ignore
38     /// if foo() {
39     ///     …
40     /// } else if foo() { // not linted
41     ///     …
42     /// }
43     /// ```
44     pub IFS_SAME_COND,
45     correctness,
46     "consecutive `if`s with the same condition"
47 }
48
49 declare_clippy_lint! {
50     /// **What it does:** Checks for consecutive `if`s with the same function call.
51     ///
52     /// **Why is this bad?** This is probably a copy & paste error.
53     /// Despite the fact that function can have side effects and `if` works as
54     /// intended, such an approach is implicit and can be considered a "code smell".
55     ///
56     /// **Known problems:** Hopefully none.
57     ///
58     /// **Example:**
59     /// ```ignore
60     /// if foo() == bar {
61     ///     …
62     /// } else if foo() == bar {
63     ///     …
64     /// }
65     /// ```
66     ///
67     /// This probably should be:
68     /// ```ignore
69     /// if foo() == bar {
70     ///     …
71     /// } else if foo() == baz {
72     ///     …
73     /// }
74     /// ```
75     ///
76     /// or if the original code was not a typo and called function mutates a state,
77     /// consider move the mutation out of the `if` condition to avoid similarity to
78     /// a copy & paste error:
79     ///
80     /// ```ignore
81     /// let first = foo();
82     /// if first == bar {
83     ///     …
84     /// } else {
85     ///     let second = foo();
86     ///     if second == bar {
87     ///     …
88     ///     }
89     /// }
90     /// ```
91     pub SAME_FUNCTIONS_IN_IF_CONDITION,
92     pedantic,
93     "consecutive `if`s with the same function call"
94 }
95
96 declare_clippy_lint! {
97     /// **What it does:** Checks for `if/else` with the same body as the *then* part
98     /// and the *else* part.
99     ///
100     /// **Why is this bad?** This is probably a copy & paste error.
101     ///
102     /// **Known problems:** Hopefully none.
103     ///
104     /// **Example:**
105     /// ```ignore
106     /// let foo = if … {
107     ///     42
108     /// } else {
109     ///     42
110     /// };
111     /// ```
112     pub IF_SAME_THEN_ELSE,
113     correctness,
114     "`if` with the same `then` and `else` blocks"
115 }
116
117 declare_clippy_lint! {
118     /// **What it does:** Checks if the `if` and `else` block contain shared code that can be
119     /// moved out of the blocks.
120     ///
121     /// **Why is this bad?** Duplicate code is less maintainable.
122     ///
123     /// **Known problems:** Hopefully none.
124     ///
125     /// **Example:**
126     /// ```ignore
127     /// let foo = if … {
128     ///     println!("Hello World");
129     ///     13
130     /// } else {
131     ///     println!("Hello World");
132     ///     42
133     /// };
134     /// ```
135     ///
136     /// Could be written as:
137     /// ```ignore
138     /// println!("Hello World");
139     /// let foo = if … {
140     ///     13
141     /// } else {
142     ///     42
143     /// };
144     /// ```
145     pub BRANCHES_SHARING_CODE,
146     complexity,
147     "`if` statement with shared code in all blocks"
148 }
149
150 declare_lint_pass!(CopyAndPaste => [
151     IFS_SAME_COND,
152     SAME_FUNCTIONS_IN_IF_CONDITION,
153     IF_SAME_THEN_ELSE,
154     BRANCHES_SHARING_CODE
155 ]);
156
157 impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
158     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
159         if !expr.span.from_expansion() {
160             if let ExprKind::If(_, _, _) = expr.kind {
161                 // skip ifs directly in else, it will be checked in the parent if
162                 if let Some(&Expr {
163                     kind: ExprKind::If(_, _, Some(else_expr)),
164                     ..
165                 }) = get_parent_expr(cx, expr)
166                 {
167                     if else_expr.hir_id == expr.hir_id {
168                         return;
169                     }
170                 }
171
172                 let (conds, blocks) = if_sequence(expr);
173                 // Conditions
174                 lint_same_cond(cx, &conds);
175                 lint_same_fns_in_if_cond(cx, &conds);
176                 // Block duplication
177                 lint_same_then_else(cx, &blocks, conds.len() == blocks.len(), expr);
178             }
179         }
180     }
181 }
182
183 /// Implementation of `BRANCHES_SHARING_CODE` and `IF_SAME_THEN_ELSE` if the blocks are equal.
184 fn lint_same_then_else<'tcx>(
185     cx: &LateContext<'tcx>,
186     blocks: &[&Block<'tcx>],
187     has_conditional_else: bool,
188     expr: &'tcx Expr<'_>,
189 ) {
190     // We only lint ifs with multiple blocks
191     if blocks.len() < 2 || parent_node_is_if_expr(expr, cx) {
192         return;
193     }
194
195     // Check if each block has shared code
196     let has_expr = blocks[0].expr.is_some();
197     let (start_eq, mut end_eq, expr_eq) = scan_block_for_eq(cx, blocks);
198
199     // BRANCHES_SHARING_CODE prerequisites
200     if has_conditional_else || (start_eq == 0 && end_eq == 0 && (has_expr && !expr_eq)) {
201         return;
202     }
203
204     // Only the start is the same
205     if start_eq != 0 && end_eq == 0 && (!has_expr || !expr_eq) {
206         let block = blocks[0];
207         let start_stmts = block.stmts.split_at(start_eq).0;
208
209         let mut start_walker = UsedValueFinderVisitor::new(cx);
210         for stmt in start_stmts {
211             intravisit::walk_stmt(&mut start_walker, stmt);
212         }
213
214         emit_branches_sharing_code_lint(
215             cx,
216             start_eq,
217             0,
218             false,
219             check_for_warn_of_moved_symbol(cx, &start_walker.def_symbols, expr),
220             blocks,
221             expr,
222         );
223     } else if end_eq != 0 || (has_expr && expr_eq) {
224         let block = blocks[blocks.len() - 1];
225         let (start_stmts, block_stmts) = block.stmts.split_at(start_eq);
226         let (block_stmts, end_stmts) = block_stmts.split_at(block_stmts.len() - end_eq);
227
228         // Scan start
229         let mut start_walker = UsedValueFinderVisitor::new(cx);
230         for stmt in start_stmts {
231             intravisit::walk_stmt(&mut start_walker, stmt);
232         }
233         let mut moved_syms = start_walker.def_symbols;
234
235         // Scan block
236         let mut block_walker = UsedValueFinderVisitor::new(cx);
237         for stmt in block_stmts {
238             intravisit::walk_stmt(&mut block_walker, stmt);
239         }
240         let mut block_defs = block_walker.defs;
241
242         // Scan moved stmts
243         let mut moved_start: Option<usize> = None;
244         let mut end_walker = UsedValueFinderVisitor::new(cx);
245         for (index, stmt) in end_stmts.iter().enumerate() {
246             intravisit::walk_stmt(&mut end_walker, stmt);
247
248             for value in &end_walker.uses {
249                 // Well we can't move this and all prev statements. So reset
250                 if block_defs.contains(value) {
251                     moved_start = Some(index + 1);
252                     end_walker.defs.drain().for_each(|x| {
253                         block_defs.insert(x);
254                     });
255
256                     end_walker.def_symbols.clear();
257                 }
258             }
259
260             end_walker.uses.clear();
261         }
262
263         if let Some(moved_start) = moved_start {
264             end_eq -= moved_start;
265         }
266
267         let end_linable = block.expr.map_or_else(
268             || end_eq != 0,
269             |expr| {
270                 intravisit::walk_expr(&mut end_walker, expr);
271                 end_walker.uses.iter().any(|x| !block_defs.contains(x))
272             },
273         );
274
275         if end_linable {
276             end_walker.def_symbols.drain().for_each(|x| {
277                 moved_syms.insert(x);
278             });
279         }
280
281         emit_branches_sharing_code_lint(
282             cx,
283             start_eq,
284             end_eq,
285             end_linable,
286             check_for_warn_of_moved_symbol(cx, &moved_syms, expr),
287             blocks,
288             expr,
289         );
290     }
291 }
292
293 /// The return tuple is structured as follows:
294 /// 1. The amount of equal statements from the start
295 /// 2. The amount of equal statements from the end
296 /// 3. An indication if the block expressions are the same. This will also be true if both are
297 /// `None`
298 ///
299 /// This function can also trigger the `IF_SAME_THEN_ELSE` in which case it'll return `(0, 0,
300 /// false)` to aboard any further processing and avoid duplicate lint triggers.
301 fn scan_block_for_eq(cx: &LateContext<'tcx>, blocks: &[&Block<'tcx>]) -> (usize, usize, bool) {
302     let mut start_eq = usize::MAX;
303     let mut end_eq = usize::MAX;
304     let mut expr_eq = true;
305     for win in blocks.windows(2) {
306         let l_stmts = win[0].stmts;
307         let r_stmts = win[1].stmts;
308
309         // `SpanlessEq` now keeps track of the locals and is therefore context sensitive clippy#6752.
310         // The comparison therefore needs to be done in a way that builds the correct context.
311         let mut evaluator = SpanlessEq::new(cx).enable_check_inferred_local_types();
312         let mut evaluator = evaluator.inter_expr();
313
314         let current_start_eq = count_eq(&mut l_stmts.iter(), &mut r_stmts.iter(), |l, r| evaluator.eq_stmt(l, r));
315
316         let current_end_eq = {
317             // We skip the middle statements which can't be equal
318             let end_comparison_count = l_stmts.len().min(r_stmts.len()) - current_start_eq;
319             let it1 = l_stmts.iter().skip(l_stmts.len() - end_comparison_count);
320             let it2 = r_stmts.iter().skip(r_stmts.len() - end_comparison_count);
321             it1.zip(it2)
322                 .fold(0, |acc, (l, r)| if evaluator.eq_stmt(l, r) { acc + 1 } else { 0 })
323         };
324         let block_expr_eq = both(&win[0].expr, &win[1].expr, |l, r| evaluator.eq_expr(l, r));
325
326         // IF_SAME_THEN_ELSE
327         if_chain! {
328             if block_expr_eq;
329             if l_stmts.len() == r_stmts.len();
330             if l_stmts.len() == current_start_eq;
331             if run_lints(cx, &[IF_SAME_THEN_ELSE], win[0].hir_id);
332             if run_lints(cx, &[IF_SAME_THEN_ELSE], win[1].hir_id);
333             then {
334                 span_lint_and_note(
335                     cx,
336                     IF_SAME_THEN_ELSE,
337                     win[0].span,
338                     "this `if` has identical blocks",
339                     Some(win[1].span),
340                     "same as this",
341                 );
342
343                 return (0, 0, false);
344             }
345         }
346
347         start_eq = start_eq.min(current_start_eq);
348         end_eq = end_eq.min(current_end_eq);
349         expr_eq &= block_expr_eq;
350     }
351
352     let has_expr = blocks[0].expr.is_some();
353     if has_expr && !expr_eq {
354         end_eq = 0;
355     }
356
357     // Check if the regions are overlapping. Set `end_eq` to prevent the overlap
358     let min_block_size = blocks.iter().map(|x| x.stmts.len()).min().unwrap();
359     if (start_eq + end_eq) > min_block_size {
360         end_eq = min_block_size - start_eq;
361     }
362
363     (start_eq, end_eq, expr_eq)
364 }
365
366 fn check_for_warn_of_moved_symbol(
367     cx: &LateContext<'tcx>,
368     symbols: &FxHashSet<Symbol>,
369     if_expr: &'tcx Expr<'_>,
370 ) -> bool {
371     get_enclosing_block(cx, if_expr.hir_id).map_or(false, |block| {
372         let ignore_span = block.span.shrink_to_lo().to(if_expr.span);
373
374         symbols
375             .iter()
376             .filter(|sym| !sym.as_str().starts_with('_'))
377             .any(move |sym| {
378                 let mut walker = ContainsName {
379                     name: *sym,
380                     result: false,
381                 };
382
383                 // Scan block
384                 block
385                     .stmts
386                     .iter()
387                     .filter(|stmt| !ignore_span.overlaps(stmt.span))
388                     .for_each(|stmt| intravisit::walk_stmt(&mut walker, stmt));
389
390                 if let Some(expr) = block.expr {
391                     intravisit::walk_expr(&mut walker, expr);
392                 }
393
394                 walker.result
395             })
396     })
397 }
398
399 fn emit_branches_sharing_code_lint(
400     cx: &LateContext<'tcx>,
401     start_stmts: usize,
402     end_stmts: usize,
403     lint_end: bool,
404     warn_about_moved_symbol: bool,
405     blocks: &[&Block<'tcx>],
406     if_expr: &'tcx Expr<'_>,
407 ) {
408     if start_stmts == 0 && !lint_end {
409         return;
410     }
411
412     // (help, span, suggestion)
413     let mut suggestions: Vec<(&str, Span, String)> = vec![];
414     let mut add_expr_note = false;
415
416     // Construct suggestions
417     if start_stmts > 0 {
418         let block = blocks[0];
419         let span_start = first_line_of_span(cx, if_expr.span).shrink_to_lo();
420         let span_end = block.stmts[start_stmts - 1].span.source_callsite();
421
422         let cond_span = first_line_of_span(cx, if_expr.span).until(block.span);
423         let cond_snippet = reindent_multiline(snippet(cx, cond_span, "_"), false, None);
424         let cond_indent = indent_of(cx, cond_span);
425         let moved_span = block.stmts[0].span.source_callsite().to(span_end);
426         let moved_snippet = reindent_multiline(snippet(cx, moved_span, "_"), true, None);
427         let suggestion = moved_snippet.to_string() + "\n" + &cond_snippet + "{";
428         let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, cond_indent);
429
430         let span = span_start.to(span_end);
431         suggestions.push(("start", span, suggestion.to_string()));
432     }
433
434     if lint_end {
435         let block = blocks[blocks.len() - 1];
436         let span_end = block.span.shrink_to_hi();
437
438         let moved_start = if end_stmts == 0 && block.expr.is_some() {
439             block.expr.unwrap().span
440         } else {
441             block.stmts[block.stmts.len() - end_stmts].span
442         }
443         .source_callsite();
444         let moved_end = block
445             .expr
446             .map_or_else(|| block.stmts[block.stmts.len() - 1].span, |expr| expr.span)
447             .source_callsite();
448
449         let moved_span = moved_start.to(moved_end);
450         let moved_snipped = reindent_multiline(snippet(cx, moved_span, "_"), true, None);
451         let indent = indent_of(cx, if_expr.span.shrink_to_hi());
452         let suggestion = "}\n".to_string() + &moved_snipped;
453         let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, indent);
454
455         let mut span = moved_start.to(span_end);
456         // Improve formatting if the inner block has indention (i.e. normal Rust formatting)
457         let test_span = Span::new(span.lo() - BytePos(4), span.lo(), span.ctxt());
458         if snippet_opt(cx, test_span)
459             .map(|snip| snip == "    ")
460             .unwrap_or_default()
461         {
462             span = span.with_lo(test_span.lo());
463         }
464
465         suggestions.push(("end", span, suggestion.to_string()));
466         add_expr_note = !cx.typeck_results().expr_ty(if_expr).is_unit()
467     }
468
469     let add_optional_msgs = |diag: &mut DiagnosticBuilder<'_>| {
470         if add_expr_note {
471             diag.note("The end suggestion probably needs some adjustments to use the expression result correctly");
472         }
473
474         if warn_about_moved_symbol {
475             diag.warn("Some moved values might need to be renamed to avoid wrong references");
476         }
477     };
478
479     // Emit lint
480     if suggestions.len() == 1 {
481         let (place_str, span, sugg) = suggestions.pop().unwrap();
482         let msg = format!("all if blocks contain the same code at the {}", place_str);
483         let help = format!("consider moving the {} statements out like this", place_str);
484         span_lint_and_then(cx, BRANCHES_SHARING_CODE, span, msg.as_str(), |diag| {
485             diag.span_suggestion(span, help.as_str(), sugg, Applicability::Unspecified);
486
487             add_optional_msgs(diag);
488         });
489     } else if suggestions.len() == 2 {
490         let (_, end_span, end_sugg) = suggestions.pop().unwrap();
491         let (_, start_span, start_sugg) = suggestions.pop().unwrap();
492         span_lint_and_then(
493             cx,
494             BRANCHES_SHARING_CODE,
495             start_span,
496             "all if blocks contain the same code at the start and the end. Here at the start",
497             move |diag| {
498                 diag.span_note(end_span, "and here at the end");
499
500                 diag.span_suggestion(
501                     start_span,
502                     "consider moving the start statements out like this",
503                     start_sugg,
504                     Applicability::Unspecified,
505                 );
506
507                 diag.span_suggestion(
508                     end_span,
509                     "and consider moving the end statements out like this",
510                     end_sugg,
511                     Applicability::Unspecified,
512                 );
513
514                 add_optional_msgs(diag);
515             },
516         );
517     }
518 }
519
520 /// This visitor collects `HirId`s and Symbols of defined symbols and `HirId`s of used values.
521 struct UsedValueFinderVisitor<'a, 'tcx> {
522     cx: &'a LateContext<'tcx>,
523
524     /// The `HirId`s of defined values in the scanned statements
525     defs: FxHashSet<HirId>,
526
527     /// The Symbols of the defined symbols in the scanned statements
528     def_symbols: FxHashSet<Symbol>,
529
530     /// The `HirId`s of the used values
531     uses: FxHashSet<HirId>,
532 }
533
534 impl<'a, 'tcx> UsedValueFinderVisitor<'a, 'tcx> {
535     fn new(cx: &'a LateContext<'tcx>) -> Self {
536         UsedValueFinderVisitor {
537             cx,
538             defs: FxHashSet::default(),
539             def_symbols: FxHashSet::default(),
540             uses: FxHashSet::default(),
541         }
542     }
543 }
544
545 impl<'a, 'tcx> Visitor<'tcx> for UsedValueFinderVisitor<'a, 'tcx> {
546     type Map = Map<'tcx>;
547
548     fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
549         NestedVisitorMap::All(self.cx.tcx.hir())
550     }
551
552     fn visit_local(&mut self, l: &'tcx rustc_hir::Local<'tcx>) {
553         let local_id = l.pat.hir_id;
554         self.defs.insert(local_id);
555
556         if let Some(sym) = l.pat.simple_ident() {
557             self.def_symbols.insert(sym.name);
558         }
559
560         if let Some(expr) = l.init {
561             intravisit::walk_expr(self, expr);
562         }
563     }
564
565     fn visit_qpath(&mut self, qpath: &'tcx rustc_hir::QPath<'tcx>, id: HirId, _span: rustc_span::Span) {
566         if let rustc_hir::QPath::Resolved(_, path) = *qpath {
567             if path.segments.len() == 1 {
568                 if let rustc_hir::def::Res::Local(var) = self.cx.qpath_res(qpath, id) {
569                     self.uses.insert(var);
570                 }
571             }
572         }
573     }
574 }
575
576 /// Implementation of `IFS_SAME_COND`.
577 fn lint_same_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
578     let hash: &dyn Fn(&&Expr<'_>) -> u64 = &|expr| -> u64 {
579         let mut h = SpanlessHash::new(cx);
580         h.hash_expr(expr);
581         h.finish()
582     };
583
584     let eq: &dyn Fn(&&Expr<'_>, &&Expr<'_>) -> bool = &|&lhs, &rhs| -> bool { eq_expr_value(cx, lhs, rhs) };
585
586     for (i, j) in search_same(conds, hash, eq) {
587         span_lint_and_note(
588             cx,
589             IFS_SAME_COND,
590             j.span,
591             "this `if` has the same condition as a previous `if`",
592             Some(i.span),
593             "same as this",
594         );
595     }
596 }
597
598 /// Implementation of `SAME_FUNCTIONS_IN_IF_CONDITION`.
599 fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
600     let hash: &dyn Fn(&&Expr<'_>) -> u64 = &|expr| -> u64 {
601         let mut h = SpanlessHash::new(cx);
602         h.hash_expr(expr);
603         h.finish()
604     };
605
606     let eq: &dyn Fn(&&Expr<'_>, &&Expr<'_>) -> bool = &|&lhs, &rhs| -> bool {
607         // Do not lint if any expr originates from a macro
608         if in_macro(lhs.span) || in_macro(rhs.span) {
609             return false;
610         }
611         // Do not spawn warning if `IFS_SAME_COND` already produced it.
612         if eq_expr_value(cx, lhs, rhs) {
613             return false;
614         }
615         SpanlessEq::new(cx).eq_expr(lhs, rhs)
616     };
617
618     for (i, j) in search_same(conds, hash, eq) {
619         span_lint_and_note(
620             cx,
621             SAME_FUNCTIONS_IN_IF_CONDITION,
622             j.span,
623             "this `if` has the same function call as a previous `if`",
624             Some(i.span),
625             "same as this",
626         );
627     }
628 }