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