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
{
538 .filter(|stmt
| !ignore_span
.overlaps(stmt
.span
))
539 .for_each(|stmt
| intravisit
::walk_stmt(&mut walker
, stmt
));
541 if let Some(expr
) = block
.expr
{
542 intravisit
::walk_expr(&mut walker
, expr
);
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
)) {
557 "this `if` has the same condition as a previous `if`",
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() {
571 // Do not spawn warning if `IFS_SAME_COND` already produced it.
572 if eq_expr_value(cx
, lhs
, rhs
) {
575 SpanlessEq
::new(cx
).eq_expr(lhs
, rhs
)
578 for (i
, j
) in search_same(conds
, |e
| hash_expr(cx
, e
), eq
) {
581 SAME_FUNCTIONS_IN_IF_CONDITION
,
583 "this `if` has the same function call as a previous `if`",