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