2022-12-01 11:29:38 -06:00
|
|
|
use clippy_utils::msrvs::{self, Msrv};
|
2022-12-17 07:12:54 -06:00
|
|
|
use clippy_utils::{diagnostics::span_lint_and_sugg, higher, in_constant, macros::root_macro_call, source::snippet};
|
|
|
|
use rustc_ast::ast::RangeLimits;
|
2022-11-21 13:34:47 -06:00
|
|
|
use rustc_ast::LitKind::{Byte, Char};
|
|
|
|
use rustc_errors::Applicability;
|
2022-12-17 07:12:54 -06:00
|
|
|
use rustc_hir::{BorrowKind, Expr, ExprKind, PatKind, RangeEnd};
|
2022-11-21 13:34:47 -06:00
|
|
|
use rustc_lint::{LateContext, LateLintPass};
|
|
|
|
use rustc_session::{declare_tool_lint, impl_lint_pass};
|
2022-12-17 07:12:54 -06:00
|
|
|
use rustc_span::{def_id::DefId, sym, Span};
|
2022-11-21 13:34:47 -06:00
|
|
|
|
|
|
|
declare_clippy_lint! {
|
|
|
|
/// ### What it does
|
|
|
|
/// Suggests to use dedicated built-in methods,
|
|
|
|
/// `is_ascii_(lowercase|uppercase|digit)` for checking on corresponding ascii range
|
|
|
|
///
|
|
|
|
/// ### Why is this bad?
|
|
|
|
/// Using the built-in functions is more readable and makes it
|
|
|
|
/// clear that it's not a specific subset of characters, but all
|
|
|
|
/// ASCII (lowercase|uppercase|digit) characters.
|
|
|
|
/// ### Example
|
|
|
|
/// ```rust
|
|
|
|
/// fn main() {
|
|
|
|
/// assert!(matches!('x', 'a'..='z'));
|
|
|
|
/// assert!(matches!(b'X', b'A'..=b'Z'));
|
|
|
|
/// assert!(matches!('2', '0'..='9'));
|
|
|
|
/// assert!(matches!('x', 'A'..='Z' | 'a'..='z'));
|
2022-12-17 07:12:54 -06:00
|
|
|
///
|
|
|
|
/// ('0'..='9').contains(&'0');
|
|
|
|
/// ('a'..='z').contains(&'a');
|
|
|
|
/// ('A'..='Z').contains(&'A');
|
2022-11-21 13:34:47 -06:00
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
/// Use instead:
|
|
|
|
/// ```rust
|
|
|
|
/// fn main() {
|
|
|
|
/// assert!('x'.is_ascii_lowercase());
|
|
|
|
/// assert!(b'X'.is_ascii_uppercase());
|
|
|
|
/// assert!('2'.is_ascii_digit());
|
|
|
|
/// assert!('x'.is_ascii_alphabetic());
|
2022-12-17 07:12:54 -06:00
|
|
|
///
|
|
|
|
/// '0'.is_ascii_digit();
|
|
|
|
/// 'a'.is_ascii_lowercase();
|
|
|
|
/// 'A'.is_ascii_uppercase();
|
2022-11-21 13:34:47 -06:00
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
#[clippy::version = "1.66.0"]
|
|
|
|
pub MANUAL_IS_ASCII_CHECK,
|
|
|
|
style,
|
|
|
|
"use dedicated method to check ascii range"
|
|
|
|
}
|
|
|
|
impl_lint_pass!(ManualIsAsciiCheck => [MANUAL_IS_ASCII_CHECK]);
|
|
|
|
|
|
|
|
pub struct ManualIsAsciiCheck {
|
2022-12-01 11:29:38 -06:00
|
|
|
msrv: Msrv,
|
2022-11-21 13:34:47 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ManualIsAsciiCheck {
|
|
|
|
#[must_use]
|
2022-12-01 11:29:38 -06:00
|
|
|
pub fn new(msrv: Msrv) -> Self {
|
2022-11-21 13:34:47 -06:00
|
|
|
Self { msrv }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
enum CharRange {
|
|
|
|
/// 'a'..='z' | b'a'..=b'z'
|
|
|
|
LowerChar,
|
|
|
|
/// 'A'..='Z' | b'A'..=b'Z'
|
|
|
|
UpperChar,
|
|
|
|
/// AsciiLower | AsciiUpper
|
|
|
|
FullChar,
|
|
|
|
/// '0..=9'
|
|
|
|
Digit,
|
|
|
|
Otherwise,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'tcx> LateLintPass<'tcx> for ManualIsAsciiCheck {
|
|
|
|
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
|
2022-12-01 11:29:38 -06:00
|
|
|
if !self.msrv.meets(msrvs::IS_ASCII_DIGIT) {
|
2022-11-21 13:34:47 -06:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-12-01 11:29:38 -06:00
|
|
|
if in_constant(cx, expr.hir_id) && !self.msrv.meets(msrvs::IS_ASCII_DIGIT_CONST) {
|
2022-11-21 13:34:47 -06:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-12-17 07:12:54 -06:00
|
|
|
if let Some(macro_call) = root_macro_call(expr.span)
|
|
|
|
&& is_matches_macro(cx, macro_call.def_id) {
|
2022-11-21 13:34:47 -06:00
|
|
|
if let ExprKind::Match(recv, [arm, ..], _) = expr.kind {
|
|
|
|
let range = check_pat(&arm.pat.kind);
|
2022-12-17 07:12:54 -06:00
|
|
|
check_is_ascii(cx, macro_call.span, recv, &range);
|
|
|
|
}
|
|
|
|
} else if let ExprKind::MethodCall(path, receiver, [arg], ..) = expr.kind
|
|
|
|
&& path.ident.name == sym!(contains)
|
|
|
|
&& let Some(higher::Range { start: Some(start), end: Some(end), limits: RangeLimits::Closed })
|
|
|
|
= higher::Range::hir(receiver) {
|
|
|
|
let range = check_range(start, end);
|
|
|
|
if let ExprKind::AddrOf(BorrowKind::Ref, _, e) = arg.kind {
|
|
|
|
check_is_ascii(cx, expr.span, e, &range);
|
|
|
|
} else {
|
|
|
|
check_is_ascii(cx, expr.span, arg, &range);
|
2022-11-21 13:34:47 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extract_msrv_attr!(LateContext);
|
|
|
|
}
|
|
|
|
|
2022-12-17 07:12:54 -06:00
|
|
|
fn check_is_ascii(cx: &LateContext<'_>, span: Span, recv: &Expr<'_>, range: &CharRange) {
|
|
|
|
if let Some(sugg) = match range {
|
|
|
|
CharRange::UpperChar => Some("is_ascii_uppercase"),
|
|
|
|
CharRange::LowerChar => Some("is_ascii_lowercase"),
|
|
|
|
CharRange::FullChar => Some("is_ascii_alphabetic"),
|
|
|
|
CharRange::Digit => Some("is_ascii_digit"),
|
|
|
|
CharRange::Otherwise => None,
|
|
|
|
} {
|
|
|
|
let default_snip = "..";
|
|
|
|
// `snippet_with_applicability` may set applicability to `MaybeIncorrect` for
|
|
|
|
// macro span, so we check applicability manually by comparing `recv` is not default.
|
|
|
|
let recv = snippet(cx, recv.span, default_snip);
|
|
|
|
|
|
|
|
let applicability = if recv == default_snip {
|
|
|
|
Applicability::HasPlaceholders
|
|
|
|
} else {
|
|
|
|
Applicability::MachineApplicable
|
|
|
|
};
|
|
|
|
|
|
|
|
span_lint_and_sugg(
|
|
|
|
cx,
|
|
|
|
MANUAL_IS_ASCII_CHECK,
|
|
|
|
span,
|
|
|
|
"manual check for common ascii range",
|
|
|
|
"try",
|
|
|
|
format!("{recv}.{sugg}()"),
|
|
|
|
applicability,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-21 13:34:47 -06:00
|
|
|
fn check_pat(pat_kind: &PatKind<'_>) -> CharRange {
|
|
|
|
match pat_kind {
|
|
|
|
PatKind::Or(pats) => {
|
|
|
|
let ranges = pats.iter().map(|p| check_pat(&p.kind)).collect::<Vec<_>>();
|
|
|
|
|
|
|
|
if ranges.len() == 2 && ranges.contains(&CharRange::UpperChar) && ranges.contains(&CharRange::LowerChar) {
|
|
|
|
CharRange::FullChar
|
|
|
|
} else {
|
|
|
|
CharRange::Otherwise
|
|
|
|
}
|
|
|
|
},
|
|
|
|
PatKind::Range(Some(start), Some(end), kind) if *kind == RangeEnd::Included => check_range(start, end),
|
|
|
|
_ => CharRange::Otherwise,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn check_range(start: &Expr<'_>, end: &Expr<'_>) -> CharRange {
|
|
|
|
if let ExprKind::Lit(start_lit) = &start.kind
|
|
|
|
&& let ExprKind::Lit(end_lit) = &end.kind {
|
|
|
|
match (&start_lit.node, &end_lit.node) {
|
|
|
|
(Char('a'), Char('z')) | (Byte(b'a'), Byte(b'z')) => CharRange::LowerChar,
|
|
|
|
(Char('A'), Char('Z')) | (Byte(b'A'), Byte(b'Z')) => CharRange::UpperChar,
|
|
|
|
(Char('0'), Char('9')) | (Byte(b'0'), Byte(b'9')) => CharRange::Digit,
|
|
|
|
_ => CharRange::Otherwise,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
CharRange::Otherwise
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_matches_macro(cx: &LateContext<'_>, macro_def_id: DefId) -> bool {
|
|
|
|
if let Some(name) = cx.tcx.get_diagnostic_name(macro_def_id) {
|
|
|
|
return sym::matches_macro == name;
|
|
|
|
}
|
|
|
|
|
|
|
|
false
|
|
|
|
}
|