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}
;
4 eq_expr_value
, get_enclosing_block
, hash_expr
, hash_stmt
, if_sequence
, is_else_clause
, is_lint_allowed
,
5 search_same
, ContainsName
, HirEqInterExpr
, SpanlessEq
,
8 use rustc_errors
::Applicability
;
9 use rustc_hir
::intravisit
;
10 use rustc_hir
::{BinOpKind, Block, Expr, ExprKind, HirId, Stmt, StmtKind}
;
11 use rustc_lint
::{LateContext, LateLintPass}
;
12 use rustc_session
::{declare_lint_pass, declare_tool_lint}
;
13 use rustc_span
::hygiene
::walk_chain
;
14 use rustc_span
::source_map
::SourceMap
;
15 use rustc_span
::{BytePos, Span, Symbol}
;
18 declare_clippy_lint
! {
20 /// Checks for consecutive `if`s with the same condition.
22 /// ### Why is this bad?
23 /// This is probably a copy & paste error.
29 /// } else if a == b {
34 /// Note that this lint ignores all conditions with a function call as it could
35 /// have side effects:
40 /// } else if foo() { // not linted
44 #[clippy::version = "pre 1.29.0"]
47 "consecutive `if`s with the same condition"
50 declare_clippy_lint
! {
52 /// Checks for consecutive `if`s with the same function call.
54 /// ### Why is this bad?
55 /// This is probably a copy & paste error.
56 /// Despite the fact that function can have side effects and `if` works as
57 /// intended, such an approach is implicit and can be considered a "code smell".
63 /// } else if foo() == bar {
68 /// This probably should be:
72 /// } else if foo() == baz {
77 /// or if the original code was not a typo and called function mutates a state,
78 /// consider move the mutation out of the `if` condition to avoid similarity to
79 /// a copy & paste error:
82 /// let first = foo();
86 /// let second = foo();
87 /// if second == bar {
92 #[clippy::version = "1.41.0"]
93 pub SAME_FUNCTIONS_IN_IF_CONDITION
,
95 "consecutive `if`s with the same function call"
98 declare_clippy_lint
! {
100 /// Checks for `if/else` with the same body as the *then* part
101 /// and the *else* part.
103 /// ### Why is this bad?
104 /// This is probably a copy & paste error.
114 #[clippy::version = "pre 1.29.0"]
115 pub IF_SAME_THEN_ELSE
,
117 "`if` with the same `then` and `else` blocks"
120 declare_clippy_lint
! {
122 /// Checks if the `if` and `else` block contain shared code that can be
123 /// moved out of the blocks.
125 /// ### Why is this bad?
126 /// Duplicate code is less maintainable.
128 /// ### Known problems
129 /// * The lint doesn't check if the moved expressions modify values that are being used in
130 /// the if condition. The suggestion can in that case modify the behavior of the program.
131 /// See [rust-clippy#7452](https://github.com/rust-lang/rust-clippy/issues/7452)
136 /// println!("Hello World");
139 /// println!("Hello World");
146 /// println!("Hello World");
153 #[clippy::version = "1.53.0"]
154 pub BRANCHES_SHARING_CODE
,
156 "`if` statement with shared code in all blocks"
159 declare_lint_pass
!(CopyAndPaste
=> [
161 SAME_FUNCTIONS_IN_IF_CONDITION
,
163 BRANCHES_SHARING_CODE
166 impl<'tcx
> LateLintPass
<'tcx
> for CopyAndPaste
{
167 fn check_expr(&mut self, cx
: &LateContext
<'tcx
>, expr
: &'tcx Expr
<'_
>) {
168 if !expr
.span
.from_expansion() && matches
!(expr
.kind
, ExprKind
::If(..)) && !is_else_clause(cx
.tcx
, expr
) {
169 let (conds
, blocks
) = if_sequence(expr
);
170 lint_same_cond(cx
, &conds
);
171 lint_same_fns_in_if_cond(cx
, &conds
);
173 !is_lint_allowed(cx
, IF_SAME_THEN_ELSE
, expr
.hir_id
) && lint_if_same_then_else(cx
, &conds
, &blocks
);
174 if !all_same
&& conds
.len() != blocks
.len() {
175 lint_branches_sharing_code(cx
, &conds
, &blocks
, expr
);
181 /// Checks if the given expression is a let chain.
182 fn contains_let(e
: &Expr
<'_
>) -> bool
{
184 ExprKind
::Let(..) => true,
185 ExprKind
::Binary(op
, lhs
, rhs
) if op
.node
== BinOpKind
::And
=> {
186 matches
!(lhs
.kind
, ExprKind
::Let(..)) || contains_let(rhs
)
192 fn lint_if_same_then_else(cx
: &LateContext
<'_
>, conds
: &[&Expr
<'_
>], blocks
: &[&Block
<'_
>]) -> bool
{
193 let mut eq
= SpanlessEq
::new(cx
);
195 .array_windows
::<2>()
197 .fold(true, |all_eq
, (i
, &[lhs
, rhs
])| {
198 if eq
.eq_block(lhs
, rhs
) && !contains_let(conds
[i
]) && conds
.get(i
+ 1).map_or(true, |e
| !contains_let(e
)) {
203 "this `if` has identical blocks",
214 fn lint_branches_sharing_code
<'tcx
>(
215 cx
: &LateContext
<'tcx
>,
216 conds
: &[&'tcx Expr
<'_
>],
217 blocks
: &[&Block
<'tcx
>],
218 expr
: &'tcx Expr
<'_
>,
220 // We only lint ifs with multiple blocks
221 let &[first_block
, ref blocks @
..] = blocks
else {
224 let &[.., last_block
] = blocks
else {
228 let res
= scan_block_for_eq(cx
, conds
, first_block
, blocks
);
229 let sm
= cx
.tcx
.sess
.source_map();
230 let start_suggestion
= res
.start_span(first_block
, sm
).map(|span
| {
231 let first_line_span
= first_line_of_span(cx
, expr
.span
);
232 let replace_span
= first_line_span
.with_hi(span
.hi());
233 let cond_span
= first_line_span
.until(first_block
.span
);
234 let cond_snippet
= reindent_multiline(snippet(cx
, cond_span
, "_"), false, None
);
235 let cond_indent
= indent_of(cx
, cond_span
);
236 let moved_snippet
= reindent_multiline(snippet(cx
, span
, "_"), true, None
);
237 let suggestion
= moved_snippet
.to_string() + "\n" + &cond_snippet
+ "{";
238 let suggestion
= reindent_multiline(Cow
::Borrowed(&suggestion
), true, cond_indent
);
239 (replace_span
, suggestion
.to_string())
241 let end_suggestion
= res
.end_span(last_block
, sm
).map(|span
| {
242 let moved_snipped
= reindent_multiline(snippet(cx
, span
, "_"), true, None
);
243 let indent
= indent_of(cx
, expr
.span
.shrink_to_hi());
244 let suggestion
= "}\n".to_string() + &moved_snipped
;
245 let suggestion
= reindent_multiline(Cow
::Borrowed(&suggestion
), true, indent
);
247 let span
= span
.with_hi(last_block
.span
.hi());
248 // Improve formatting if the inner block has indention (i.e. normal Rust formatting)
249 let test_span
= Span
::new(span
.lo() - BytePos(4), span
.lo(), span
.ctxt(), span
.parent());
250 let span
= if snippet_opt(cx
, test_span
).map_or(false, |snip
| snip
== " ") {
251 span
.with_lo(test_span
.lo())
255 (span
, suggestion
.to_string())
258 let (span
, msg
, end_span
) = match (&start_suggestion
, &end_suggestion
) {
259 (&Some((span
, _
)), &Some((end_span
, _
))) => (
261 "all if blocks contain the same code at both the start and the end",
264 (&Some((span
, _
)), None
) => (span
, "all if blocks contain the same code at the start", None
),
265 (None
, &Some((span
, _
))) => (span
, "all if blocks contain the same code at the end", None
),
266 (None
, None
) => return,
268 span_lint_and_then(cx
, BRANCHES_SHARING_CODE
, span
, msg
, |diag
| {
269 if let Some(span
) = end_span
{
270 diag
.span_note(span
, "this code is shared at the end");
272 if let Some((span
, sugg
)) = start_suggestion
{
273 diag
.span_suggestion(
275 "consider moving these statements before the if",
277 Applicability
::Unspecified
,
280 if let Some((span
, sugg
)) = end_suggestion
{
281 diag
.span_suggestion(
283 "consider moving these statements after the if",
285 Applicability
::Unspecified
,
287 if !cx
.typeck_results().expr_ty(expr
).is_unit() {
288 diag
.note("the end suggestion probably needs some adjustments to use the expression result correctly");
291 if check_for_warn_of_moved_symbol(cx
, &res
.moved_locals
, expr
) {
292 diag
.warn("some moved values might need to be renamed to avoid wrong references");
298 /// The end of the range of equal stmts at the start.
300 /// The start of the range of equal stmts at the end.
301 end_begin_eq
: Option
<usize>,
302 /// The name and id of every local which can be moved at the beginning and the end.
303 moved_locals
: Vec
<(HirId
, Symbol
)>,
306 fn start_span(&self, b
: &Block
<'_
>, sm
: &SourceMap
) -> Option
<Span
> {
307 match &b
.stmts
[..self.start_end_eq
] {
308 [first
, .., last
] => Some(sm
.stmt_span(first
.span
, b
.span
).to(sm
.stmt_span(last
.span
, b
.span
))),
309 [s
] => Some(sm
.stmt_span(s
.span
, b
.span
)),
314 fn end_span(&self, b
: &Block
<'_
>, sm
: &SourceMap
) -> Option
<Span
> {
315 match (&b
.stmts
[b
.stmts
.len() - self.end_begin_eq?
..], b
.expr
) {
316 ([first
, .., last
], None
) => Some(sm
.stmt_span(first
.span
, b
.span
).to(sm
.stmt_span(last
.span
, b
.span
))),
317 ([first
, ..], Some(last
)) => Some(sm
.stmt_span(first
.span
, b
.span
).to(sm
.stmt_span(last
.span
, b
.span
))),
318 ([s
], None
) => Some(sm
.stmt_span(s
.span
, b
.span
)),
319 ([], Some(e
)) => Some(walk_chain(e
.span
, b
.span
.ctxt())),
325 /// If the statement is a local, checks if the bound names match the expected list of names.
326 fn eq_binding_names(s
: &Stmt
<'_
>, names
: &[(HirId
, Symbol
)]) -> bool
{
327 if let StmtKind
::Local(l
) = s
.kind
{
330 l
.pat
.each_binding_or_first(&mut |_
, _
, _
, name
| {
331 if names
.get(i
).map_or(false, |&(_
, n
)| n
== name
.name
) {
337 res
&& i
== names
.len()
343 /// Checks if the given statement should be considered equal to the statement in the same position
347 blocks
: &[&Block
<'_
>],
348 get_stmt
: impl for<'a
> Fn(&'a Block
<'a
>) -> Option
<&'a Stmt
<'a
>>,
349 eq
: &mut HirEqInterExpr
<'_
, '_
, '_
>,
350 moved_bindings
: &mut Vec
<(HirId
, Symbol
)>,
352 (if let StmtKind
::Local(l
) = stmt
.kind
{
353 let old_count
= moved_bindings
.len();
354 l
.pat
.each_binding_or_first(&mut |_
, id
, _
, name
| {
355 moved_bindings
.push((id
, name
.name
));
357 let new_bindings
= &moved_bindings
[old_count
..];
360 .all(|b
| get_stmt(b
).map_or(false, |s
| eq_binding_names(s
, new_bindings
)))
365 .all(|b
| get_stmt(b
).map_or(false, |s
| eq
.eq_stmt(s
, stmt
)))
368 fn scan_block_for_eq(cx
: &LateContext
<'_
>, _conds
: &[&Expr
<'_
>], block
: &Block
<'_
>, blocks
: &[&Block
<'_
>]) -> BlockEq
{
369 let mut eq
= SpanlessEq
::new(cx
);
370 let mut eq
= eq
.inter_expr();
371 let mut moved_locals
= Vec
::new();
373 let start_end_eq
= block
377 .find(|&(i
, stmt
)| !eq_stmts(stmt
, blocks
, |b
| b
.stmts
.get(i
), &mut eq
, &mut moved_locals
))
378 .map_or(block
.stmts
.len(), |(i
, _
)| i
);
380 // Walk backwards through the final expression/statements so long as their hashes are equal. Note
381 // `SpanlessHash` treats all local references as equal allowing locals declared earlier in the block
382 // to match those in other blocks. e.g. If each block ends with the following the hash value will be
383 // the same even though each `x` binding will have a different `HirId`:
386 let expr_hash_eq
= if let Some(e
) = block
.expr
{
387 let hash
= hash_expr(cx
, e
);
390 .all(|b
| b
.expr
.map_or(false, |e
| hash_expr(cx
, e
) == hash
))
392 blocks
.iter().all(|b
| b
.expr
.is_none())
401 let end_search_start
= block
.stmts
[start_end_eq
..]
405 .find(|&(offset
, stmt
)| {
406 let hash
= hash_stmt(cx
, stmt
);
407 blocks
.iter().any(|b
| {
409 // the bounds check will catch the underflow
410 .get(b
.stmts
.len().wrapping_sub(offset
+ 1))
411 .map_or(true, |s
| hash
!= hash_stmt(cx
, s
))
414 .map_or(block
.stmts
.len() - start_end_eq
, |(i
, _
)| i
);
416 let moved_locals_at_start
= moved_locals
.len();
417 let mut i
= end_search_start
;
418 let end_begin_eq
= block
.stmts
[block
.stmts
.len() - end_search_start
..]
420 .zip(iter
::repeat_with(move || {
425 .fold(end_search_start
, |init
, (stmt
, offset
)| {
429 |b
| b
.stmts
.get(b
.stmts
.len() - offset
),
435 // Clear out all locals seen at the end so far. None of them can be moved.
436 let stmts
= &blocks
[0].stmts
;
437 for stmt
in &stmts
[stmts
.len() - init
..=stmts
.len() - offset
] {
438 if let StmtKind
::Local(l
) = stmt
.kind
{
439 l
.pat
.each_binding_or_first(&mut |_
, id
, _
, _
| {
440 eq
.locals
.remove(&id
);
444 moved_locals
.truncate(moved_locals_at_start
);
448 if let Some(e
) = block
.expr
{
449 for block
in blocks
{
450 if block
.expr
.map_or(false, |expr
| !eq
.eq_expr(expr
, e
)) {
451 moved_locals
.truncate(moved_locals_at_start
);
463 end_begin_eq
: Some(end_begin_eq
),
468 fn check_for_warn_of_moved_symbol(cx
: &LateContext
<'_
>, symbols
: &[(HirId
, Symbol
)], if_expr
: &Expr
<'_
>) -> bool
{
469 get_enclosing_block(cx
, if_expr
.hir_id
).map_or(false, |block
| {
470 let ignore_span
= block
.span
.shrink_to_lo().to(if_expr
.span
);
474 .filter(|&&(_
, name
)| !name
.as_str().starts_with('_'
))
476 let mut walker
= ContainsName { name, result: false }
;
482 .filter(|stmt
| !ignore_span
.overlaps(stmt
.span
))
483 .for_each(|stmt
| intravisit
::walk_stmt(&mut walker
, stmt
));
485 if let Some(expr
) = block
.expr
{
486 intravisit
::walk_expr(&mut walker
, expr
);
494 /// Implementation of `IFS_SAME_COND`.
495 fn lint_same_cond(cx
: &LateContext
<'_
>, conds
: &[&Expr
<'_
>]) {
496 for (i
, j
) in search_same(conds
, |e
| hash_expr(cx
, e
), |lhs
, rhs
| eq_expr_value(cx
, lhs
, rhs
)) {
501 "this `if` has the same condition as a previous `if`",
508 /// Implementation of `SAME_FUNCTIONS_IN_IF_CONDITION`.
509 fn lint_same_fns_in_if_cond(cx
: &LateContext
<'_
>, conds
: &[&Expr
<'_
>]) {
510 let eq
: &dyn Fn(&&Expr
<'_
>, &&Expr
<'_
>) -> bool
= &|&lhs
, &rhs
| -> bool
{
511 // Do not lint if any expr originates from a macro
512 if lhs
.span
.from_expansion() || rhs
.span
.from_expansion() {
515 // Do not spawn warning if `IFS_SAME_COND` already produced it.
516 if eq_expr_value(cx
, lhs
, rhs
) {
519 SpanlessEq
::new(cx
).eq_expr(lhs
, rhs
)
522 for (i
, j
) in search_same(conds
, |e
| hash_expr(cx
, e
), eq
) {
525 SAME_FUNCTIONS_IN_IF_CONDITION
,
527 "this `if` has the same function call as a previous `if`",