Auto merge of #13334 - nyurik:ascii-str-eq, r=xFrednet
Add manual_ignore_cast_cmp lint
```rust
// bad
fn compare(a: &str, b: &str) -> bool {
a.to_ascii_lowercase() == b.to_ascii_lowercase()
|| a.to_ascii_lowercase() == "abc"
}
// good
fn compare(a: &str, b: &str) -> bool {
a.eq_ignore_ascii_case(b)
|| a.eq_ignore_ascii_case("abc")
}
```
- [x] Followed [lint naming conventions][lint_naming]
- [x] Added passing UI tests (including committed `.stderr` file)
- [x] `cargo test` passes locally
- [x] Executed `cargo dev update_lints`
- [x] Added lint documentation
- [x] Run `cargo dev fmt`
changelog: New lint: [`manual_ignore_case_cmp`] `perf`
[#13334](https://github.com/rust-lang/rust-clippy/pull/13334)
Closes #13204
This commit is contained in:
commit
c71f0bebd2
7 changed files with 891 additions and 0 deletions
|
|
@ -306,6 +306,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
|
|||
crate::manual_float_methods::MANUAL_IS_FINITE_INFO,
|
||||
crate::manual_float_methods::MANUAL_IS_INFINITE_INFO,
|
||||
crate::manual_hash_one::MANUAL_HASH_ONE_INFO,
|
||||
crate::manual_ignore_case_cmp::MANUAL_IGNORE_CASE_CMP_INFO,
|
||||
crate::manual_is_ascii_check::MANUAL_IS_ASCII_CHECK_INFO,
|
||||
crate::manual_is_power_of_two::MANUAL_IS_POWER_OF_TWO_INFO,
|
||||
crate::manual_let_else::MANUAL_LET_ELSE_INFO,
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ mod manual_clamp;
|
|||
mod manual_div_ceil;
|
||||
mod manual_float_methods;
|
||||
mod manual_hash_one;
|
||||
mod manual_ignore_case_cmp;
|
||||
mod manual_is_ascii_check;
|
||||
mod manual_is_power_of_two;
|
||||
mod manual_let_else;
|
||||
|
|
@ -947,5 +948,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
|
|||
store.register_late_pass(|_| Box::new(manual_is_power_of_two::ManualIsPowerOfTwo));
|
||||
store.register_late_pass(|_| Box::new(non_zero_suggestions::NonZeroSuggestions));
|
||||
store.register_late_pass(move |_| Box::new(unused_trait_names::UnusedTraitNames::new(conf)));
|
||||
store.register_late_pass(|_| Box::new(manual_ignore_case_cmp::ManualIgnoreCaseCmp));
|
||||
// add lints here, do not remove this comment, it's used in `new_lint`
|
||||
}
|
||||
|
|
|
|||
127
clippy_lints/src/manual_ignore_case_cmp.rs
Normal file
127
clippy_lints/src/manual_ignore_case_cmp.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
use crate::manual_ignore_case_cmp::MatchType::{Literal, ToAscii};
|
||||
use clippy_utils::diagnostics::span_lint_and_then;
|
||||
use clippy_utils::source::snippet_with_applicability;
|
||||
use clippy_utils::ty::{get_type_diagnostic_name, is_type_diagnostic_item, is_type_lang_item};
|
||||
use rustc_ast::LitKind;
|
||||
use rustc_errors::Applicability;
|
||||
use rustc_hir::ExprKind::{Binary, Lit, MethodCall};
|
||||
use rustc_hir::{BinOpKind, Expr, LangItem};
|
||||
use rustc_lint::{LateContext, LateLintPass};
|
||||
use rustc_middle::ty;
|
||||
use rustc_middle::ty::{Ty, UintTy};
|
||||
use rustc_session::declare_lint_pass;
|
||||
use rustc_span::{Span, sym};
|
||||
|
||||
declare_clippy_lint! {
|
||||
/// ### What it does
|
||||
/// Checks for manual case-insensitive ASCII comparison.
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
/// The `eq_ignore_ascii_case` method is faster because it does not allocate
|
||||
/// memory for the new strings, and it is more readable.
|
||||
///
|
||||
/// ### Example
|
||||
/// ```no_run
|
||||
/// fn compare(a: &str, b: &str) -> bool {
|
||||
/// a.to_ascii_lowercase() == b.to_ascii_lowercase() || a.to_ascii_lowercase() == "abc"
|
||||
/// }
|
||||
/// ```
|
||||
/// Use instead:
|
||||
/// ```no_run
|
||||
/// fn compare(a: &str, b: &str) -> bool {
|
||||
/// a.eq_ignore_ascii_case(b) || a.eq_ignore_ascii_case("abc")
|
||||
/// }
|
||||
/// ```
|
||||
#[clippy::version = "1.82.0"]
|
||||
pub MANUAL_IGNORE_CASE_CMP,
|
||||
perf,
|
||||
"manual case-insensitive ASCII comparison"
|
||||
}
|
||||
|
||||
declare_lint_pass!(ManualIgnoreCaseCmp => [MANUAL_IGNORE_CASE_CMP]);
|
||||
|
||||
enum MatchType<'a, 'b> {
|
||||
ToAscii(bool, Ty<'a>),
|
||||
Literal(&'b LitKind),
|
||||
}
|
||||
|
||||
fn get_ascii_type<'a, 'b>(cx: &LateContext<'a>, kind: rustc_hir::ExprKind<'b>) -> Option<(Span, MatchType<'a, 'b>)> {
|
||||
if let MethodCall(path, expr, _, _) = kind {
|
||||
let is_lower = match path.ident.name.as_str() {
|
||||
"to_ascii_lowercase" => true,
|
||||
"to_ascii_uppercase" => false,
|
||||
_ => return None,
|
||||
};
|
||||
let ty_raw = cx.typeck_results().expr_ty(expr);
|
||||
let ty = ty_raw.peel_refs();
|
||||
if needs_ref_to_cmp(cx, ty)
|
||||
|| ty.is_str()
|
||||
|| ty.is_slice()
|
||||
|| matches!(get_type_diagnostic_name(cx, ty), Some(sym::OsStr | sym::OsString))
|
||||
{
|
||||
return Some((expr.span, ToAscii(is_lower, ty_raw)));
|
||||
}
|
||||
} else if let Lit(expr) = kind {
|
||||
return Some((expr.span, Literal(&expr.node)));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns true if the type needs to be dereferenced to be compared
|
||||
fn needs_ref_to_cmp(cx: &LateContext<'_>, ty: Ty<'_>) -> bool {
|
||||
ty.is_char()
|
||||
|| *ty.kind() == ty::Uint(UintTy::U8)
|
||||
|| is_type_diagnostic_item(cx, ty, sym::Vec)
|
||||
|| is_type_lang_item(cx, ty, LangItem::String)
|
||||
}
|
||||
|
||||
impl LateLintPass<'_> for ManualIgnoreCaseCmp {
|
||||
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) {
|
||||
// check if expression represents a comparison of two strings
|
||||
// using .to_ascii_lowercase() or .to_ascii_uppercase() methods,
|
||||
// or one of the sides is a literal
|
||||
// Offer to replace it with .eq_ignore_ascii_case() method
|
||||
if let Binary(op, left, right) = &expr.kind
|
||||
&& (op.node == BinOpKind::Eq || op.node == BinOpKind::Ne)
|
||||
&& let Some((left_span, left_val)) = get_ascii_type(cx, left.kind)
|
||||
&& let Some((right_span, right_val)) = get_ascii_type(cx, right.kind)
|
||||
&& match (&left_val, &right_val) {
|
||||
(ToAscii(l_lower, ..), ToAscii(r_lower, ..)) if l_lower == r_lower => true,
|
||||
(ToAscii(..), Literal(..)) | (Literal(..), ToAscii(..)) => true,
|
||||
_ => false,
|
||||
}
|
||||
{
|
||||
let deref = match right_val {
|
||||
ToAscii(_, ty) if needs_ref_to_cmp(cx, ty) => "&",
|
||||
ToAscii(..) => "",
|
||||
Literal(ty) => {
|
||||
if let LitKind::Char(_) | LitKind::Byte(_) = ty {
|
||||
"&"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
},
|
||||
};
|
||||
let neg = if op.node == BinOpKind::Ne { "!" } else { "" };
|
||||
span_lint_and_then(
|
||||
cx,
|
||||
MANUAL_IGNORE_CASE_CMP,
|
||||
expr.span,
|
||||
"manual case-insensitive ASCII comparison",
|
||||
|diag| {
|
||||
let mut app = Applicability::MachineApplicable;
|
||||
diag.span_suggestion_verbose(
|
||||
expr.span,
|
||||
"consider using `.eq_ignore_ascii_case()` instead",
|
||||
format!(
|
||||
"{neg}{}.eq_ignore_ascii_case({deref}{})",
|
||||
snippet_with_applicability(cx, left_span, "_", &mut app),
|
||||
snippet_with_applicability(cx, right_span, "_", &mut app)
|
||||
),
|
||||
app,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue