-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathmanual_range_patterns.rs
159 lines (147 loc) · 5.42 KB
/
manual_range_patterns.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::source::SpanRangeExt;
use rustc_ast::LitKind;
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::Applicability;
use rustc_hir::{PatExpr, PatExprKind, PatKind, RangeEnd};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_session::declare_lint_pass;
use rustc_span::{DUMMY_SP, Span};
declare_clippy_lint! {
/// ### What it does
/// Looks for combined OR patterns that are all contained in a specific range,
/// e.g. `6 | 4 | 5 | 9 | 7 | 8` can be rewritten as `4..=9`.
///
/// ### Why is this bad?
/// Using an explicit range is more concise and easier to read.
///
/// ### Known issues
/// This lint intentionally does not handle numbers greater than `i128::MAX` for `u128` literals
/// in order to support negative numbers.
///
/// ### Example
/// ```no_run
/// let x = 6;
/// let foo = matches!(x, 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10);
/// ```
/// Use instead:
/// ```no_run
/// let x = 6;
/// let foo = matches!(x, 1..=10);
/// ```
#[clippy::version = "1.72.0"]
pub MANUAL_RANGE_PATTERNS,
complexity,
"manually writing range patterns using a combined OR pattern (`|`)"
}
declare_lint_pass!(ManualRangePatterns => [MANUAL_RANGE_PATTERNS]);
fn expr_as_i128(expr: &PatExpr<'_>) -> Option<i128> {
if let PatExprKind::Lit { lit, negated } = expr.kind
&& let LitKind::Int(num, _) = lit.node
{
// Intentionally not handling numbers greater than i128::MAX (for u128 literals) for now.
let n = i128::try_from(num.get()).ok()?;
Some(if negated { -n } else { n })
} else {
None
}
}
#[derive(Copy, Clone)]
struct Num {
val: i128,
span: Span,
}
impl Num {
fn new(expr: &PatExpr<'_>) -> Option<Self> {
Some(Self {
val: expr_as_i128(expr)?,
span: expr.span,
})
}
fn dummy(val: i128) -> Self {
Self { val, span: DUMMY_SP }
}
fn min(self, other: Self) -> Self {
if self.val < other.val { self } else { other }
}
}
impl LateLintPass<'_> for ManualRangePatterns {
fn check_pat(&mut self, cx: &LateContext<'_>, pat: &'_ rustc_hir::Pat<'_>) {
// a pattern like 1 | 2 seems fine, lint if there are at least 3 alternatives
// or more then one range (exclude triggering on stylistic using OR with one element
// like described https://github.com/rust-lang/rust-clippy/issues/11825)
if let PatKind::Or(pats) = pat.kind
&& (pats.len() >= 3 || (pats.len() > 1 && pats.iter().any(|p| matches!(p.kind, PatKind::Range(..)))))
&& !pat.span.in_external_macro(cx.sess().source_map())
{
let mut min = Num::dummy(i128::MAX);
let mut max = Num::dummy(i128::MIN);
let mut range_kind = RangeEnd::Included;
let mut numbers_found = FxHashSet::default();
let mut ranges_found = Vec::new();
for pat in pats {
if let PatKind::Expr(lit) = pat.kind
&& let Some(num) = Num::new(lit)
{
numbers_found.insert(num.val);
min = min.min(num);
if num.val >= max.val {
max = num;
range_kind = RangeEnd::Included;
}
} else if let PatKind::Range(Some(left), Some(right), end) = pat.kind
&& let Some(left) = Num::new(left)
&& let Some(mut right) = Num::new(right)
{
if let RangeEnd::Excluded = end {
right.val -= 1;
}
min = min.min(left);
if right.val > max.val {
max = right;
range_kind = end;
}
ranges_found.push(left.val..=right.val);
} else {
return;
}
}
let mut num = min.val;
while num <= max.val {
if numbers_found.contains(&num) {
num += 1;
}
// Given a list of (potentially overlapping) ranges like:
// 1..=5, 3..=7, 6..=10
// We want to find the range with the highest end that still contains the current number
else if let Some(range) = ranges_found
.iter()
.filter(|range| range.contains(&num))
.max_by_key(|range| range.end())
{
num = range.end() + 1;
} else {
return;
}
}
span_lint_and_then(
cx,
MANUAL_RANGE_PATTERNS,
pat.span,
"this OR pattern can be rewritten using a range",
|diag| {
if let Some(min) = min.span.get_source_text(cx)
&& let Some(max) = max.span.get_source_text(cx)
{
diag.span_suggestion(
pat.span,
"try",
format!("{min}{range_kind}{max}"),
Applicability::MachineApplicable,
);
}
},
);
}
}
}