Skip to content

Commit 39ab00a

Browse files
committed
match_same_arms, ifs_same_cond: lint once per same arm/condition
1 parent 8eed350 commit 39ab00a

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

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

clippy_utils/src/lib.rs

+22-21
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
// FIXME: switch to something more ergonomic here, once available.
2929
// (Currently there is no way to opt into sysroot crates without `extern crate`.)
30+
extern crate indexmap;
3031
extern crate rustc_abi;
3132
extern crate rustc_ast;
3233
extern crate rustc_attr_parsing;
@@ -85,7 +86,6 @@ pub use self::hir_utils::{
8586
use core::mem;
8687
use core::ops::ControlFlow;
8788
use std::collections::hash_map::Entry;
88-
use std::hash::BuildHasherDefault;
8989
use std::iter::{once, repeat_n};
9090
use std::sync::{Mutex, MutexGuard, OnceLock};
9191

@@ -95,7 +95,7 @@ use rustc_ast::ast::{self, LitKind, RangeLimits};
9595
use rustc_attr_parsing::{AttributeKind, find_attr};
9696
use rustc_data_structures::fx::FxHashMap;
9797
use rustc_data_structures::packed::Pu128;
98-
use rustc_data_structures::unhash::UnhashMap;
98+
use rustc_data_structures::unhash::UnindexMap;
9999
use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk};
100100
use rustc_hir::def::{DefKind, Res};
101101
use rustc_hir::def_id::{CrateNum, DefId, LOCAL_CRATE, LocalDefId, LocalModDefId};
@@ -2486,45 +2486,46 @@ pub fn is_slice_of_primitives(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<S
24862486
None
24872487
}
24882488

2489-
/// Returns list of all pairs `(a, b)` where `eq(a, b) == true`
2490-
/// and `a` is before `b` in `exprs` for all `a` and `b` in
2491-
/// `exprs`
2489+
/// Returns a list of groups where elements in each group are equal according to `eq`
2490+
///
2491+
/// - Within each group the elements are sorted by the order they appear in `exprs`
2492+
/// - The groups themselves are sorted by their first element's appearence in `exprs`
24922493
///
24932494
/// Given functions `eq` and `hash` such that `eq(a, b) == true`
24942495
/// implies `hash(a) == hash(b)`
2495-
pub fn search_same<T, Hash, Eq>(exprs: &[T], mut hash: Hash, mut eq: Eq) -> Vec<(&T, &T)>
2496+
pub fn search_same<T, Hash, Eq>(exprs: &[T], mut hash: Hash, mut eq: Eq) -> Vec<Vec<&T>>
24962497
where
24972498
Hash: FnMut(&T) -> u64,
24982499
Eq: FnMut(&T, &T) -> bool,
24992500
{
25002501
match exprs {
2501-
[a, b] if eq(a, b) => return vec![(a, b)],
2502+
[a, b] if eq(a, b) => return vec![vec![a, b]],
25022503
_ if exprs.len() <= 2 => return vec![],
25032504
_ => {},
25042505
}
25052506

2506-
let mut match_expr_list: Vec<(&T, &T)> = Vec::new();
2507-
2508-
let mut map: UnhashMap<u64, Vec<&_>> =
2509-
UnhashMap::with_capacity_and_hasher(exprs.len(), BuildHasherDefault::default());
2507+
let mut buckets: UnindexMap<u64, Vec<Vec<&T>>> = UnindexMap::default();
25102508

25112509
for expr in exprs {
2512-
match map.entry(hash(expr)) {
2513-
Entry::Occupied(mut o) => {
2514-
for o in o.get() {
2515-
if eq(o, expr) {
2516-
match_expr_list.push((o, expr));
2517-
}
2510+
match buckets.entry(hash(expr)) {
2511+
indexmap::map::Entry::Occupied(mut o) => {
2512+
let bucket = o.get_mut();
2513+
match bucket.iter_mut().find(|group| eq(expr, group[0])) {
2514+
Some(group) => group.push(expr),
2515+
None => bucket.push(vec![expr]),
25182516
}
2519-
o.get_mut().push(expr);
25202517
},
2521-
Entry::Vacant(v) => {
2522-
v.insert(vec![expr]);
2518+
indexmap::map::Entry::Vacant(v) => {
2519+
v.insert(vec![vec![expr]]);
25232520
},
25242521
}
25252522
}
25262523

2527-
match_expr_list
2524+
buckets
2525+
.into_values()
2526+
.flatten()
2527+
.filter(|group| group.len() > 1)
2528+
.collect()
25282529
}
25292530

25302531
/// 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)