]> git.proxmox.com Git - rustc.git/blob - src/tools/clippy/clippy_lints/src/copies.rs
f10c35cde52a1e9b0eed124ae7b1609efbc3d625
[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::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 {
529 name,
530 result: false,
531 cx,
532 };
533
534 // Scan block
535 block
536 .stmts
537 .iter()
538 .filter(|stmt| !ignore_span.overlaps(stmt.span))
539 .for_each(|stmt| intravisit::walk_stmt(&mut walker, stmt));
540
541 if let Some(expr) = block.expr {
542 intravisit::walk_expr(&mut walker, expr);
543 }
544
545 walker.result
546 })
547 })
548 }
549
550 /// Implementation of `IFS_SAME_COND`.
551 fn lint_same_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
552 for (i, j) in search_same(conds, |e| hash_expr(cx, e), |lhs, rhs| eq_expr_value(cx, lhs, rhs)) {
553 span_lint_and_note(
554 cx,
555 IFS_SAME_COND,
556 j.span,
557 "this `if` has the same condition as a previous `if`",
558 Some(i.span),
559 "same as this",
560 );
561 }
562 }
563
564 /// Implementation of `SAME_FUNCTIONS_IN_IF_CONDITION`.
565 fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
566 let eq: &dyn Fn(&&Expr<'_>, &&Expr<'_>) -> bool = &|&lhs, &rhs| -> bool {
567 // Do not lint if any expr originates from a macro
568 if lhs.span.from_expansion() || rhs.span.from_expansion() {
569 return false;
570 }
571 // Do not spawn warning if `IFS_SAME_COND` already produced it.
572 if eq_expr_value(cx, lhs, rhs) {
573 return false;
574 }
575 SpanlessEq::new(cx).eq_expr(lhs, rhs)
576 };
577
578 for (i, j) in search_same(conds, |e| hash_expr(cx, e), eq) {
579 span_lint_and_note(
580 cx,
581 SAME_FUNCTIONS_IN_IF_CONDITION,
582 j.span,
583 "this `if` has the same function call as a previous `if`",
584 Some(i.span),
585 "same as this",
586 );
587 }
588 }