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