Skip to content

Commit 9e57657

Browse files
committed
Auto merge of #10968 - y21:manual_range_pat, r=Centri3
new lint: `manual_range_patterns` Fixes #4931 changelog: new lint: [`manual_range_patterns`]
2 parents 6ce656f + b592d39 commit 9e57657

13 files changed

+289
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4949,6 +4949,7 @@ Released 2018-09-13
49494949
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
49504950
[`manual_ok_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_or
49514951
[`manual_range_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_contains
4952+
[`manual_range_patterns`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_patterns
49524953
[`manual_rem_euclid`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_rem_euclid
49534954
[`manual_retain`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_retain
49544955
[`manual_saturating_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_saturating_arithmetic

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
278278
crate::manual_let_else::MANUAL_LET_ELSE_INFO,
279279
crate::manual_main_separator_str::MANUAL_MAIN_SEPARATOR_STR_INFO,
280280
crate::manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE_INFO,
281+
crate::manual_range_patterns::MANUAL_RANGE_PATTERNS_INFO,
281282
crate::manual_rem_euclid::MANUAL_REM_EUCLID_INFO,
282283
crate::manual_retain::MANUAL_RETAIN_INFO,
283284
crate::manual_slice_size_calculation::MANUAL_SLICE_SIZE_CALCULATION_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ mod manual_is_ascii_check;
188188
mod manual_let_else;
189189
mod manual_main_separator_str;
190190
mod manual_non_exhaustive;
191+
mod manual_range_patterns;
191192
mod manual_rem_euclid;
192193
mod manual_retain;
193194
mod manual_slice_size_calculation;
@@ -1068,6 +1069,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10681069
needless_raw_string_hashes_allow_one,
10691070
})
10701071
});
1072+
store.register_late_pass(|_| Box::new(manual_range_patterns::ManualRangePatterns));
10711073
// add lints here, do not remove this comment, it's used in `new_lint`
10721074
}
10731075

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use rustc_ast::LitKind;
3+
use rustc_data_structures::fx::FxHashSet;
4+
use rustc_errors::Applicability;
5+
use rustc_hir::Expr;
6+
use rustc_hir::ExprKind;
7+
use rustc_hir::PatKind;
8+
use rustc_hir::RangeEnd;
9+
use rustc_lint::LintContext;
10+
use rustc_lint::{LateContext, LateLintPass};
11+
use rustc_middle::lint::in_external_macro;
12+
use rustc_session::{declare_lint_pass, declare_tool_lint};
13+
14+
declare_clippy_lint! {
15+
/// ### What it does
16+
/// Looks for combined OR patterns that are all contained in a specific range,
17+
/// e.g. `6 | 4 | 5 | 9 | 7 | 8` can be rewritten as `4..=9`.
18+
///
19+
/// ### Why is this bad?
20+
/// Using an explicit range is more concise and easier to read.
21+
///
22+
/// ### Example
23+
/// ```rust
24+
/// let x = 6;
25+
/// let foo = matches!(x, 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10);
26+
/// ```
27+
/// Use instead:
28+
/// ```rust
29+
/// let x = 6;
30+
/// let foo = matches!(x, 1..=10);
31+
/// ```
32+
#[clippy::version = "1.72.0"]
33+
pub MANUAL_RANGE_PATTERNS,
34+
complexity,
35+
"manually writing range patterns using a combined OR pattern (`|`)"
36+
}
37+
declare_lint_pass!(ManualRangePatterns => [MANUAL_RANGE_PATTERNS]);
38+
39+
fn expr_as_u128(expr: &Expr<'_>) -> Option<u128> {
40+
if let ExprKind::Lit(lit) = expr.kind
41+
&& let LitKind::Int(num, _) = lit.node
42+
{
43+
Some(num)
44+
} else {
45+
None
46+
}
47+
}
48+
49+
impl LateLintPass<'_> for ManualRangePatterns {
50+
fn check_pat(&mut self, cx: &LateContext<'_>, pat: &'_ rustc_hir::Pat<'_>) {
51+
if in_external_macro(cx.sess(), pat.span) {
52+
return;
53+
}
54+
55+
// a pattern like 1 | 2 seems fine, lint if there are at least 3 alternatives
56+
if let PatKind::Or(pats) = pat.kind
57+
&& pats.len() >= 3
58+
{
59+
let mut min = u128::MAX;
60+
let mut max = 0;
61+
let mut numbers_found = FxHashSet::default();
62+
let mut ranges_found = Vec::new();
63+
64+
for pat in pats {
65+
if let PatKind::Lit(lit) = pat.kind
66+
&& let Some(num) = expr_as_u128(lit)
67+
{
68+
numbers_found.insert(num);
69+
70+
min = min.min(num);
71+
max = max.max(num);
72+
} else if let PatKind::Range(Some(left), Some(right), end) = pat.kind
73+
&& let Some(left) = expr_as_u128(left)
74+
&& let Some(right) = expr_as_u128(right)
75+
&& right >= left
76+
{
77+
min = min.min(left);
78+
max = max.max(right);
79+
ranges_found.push(left..=match end {
80+
RangeEnd::Included => right,
81+
RangeEnd::Excluded => right - 1,
82+
});
83+
} else {
84+
return;
85+
}
86+
}
87+
88+
let contains_whole_range = 'contains: {
89+
let mut num = min;
90+
while num <= max {
91+
if numbers_found.contains(&num) {
92+
num += 1;
93+
}
94+
// Given a list of (potentially overlapping) ranges like:
95+
// 1..=5, 3..=7, 6..=10
96+
// We want to find the range with the highest end that still contains the current number
97+
else if let Some(range) = ranges_found
98+
.iter()
99+
.filter(|range| range.contains(&num))
100+
.max_by_key(|range| range.end())
101+
{
102+
num = range.end() + 1;
103+
} else {
104+
break 'contains false;
105+
}
106+
}
107+
break 'contains true;
108+
};
109+
110+
if contains_whole_range {
111+
span_lint_and_sugg(
112+
cx,
113+
MANUAL_RANGE_PATTERNS,
114+
pat.span,
115+
"this OR pattern can be rewritten using a range",
116+
"try",
117+
format!("{min}..={max}"),
118+
Applicability::MachineApplicable,
119+
);
120+
}
121+
}
122+
}
123+
}

tests/ui/manual_range_patterns.fixed

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//@run-rustfix
2+
3+
#![allow(unused)]
4+
#![warn(clippy::manual_range_patterns)]
5+
#![feature(exclusive_range_pattern)]
6+
7+
fn main() {
8+
let f = 6;
9+
10+
let _ = matches!(f, 1..=10);
11+
let _ = matches!(f, 1..=10);
12+
let _ = matches!(f, 4 | 2 | 3 | 1 | 5 | 6 | 9 | 8 | 10); // 7 is missing
13+
let _ = matches!(f, | 4);
14+
let _ = matches!(f, 4 | 5);
15+
let _ = matches!(f, 1 | 2147483647);
16+
let _ = matches!(f, 0 | 2147483647);
17+
let _ = matches!(f, -2147483647 | 2147483647);
18+
let _ = matches!(f, 1 | (2..=4));
19+
let _ = matches!(f, 1 | (2..4));
20+
let _ = matches!(f, 1..=48324729);
21+
let _ = matches!(f, 0..=48324730);
22+
let _ = matches!(f, 0..=3);
23+
#[allow(clippy::match_like_matches_macro)]
24+
let _ = match f {
25+
1..=10 => true,
26+
_ => false,
27+
};
28+
29+
macro_rules! mac {
30+
($e:expr) => {
31+
matches!($e, 1..=10)
32+
};
33+
}
34+
mac!(f);
35+
}

tests/ui/manual_range_patterns.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//@run-rustfix
2+
3+
#![allow(unused)]
4+
#![warn(clippy::manual_range_patterns)]
5+
#![feature(exclusive_range_pattern)]
6+
7+
fn main() {
8+
let f = 6;
9+
10+
let _ = matches!(f, 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10);
11+
let _ = matches!(f, 4 | 2 | 3 | 1 | 5 | 6 | 9 | 7 | 8 | 10);
12+
let _ = matches!(f, 4 | 2 | 3 | 1 | 5 | 6 | 9 | 8 | 10); // 7 is missing
13+
let _ = matches!(f, | 4);
14+
let _ = matches!(f, 4 | 5);
15+
let _ = matches!(f, 1 | 2147483647);
16+
let _ = matches!(f, 0 | 2147483647);
17+
let _ = matches!(f, -2147483647 | 2147483647);
18+
let _ = matches!(f, 1 | (2..=4));
19+
let _ = matches!(f, 1 | (2..4));
20+
let _ = matches!(f, (1..=10) | (2..=13) | (14..=48324728) | 48324729);
21+
let _ = matches!(f, 0 | (1..=10) | 48324730 | (2..=13) | (14..=48324728) | 48324729);
22+
let _ = matches!(f, 0..=1 | 0..=2 | 0..=3);
23+
#[allow(clippy::match_like_matches_macro)]
24+
let _ = match f {
25+
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 => true,
26+
_ => false,
27+
};
28+
29+
macro_rules! mac {
30+
($e:expr) => {
31+
matches!($e, 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10)
32+
};
33+
}
34+
mac!(f);
35+
}

tests/ui/manual_range_patterns.stderr

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
error: this OR pattern can be rewritten using a range
2+
--> $DIR/manual_range_patterns.rs:10:25
3+
|
4+
LL | let _ = matches!(f, 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10);
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `1..=10`
6+
|
7+
= note: `-D clippy::manual-range-patterns` implied by `-D warnings`
8+
9+
error: this OR pattern can be rewritten using a range
10+
--> $DIR/manual_range_patterns.rs:11:25
11+
|
12+
LL | let _ = matches!(f, 4 | 2 | 3 | 1 | 5 | 6 | 9 | 7 | 8 | 10);
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `1..=10`
14+
15+
error: this OR pattern can be rewritten using a range
16+
--> $DIR/manual_range_patterns.rs:20:25
17+
|
18+
LL | let _ = matches!(f, (1..=10) | (2..=13) | (14..=48324728) | 48324729);
19+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `1..=48324729`
20+
21+
error: this OR pattern can be rewritten using a range
22+
--> $DIR/manual_range_patterns.rs:21:25
23+
|
24+
LL | let _ = matches!(f, 0 | (1..=10) | 48324730 | (2..=13) | (14..=48324728) | 48324729);
25+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `0..=48324730`
26+
27+
error: this OR pattern can be rewritten using a range
28+
--> $DIR/manual_range_patterns.rs:22:25
29+
|
30+
LL | let _ = matches!(f, 0..=1 | 0..=2 | 0..=3);
31+
| ^^^^^^^^^^^^^^^^^^^^^ help: try: `0..=3`
32+
33+
error: this OR pattern can be rewritten using a range
34+
--> $DIR/manual_range_patterns.rs:25:9
35+
|
36+
LL | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 => true,
37+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `1..=10`
38+
39+
error: this OR pattern can be rewritten using a range
40+
--> $DIR/manual_range_patterns.rs:31:26
41+
|
42+
LL | matches!($e, 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10)
43+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `1..=10`
44+
...
45+
LL | mac!(f);
46+
| ------- in this macro invocation
47+
|
48+
= note: this error originates in the macro `mac` (in Nightly builds, run with -Z macro-backtrace for more info)
49+
50+
error: aborting due to 7 previous errors
51+

tests/ui/unnested_or_patterns.fixed

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
clippy::cognitive_complexity,
77
clippy::match_ref_pats,
88
clippy::upper_case_acronyms,
9-
clippy::needless_if
9+
clippy::needless_if,
10+
clippy::manual_range_patterns
1011
)]
1112
#![allow(unreachable_patterns, irrefutable_let_patterns, unused)]
1213

tests/ui/unnested_or_patterns.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
clippy::cognitive_complexity,
77
clippy::match_ref_pats,
88
clippy::upper_case_acronyms,
9-
clippy::needless_if
9+
clippy::needless_if,
10+
clippy::manual_range_patterns
1011
)]
1112
#![allow(unreachable_patterns, irrefutable_let_patterns, unused)]
1213

0 commit comments

Comments
 (0)