• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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