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
expr_as_u128(expr: &Expr<'_>) -> Option<u128>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 {
check_pat(&mut self, cx: &LateContext<'_>, pat: &'_ rustc_hir::Pat<'_>)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 }
124