]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/copies.rs
Rollup merge of #95916 - solid-rs:feat-kmc-solid-abort, r=Mark-Simulacrum
[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::ty::needs_ordered_drop;
4 use clippy_utils::visitors::for_each_expr;
5 use clippy_utils::{
6     capture_local_usage, eq_expr_value, get_enclosing_block, hash_expr, hash_stmt, if_sequence, is_else_clause,
7     is_lint_allowed, path_to_local, search_same, ContainsName, HirEqInterExpr, SpanlessEq,
8 };
9 use core::iter;
10 use core::ops::ControlFlow;
11 use rustc_errors::Applicability;
12 use rustc_hir::intravisit;
13 use rustc_hir::{BinOpKind, Block, Expr, ExprKind, HirId, HirIdSet, Stmt, StmtKind};
14 use rustc_lint::{LateContext, LateLintPass};
15 use rustc_session::{declare_lint_pass, declare_tool_lint};
16 use rustc_span::hygiene::walk_chain;
17 use rustc_span::source_map::SourceMap;
18 use rustc_span::{BytePos, Span, Symbol};
19 use std::borrow::Cow;
20
21 declare_clippy_lint! {
22     /// ### What it does
23     /// Checks for consecutive `if`s with the same condition.
24     ///
25     /// ### Why is this bad?
26     /// This is probably a copy & paste error.
27     ///
28     /// ### Example
29     /// ```ignore
30     /// if a == b {
31     ///     …
32     /// } else if a == b {
33     ///     …
34     /// }
35     /// ```
36     ///
37     /// Note that this lint ignores all conditions with a function call as it could
38     /// have side effects:
39     ///
40     /// ```ignore
41     /// if foo() {
42     ///     …
43     /// } else if foo() { // not linted
44     ///     …
45     /// }
46     /// ```
47     #[clippy::version = "pre 1.29.0"]
48     pub IFS_SAME_COND,
49     correctness,
50     "consecutive `if`s with the same condition"
51 }
52
53 declare_clippy_lint! {
54     /// ### What it does
55     /// Checks for consecutive `if`s with the same function call.
56     ///
57     /// ### Why is this bad?
58     /// This is probably a copy & paste error.
59     /// Despite the fact that function can have side effects and `if` works as
60     /// intended, such an approach is implicit and can be considered a "code smell".
61     ///
62     /// ### Example
63     /// ```ignore
64     /// if foo() == bar {
65     ///     …
66     /// } else if foo() == bar {
67     ///     …
68     /// }
69     /// ```
70     ///
71     /// This probably should be:
72     /// ```ignore
73     /// if foo() == bar {
74     ///     …
75     /// } else if foo() == baz {
76     ///     …
77     /// }
78     /// ```
79     ///
80     /// or if the original code was not a typo and called function mutates a state,
81     /// consider move the mutation out of the `if` condition to avoid similarity to
82     /// a copy & paste error:
83     ///
84     /// ```ignore
85     /// let first = foo();
86     /// if first == bar {
87     ///     …
88     /// } else {
89     ///     let second = foo();
90     ///     if second == bar {
91     ///     …
92     ///     }
93     /// }
94     /// ```
95     #[clippy::version = "1.41.0"]
96     pub SAME_FUNCTIONS_IN_IF_CONDITION,
97     pedantic,
98     "consecutive `if`s with the same function call"
99 }
100
101 declare_clippy_lint! {
102     /// ### What it does
103     /// Checks for `if/else` with the same body as the *then* part
104     /// and the *else* part.
105     ///
106     /// ### Why is this bad?
107     /// This is probably a copy & paste error.
108     ///
109     /// ### Example
110     /// ```ignore
111     /// let foo = if … {
112     ///     42
113     /// } else {
114     ///     42
115     /// };
116     /// ```
117     #[clippy::version = "pre 1.29.0"]
118     pub IF_SAME_THEN_ELSE,
119     correctness,
120     "`if` with the same `then` and `else` blocks"
121 }
122
123 declare_clippy_lint! {
124     /// ### What it does
125     /// Checks if the `if` and `else` block contain shared code that can be
126     /// moved out of the blocks.
127     ///
128     /// ### Why is this bad?
129     /// Duplicate code is less maintainable.
130     ///
131     /// ### Known problems
132     /// * The lint doesn't check if the moved expressions modify values that are being used in
133     ///   the if condition. The suggestion can in that case modify the behavior of the program.
134     ///   See [rust-clippy#7452](https://github.com/rust-lang/rust-clippy/issues/7452)
135     ///
136     /// ### Example
137     /// ```ignore
138     /// let foo = if … {
139     ///     println!("Hello World");
140     ///     13
141     /// } else {
142     ///     println!("Hello World");
143     ///     42
144     /// };
145     /// ```
146     ///
147     /// Use instead:
148     /// ```ignore
149     /// println!("Hello World");
150     /// let foo = if … {
151     ///     13
152     /// } else {
153     ///     42
154     /// };
155     /// ```
156     #[clippy::version = "1.53.0"]
157     pub BRANCHES_SHARING_CODE,
158     nursery,
159     "`if` statement with shared code in all blocks"
160 }
161
162 declare_lint_pass!(CopyAndPaste => [
163     IFS_SAME_COND,
164     SAME_FUNCTIONS_IN_IF_CONDITION,
165     IF_SAME_THEN_ELSE,
166     BRANCHES_SHARING_CODE
167 ]);
168
169 impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
170     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
171         if !expr.span.from_expansion() && matches!(expr.kind, ExprKind::If(..)) && !is_else_clause(cx.tcx, expr) {
172             let (conds, blocks) = if_sequence(expr);
173             lint_same_cond(cx, &conds);
174             lint_same_fns_in_if_cond(cx, &conds);
175             let all_same =
176                 !is_lint_allowed(cx, IF_SAME_THEN_ELSE, expr.hir_id) && lint_if_same_then_else(cx, &conds, &blocks);
177             if !all_same && conds.len() != blocks.len() {
178                 lint_branches_sharing_code(cx, &conds, &blocks, expr);
179             }
180         }
181     }
182 }
183
184 /// Checks if the given expression is a let chain.
185 fn contains_let(e: &Expr<'_>) -> bool {
186     match e.kind {
187         ExprKind::Let(..) => true,
188         ExprKind::Binary(op, lhs, rhs) if op.node == BinOpKind::And => {
189             matches!(lhs.kind, ExprKind::Let(..)) || contains_let(rhs)
190         },
191         _ => false,
192     }
193 }
194
195 fn lint_if_same_then_else(cx: &LateContext<'_>, conds: &[&Expr<'_>], blocks: &[&Block<'_>]) -> bool {
196     let mut eq = SpanlessEq::new(cx);
197     blocks
198         .array_windows::<2>()
199         .enumerate()
200         .fold(true, |all_eq, (i, &[lhs, rhs])| {
201             if eq.eq_block(lhs, rhs) && !contains_let(conds[i]) && conds.get(i + 1).map_or(true, |e| !contains_let(e)) {
202                 span_lint_and_note(
203                     cx,
204                     IF_SAME_THEN_ELSE,
205                     lhs.span,
206                     "this `if` has identical blocks",
207                     Some(rhs.span),
208                     "same as this",
209                 );
210                 all_eq
211             } else {
212                 false
213             }
214         })
215 }
216
217 fn lint_branches_sharing_code<'tcx>(
218     cx: &LateContext<'tcx>,
219     conds: &[&'tcx Expr<'_>],
220     blocks: &[&'tcx Block<'_>],
221     expr: &'tcx Expr<'_>,
222 ) {
223     // We only lint ifs with multiple blocks
224     let &[first_block, ref blocks @ ..] = blocks else {
225         return;
226     };
227     let &[.., last_block] = blocks else {
228         return;
229     };
230
231     let res = scan_block_for_eq(cx, conds, first_block, blocks);
232     let sm = cx.tcx.sess.source_map();
233     let start_suggestion = res.start_span(first_block, sm).map(|span| {
234         let first_line_span = first_line_of_span(cx, expr.span);
235         let replace_span = first_line_span.with_hi(span.hi());
236         let cond_span = first_line_span.until(first_block.span);
237         let cond_snippet = reindent_multiline(snippet(cx, cond_span, "_"), false, None);
238         let cond_indent = indent_of(cx, cond_span);
239         let moved_snippet = reindent_multiline(snippet(cx, span, "_"), true, None);
240         let suggestion = moved_snippet.to_string() + "\n" + &cond_snippet + "{";
241         let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, cond_indent);
242         (replace_span, suggestion.to_string())
243     });
244     let end_suggestion = res.end_span(last_block, sm).map(|span| {
245         let moved_snipped = reindent_multiline(snippet(cx, span, "_"), true, None);
246         let indent = indent_of(cx, expr.span.shrink_to_hi());
247         let suggestion = "}\n".to_string() + &moved_snipped;
248         let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, indent);
249
250         let span = span.with_hi(last_block.span.hi());
251         // Improve formatting if the inner block has indention (i.e. normal Rust formatting)
252         let test_span = Span::new(span.lo() - BytePos(4), span.lo(), span.ctxt(), span.parent());
253         let span = if snippet_opt(cx, test_span).map_or(false, |snip| snip == "    ") {
254             span.with_lo(test_span.lo())
255         } else {
256             span
257         };
258         (span, suggestion.to_string())
259     });
260
261     let (span, msg, end_span) = match (&start_suggestion, &end_suggestion) {
262         (&Some((span, _)), &Some((end_span, _))) => (
263             span,
264             "all if blocks contain the same code at both the start and the end",
265             Some(end_span),
266         ),
267         (&Some((span, _)), None) => (span, "all if blocks contain the same code at the start", None),
268         (None, &Some((span, _))) => (span, "all if blocks contain the same code at the end", None),
269         (None, None) => return,
270     };
271     span_lint_and_then(cx, BRANCHES_SHARING_CODE, span, msg, |diag| {
272         if let Some(span) = end_span {
273             diag.span_note(span, "this code is shared at the end");
274         }
275         if let Some((span, sugg)) = start_suggestion {
276             diag.span_suggestion(
277                 span,
278                 "consider moving these statements before the if",
279                 sugg,
280                 Applicability::Unspecified,
281             );
282         }
283         if let Some((span, sugg)) = end_suggestion {
284             diag.span_suggestion(
285                 span,
286                 "consider moving these statements after the if",
287                 sugg,
288                 Applicability::Unspecified,
289             );
290             if !cx.typeck_results().expr_ty(expr).is_unit() {
291                 diag.note("the end suggestion probably needs some adjustments to use the expression result correctly");
292             }
293         }
294         if check_for_warn_of_moved_symbol(cx, &res.moved_locals, expr) {
295             diag.warn("some moved values might need to be renamed to avoid wrong references");
296         }
297     });
298 }
299
300 struct BlockEq {
301     /// The end of the range of equal stmts at the start.
302     start_end_eq: usize,
303     /// The start of the range of equal stmts at the end.
304     end_begin_eq: Option<usize>,
305     /// The name and id of every local which can be moved at the beginning and the end.
306     moved_locals: Vec<(HirId, Symbol)>,
307 }
308 impl BlockEq {
309     fn start_span(&self, b: &Block<'_>, sm: &SourceMap) -> Option<Span> {
310         match &b.stmts[..self.start_end_eq] {
311             [first, .., last] => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))),
312             [s] => Some(sm.stmt_span(s.span, b.span)),
313             [] => None,
314         }
315     }
316
317     fn end_span(&self, b: &Block<'_>, sm: &SourceMap) -> Option<Span> {
318         match (&b.stmts[b.stmts.len() - self.end_begin_eq?..], b.expr) {
319             ([first, .., last], None) => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))),
320             ([first, ..], Some(last)) => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))),
321             ([s], None) => Some(sm.stmt_span(s.span, b.span)),
322             ([], Some(e)) => Some(walk_chain(e.span, b.span.ctxt())),
323             ([], None) => None,
324         }
325     }
326 }
327
328 /// If the statement is a local, checks if the bound names match the expected list of names.
329 fn eq_binding_names(s: &Stmt<'_>, names: &[(HirId, Symbol)]) -> bool {
330     if let StmtKind::Local(l) = s.kind {
331         let mut i = 0usize;
332         let mut res = true;
333         l.pat.each_binding_or_first(&mut |_, _, _, name| {
334             if names.get(i).map_or(false, |&(_, n)| n == name.name) {
335                 i += 1;
336             } else {
337                 res = false;
338             }
339         });
340         res && i == names.len()
341     } else {
342         false
343     }
344 }
345
346 /// Checks if the statement modifies or moves any of the given locals.
347 fn modifies_any_local<'tcx>(cx: &LateContext<'tcx>, s: &'tcx Stmt<'_>, locals: &HirIdSet) -> bool {
348     for_each_expr(s, |e| {
349         if let Some(id) = path_to_local(e)
350             && locals.contains(&id)
351             && !capture_local_usage(cx, e).is_imm_ref()
352         {
353             ControlFlow::Break(())
354         } else {
355             ControlFlow::Continue(())
356         }
357     })
358     .is_some()
359 }
360
361 /// Checks if the given statement should be considered equal to the statement in the same position
362 /// for each block.
363 fn eq_stmts(
364     stmt: &Stmt<'_>,
365     blocks: &[&Block<'_>],
366     get_stmt: impl for<'a> Fn(&'a Block<'a>) -> Option<&'a Stmt<'a>>,
367     eq: &mut HirEqInterExpr<'_, '_, '_>,
368     moved_bindings: &mut Vec<(HirId, Symbol)>,
369 ) -> bool {
370     (if let StmtKind::Local(l) = stmt.kind {
371         let old_count = moved_bindings.len();
372         l.pat.each_binding_or_first(&mut |_, id, _, name| {
373             moved_bindings.push((id, name.name));
374         });
375         let new_bindings = &moved_bindings[old_count..];
376         blocks
377             .iter()
378             .all(|b| get_stmt(b).map_or(false, |s| eq_binding_names(s, new_bindings)))
379     } else {
380         true
381     }) && blocks
382         .iter()
383         .all(|b| get_stmt(b).map_or(false, |s| eq.eq_stmt(s, stmt)))
384 }
385
386 #[expect(clippy::too_many_lines)]
387 fn scan_block_for_eq<'tcx>(
388     cx: &LateContext<'tcx>,
389     conds: &[&'tcx Expr<'_>],
390     block: &'tcx Block<'_>,
391     blocks: &[&'tcx Block<'_>],
392 ) -> BlockEq {
393     let mut eq = SpanlessEq::new(cx);
394     let mut eq = eq.inter_expr();
395     let mut moved_locals = Vec::new();
396
397     let mut cond_locals = HirIdSet::default();
398     for &cond in conds {
399         let _: Option<!> = for_each_expr(cond, |e| {
400             if let Some(id) = path_to_local(e) {
401                 cond_locals.insert(id);
402             }
403             ControlFlow::Continue(())
404         });
405     }
406
407     let mut local_needs_ordered_drop = false;
408     let start_end_eq = block
409         .stmts
410         .iter()
411         .enumerate()
412         .find(|&(i, stmt)| {
413             if let StmtKind::Local(l) = stmt.kind
414                 && needs_ordered_drop(cx, cx.typeck_results().node_type(l.hir_id))
415             {
416                 local_needs_ordered_drop = true;
417                 return true;
418             }
419             modifies_any_local(cx, stmt, &cond_locals)
420                 || !eq_stmts(stmt, blocks, |b| b.stmts.get(i), &mut eq, &mut moved_locals)
421         })
422         .map_or(block.stmts.len(), |(i, _)| i);
423
424     if local_needs_ordered_drop {
425         return BlockEq {
426             start_end_eq,
427             end_begin_eq: None,
428             moved_locals,
429         };
430     }
431
432     // Walk backwards through the final expression/statements so long as their hashes are equal. Note
433     // `SpanlessHash` treats all local references as equal allowing locals declared earlier in the block
434     // to match those in other blocks. e.g. If each block ends with the following the hash value will be
435     // the same even though each `x` binding will have a different `HirId`:
436     //     let x = foo();
437     //     x + 50
438     let expr_hash_eq = if let Some(e) = block.expr {
439         let hash = hash_expr(cx, e);
440         blocks
441             .iter()
442             .all(|b| b.expr.map_or(false, |e| hash_expr(cx, e) == hash))
443     } else {
444         blocks.iter().all(|b| b.expr.is_none())
445     };
446     if !expr_hash_eq {
447         return BlockEq {
448             start_end_eq,
449             end_begin_eq: None,
450             moved_locals,
451         };
452     }
453     let end_search_start = block.stmts[start_end_eq..]
454         .iter()
455         .rev()
456         .enumerate()
457         .find(|&(offset, stmt)| {
458             let hash = hash_stmt(cx, stmt);
459             blocks.iter().any(|b| {
460                 b.stmts
461                     // the bounds check will catch the underflow
462                     .get(b.stmts.len().wrapping_sub(offset + 1))
463                     .map_or(true, |s| hash != hash_stmt(cx, s))
464             })
465         })
466         .map_or(block.stmts.len() - start_end_eq, |(i, _)| i);
467
468     let moved_locals_at_start = moved_locals.len();
469     let mut i = end_search_start;
470     let end_begin_eq = block.stmts[block.stmts.len() - end_search_start..]
471         .iter()
472         .zip(iter::repeat_with(move || {
473             let x = i;
474             i -= 1;
475             x
476         }))
477         .fold(end_search_start, |init, (stmt, offset)| {
478             if eq_stmts(
479                 stmt,
480                 blocks,
481                 |b| b.stmts.get(b.stmts.len() - offset),
482                 &mut eq,
483                 &mut moved_locals,
484             ) {
485                 init
486             } else {
487                 // Clear out all locals seen at the end so far. None of them can be moved.
488                 let stmts = &blocks[0].stmts;
489                 for stmt in &stmts[stmts.len() - init..=stmts.len() - offset] {
490                     if let StmtKind::Local(l) = stmt.kind {
491                         l.pat.each_binding_or_first(&mut |_, id, _, _| {
492                             eq.locals.remove(&id);
493                         });
494                     }
495                 }
496                 moved_locals.truncate(moved_locals_at_start);
497                 offset - 1
498             }
499         });
500     if let Some(e) = block.expr {
501         for block in blocks {
502             if block.expr.map_or(false, |expr| !eq.eq_expr(expr, e)) {
503                 moved_locals.truncate(moved_locals_at_start);
504                 return BlockEq {
505                     start_end_eq,
506                     end_begin_eq: None,
507                     moved_locals,
508                 };
509             }
510         }
511     }
512
513     BlockEq {
514         start_end_eq,
515         end_begin_eq: Some(end_begin_eq),
516         moved_locals,
517     }
518 }
519
520 fn check_for_warn_of_moved_symbol(cx: &LateContext<'_>, symbols: &[(HirId, Symbol)], if_expr: &Expr<'_>) -> bool {
521     get_enclosing_block(cx, if_expr.hir_id).map_or(false, |block| {
522         let ignore_span = block.span.shrink_to_lo().to(if_expr.span);
523
524         symbols
525             .iter()
526             .filter(|&&(_, name)| !name.as_str().starts_with('_'))
527             .any(|&(_, name)| {
528                 let mut walker = ContainsName { name, result: false };
529
530                 // Scan block
531                 block
532                     .stmts
533                     .iter()
534                     .filter(|stmt| !ignore_span.overlaps(stmt.span))
535                     .for_each(|stmt| intravisit::walk_stmt(&mut walker, stmt));
536
537                 if let Some(expr) = block.expr {
538                     intravisit::walk_expr(&mut walker, expr);
539                 }
540
541                 walker.result
542             })
543     })
544 }
545
546 /// Implementation of `IFS_SAME_COND`.
547 fn lint_same_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
548     for (i, j) in search_same(conds, |e| hash_expr(cx, e), |lhs, rhs| eq_expr_value(cx, lhs, rhs)) {
549         span_lint_and_note(
550             cx,
551             IFS_SAME_COND,
552             j.span,
553             "this `if` has the same condition as a previous `if`",
554             Some(i.span),
555             "same as this",
556         );
557     }
558 }
559
560 /// Implementation of `SAME_FUNCTIONS_IN_IF_CONDITION`.
561 fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
562     let eq: &dyn Fn(&&Expr<'_>, &&Expr<'_>) -> bool = &|&lhs, &rhs| -> bool {
563         // Do not lint if any expr originates from a macro
564         if lhs.span.from_expansion() || rhs.span.from_expansion() {
565             return false;
566         }
567         // Do not spawn warning if `IFS_SAME_COND` already produced it.
568         if eq_expr_value(cx, lhs, rhs) {
569             return false;
570         }
571         SpanlessEq::new(cx).eq_expr(lhs, rhs)
572     };
573
574     for (i, j) in search_same(conds, |e| hash_expr(cx, e), eq) {
575         span_lint_and_note(
576             cx,
577             SAME_FUNCTIONS_IN_IF_CONDITION,
578             j.span,
579             "this `if` has the same function call as a previous `if`",
580             Some(i.span),
581             "same as this",
582         );
583     }
584 }