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
;
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
,
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}
;
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 pub struct CopyAndPaste
{
163 ignore_interior_mutability
: Vec
<String
>,
164 ignored_ty_ids
: DefIdSet
,
168 pub fn new(ignore_interior_mutability
: Vec
<String
>) -> Self {
170 ignore_interior_mutability
,
171 ignored_ty_ids
: DefIdSet
::new(),
176 impl_lint_pass
!(CopyAndPaste
=> [
178 SAME_FUNCTIONS_IN_IF_CONDITION
,
180 BRANCHES_SHARING_CODE
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
);
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
);
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
);
206 /// Checks if the given expression is a let chain.
207 fn contains_let(e
: &Expr
<'_
>) -> bool
{
209 ExprKind
::Let(..) => true,
210 ExprKind
::Binary(op
, lhs
, rhs
) if op
.node
== BinOpKind
::And
=> {
211 matches
!(lhs
.kind
, ExprKind
::Let(..)) || contains_let(rhs
)
217 fn lint_if_same_then_else(cx
: &LateContext
<'_
>, conds
: &[&Expr
<'_
>], blocks
: &[&Block
<'_
>]) -> bool
{
218 let mut eq
= SpanlessEq
::new(cx
);
220 .array_windows
::<2>()
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
)) {
228 "this `if` has identical blocks",
239 fn lint_branches_sharing_code
<'tcx
>(
240 cx
: &LateContext
<'tcx
>,
241 conds
: &[&'tcx Expr
<'_
>],
242 blocks
: &[&'tcx Block
<'_
>],
243 expr
: &'tcx Expr
<'_
>,
245 // We only lint ifs with multiple blocks
246 let &[first_block
, ref blocks @
..] = blocks
else {
249 let &[.., last_block
] = blocks
else {
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())
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
);
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())
280 (span
, suggestion
.to_string())
283 let (span
, msg
, end_span
) = match (&start_suggestion
, &end_suggestion
) {
284 (&Some((span
, _
)), &Some((end_span
, _
))) => (
286 "all if blocks contain the same code at both the start and the end",
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,
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");
297 if let Some((span
, sugg
)) = start_suggestion
{
298 diag
.span_suggestion(
300 "consider moving these statements before the if",
302 Applicability
::Unspecified
,
305 if let Some((span
, sugg
)) = end_suggestion
{
306 diag
.span_suggestion(
308 "consider moving these statements after the if",
310 Applicability
::Unspecified
,
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");
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");
323 /// The end of the range of equal stmts at the start.
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
)>,
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
)),
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())),
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
{
355 l
.pat
.each_binding_or_first(&mut |_
, _
, _
, name
| {
356 if names
.get(i
).map_or(false, |&(_
, n
)| n
== name
.name
) {
362 res
&& i
== names
.len()
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()
375 ControlFlow
::Break(())
377 ControlFlow
::Continue(())
383 /// Checks if the given statement should be considered equal to the statement in the same position
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
)>,
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
));
397 let new_bindings
= &moved_bindings
[old_count
..];
400 .all(|b
| get_stmt(b
).map_or(false, |s
| eq_binding_names(s
, new_bindings
)))
405 .all(|b
| get_stmt(b
).map_or(false, |s
| eq
.eq_stmt(s
, stmt
)))
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
<'_
>],
415 let mut eq
= SpanlessEq
::new(cx
);
416 let mut eq
= eq
.inter_expr();
417 let mut moved_locals
= Vec
::new();
419 let mut cond_locals
= HirIdSet
::default();
421 let _
: Option
<!> = for_each_expr(cond
, |e
| {
422 if let Some(id
) = path_to_local(e
) {
423 cond_locals
.insert(id
);
425 ControlFlow
::Continue(())
429 let mut local_needs_ordered_drop
= false;
430 let start_end_eq
= block
435 if let StmtKind
::Let(l
) = stmt
.kind
436 && needs_ordered_drop(cx
, cx
.typeck_results().node_type(l
.hir_id
))
438 local_needs_ordered_drop
= true;
441 modifies_any_local(cx
, stmt
, &cond_locals
)
442 || !eq_stmts(stmt
, blocks
, |b
| b
.stmts
.get(i
), &mut eq
, &mut moved_locals
)
444 .map_or(block
.stmts
.len(), |(i
, _
)| i
);
446 if local_needs_ordered_drop
{
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`:
460 let expr_hash_eq
= if let Some(e
) = block
.expr
{
461 let hash
= hash_expr(cx
, e
);
464 .all(|b
| b
.expr
.map_or(false, |e
| hash_expr(cx
, e
) == hash
))
466 blocks
.iter().all(|b
| b
.expr
.is_none())
475 let end_search_start
= block
.stmts
[start_end_eq
..]
479 .find(|&(offset
, stmt
)| {
480 let hash
= hash_stmt(cx
, stmt
);
481 blocks
.iter().any(|b
| {
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
))
488 .map_or(block
.stmts
.len() - start_end_eq
, |(i
, _
)| i
);
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
..]
494 .zip(iter
::repeat_with(move || {
499 .fold(end_search_start
, |init
, (stmt
, offset
)| {
503 |b
| b
.stmts
.get(b
.stmts
.len() - offset
),
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
);
519 moved_locals
.truncate(moved_locals_at_start
);
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
);
538 end_begin_eq
: Some(end_begin_eq
),
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
);
549 .filter(|&&(_
, name
)| !name
.as_str().starts_with('_'
))
551 let mut walker
= ContainsName
{
561 .filter(|stmt
| !ignore_span
.overlaps(stmt
.span
))
562 .for_each(|stmt
| intravisit
::walk_stmt(&mut walker
, stmt
));
564 if let Some(expr
) = block
.expr
{
565 intravisit
::walk_expr(&mut walker
, expr
);
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()));
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
))
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(
591 |e
| hash_expr(cx
, e
),
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
) {
599 SpanlessEq
::new(cx
).eq_expr(lhs
, rhs
)
602 eq_expr_value(cx
, lhs
, rhs
)
610 "this `if` has the same condition as a previous `if`",
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() {
624 // Do not spawn warning if `IFS_SAME_COND` already produced it.
625 if eq_expr_value(cx
, lhs
, rhs
) {
628 SpanlessEq
::new(cx
).eq_expr(lhs
, rhs
)
631 for (i
, j
) in search_same(conds
, |e
| hash_expr(cx
, e
), eq
) {
634 SAME_FUNCTIONS_IN_IF_CONDITION
,
636 "this `if` has the same function call as a previous `if`",