1 use clippy_utils::diagnostics::{span_lint_and_note, span_lint_and_then};
2 use clippy_utils::macros::macro_backtrace;
3 use clippy_utils::source::{first_line_of_span, indent_of, reindent_multiline, snippet, snippet_opt};
5 eq_expr_value, get_enclosing_block, hash_expr, hash_stmt, if_sequence, is_else_clause, is_lint_allowed,
6 search_same, ContainsName, HirEqInterExpr, SpanlessEq,
9 use rustc_errors::Applicability;
10 use rustc_hir::intravisit;
11 use rustc_hir::{BinOpKind, Block, Expr, ExprKind, HirId, QPath, Stmt, StmtKind};
12 use rustc_lint::{LateContext, LateLintPass};
13 use rustc_session::{declare_lint_pass, declare_tool_lint};
14 use rustc_span::hygiene::walk_chain;
15 use rustc_span::source_map::SourceMap;
16 use rustc_span::{sym, BytePos, Span, Symbol};
19 declare_clippy_lint! {
21 /// Checks for consecutive `if`s with the same condition.
23 /// ### Why is this bad?
24 /// This is probably a copy & paste error.
30 /// } else if a == b {
35 /// Note that this lint ignores all conditions with a function call as it could
36 /// have side effects:
41 /// } else if foo() { // not linted
45 #[clippy::version = "pre 1.29.0"]
48 "consecutive `if`s with the same condition"
51 declare_clippy_lint! {
53 /// Checks for consecutive `if`s with the same function call.
55 /// ### Why is this bad?
56 /// This is probably a copy & paste error.
57 /// Despite the fact that function can have side effects and `if` works as
58 /// intended, such an approach is implicit and can be considered a "code smell".
64 /// } else if foo() == bar {
69 /// This probably should be:
73 /// } else if foo() == baz {
78 /// or if the original code was not a typo and called function mutates a state,
79 /// consider move the mutation out of the `if` condition to avoid similarity to
80 /// a copy & paste error:
83 /// let first = foo();
87 /// let second = foo();
88 /// if second == bar {
93 #[clippy::version = "1.41.0"]
94 pub SAME_FUNCTIONS_IN_IF_CONDITION,
96 "consecutive `if`s with the same function call"
99 declare_clippy_lint! {
101 /// Checks for `if/else` with the same body as the *then* part
102 /// and the *else* part.
104 /// ### Why is this bad?
105 /// This is probably a copy & paste error.
115 #[clippy::version = "pre 1.29.0"]
116 pub IF_SAME_THEN_ELSE,
118 "`if` with the same `then` and `else` blocks"
121 declare_clippy_lint! {
123 /// Checks if the `if` and `else` block contain shared code that can be
124 /// moved out of the blocks.
126 /// ### Why is this bad?
127 /// Duplicate code is less maintainable.
129 /// ### Known problems
130 /// * The lint doesn't check if the moved expressions modify values that are being used in
131 /// the if condition. The suggestion can in that case modify the behavior of the program.
132 /// See [rust-clippy#7452](https://github.com/rust-lang/rust-clippy/issues/7452)
137 /// println!("Hello World");
140 /// println!("Hello World");
147 /// println!("Hello World");
154 #[clippy::version = "1.53.0"]
155 pub BRANCHES_SHARING_CODE,
157 "`if` statement with shared code in all blocks"
160 declare_lint_pass!(CopyAndPaste => [
162 SAME_FUNCTIONS_IN_IF_CONDITION,
164 BRANCHES_SHARING_CODE
167 impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
168 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
169 if !expr.span.from_expansion() && matches!(expr.kind, ExprKind::If(..)) && !is_else_clause(cx.tcx, expr) {
170 let (conds, blocks) = if_sequence(expr);
171 lint_same_cond(cx, &conds);
172 lint_same_fns_in_if_cond(cx, &conds);
174 !is_lint_allowed(cx, IF_SAME_THEN_ELSE, expr.hir_id) && lint_if_same_then_else(cx, &conds, &blocks);
175 if !all_same && conds.len() != blocks.len() {
176 lint_branches_sharing_code(cx, &conds, &blocks, expr);
182 /// Checks if the given expression is a let chain.
183 fn contains_let(e: &Expr<'_>) -> bool {
185 ExprKind::Let(..) => true,
186 ExprKind::Binary(op, lhs, rhs) if op.node == BinOpKind::And => {
187 matches!(lhs.kind, ExprKind::Let(..)) || contains_let(rhs)
193 fn lint_if_same_then_else(cx: &LateContext<'_>, conds: &[&Expr<'_>], blocks: &[&Block<'_>]) -> bool {
194 let mut eq = SpanlessEq::new(cx);
196 .array_windows::<2>()
198 .fold(true, |all_eq, (i, &[lhs, rhs])| {
199 if eq.eq_block(lhs, rhs)
200 && !contains_acceptable_macro(cx, lhs)
201 && !contains_acceptable_macro(cx, rhs)
202 && !contains_let(conds[i])
203 && conds.get(i + 1).map_or(true, |e| !contains_let(e))
209 "this `if` has identical blocks",
220 fn lint_branches_sharing_code<'tcx>(
221 cx: &LateContext<'tcx>,
222 conds: &[&'tcx Expr<'_>],
223 blocks: &[&Block<'tcx>],
224 expr: &'tcx Expr<'_>,
226 // We only lint ifs with multiple blocks
227 let &[first_block, ref blocks @ ..] = blocks else {
230 let &[.., last_block] = blocks else {
234 let res = scan_block_for_eq(cx, conds, first_block, blocks);
235 let sm = cx.tcx.sess.source_map();
236 let start_suggestion = res.start_span(first_block, sm).map(|span| {
237 let first_line_span = first_line_of_span(cx, expr.span);
238 let replace_span = first_line_span.with_hi(span.hi());
239 let cond_span = first_line_span.until(first_block.span);
240 let cond_snippet = reindent_multiline(snippet(cx, cond_span, "_"), false, None);
241 let cond_indent = indent_of(cx, cond_span);
242 let moved_snippet = reindent_multiline(snippet(cx, span, "_"), true, None);
243 let suggestion = moved_snippet.to_string() + "\n" + &cond_snippet + "{";
244 let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, cond_indent);
245 (replace_span, suggestion.to_string())
247 let end_suggestion = res.end_span(last_block, sm).map(|span| {
248 let moved_snipped = reindent_multiline(snippet(cx, span, "_"), true, None);
249 let indent = indent_of(cx, expr.span.shrink_to_hi());
250 let suggestion = "}\n".to_string() + &moved_snipped;
251 let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, indent);
253 let span = span.with_hi(last_block.span.hi());
254 // Improve formatting if the inner block has indention (i.e. normal Rust formatting)
255 let test_span = Span::new(span.lo() - BytePos(4), span.lo(), span.ctxt(), span.parent());
256 let span = if snippet_opt(cx, test_span).map_or(false, |snip| snip == " ") {
257 span.with_lo(test_span.lo())
261 (span, suggestion.to_string())
264 let (span, msg, end_span) = match (&start_suggestion, &end_suggestion) {
265 (&Some((span, _)), &Some((end_span, _))) => (
267 "all if blocks contain the same code at both the start and the end",
270 (&Some((span, _)), None) => (span, "all if blocks contain the same code at the start", None),
271 (None, &Some((span, _))) => (span, "all if blocks contain the same code at the end", None),
272 (None, None) => return,
274 span_lint_and_then(cx, BRANCHES_SHARING_CODE, span, msg, |diag| {
275 if let Some(span) = end_span {
276 diag.span_note(span, "this code is shared at the end");
278 if let Some((span, sugg)) = start_suggestion {
279 diag.span_suggestion(
281 "consider moving these statements before the if",
283 Applicability::Unspecified,
286 if let Some((span, sugg)) = end_suggestion {
287 diag.span_suggestion(
289 "consider moving these statements after the if",
291 Applicability::Unspecified,
293 if !cx.typeck_results().expr_ty(expr).is_unit() {
294 diag.note("the end suggestion probably needs some adjustments to use the expression result correctly");
297 if check_for_warn_of_moved_symbol(cx, &res.moved_locals, expr) {
298 diag.warn("some moved values might need to be renamed to avoid wrong references");
304 /// The end of the range of equal stmts at the start.
306 /// The start of the range of equal stmts at the end.
307 end_begin_eq: Option<usize>,
308 /// The name and id of every local which can be moved at the beginning and the end.
309 moved_locals: Vec<(HirId, Symbol)>,
312 fn start_span(&self, b: &Block<'_>, sm: &SourceMap) -> Option<Span> {
313 match &b.stmts[..self.start_end_eq] {
314 [first, .., last] => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))),
315 [s] => Some(sm.stmt_span(s.span, b.span)),
320 fn end_span(&self, b: &Block<'_>, sm: &SourceMap) -> Option<Span> {
321 match (&b.stmts[b.stmts.len() - self.end_begin_eq?..], b.expr) {
322 ([first, .., last], None) => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))),
323 ([first, ..], Some(last)) => Some(sm.stmt_span(first.span, b.span).to(sm.stmt_span(last.span, b.span))),
324 ([s], None) => Some(sm.stmt_span(s.span, b.span)),
325 ([], Some(e)) => Some(walk_chain(e.span, b.span.ctxt())),
331 /// If the statement is a local, checks if the bound names match the expected list of names.
332 fn eq_binding_names(s: &Stmt<'_>, names: &[(HirId, Symbol)]) -> bool {
333 if let StmtKind::Local(l) = s.kind {
336 l.pat.each_binding_or_first(&mut |_, _, _, name| {
337 if names.get(i).map_or(false, |&(_, n)| n == name.name) {
343 res && i == names.len()
349 /// Checks if the given statement should be considered equal to the statement in the same position
353 blocks: &[&Block<'_>],
354 get_stmt: impl for<'a> Fn(&'a Block<'a>) -> Option<&'a Stmt<'a>>,
355 eq: &mut HirEqInterExpr<'_, '_, '_>,
356 moved_bindings: &mut Vec<(HirId, Symbol)>,
358 (if let StmtKind::Local(l) = stmt.kind {
359 let old_count = moved_bindings.len();
360 l.pat.each_binding_or_first(&mut |_, id, _, name| {
361 moved_bindings.push((id, name.name));
363 let new_bindings = &moved_bindings[old_count..];
366 .all(|b| get_stmt(b).map_or(false, |s| eq_binding_names(s, new_bindings)))
371 .all(|b| get_stmt(b).map_or(false, |s| eq.eq_stmt(s, stmt)))
374 fn contains_acceptable_macro(cx: &LateContext<'_>, block: &Block<'_>) -> bool {
375 for stmt in block.stmts {
377 StmtKind::Semi(semi_expr) if acceptable_macro(cx, semi_expr) => return true,
382 if let Some(block_expr) = block.expr
383 && acceptable_macro(cx, block_expr)
391 fn acceptable_macro(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
392 if let ExprKind::Call(call_expr, _) = expr.kind
393 && let ExprKind::Path(QPath::Resolved(None, path)) = call_expr.kind
394 && macro_backtrace(path.span).any(|macro_call| {
396 &cx.tcx.get_diagnostic_name(macro_call.def_id),
397 Some(sym::todo_macro | sym::unimplemented_macro)
406 fn scan_block_for_eq(cx: &LateContext<'_>, _conds: &[&Expr<'_>], block: &Block<'_>, blocks: &[&Block<'_>]) -> BlockEq {
407 let mut eq = SpanlessEq::new(cx);
408 let mut eq = eq.inter_expr();
409 let mut moved_locals = Vec::new();
411 let start_end_eq = block
415 .find(|&(i, stmt)| !eq_stmts(stmt, blocks, |b| b.stmts.get(i), &mut eq, &mut moved_locals))
416 .map_or(block.stmts.len(), |(i, _)| i);
418 // Walk backwards through the final expression/statements so long as their hashes are equal. Note
419 // `SpanlessHash` treats all local references as equal allowing locals declared earlier in the block
420 // to match those in other blocks. e.g. If each block ends with the following the hash value will be
421 // the same even though each `x` binding will have a different `HirId`:
424 let expr_hash_eq = if let Some(e) = block.expr {
425 let hash = hash_expr(cx, e);
428 .all(|b| b.expr.map_or(false, |e| hash_expr(cx, e) == hash))
430 blocks.iter().all(|b| b.expr.is_none())
439 let end_search_start = block.stmts[start_end_eq..]
443 .find(|&(offset, stmt)| {
444 let hash = hash_stmt(cx, stmt);
445 blocks.iter().any(|b| {
447 // the bounds check will catch the underflow
448 .get(b.stmts.len().wrapping_sub(offset + 1))
449 .map_or(true, |s| hash != hash_stmt(cx, s))
452 .map_or(block.stmts.len() - start_end_eq, |(i, _)| i);
454 let moved_locals_at_start = moved_locals.len();
455 let mut i = end_search_start;
456 let end_begin_eq = block.stmts[block.stmts.len() - end_search_start..]
458 .zip(iter::repeat_with(move || {
463 .fold(end_search_start, |init, (stmt, offset)| {
467 |b| b.stmts.get(b.stmts.len() - offset),
473 // Clear out all locals seen at the end so far. None of them can be moved.
474 let stmts = &blocks[0].stmts;
475 for stmt in &stmts[stmts.len() - init..=stmts.len() - offset] {
476 if let StmtKind::Local(l) = stmt.kind {
477 l.pat.each_binding_or_first(&mut |_, id, _, _| {
478 eq.locals.remove(&id);
482 moved_locals.truncate(moved_locals_at_start);
486 if let Some(e) = block.expr {
487 for block in blocks {
488 if block.expr.map_or(false, |expr| !eq.eq_expr(expr, e)) {
489 moved_locals.truncate(moved_locals_at_start);
501 end_begin_eq: Some(end_begin_eq),
506 fn check_for_warn_of_moved_symbol(cx: &LateContext<'_>, symbols: &[(HirId, Symbol)], if_expr: &Expr<'_>) -> bool {
507 get_enclosing_block(cx, if_expr.hir_id).map_or(false, |block| {
508 let ignore_span = block.span.shrink_to_lo().to(if_expr.span);
512 .filter(|&&(_, name)| !name.as_str().starts_with('_'))
514 let mut walker = ContainsName { name, result: false };
520 .filter(|stmt| !ignore_span.overlaps(stmt.span))
521 .for_each(|stmt| intravisit::walk_stmt(&mut walker, stmt));
523 if let Some(expr) = block.expr {
524 intravisit::walk_expr(&mut walker, expr);
532 /// Implementation of `IFS_SAME_COND`.
533 fn lint_same_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
534 for (i, j) in search_same(conds, |e| hash_expr(cx, e), |lhs, rhs| eq_expr_value(cx, lhs, rhs)) {
539 "this `if` has the same condition as a previous `if`",
546 /// Implementation of `SAME_FUNCTIONS_IN_IF_CONDITION`.
547 fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
548 let eq: &dyn Fn(&&Expr<'_>, &&Expr<'_>) -> bool = &|&lhs, &rhs| -> bool {
549 // Do not lint if any expr originates from a macro
550 if lhs.span.from_expansion() || rhs.span.from_expansion() {
553 // Do not spawn warning if `IFS_SAME_COND` already produced it.
554 if eq_expr_value(cx, lhs, rhs) {
557 SpanlessEq::new(cx).eq_expr(lhs, rhs)
560 for (i, j) in search_same(conds, |e| hash_expr(cx, e), eq) {
563 SAME_FUNCTIONS_IN_IF_CONDITION,
565 "this `if` has the same function call as a previous `if`",