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};
11 declare_clippy_lint! {
13 /// Suggests to use dedicated built-in methods,
14 /// `is_ascii_(lowercase|uppercase|digit)` for checking on corresponding ascii range
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.
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'));
28 /// ('0'..='9').contains(&'0');
29 /// ('a'..='z').contains(&'a');
30 /// ('A'..='Z').contains(&'A');
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());
41 /// '0'.is_ascii_digit();
42 /// 'a'.is_ascii_lowercase();
43 /// 'A'.is_ascii_uppercase();
46 #[clippy::version = "1.66.0"]
47 pub MANUAL_IS_ASCII_CHECK,
49 "use dedicated method to check ascii range"
51 impl_lint_pass!(ManualIsAsciiCheck => [MANUAL_IS_ASCII_CHECK]);
53 pub struct ManualIsAsciiCheck {
57 impl ManualIsAsciiCheck {
59 pub fn new(msrv: Msrv) -> Self {
64 #[derive(Debug, PartialEq)]
66 /// 'a'..='z' | b'a'..=b'z'
68 /// 'A'..='Z' | b'A'..=b'Z'
70 /// AsciiLower | AsciiUpper
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) {
83 if in_constant(cx, expr.hir_id) && !self.msrv.meets(msrvs::IS_ASCII_DIGIT_CONST) {
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);
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);
101 check_is_ascii(cx, expr.span, arg, &range);
106 extract_msrv_attr!(LateContext);
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,
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);
122 let applicability = if recv == default_snip {
123 Applicability::HasPlaceholders
125 Applicability::MachineApplicable
130 MANUAL_IS_ASCII_CHECK,
132 "manual check for common ascii range",
134 format!("{recv}.{sugg}()"),
140 fn check_pat(pat_kind: &PatKind<'_>) -> CharRange {
142 PatKind::Or(pats) => {
143 let ranges = pats.iter().map(|p| check_pat(&p.kind)).collect::<Vec<_>>();
145 if ranges.len() == 2 && ranges.contains(&CharRange::UpperChar) && ranges.contains(&CharRange::LowerChar) {
151 PatKind::Range(Some(start), Some(end), kind) if *kind == RangeEnd::Included => check_range(start, end),
152 _ => CharRange::Otherwise,
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,
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;