]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/manual_is_ascii_check.rs
Rollup merge of #104901 - krtab:filetype_compare, r=the8472
[rust.git] / src / tools / clippy / clippy_lints / src / manual_is_ascii_check.rs
1 use clippy_utils::msrvs::{self, Msrv};
2 use clippy_utils::{diagnostics::span_lint_and_sugg, in_constant, macros::root_macro_call, source::snippet};
3 use rustc_ast::LitKind::{Byte, Char};
4 use rustc_errors::Applicability;
5 use rustc_hir::{Expr, ExprKind, PatKind, RangeEnd};
6 use rustc_lint::{LateContext, LateLintPass};
7 use rustc_session::{declare_tool_lint, impl_lint_pass};
8 use rustc_span::{def_id::DefId, sym};
9
10 declare_clippy_lint! {
11     /// ### What it does
12     /// Suggests to use dedicated built-in methods,
13     /// `is_ascii_(lowercase|uppercase|digit)` for checking on corresponding ascii range
14     ///
15     /// ### Why is this bad?
16     /// Using the built-in functions is more readable and makes it
17     /// clear that it's not a specific subset of characters, but all
18     /// ASCII (lowercase|uppercase|digit) characters.
19     /// ### Example
20     /// ```rust
21     /// fn main() {
22     ///     assert!(matches!('x', 'a'..='z'));
23     ///     assert!(matches!(b'X', b'A'..=b'Z'));
24     ///     assert!(matches!('2', '0'..='9'));
25     ///     assert!(matches!('x', 'A'..='Z' | 'a'..='z'));
26     /// }
27     /// ```
28     /// Use instead:
29     /// ```rust
30     /// fn main() {
31     ///     assert!('x'.is_ascii_lowercase());
32     ///     assert!(b'X'.is_ascii_uppercase());
33     ///     assert!('2'.is_ascii_digit());
34     ///     assert!('x'.is_ascii_alphabetic());
35     /// }
36     /// ```
37     #[clippy::version = "1.66.0"]
38     pub MANUAL_IS_ASCII_CHECK,
39     style,
40     "use dedicated method to check ascii range"
41 }
42 impl_lint_pass!(ManualIsAsciiCheck => [MANUAL_IS_ASCII_CHECK]);
43
44 pub struct ManualIsAsciiCheck {
45     msrv: Msrv,
46 }
47
48 impl ManualIsAsciiCheck {
49     #[must_use]
50     pub fn new(msrv: Msrv) -> Self {
51         Self { msrv }
52     }
53 }
54
55 #[derive(Debug, PartialEq)]
56 enum CharRange {
57     /// 'a'..='z' | b'a'..=b'z'
58     LowerChar,
59     /// 'A'..='Z' | b'A'..=b'Z'
60     UpperChar,
61     /// AsciiLower | AsciiUpper
62     FullChar,
63     /// '0..=9'
64     Digit,
65     Otherwise,
66 }
67
68 impl<'tcx> LateLintPass<'tcx> for ManualIsAsciiCheck {
69     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
70         if !self.msrv.meets(msrvs::IS_ASCII_DIGIT) {
71             return;
72         }
73
74         if in_constant(cx, expr.hir_id) && !self.msrv.meets(msrvs::IS_ASCII_DIGIT_CONST) {
75             return;
76         }
77
78         let Some(macro_call) = root_macro_call(expr.span) else { return };
79
80         if is_matches_macro(cx, macro_call.def_id) {
81             if let ExprKind::Match(recv, [arm, ..], _) = expr.kind {
82                 let range = check_pat(&arm.pat.kind);
83
84                 if let Some(sugg) = match range {
85                     CharRange::UpperChar => Some("is_ascii_uppercase"),
86                     CharRange::LowerChar => Some("is_ascii_lowercase"),
87                     CharRange::FullChar => Some("is_ascii_alphabetic"),
88                     CharRange::Digit => Some("is_ascii_digit"),
89                     CharRange::Otherwise => None,
90                 } {
91                     let default_snip = "..";
92                     // `snippet_with_applicability` may set applicability to `MaybeIncorrect` for
93                     // macro span, so we check applicability manually by comparing `recv` is not default.
94                     let recv = snippet(cx, recv.span, default_snip);
95
96                     let applicability = if recv == default_snip {
97                         Applicability::HasPlaceholders
98                     } else {
99                         Applicability::MachineApplicable
100                     };
101
102                     span_lint_and_sugg(
103                         cx,
104                         MANUAL_IS_ASCII_CHECK,
105                         macro_call.span,
106                         "manual check for common ascii range",
107                         "try",
108                         format!("{recv}.{sugg}()"),
109                         applicability,
110                     );
111                 }
112             }
113         }
114     }
115
116     extract_msrv_attr!(LateContext);
117 }
118
119 fn check_pat(pat_kind: &PatKind<'_>) -> CharRange {
120     match pat_kind {
121         PatKind::Or(pats) => {
122             let ranges = pats.iter().map(|p| check_pat(&p.kind)).collect::<Vec<_>>();
123
124             if ranges.len() == 2 && ranges.contains(&CharRange::UpperChar) && ranges.contains(&CharRange::LowerChar) {
125                 CharRange::FullChar
126             } else {
127                 CharRange::Otherwise
128             }
129         },
130         PatKind::Range(Some(start), Some(end), kind) if *kind == RangeEnd::Included => check_range(start, end),
131         _ => CharRange::Otherwise,
132     }
133 }
134
135 fn check_range(start: &Expr<'_>, end: &Expr<'_>) -> CharRange {
136     if let ExprKind::Lit(start_lit) = &start.kind
137         && let ExprKind::Lit(end_lit) = &end.kind {
138         match (&start_lit.node, &end_lit.node) {
139             (Char('a'), Char('z')) | (Byte(b'a'), Byte(b'z')) => CharRange::LowerChar,
140             (Char('A'), Char('Z')) | (Byte(b'A'), Byte(b'Z')) => CharRange::UpperChar,
141             (Char('0'), Char('9')) | (Byte(b'0'), Byte(b'9')) => CharRange::Digit,
142             _ => CharRange::Otherwise,
143         }
144     } else {
145         CharRange::Otherwise
146     }
147 }
148
149 fn is_matches_macro(cx: &LateContext<'_>, macro_def_id: DefId) -> bool {
150     if let Some(name) = cx.tcx.get_diagnostic_name(macro_def_id) {
151         return sym::matches_macro == name;
152     }
153
154     false
155 }