match_same_arms, ifs_same_cond: lint once per same arm/condition

This commit is contained in:
Alex Macleod 2025-04-16 21:22:07 +00:00
parent 8eed35023f
commit 39ab00a3a1
18 changed files with 747 additions and 399 deletions

View file

@ -1,5 +1,5 @@
use clippy_config::Conf;
use clippy_utils::diagnostics::{span_lint_and_note, span_lint_and_then};
use clippy_utils::diagnostics::{span_lint, span_lint_and_note, span_lint_and_then};
use clippy_utils::source::{IntoSpan, SpanRangeExt, first_line_of_span, indent_of, reindent_multiline, snippet};
use clippy_utils::ty::{InteriorMut, needs_ordered_drop};
use clippy_utils::visitors::for_each_expr_without_closures;
@ -567,7 +567,7 @@ fn method_caller_is_mutable<'tcx>(
/// Implementation of `IFS_SAME_COND`.
fn lint_same_cond<'tcx>(cx: &LateContext<'tcx>, conds: &[&Expr<'_>], interior_mut: &mut InteriorMut<'tcx>) {
for (i, j) in search_same(
for group in search_same(
conds,
|e| hash_expr(cx, e),
|lhs, rhs| {
@ -584,14 +584,8 @@ fn lint_same_cond<'tcx>(cx: &LateContext<'tcx>, conds: &[&Expr<'_>], interior_mu
}
},
) {
span_lint_and_note(
cx,
IFS_SAME_COND,
j.span,
"this `if` has the same condition as a previous `if`",
Some(i.span),
"same as this",
);
let spans: Vec<_> = group.into_iter().map(|expr| expr.span).collect();
span_lint(cx, IFS_SAME_COND, spans, "these `if` branches have the same condition");
}
}
@ -609,14 +603,13 @@ fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
SpanlessEq::new(cx).eq_expr(lhs, rhs)
};
for (i, j) in search_same(conds, |e| hash_expr(cx, e), eq) {
span_lint_and_note(
for group in search_same(conds, |e| hash_expr(cx, e), eq) {
let spans: Vec<_> = group.into_iter().map(|expr| expr.span).collect();
span_lint(
cx,
SAME_FUNCTIONS_IN_IF_CONDITION,
j.span,
"this `if` has the same function call as a previous `if`",
Some(i.span),
"same as this",
spans,
"these `if` branches have the same function call",
);
}
}

View file

@ -1,8 +1,9 @@
use clippy_utils::diagnostics::span_lint_hir_and_then;
use clippy_utils::source::snippet_with_applicability;
use clippy_utils::{SpanlessEq, SpanlessHash, is_lint_allowed, path_to_local, search_same};
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::source::SpanRangeExt;
use clippy_utils::{SpanlessEq, SpanlessHash, fulfill_or_allowed, is_lint_allowed, path_to_local, search_same};
use core::cmp::Ordering;
use core::{iter, slice};
use itertools::Itertools;
use rustc_arena::DroplessArena;
use rustc_ast::ast::LitKind;
use rustc_errors::Applicability;
@ -110,57 +111,68 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>]) {
&& check_same_body()
};
let mut appl = Applicability::MaybeIncorrect;
let indexed_arms: Vec<(usize, &Arm<'_>)> = arms.iter().enumerate().collect();
for (&(i, arm1), &(j, arm2)) in search_same(&indexed_arms, hash, eq) {
if matches!(arm2.pat.kind, PatKind::Wild) {
if !cx.tcx.features().non_exhaustive_omitted_patterns_lint()
|| is_lint_allowed(cx, NON_EXHAUSTIVE_OMITTED_PATTERNS, arm2.hir_id)
{
let arm_span = adjusted_arm_span(cx, arm1.span);
span_lint_hir_and_then(
cx,
MATCH_SAME_ARMS,
arm1.hir_id,
arm_span,
"this match arm has an identical body to the `_` wildcard arm",
|diag| {
diag.span_suggestion(arm_span, "try removing the arm", "", appl)
.help("or try changing either arm body")
.span_note(arm2.span, "`_` wildcard arm here");
},
);
}
} else {
let back_block = backwards_blocking_idxs[j];
let (keep_arm, move_arm) = if back_block < i || (back_block == 0 && forwards_blocking_idxs[i] <= j) {
(arm1, arm2)
} else {
(arm2, arm1)
};
for mut group in search_same(&indexed_arms, hash, eq) {
// Filter out (and fulfill) `#[allow]`ed and `#[expect]`ed arms
group.retain(|(_, arm)| !fulfill_or_allowed(cx, MATCH_SAME_ARMS, [arm.hir_id]));
span_lint_hir_and_then(
cx,
MATCH_SAME_ARMS,
keep_arm.hir_id,
keep_arm.span,
"this match arm has an identical body to another arm",
|diag| {
let move_pat_snip = snippet_with_applicability(cx, move_arm.pat.span, "<pat2>", &mut appl);
let keep_pat_snip = snippet_with_applicability(cx, keep_arm.pat.span, "<pat1>", &mut appl);
diag.multipart_suggestion(
"or try merging the arm patterns and removing the obsolete arm",
vec![
(keep_arm.pat.span, format!("{keep_pat_snip} | {move_pat_snip}")),
(adjusted_arm_span(cx, move_arm.span), String::new()),
],
appl,
)
.help("try changing either arm body");
},
);
if group.len() < 2 {
continue;
}
span_lint_and_then(
cx,
MATCH_SAME_ARMS,
group.iter().map(|(_, arm)| arm.span).collect_vec(),
"these match arms have identical bodies",
|diag| {
diag.help("if this is unintentional make the arms return different values");
if let [prev @ .., (_, last)] = group.as_slice()
&& is_wildcard_arm(last.pat)
&& is_lint_allowed(cx, NON_EXHAUSTIVE_OMITTED_PATTERNS, last.hir_id)
{
diag.span_label(last.span, "the wildcard arm");
let s = if prev.len() > 1 { "s" } else { "" };
diag.multipart_suggestion_verbose(
format!("otherwise remove the non-wildcard arm{s}"),
prev.iter()
.map(|(_, arm)| (adjusted_arm_span(cx, arm.span), String::new()))
.collect(),
Applicability::MaybeIncorrect,
);
} else if let &[&(first_idx, _), .., &(last_idx, _)] = group.as_slice() {
let back_block = backwards_blocking_idxs[last_idx];
let split = if back_block < first_idx
|| (back_block == 0 && forwards_blocking_idxs[first_idx] <= last_idx)
{
group.split_first()
} else {
group.split_last()
};
if let Some(((_, dest), src)) = split
&& let Some(pat_snippets) = group
.iter()
.map(|(_, arm)| arm.pat.span.get_source_text(cx))
.collect::<Option<Vec<_>>>()
{
let mut suggs = src
.iter()
.map(|(_, arm)| (adjusted_arm_span(cx, arm.span), String::new()))
.collect_vec();
suggs.push((dest.pat.span, pat_snippets.iter().join(" | ")));
diag.multipart_suggestion_verbose(
"otherwise merge the patterns into a single arm",
suggs,
Applicability::MaybeIncorrect,
);
}
}
},
);
}
}
@ -449,3 +461,11 @@ fn bindings_eq(pat: &Pat<'_>, mut ids: HirIdSet) -> bool {
pat.each_binding_or_first(&mut |_, id, _, _| result &= ids.swap_remove(&id));
result && ids.is_empty()
}
fn is_wildcard_arm(pat: &Pat<'_>) -> bool {
match pat.kind {
PatKind::Wild => true,
PatKind::Or([.., last]) => matches!(last.kind, PatKind::Wild),
_ => false,
}
}