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;
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,
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};
21 declare_clippy_lint! {
23 /// Checks for consecutive `if`s with the same condition.
25 /// ### Why is this bad?
26 /// This is probably a copy & paste error.
32 /// } else if a == b {
37 /// Note that this lint ignores all conditions with a function call as it could
38 /// have side effects:
43 /// } else if foo() { // not linted
47 #[clippy::version = "pre 1.29.0"]
50 "consecutive `if`s with the same condition"
53 declare_clippy_lint! {
55 /// Checks for consecutive `if`s with the same function call.
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".
66 /// } else if foo() == bar {
71 /// This probably should be:
75 /// } else if foo() == baz {
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:
85 /// let first = foo();
89 /// let second = foo();
90 /// if second == bar {
95 #[clippy::version = "1.41.0"]
96 pub SAME_FUNCTIONS_IN_IF_CONDITION,
98 "consecutive `if`s with the same function call"
101 declare_clippy_lint! {
103 /// Checks for `if/else` with the same body as the *then* part
104 /// and the *else* part.
106 /// ### Why is this bad?
107 /// This is probably a copy & paste error.
117 #[clippy::version = "pre 1.29.0"]
118 pub IF_SAME_THEN_ELSE,
120 "`if` with the same `then` and `else` blocks"
123 declare_clippy_lint! {
125 /// Checks if the `if` and `else` block contain shared code that can be
126 /// moved out of the blocks.
128 /// ### Why is this bad?
129 /// Duplicate code is less maintainable.
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)
139 /// println!("Hello World");
142 /// println!("Hello World");
149 /// println!("Hello World");
156 #[clippy::version = "1.53.0"]
157 pub BRANCHES_SHARING_CODE,
159 "`if` statement with shared code in all blocks"
162 declare_lint_pass!(CopyAndPaste => [
164 SAME_FUNCTIONS_IN_IF_CONDITION,
166 BRANCHES_SHARING_CODE
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);
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);
184 /// Checks if the given expression is a let chain.
185 fn contains_let(e: &Expr<'_>) -> bool {
187 ExprKind::Let(..) => true,
188 ExprKind::Binary(op, lhs, rhs) if op.node == BinOpKind::And => {
189 matches!(lhs.kind, ExprKind::Let(..)) || contains_let(rhs)
195 fn lint_if_same_then_else(cx: &LateContext<'_>, conds: &[&Expr<'_>], blocks: &[&Block<'_>]) -> bool {
196 let mut eq = SpanlessEq::new(cx);
198 .array_windows::<2>()
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)) {
206 "this `if` has identical blocks",
217 fn lint_branches_sharing_code<'tcx>(
218 cx: &LateContext<'tcx>,
219 conds: &[&'tcx Expr<'_>],
220 blocks: &[&'tcx Block<'_>],
221 expr: &'tcx Expr<'_>,
223 // We only lint ifs with multiple blocks
224 let &[first_block, ref blocks @ ..] = blocks else {
227 let &[.., last_block] = blocks else {
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())
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);
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())
258 (span, suggestion.to_string())
261 let (span, msg, end_span) = match (&start_suggestion, &end_suggestion) {
262 (&Some((span, _)), &Some((end_span, _))) => (
264 "all if blocks contain the same code at both the start and the end",
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,
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");
275 if let Some((span, sugg)) = start_suggestion {
276 diag.span_suggestion(
278 "consider moving these statements before the if",
280 Applicability::Unspecified,
283 if let Some((span, sugg)) = end_suggestion {
284 diag.span_suggestion(
286 "consider moving these statements after the if",
288 Applicability::Unspecified,
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");
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");
301 /// The end of the range of equal stmts at the start.
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)>,
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)),
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())),
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 {
333 l.pat.each_binding_or_first(&mut |_, _, _, name| {
334 if names.get(i).map_or(false, |&(_, n)| n == name.name) {
340 res && i == names.len()
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()
353 ControlFlow::Break(())
355 ControlFlow::Continue(())
361 /// Checks if the given statement should be considered equal to the statement in the same position
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)>,
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));
375 let new_bindings = &moved_bindings[old_count..];
378 .all(|b| get_stmt(b).map_or(false, |s| eq_binding_names(s, new_bindings)))
383 .all(|b| get_stmt(b).map_or(false, |s| eq.eq_stmt(s, stmt)))
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<'_>],
393 let mut eq = SpanlessEq::new(cx);
394 let mut eq = eq.inter_expr();
395 let mut moved_locals = Vec::new();
397 let mut cond_locals = HirIdSet::default();
399 let _: Option<!> = for_each_expr(cond, |e| {
400 if let Some(id) = path_to_local(e) {
401 cond_locals.insert(id);
403 ControlFlow::Continue(())
407 let mut local_needs_ordered_drop = false;
408 let start_end_eq = block
413 if let StmtKind::Local(l) = stmt.kind
414 && needs_ordered_drop(cx, cx.typeck_results().node_type(l.hir_id))
416 local_needs_ordered_drop = true;
419 modifies_any_local(cx, stmt, &cond_locals)
420 || !eq_stmts(stmt, blocks, |b| b.stmts.get(i), &mut eq, &mut moved_locals)
422 .map_or(block.stmts.len(), |(i, _)| i);
424 if local_needs_ordered_drop {
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`:
438 let expr_hash_eq = if let Some(e) = block.expr {
439 let hash = hash_expr(cx, e);
442 .all(|b| b.expr.map_or(false, |e| hash_expr(cx, e) == hash))
444 blocks.iter().all(|b| b.expr.is_none())
453 let end_search_start = block.stmts[start_end_eq..]
457 .find(|&(offset, stmt)| {
458 let hash = hash_stmt(cx, stmt);
459 blocks.iter().any(|b| {
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))
466 .map_or(block.stmts.len() - start_end_eq, |(i, _)| i);
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..]
472 .zip(iter::repeat_with(move || {
477 .fold(end_search_start, |init, (stmt, offset)| {
481 |b| b.stmts.get(b.stmts.len() - offset),
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);
496 moved_locals.truncate(moved_locals_at_start);
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);
515 end_begin_eq: Some(end_begin_eq),
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);
526 .filter(|&&(_, name)| !name.as_str().starts_with('_'))
528 let mut walker = ContainsName { name, result: false };
534 .filter(|stmt| !ignore_span.overlaps(stmt.span))
535 .for_each(|stmt| intravisit::walk_stmt(&mut walker, stmt));
537 if let Some(expr) = block.expr {
538 intravisit::walk_expr(&mut walker, expr);
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)) {
553 "this `if` has the same condition as a previous `if`",
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() {
567 // Do not spawn warning if `IFS_SAME_COND` already produced it.
568 if eq_expr_value(cx, lhs, rhs) {
571 SpanlessEq::new(cx).eq_expr(lhs, rhs)
574 for (i, j) in search_same(conds, |e| hash_expr(cx, e), eq) {
577 SAME_FUNCTIONS_IN_IF_CONDITION,
579 "this `if` has the same function call as a previous `if`",