-
Notifications
You must be signed in to change notification settings - Fork 1.8k
WIP: New lint: duplicate_match_guards
#14986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||||||||||||||||
nursery, | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||||||||||||||||
"a condition in match body duplicating the match guard" | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
declare_lint_pass!(DuplicateMatchGuards => [DUPLICATE_MATCH_GUARDS]); | ||||||||||||||||
|
||||||||||||||||
impl<'tcx> LateLintPass<'tcx> for DuplicateMatchGuards { | ||||||||||||||||
ada4a marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason to not also lint if there's an There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Given this, maybe it would make sense to lint the |
||||||||||||||||
&& eq_expr_value(cx, guard, cond.peel_drop_temps()) | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? rust-clippy/tests/ui/duplicate_match_guards.rs Lines 22 to 28 in 3e4ace9
|
||||||||||||||||
{ | ||||||||||||||||
// 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, | ||||||||||||||||
); | ||||||||||||||||
}, | ||||||||||||||||
); | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} |
There was a problem hiding this comment.
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
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?