Skip to content

Commit feb70b5

Browse files
match_same_arms, ifs_same_cond: lint once per same arm/condition (#14637)
A large fraction of the lints emitted in CI lintcheck come from [this `match`](https://github.com/unicode-rs/unicode-normalization/blob/v0.1.23/src/tables.rs#L34289), currently for `n` same arms `((n - 1) * n)/2` lints are emitted, with this change it will be emitted as a single lint Also fixes #13835 changelog: none
2 parents 95d7eda + 39ab00a commit feb70b5

18 files changed

+745
-397
lines changed

clippy_lints/src/copies.rs

+9-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use clippy_config::Conf;
2-
use clippy_utils::diagnostics::{span_lint_and_note, span_lint_and_then};
2+
use clippy_utils::diagnostics::{span_lint, span_lint_and_note, span_lint_and_then};
33
use clippy_utils::source::{IntoSpan, SpanRangeExt, first_line_of_span, indent_of, reindent_multiline, snippet};
44
use clippy_utils::ty::{InteriorMut, needs_ordered_drop};
55
use clippy_utils::visitors::for_each_expr_without_closures;
@@ -567,7 +567,7 @@ fn method_caller_is_mutable<'tcx>(
567567

568568
/// Implementation of `IFS_SAME_COND`.
569569
fn lint_same_cond<'tcx>(cx: &LateContext<'tcx>, conds: &[&Expr<'_>], interior_mut: &mut InteriorMut<'tcx>) {
570-
for (i, j) in search_same(
570+
for group in search_same(
571571
conds,
572572
|e| hash_expr(cx, e),
573573
|lhs, rhs| {
@@ -584,14 +584,8 @@ fn lint_same_cond<'tcx>(cx: &LateContext<'tcx>, conds: &[&Expr<'_>], interior_mu
584584
}
585585
},
586586
) {
587-
span_lint_and_note(
588-
cx,
589-
IFS_SAME_COND,
590-
j.span,
591-
"this `if` has the same condition as a previous `if`",
592-
Some(i.span),
593-
"same as this",
594-
);
587+
let spans: Vec<_> = group.into_iter().map(|expr| expr.span).collect();
588+
span_lint(cx, IFS_SAME_COND, spans, "these `if` branches have the same condition");
595589
}
596590
}
597591

@@ -609,14 +603,13 @@ fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
609603
SpanlessEq::new(cx).eq_expr(lhs, rhs)
610604
};
611605

612-
for (i, j) in search_same(conds, |e| hash_expr(cx, e), eq) {
613-
span_lint_and_note(
606+
for group in search_same(conds, |e| hash_expr(cx, e), eq) {
607+
let spans: Vec<_> = group.into_iter().map(|expr| expr.span).collect();
608+
span_lint(
614609
cx,
615610
SAME_FUNCTIONS_IN_IF_CONDITION,
616-
j.span,
617-
"this `if` has the same function call as a previous `if`",
618-
Some(i.span),
619-
"same as this",
611+
spans,
612+
"these `if` branches have the same function call",
620613
);
621614
}
622615
}

clippy_lints/src/matches/match_same_arms.rs

+71-51
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use clippy_utils::diagnostics::span_lint_hir_and_then;
2-
use clippy_utils::source::snippet_with_applicability;
3-
use clippy_utils::{SpanlessEq, SpanlessHash, is_lint_allowed, path_to_local, search_same};
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::source::SpanRangeExt;
3+
use clippy_utils::{SpanlessEq, SpanlessHash, fulfill_or_allowed, is_lint_allowed, path_to_local, search_same};
44
use core::cmp::Ordering;
55
use core::{iter, slice};
6+
use itertools::Itertools;
67
use rustc_arena::DroplessArena;
78
use rustc_ast::ast::LitKind;
89
use rustc_errors::Applicability;
@@ -110,57 +111,68 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>]) {
110111
&& check_same_body()
111112
};
112113

113-
let mut appl = Applicability::MaybeIncorrect;
114114
let indexed_arms: Vec<(usize, &Arm<'_>)> = arms.iter().enumerate().collect();
115-
for (&(i, arm1), &(j, arm2)) in search_same(&indexed_arms, hash, eq) {
116-
if matches!(arm2.pat.kind, PatKind::Wild) {
117-
if !cx.tcx.features().non_exhaustive_omitted_patterns_lint()
118-
|| is_lint_allowed(cx, NON_EXHAUSTIVE_OMITTED_PATTERNS, arm2.hir_id)
119-
{
120-
let arm_span = adjusted_arm_span(cx, arm1.span);
121-
span_lint_hir_and_then(
122-
cx,
123-
MATCH_SAME_ARMS,
124-
arm1.hir_id,
125-
arm_span,
126-
"this match arm has an identical body to the `_` wildcard arm",
127-
|diag| {
128-
diag.span_suggestion(arm_span, "try removing the arm", "", appl)
129-
.help("or try changing either arm body")
130-
.span_note(arm2.span, "`_` wildcard arm here");
131-
},
132-
);
133-
}
134-
} else {
135-
let back_block = backwards_blocking_idxs[j];
136-
let (keep_arm, move_arm) = if back_block < i || (back_block == 0 && forwards_blocking_idxs[i] <= j) {
137-
(arm1, arm2)
138-
} else {
139-
(arm2, arm1)
140-
};
141-
142-
span_lint_hir_and_then(
143-
cx,
144-
MATCH_SAME_ARMS,
145-
keep_arm.hir_id,
146-
keep_arm.span,
147-
"this match arm has an identical body to another arm",
148-
|diag| {
149-
let move_pat_snip = snippet_with_applicability(cx, move_arm.pat.span, "<pat2>", &mut appl);
150-
let keep_pat_snip = snippet_with_applicability(cx, keep_arm.pat.span, "<pat1>", &mut appl);
115+
for mut group in search_same(&indexed_arms, hash, eq) {
116+
// Filter out (and fulfill) `#[allow]`ed and `#[expect]`ed arms
117+
group.retain(|(_, arm)| !fulfill_or_allowed(cx, MATCH_SAME_ARMS, [arm.hir_id]));
151118

152-
diag.multipart_suggestion(
153-
"or try merging the arm patterns and removing the obsolete arm",
154-
vec![
155-
(keep_arm.pat.span, format!("{keep_pat_snip} | {move_pat_snip}")),
156-
(adjusted_arm_span(cx, move_arm.span), String::new()),
157-
],
158-
appl,
159-
)
160-
.help("try changing either arm body");
161-
},
162-
);
119+
if group.len() < 2 {
120+
continue;
163121
}
122+
123+
span_lint_and_then(
124+
cx,
125+
MATCH_SAME_ARMS,
126+
group.iter().map(|(_, arm)| arm.span).collect_vec(),
127+
"these match arms have identical bodies",
128+
|diag| {
129+
diag.help("if this is unintentional make the arms return different values");
130+
131+
if let [prev @ .., (_, last)] = group.as_slice()
132+
&& is_wildcard_arm(last.pat)
133+
&& is_lint_allowed(cx, NON_EXHAUSTIVE_OMITTED_PATTERNS, last.hir_id)
134+
{
135+
diag.span_label(last.span, "the wildcard arm");
136+
137+
let s = if prev.len() > 1 { "s" } else { "" };
138+
diag.multipart_suggestion_verbose(
139+
format!("otherwise remove the non-wildcard arm{s}"),
140+
prev.iter()
141+
.map(|(_, arm)| (adjusted_arm_span(cx, arm.span), String::new()))
142+
.collect(),
143+
Applicability::MaybeIncorrect,
144+
);
145+
} else if let &[&(first_idx, _), .., &(last_idx, _)] = group.as_slice() {
146+
let back_block = backwards_blocking_idxs[last_idx];
147+
let split = if back_block < first_idx
148+
|| (back_block == 0 && forwards_blocking_idxs[first_idx] <= last_idx)
149+
{
150+
group.split_first()
151+
} else {
152+
group.split_last()
153+
};
154+
155+
if let Some(((_, dest), src)) = split
156+
&& let Some(pat_snippets) = group
157+
.iter()
158+
.map(|(_, arm)| arm.pat.span.get_source_text(cx))
159+
.collect::<Option<Vec<_>>>()
160+
{
161+
let mut suggs = src
162+
.iter()
163+
.map(|(_, arm)| (adjusted_arm_span(cx, arm.span), String::new()))
164+
.collect_vec();
165+
166+
suggs.push((dest.pat.span, pat_snippets.iter().join(" | ")));
167+
diag.multipart_suggestion_verbose(
168+
"otherwise merge the patterns into a single arm",
169+
suggs,
170+
Applicability::MaybeIncorrect,
171+
);
172+
}
173+
}
174+
},
175+
);
164176
}
165177
}
166178

@@ -450,3 +462,11 @@ fn bindings_eq(pat: &Pat<'_>, mut ids: HirIdSet) -> bool {
450462
pat.each_binding_or_first(&mut |_, id, _, _| result &= ids.swap_remove(&id));
451463
result && ids.is_empty()
452464
}
465+
466+
fn is_wildcard_arm(pat: &Pat<'_>) -> bool {
467+
match pat.kind {
468+
PatKind::Wild => true,
469+
PatKind::Or([.., last]) => matches!(last.kind, PatKind::Wild),
470+
_ => false,
471+
}
472+
}

clippy_utils/src/lib.rs

+22-21
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
// FIXME: switch to something more ergonomic here, once available.
2727
// (Currently there is no way to opt into sysroot crates without `extern crate`.)
28+
extern crate indexmap;
2829
extern crate rustc_abi;
2930
extern crate rustc_ast;
3031
extern crate rustc_attr_parsing;
@@ -81,7 +82,6 @@ pub use self::hir_utils::{
8182
use core::mem;
8283
use core::ops::ControlFlow;
8384
use std::collections::hash_map::Entry;
84-
use std::hash::BuildHasherDefault;
8585
use std::iter::{once, repeat_n};
8686
use std::sync::{Mutex, MutexGuard, OnceLock};
8787

@@ -91,7 +91,7 @@ use rustc_ast::ast::{self, LitKind, RangeLimits};
9191
use rustc_attr_parsing::{AttributeKind, find_attr};
9292
use rustc_data_structures::fx::FxHashMap;
9393
use rustc_data_structures::packed::Pu128;
94-
use rustc_data_structures::unhash::UnhashMap;
94+
use rustc_data_structures::unhash::UnindexMap;
9595
use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk};
9696
use rustc_hir::def::{DefKind, Res};
9797
use rustc_hir::def_id::{DefId, LocalDefId, LocalModDefId};
@@ -2196,45 +2196,46 @@ pub fn is_slice_of_primitives(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<S
21962196
None
21972197
}
21982198

2199-
/// Returns list of all pairs `(a, b)` where `eq(a, b) == true`
2200-
/// and `a` is before `b` in `exprs` for all `a` and `b` in
2201-
/// `exprs`
2199+
/// Returns a list of groups where elements in each group are equal according to `eq`
2200+
///
2201+
/// - Within each group the elements are sorted by the order they appear in `exprs`
2202+
/// - The groups themselves are sorted by their first element's appearence in `exprs`
22022203
///
22032204
/// Given functions `eq` and `hash` such that `eq(a, b) == true`
22042205
/// implies `hash(a) == hash(b)`
2205-
pub fn search_same<T, Hash, Eq>(exprs: &[T], mut hash: Hash, mut eq: Eq) -> Vec<(&T, &T)>
2206+
pub fn search_same<T, Hash, Eq>(exprs: &[T], mut hash: Hash, mut eq: Eq) -> Vec<Vec<&T>>
22062207
where
22072208
Hash: FnMut(&T) -> u64,
22082209
Eq: FnMut(&T, &T) -> bool,
22092210
{
22102211
match exprs {
2211-
[a, b] if eq(a, b) => return vec![(a, b)],
2212+
[a, b] if eq(a, b) => return vec![vec![a, b]],
22122213
_ if exprs.len() <= 2 => return vec![],
22132214
_ => {},
22142215
}
22152216

2216-
let mut match_expr_list: Vec<(&T, &T)> = Vec::new();
2217-
2218-
let mut map: UnhashMap<u64, Vec<&_>> =
2219-
UnhashMap::with_capacity_and_hasher(exprs.len(), BuildHasherDefault::default());
2217+
let mut buckets: UnindexMap<u64, Vec<Vec<&T>>> = UnindexMap::default();
22202218

22212219
for expr in exprs {
2222-
match map.entry(hash(expr)) {
2223-
Entry::Occupied(mut o) => {
2224-
for o in o.get() {
2225-
if eq(o, expr) {
2226-
match_expr_list.push((o, expr));
2227-
}
2220+
match buckets.entry(hash(expr)) {
2221+
indexmap::map::Entry::Occupied(mut o) => {
2222+
let bucket = o.get_mut();
2223+
match bucket.iter_mut().find(|group| eq(expr, group[0])) {
2224+
Some(group) => group.push(expr),
2225+
None => bucket.push(vec![expr]),
22282226
}
2229-
o.get_mut().push(expr);
22302227
},
2231-
Entry::Vacant(v) => {
2232-
v.insert(vec![expr]);
2228+
indexmap::map::Entry::Vacant(v) => {
2229+
v.insert(vec![vec![expr]]);
22332230
},
22342231
}
22352232
}
22362233

2237-
match_expr_list
2234+
buckets
2235+
.into_values()
2236+
.flatten()
2237+
.filter(|group| group.len() > 1)
2238+
.collect()
22382239
}
22392240

22402241
/// Peels off all references on the pattern. Returns the underlying pattern and the number of

tests/ui-toml/ifs_same_cond/ifs_same_cond.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ fn issue10272() {
1111
// should trigger warning
1212
let x = Cell::new(true);
1313
if x.get() {
14+
//~^ ifs_same_cond
1415
} else if !x.take() {
1516
} else if x.get() {
16-
//~^ ifs_same_cond
1717
} else {
1818
}
1919
}

tests/ui-toml/ifs_same_cond/ifs_same_cond.stderr

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
error: this `if` has the same condition as a previous `if`
2-
--> tests/ui-toml/ifs_same_cond/ifs_same_cond.rs:15:15
3-
|
4-
LL | } else if x.get() {
5-
| ^^^^^^^
6-
|
7-
note: same as this
1+
error: these `if` branches have the same condition
82
--> tests/ui-toml/ifs_same_cond/ifs_same_cond.rs:13:8
93
|
104
LL | if x.get() {
115
| ^^^^^^^
6+
...
7+
LL | } else if x.get() {
8+
| ^^^^^^^
9+
|
1210
= note: `-D clippy::ifs-same-cond` implied by `-D warnings`
1311
= help: to override `-D warnings` add `#[allow(clippy::ifs_same_cond)]`
1412

tests/ui/ifs_same_cond.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,25 @@ fn ifs_same_cond() {
66
let b = false;
77

88
if b {
9+
//~^ ifs_same_cond
910
} else if b {
11+
}
12+
13+
if b {
1014
//~^ ifs_same_cond
15+
} else if b {
16+
} else if b {
1117
}
1218

1319
if a == 1 {
14-
} else if a == 1 {
1520
//~^ ifs_same_cond
21+
} else if a == 1 {
1622
}
1723

1824
if 2 * a == 1 {
25+
//~^ ifs_same_cond
1926
} else if 2 * a == 2 {
2027
} else if 2 * a == 1 {
21-
//~^ ifs_same_cond
2228
} else if a == 1 {
2329
}
2430

@@ -50,8 +56,8 @@ fn ifs_same_cond() {
5056
fn issue10272() {
5157
let a = String::from("ha");
5258
if a.contains("ah") {
53-
} else if a.contains("ah") {
5459
//~^ ifs_same_cond
60+
} else if a.contains("ah") {
5561

5662
// Trigger this lint
5763
} else if a.contains("ha") {

0 commit comments

Comments
 (0)