]> git.proxmox.com Git - rustc.git/blob - src/tools/clippy/clippy_lints/src/copies.rs
bump version to 1.80.1+dfsg1-1~bpo12+pve1
[rustc.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::{is_interior_mut_ty, needs_ordered_drop};
4 use clippy_utils::visitors::for_each_expr;
5 use clippy_utils::{
6 capture_local_usage, def_path_def_ids, eq_expr_value, find_binding_init, get_enclosing_block, hash_expr, hash_stmt,
7 if_sequence, is_else_clause, 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::def_id::DefIdSet;
13 use rustc_hir::{intravisit, BinOpKind, Block, Expr, ExprKind, HirId, HirIdSet, Stmt, StmtKind};
14 use rustc_lint::{LateContext, LateLintPass};
15 use rustc_session::impl_lint_pass;
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 style,
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 pub struct CopyAndPaste {
163 ignore_interior_mutability: Vec<String>,
164 ignored_ty_ids: DefIdSet,
165 }
166
167 impl CopyAndPaste {
168 pub fn new(ignore_interior_mutability: Vec<String>) -> Self {
169 Self {
170 ignore_interior_mutability,
171 ignored_ty_ids: DefIdSet::new(),
172 }
173 }
174 }
175
176 impl_lint_pass!(CopyAndPaste => [
177 IFS_SAME_COND,
178 SAME_FUNCTIONS_IN_IF_CONDITION,
179 IF_SAME_THEN_ELSE,
180 BRANCHES_SHARING_CODE
181 ]);
182
183 impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
184 fn check_crate(&mut self, cx: &LateContext<'tcx>) {
185 for ignored_ty in &self.ignore_interior_mutability {
186 let path: Vec<&str> = ignored_ty.split("::").collect();
187 for id in def_path_def_ids(cx, path.as_slice()) {
188 self.ignored_ty_ids.insert(id);
189 }
190 }
191 }
192 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
193 if !expr.span.from_expansion() && matches!(expr.kind, ExprKind::If(..)) && !is_else_clause(cx.tcx, expr) {
194 let (conds, blocks) = if_sequence(expr);
195 lint_same_cond(cx, &conds, &self.ignored_ty_ids);
196 lint_same_fns_in_if_cond(cx, &conds);
197 let all_same =
198 !is_lint_allowed(cx, IF_SAME_THEN_ELSE, expr.hir_id) && lint_if_same_then_else(cx, &conds, &blocks);
199 if !all_same && conds.len() != blocks.len() {
200 lint_branches_sharing_code(cx, &conds, &blocks, expr);
201 }
202 }
203 }
204 }
205
206 /// Checks if the given expression is a let chain.
207 fn contains_let(e: &Expr<'_>) -> bool {
208 match e.kind {
209 ExprKind::Let(..) => true,
210 ExprKind::Binary(op, lhs, rhs) if op.node == BinOpKind::And => {
211 matches!(lhs.kind, ExprKind::Let(..)) || contains_let(rhs)
212 },
213 _ => false,
214 }
215 }
216
217 fn lint_if_same_then_else(cx: &LateContext<'_>, conds: &[&Expr<'_>], blocks: &[&Block<'_>]) -> bool {
218 let mut eq = SpanlessEq::new(cx);
219 blocks
220 .array_windows::<2>()
221 .enumerate()
222 .fold(true, |all_eq, (i, &[lhs, rhs])| {
223 if eq.eq_block(lhs, rhs) && !contains_let(conds[i]) && conds.get(i + 1).map_or(true, |e| !contains_let(e)) {
224 span_lint_and_note(
225 cx,
226 IF_SAME_THEN_ELSE,
227 lhs.span,
228 "this `if` has identical blocks",
229 Some(rhs.span),
230 "same as this",
231 );
232 all_eq
233 } else {
234 false
235 }
236 })
237 }
238
239 fn lint_branches_sharing_code<'tcx>(
240 cx: &LateContext<'tcx>,
241 conds: &[&'tcx Expr<'_>],
242 blocks: &[&'tcx Block<'_>],
243 expr: &'tcx Expr<'_>,
244 ) {
245 // We only lint ifs with multiple blocks
246 let &[first_block, ref blocks @ ..] = blocks else {
247 return;
248 };
249 let &[.., last_block] = blocks else {
250 return;
251 };
252
253 let res = scan_block_for_eq(cx, conds, first_block, blocks);
254 let sm = cx.tcx.sess.source_map();
255 let start_suggestion = res.start_span(first_block, sm).map(|span| {
256 let first_line_span = first_line_of_span(cx, expr.span);
257 let replace_span = first_line_span.with_hi(span.hi());
258 let cond_span = first_line_span.until(first_block.span);
259 let cond_snippet = reindent_multiline(snippet(cx, cond_span, "_"), false, None);
260 let cond_indent = indent_of(cx, cond_span);
261 let moved_snippet = reindent_multiline(snippet(cx, span, "_"), true, None);
262 let suggestion = moved_snippet.to_string() + "\n" + &cond_snippet + "{";
263 let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, cond_indent);
264 (replace_span, suggestion.to_string())
265 });
266 let end_suggestion = res.end_span(last_block, sm).map(|span| {
267 let moved_snipped = reindent_multiline(snippet(cx, span, "_"), true, None);
268 let indent = indent_of(cx, expr.span.shrink_to_hi());
269 let suggestion = "}\n".to_string() + &moved_snipped;
270 let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, indent);
271
272 let span = span.with_hi(last_block.span.hi());
273 // Improve formatting if the inner block has indention (i.e. normal Rust formatting)
274 let test_span = Span::new(span.lo() - BytePos(4), span.lo(), span.ctxt(), span.parent());
275 let span = if snippet_opt(cx, test_span).map_or(false, |snip| snip == " ") {
276 span.with_lo(test_span.lo())
277 } else {
278 span
279 };
280 (span, suggestion.to_string())
281 });
282
283 let (span, msg, end_span) = match (&start_suggestion, &end_suggestion) {
284 (&Some((span, _)), &Some((end_span, _))) => (
285 span,
286 "all if blocks contain the same code at both the start and the end",
287 Some(end_span),
288 ),
289 (&Some((span, _)), None) => (span, "all if blocks contain the same code at the start", None),
290 (None, &Some((span, _))) => (span, "all if blocks contain the same code at the end", None),
291 (None, None) => return,
292 };
293 span_lint_and_then(cx, BRANCHES_SHARING_CODE, span, msg, |diag| {
294 if let Some(span) = end_span {
295 diag.span_note(span, "this code is shared at the end");
296 }
297 if let Some((span, sugg)) = start_suggestion {
298 diag.span_suggestion(
299 span,
300 "consider moving these statements before the if",
301 sugg,
302 Applicability::Unspecified,
303 );
304 }
305 if let Some((span, sugg)) = end_suggestion {
306 diag.span_suggestion(
307 span,
308 "consider moving these statements after the if",
309 sugg,
310 Applicability::Unspecified,
311 );
312 if !cx.typeck_results().expr_ty(expr).is_unit() {
313 diag.note("the end suggestion probably needs some adjustments to use the expression result correctly");
314 }
315 }
316 if check_for_warn_of_moved_symbol(cx, &res.moved_locals, expr) {
317 diag.warn("some moved values might need to be renamed to avoid wrong references");
318 }
319 });
320 }
321
322 struct BlockEq {
323 /// The end of the range of equal stmts at the start.
324 start_end_eq: usize,
325 /// The start of the range of equal stmts at the end.
326 end_begin_eq: Option<usize>,
327 /// The name and id of every local which can be moved at the beginning and the end.
328 moved_locals: Vec<(HirId, Symbol)>,
329 }
330 impl BlockEq {
331 fn start_span(&self, b: &Block<'_>, sm: &SourceMap) -> Option<Span> {
332 match &b.stmts[..self.start_end_eq] {
333 [first, .., last] => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))),
334 [s] => Some(sm.stmt_span(s.span, b.span)),
335 [] => None,
336 }
337 }
338
339 fn end_span(&self, b: &Block<'_>, sm: &SourceMap) -> Option<Span> {
340 match (&b.stmts[b.stmts.len() - self.end_begin_eq?..], b.expr) {
341 ([first, .., last], None) => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))),
342 ([first, ..], Some(last)) => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))),
343 ([s], None) => Some(sm.stmt_span(s.span, b.span)),
344 ([], Some(e)) => Some(walk_chain(e.span, b.span.ctxt())),
345 ([], None) => None,
346 }
347 }
348 }
349
350 /// If the statement is a local, checks if the bound names match the expected list of names.
351 fn eq_binding_names(s: &Stmt<'_>, names: &[(HirId, Symbol)]) -> bool {
352 if let StmtKind::Let(l) = s.kind {
353 let mut i = 0usize;
354 let mut res = true;
355 l.pat.each_binding_or_first(&mut |_, _, _, name| {
356 if names.get(i).map_or(false, |&(_, n)| n == name.name) {
357 i += 1;
358 } else {
359 res = false;
360 }
361 });
362 res && i == names.len()
363 } else {
364 false
365 }
366 }
367
368 /// Checks if the statement modifies or moves any of the given locals.
369 fn modifies_any_local<'tcx>(cx: &LateContext<'tcx>, s: &'tcx Stmt<'_>, locals: &HirIdSet) -> bool {
370 for_each_expr(s, |e| {
371 if let Some(id) = path_to_local(e)
372 && locals.contains(&id)
373 && !capture_local_usage(cx, e).is_imm_ref()
374 {
375 ControlFlow::Break(())
376 } else {
377 ControlFlow::Continue(())
378 }
379 })
380 .is_some()
381 }
382
383 /// Checks if the given statement should be considered equal to the statement in the same position
384 /// for each block.
385 fn eq_stmts(
386 stmt: &Stmt<'_>,
387 blocks: &[&Block<'_>],
388 get_stmt: impl for<'a> Fn(&'a Block<'a>) -> Option<&'a Stmt<'a>>,
389 eq: &mut HirEqInterExpr<'_, '_, '_>,
390 moved_bindings: &mut Vec<(HirId, Symbol)>,
391 ) -> bool {
392 (if let StmtKind::Let(l) = stmt.kind {
393 let old_count = moved_bindings.len();
394 l.pat.each_binding_or_first(&mut |_, id, _, name| {
395 moved_bindings.push((id, name.name));
396 });
397 let new_bindings = &moved_bindings[old_count..];
398 blocks
399 .iter()
400 .all(|b| get_stmt(b).map_or(false, |s| eq_binding_names(s, new_bindings)))
401 } else {
402 true
403 }) && blocks
404 .iter()
405 .all(|b| get_stmt(b).map_or(false, |s| eq.eq_stmt(s, stmt)))
406 }
407
408 #[expect(clippy::too_many_lines)]
409 fn scan_block_for_eq<'tcx>(
410 cx: &LateContext<'tcx>,
411 conds: &[&'tcx Expr<'_>],
412 block: &'tcx Block<'_>,
413 blocks: &[&'tcx Block<'_>],
414 ) -> BlockEq {
415 let mut eq = SpanlessEq::new(cx);
416 let mut eq = eq.inter_expr();
417 let mut moved_locals = Vec::new();
418
419 let mut cond_locals = HirIdSet::default();
420 for &cond in conds {
421 let _: Option<!> = for_each_expr(cond, |e| {
422 if let Some(id) = path_to_local(e) {
423 cond_locals.insert(id);
424 }
425 ControlFlow::Continue(())
426 });
427 }
428
429 let mut local_needs_ordered_drop = false;
430 let start_end_eq = block
431 .stmts
432 .iter()
433 .enumerate()
434 .find(|&(i, stmt)| {
435 if let StmtKind::Let(l) = stmt.kind
436 && needs_ordered_drop(cx, cx.typeck_results().node_type(l.hir_id))
437 {
438 local_needs_ordered_drop = true;
439 return true;
440 }
441 modifies_any_local(cx, stmt, &cond_locals)
442 || !eq_stmts(stmt, blocks, |b| b.stmts.get(i), &mut eq, &mut moved_locals)
443 })
444 .map_or(block.stmts.len(), |(i, _)| i);
445
446 if local_needs_ordered_drop {
447 return BlockEq {
448 start_end_eq,
449 end_begin_eq: None,
450 moved_locals,
451 };
452 }
453
454 // Walk backwards through the final expression/statements so long as their hashes are equal. Note
455 // `SpanlessHash` treats all local references as equal allowing locals declared earlier in the block
456 // to match those in other blocks. e.g. If each block ends with the following the hash value will be
457 // the same even though each `x` binding will have a different `HirId`:
458 // let x = foo();
459 // x + 50
460 let expr_hash_eq = if let Some(e) = block.expr {
461 let hash = hash_expr(cx, e);
462 blocks
463 .iter()
464 .all(|b| b.expr.map_or(false, |e| hash_expr(cx, e) == hash))
465 } else {
466 blocks.iter().all(|b| b.expr.is_none())
467 };
468 if !expr_hash_eq {
469 return BlockEq {
470 start_end_eq,
471 end_begin_eq: None,
472 moved_locals,
473 };
474 }
475 let end_search_start = block.stmts[start_end_eq..]
476 .iter()
477 .rev()
478 .enumerate()
479 .find(|&(offset, stmt)| {
480 let hash = hash_stmt(cx, stmt);
481 blocks.iter().any(|b| {
482 b.stmts
483 // the bounds check will catch the underflow
484 .get(b.stmts.len().wrapping_sub(offset + 1))
485 .map_or(true, |s| hash != hash_stmt(cx, s))
486 })
487 })
488 .map_or(block.stmts.len() - start_end_eq, |(i, _)| i);
489
490 let moved_locals_at_start = moved_locals.len();
491 let mut i = end_search_start;
492 let end_begin_eq = block.stmts[block.stmts.len() - end_search_start..]
493 .iter()
494 .zip(iter::repeat_with(move || {
495 let x = i;
496 i -= 1;
497 x
498 }))
499 .fold(end_search_start, |init, (stmt, offset)| {
500 if eq_stmts(
501 stmt,
502 blocks,
503 |b| b.stmts.get(b.stmts.len() - offset),
504 &mut eq,
505 &mut moved_locals,
506 ) {
507 init
508 } else {
509 // Clear out all locals seen at the end so far. None of them can be moved.
510 let stmts = &blocks[0].stmts;
511 for stmt in &stmts[stmts.len() - init..=stmts.len() - offset] {
512 if let StmtKind::Let(l) = stmt.kind {
513 l.pat.each_binding_or_first(&mut |_, id, _, _| {
514 // FIXME(rust/#120456) - is `swap_remove` correct?
515 eq.locals.swap_remove(&id);
516 });
517 }
518 }
519 moved_locals.truncate(moved_locals_at_start);
520 offset - 1
521 }
522 });
523 if let Some(e) = block.expr {
524 for block in blocks {
525 if block.expr.map_or(false, |expr| !eq.eq_expr(expr, e)) {
526 moved_locals.truncate(moved_locals_at_start);
527 return BlockEq {
528 start_end_eq,
529 end_begin_eq: None,
530 moved_locals,
531 };
532 }
533 }
534 }
535
536 BlockEq {
537 start_end_eq,
538 end_begin_eq: Some(end_begin_eq),
539 moved_locals,
540 }
541 }
542
543 fn check_for_warn_of_moved_symbol(cx: &LateContext<'_>, symbols: &[(HirId, Symbol)], if_expr: &Expr<'_>) -> bool {
544 get_enclosing_block(cx, if_expr.hir_id).map_or(false, |block| {
545 let ignore_span = block.span.shrink_to_lo().to(if_expr.span);
546
547 symbols
548 .iter()
549 .filter(|&&(_, name)| !name.as_str().starts_with('_'))
550 .any(|&(_, name)| {
551 let mut walker = ContainsName {
552 name,
553 result: false,
554 cx,
555 };
556
557 // Scan block
558 block
559 .stmts
560 .iter()
561 .filter(|stmt| !ignore_span.overlaps(stmt.span))
562 .for_each(|stmt| intravisit::walk_stmt(&mut walker, stmt));
563
564 if let Some(expr) = block.expr {
565 intravisit::walk_expr(&mut walker, expr);
566 }
567
568 walker.result
569 })
570 })
571 }
572
573 fn method_caller_is_mutable(cx: &LateContext<'_>, caller_expr: &Expr<'_>, ignored_ty_ids: &DefIdSet) -> bool {
574 let caller_ty = cx.typeck_results().expr_ty(caller_expr);
575 // Check if given type has inner mutability and was not set to ignored by the configuration
576 let is_inner_mut_ty = is_interior_mut_ty(cx, caller_ty)
577 && !matches!(caller_ty.ty_adt_def(), Some(adt) if ignored_ty_ids.contains(&adt.did()));
578
579 is_inner_mut_ty
580 || caller_ty.is_mutable_ptr()
581 // `find_binding_init` will return the binding iff its not mutable
582 || path_to_local(caller_expr)
583 .and_then(|hid| find_binding_init(cx, hid))
584 .is_none()
585 }
586
587 /// Implementation of `IFS_SAME_COND`.
588 fn lint_same_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>], ignored_ty_ids: &DefIdSet) {
589 for (i, j) in search_same(
590 conds,
591 |e| hash_expr(cx, e),
592 |lhs, rhs| {
593 // Ignore eq_expr side effects iff one of the expression kind is a method call
594 // and the caller is not a mutable, including inner mutable type.
595 if let ExprKind::MethodCall(_, caller, _, _) = lhs.kind {
596 if method_caller_is_mutable(cx, caller, ignored_ty_ids) {
597 false
598 } else {
599 SpanlessEq::new(cx).eq_expr(lhs, rhs)
600 }
601 } else {
602 eq_expr_value(cx, lhs, rhs)
603 }
604 },
605 ) {
606 span_lint_and_note(
607 cx,
608 IFS_SAME_COND,
609 j.span,
610 "this `if` has the same condition as a previous `if`",
611 Some(i.span),
612 "same as this",
613 );
614 }
615 }
616
617 /// Implementation of `SAME_FUNCTIONS_IN_IF_CONDITION`.
618 fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
619 let eq: &dyn Fn(&&Expr<'_>, &&Expr<'_>) -> bool = &|&lhs, &rhs| -> bool {
620 // Do not lint if any expr originates from a macro
621 if lhs.span.from_expansion() || rhs.span.from_expansion() {
622 return false;
623 }
624 // Do not spawn warning if `IFS_SAME_COND` already produced it.
625 if eq_expr_value(cx, lhs, rhs) {
626 return false;
627 }
628 SpanlessEq::new(cx).eq_expr(lhs, rhs)
629 };
630
631 for (i, j) in search_same(conds, |e| hash_expr(cx, e), eq) {
632 span_lint_and_note(
633 cx,
634 SAME_FUNCTIONS_IN_IF_CONDITION,
635 j.span,
636 "this `if` has the same function call as a previous `if`",
637 Some(i.span),
638 "same as this",
639 );
640 }
641 }