Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6084,6 +6084,7 @@ Released 2018-09-13
[`drop_copy`]: https://rust-lang.github.io/rust-clippy/master/index.html#drop_copy
[`drop_non_drop`]: https://rust-lang.github.io/rust-clippy/master/index.html#drop_non_drop
[`drop_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#drop_ref
[`duplicate_match_guards`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicate_match_guards
[`duplicate_mod`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicate_mod
[`duplicate_underscore_argument`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicate_underscore_argument
[`duplicated_attributes`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicated_attributes
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::drop_forget_ref::DROP_NON_DROP_INFO,
crate::drop_forget_ref::FORGET_NON_DROP_INFO,
crate::drop_forget_ref::MEM_FORGET_INFO,
crate::duplicate_match_guards::DUPLICATE_MATCH_GUARDS_INFO,
crate::duplicate_mod::DUPLICATE_MOD_INFO,
crate::else_if_without_else::ELSE_IF_WITHOUT_ELSE_INFO,
crate::empty_drop::EMPTY_DROP_INFO,
Expand Down
187 changes: 187 additions & 0 deletions clippy_lints/src/duplicate_match_guards.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
use clippy_utils::source::{
HasSession, IntoSpan, SpanRangeExt, indent_of, reindent_multiline, snippet_with_applicability, trim_span,
};
use clippy_utils::{eq_expr_value, span_contains_comment};
use rustc_errors::Applicability;
use rustc_hir::{Arm, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::BytePos;

declare_clippy_lint! {
/// ### What it does
/// Checks for the same condition being checked in a match guard and in the match body
///
/// ### Why is this bad?
/// This is usually just a typo or a copy and paste error.
///
/// ### Example
/// ```no_run
/// # let n = 0;
/// # let a = 3;
/// # let b = 4;
/// match n {
/// 0 if a > b => {
/// if a > b {
/// return;
/// }
/// }
/// _ => {}
/// }
/// ```
/// Use instead:
/// ```no_run
/// # let n = 0;
/// # let a = 3;
/// # let b = 4;
/// match n {
/// 0 if a > b => {
/// return;
/// }
/// _ => {}
/// }
/// ```
#[clippy::version = "1.92.0"]
pub DUPLICATE_MATCH_GUARDS,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this lint in principle apply to regular if conditions too? E.g. this currently has no warning

let x = 1;
if x > 1 {
  if x > 1 {
    // always executes
  } else {
    // this block of code is simply unreachable
    todo!()
  }
} else { todo!() }

I'm not saying that this PR should implement that, but it makes me wonder if we should make the lint name a bit more general, like duplicate_nested_conditions or something so it could be extended in the future without needing a name change?

nursery,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think suspicious would be fine for the category

"a condition in match body duplicating the match guard"
}

declare_lint_pass!(DuplicateMatchGuards => [DUPLICATE_MATCH_GUARDS]);

impl<'tcx> LateLintPass<'tcx> for DuplicateMatchGuards {
fn check_arm(&mut self, cx: &LateContext<'tcx>, arm: &'tcx Arm<'_>) {
let Some(guard) = arm.guard else {
return;
};

let (arm_body_expr, body_has_block) = if let ExprKind::Block(block, _) = arm.body.kind {
if block.stmts.is_empty()
&& let Some(trailing_expr) = block.expr
Comment on lines +60 to +61
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this is already making sure that there are no other statements, but we might also want to check that there's no #[cfg]'d out code in between. So e.g. I assume the following is currently a false positive:

match () {
  () if condition {
    #[cfg(...)]
    condition = true;
    if condition {
      
    }
  }
}

{
(trailing_expr, true)
} else {
// the body contains something other than the `if` clause -- bail out
return;
}
} else {
(arm.body, false)
};

if let ExprKind::If(cond, then, None) = arm_body_expr.kind
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to not also lint if there's an else? It still seems just as (arguably even more) wrong if there's an else {} because the block will additionally just be unreachable, wouldn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is, no. It's just that my motivating case in particular didn't have an else

Copy link
Contributor Author

@ada4a ada4a Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(arguably even more) wrong

Given this, maybe it would make sense to lint the else is Some case with a separate lint, placed into the correctness group?

&& eq_expr_value(cx, guard, cond.peel_drop_temps())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the lint is directed at copy-paste errors, maybe we should make this stricter, and make sure that the expr are "syntactically" equal, and don't just evaluate to the result.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eq_expr_value already does that (it uses SpanlessEq, which checks for syntactic equality). Or how do you mean this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No yes, that's what I meant. If that's the case though, how is the following able to pass?

// not _identical_, but the meaning is the same
0 if a > b => {
if b < a {
//~^ duplicate_match_guards
return;
}
},

{
// Make sure that we won't swallow any comments.
// Be extra conservative and bail out on _any_ comment outside of `then`:
//
// <pat> if <guard> => { if <cond> <then> }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^
let sm = cx.sess().source_map();
if span_contains_comment(sm, arm.span.with_hi(then.span.lo()))
|| span_contains_comment(sm, arm.span.with_lo(then.span.hi()))
{
return;
}

// The two expressions may be syntactically different, even if identical semantically
// -- the user might want to replace the condition in the guard with the one in the body.
let mut applicability = Applicability::MaybeIncorrect;

if body_has_block {
// the common case:
// ```
// match 0u32 {
// 0 if true => {
// if true {
// return;
// }
// }
// }
// ```
//
// The arm body already has curlies, so we can remove the ones around `then`

if !then
.span
.check_source_text(cx, |s| s.starts_with('{') && s.ends_with('}'))
{
// Despite being a block, `then` somehow does not start and end with curlies.
// Bail out just to be sure
return;
}

let sugg_span = then
.span
.with_lo(then.span.lo() + BytePos(1))
.with_hi(then.span.hi() - BytePos(1));

let sugg_span = trim_span(sm, sugg_span);

let sugg = snippet_with_applicability(cx, sugg_span, "..", &mut applicability);

// Since we remove one level of curlies, we should be able to dedent `then` left one level, so this:
// ```
// <pat> if <guard> => {
// if <cond> {
// then
// without
// curlies
// }
// }
// ```
// becomes this:
// ```
// <pat> if <guard> => {
// then
// without
// curlies
// }
// ```
let indent = indent_of(cx, sugg_span);
let sugg = reindent_multiline(&sugg, true, indent.map(|i| i.saturating_sub(4)));

span_lint_and_sugg(
cx,
DUPLICATE_MATCH_GUARDS,
arm_body_expr.span,
"condition duplicates match guard",
"remove the condition",
sugg,
applicability,
);
} else {
// The uncommon case (rusfmt would add the curlies here automatically)
// ```
// match 0u32 {
// <pat> if <guard> => if <cond> { return; }
// }
// ```
//
// the arm body doesn't have its own curlies,
// so we need to retain the ones around `then`

let span_to_remove = arm_body_expr
.span
.with_hi(cond.span.hi())
// NOTE: we don't remove trailing whitespace so that there's one space left between `<cond>` and `{`
.with_leading_whitespace(cx)
.into_span();

span_lint_and_then(
cx,
DUPLICATE_MATCH_GUARDS,
arm_body_expr.span,
"condition duplicates match guard",
|diag| {
diag.multipart_suggestion_verbose(
"remove the condition",
vec![(span_to_remove, String::new())],
applicability,
);
},
);
}
}
}
}
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ mod disallowed_types;
mod doc;
mod double_parens;
mod drop_forget_ref;
mod duplicate_match_guards;
mod duplicate_mod;
mod else_if_without_else;
mod empty_drop;
Expand Down Expand Up @@ -832,5 +833,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(|_| Box::new(coerce_container_to_any::CoerceContainerToAny));
store.register_late_pass(|_| Box::new(toplevel_ref_arg::ToplevelRefArg));
store.register_late_pass(|_| Box::new(volatile_composites::VolatileComposites));
store.register_late_pass(|_| Box::new(duplicate_match_guards::DuplicateMatchGuards));
// add lints here, do not remove this comment, it's used in `new_lint`
}
Loading