| use std::ops::ControlFlow; | |
| use clippy_config::Conf; | |
| use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; | |
| use clippy_utils::eager_or_lazy::switch_to_eager_eval; | |
| use clippy_utils::macros::matching_root_macro_call; | |
| use clippy_utils::msrvs::{self, Msrv}; | |
| use clippy_utils::res::MaybeResPath as _; | |
| use clippy_utils::source::{snippet, str_literal_to_char_literal}; | |
| use clippy_utils::sym; | |
| use clippy_utils::visitors::{Descend, for_each_expr}; | |
| use itertools::Itertools as _; | |
| use rustc_ast::{BinOpKind, LitKind}; | |
| use rustc_errors::Applicability; | |
| use rustc_hir::{Expr, ExprKind, PatExprKind, PatKind}; | |
| use rustc_lint::{LateContext, LateLintPass, impl_lint_pass}; | |
| use rustc_middle::ty; | |
| use rustc_span::{Span, Symbol}; | |
| declare_clippy_lint! { | |
| /// ### What it does | |
| /// Checks for manual `char` comparison in string patterns | |
| /// | |
| /// ### Why is this bad? | |
| /// This can be written more concisely using a `char` or an array of `char`. | |
| /// This is more readable and more optimized when comparing to only one `char`. | |
| /// | |
| /// ### Example | |
| /// ```no_run | |
| /// "Hello World!".trim_end_matches(|c| c == '.' || c == ',' || c == '!' || c == '?'); | |
| /// ``` | |
| /// Use instead: | |
| /// ```no_run | |
| /// "Hello World!".trim_end_matches(['.', ',', '!', '?']); | |
| /// ``` | |
| #[clippy::version = "1.81.0"] | |
| pub MANUAL_PATTERN_CHAR_COMPARISON, | |
| style, | |
| "manual char comparison in string patterns" | |
| } | |
| declare_clippy_lint! { | |
| /// ### What it does | |
| /// Checks for string methods that receive a single-character | |
| /// `str` as an argument, e.g., `_.split("x")`. | |
| /// | |
| /// ### Why is this bad? | |
| /// While this can make a perf difference on some systems, | |
| /// benchmarks have proven inconclusive. But at least using a | |
| /// char literal makes it clear that we are looking at a single | |
| /// character. | |
| /// | |
| /// ### Known problems | |
| /// Does not catch multi-byte unicode characters. This is by | |
| /// design, on many machines, splitting by a non-ascii char is | |
| /// actually slower. Please do your own measurements instead of | |
| /// relying solely on the results of this lint. | |
| /// | |
| /// ### Example | |
| /// ```rust,ignore | |
| /// _.split("x"); | |
| /// ``` | |
| /// | |
| /// Use instead: | |
| /// ```rust,ignore | |
| /// _.split('x'); | |
| /// ``` | |
| #[clippy::version = "pre 1.29.0"] | |
| pub SINGLE_CHAR_PATTERN, | |
| pedantic, | |
| "using a single-character str where a char could be used, e.g., `_.split(\"x\")`" | |
| } | |
| impl_lint_pass!(StringPatterns => [ | |
| MANUAL_PATTERN_CHAR_COMPARISON, | |
| SINGLE_CHAR_PATTERN, | |
| ]); | |
| pub struct StringPatterns { | |
| msrv: Msrv, | |
| } | |
| impl StringPatterns { | |
| pub fn new(conf: &'static Conf) -> Self { | |
| Self { msrv: conf.msrv.into() } | |
| } | |
| } | |
| const PATTERN_METHODS: [(Symbol, usize); 22] = [ | |
| (sym::contains, 0), | |
| (sym::starts_with, 0), | |
| (sym::ends_with, 0), | |
| (sym::find, 0), | |
| (sym::rfind, 0), | |
| (sym::split, 0), | |
| (sym::split_inclusive, 0), | |
| (sym::rsplit, 0), | |
| (sym::split_terminator, 0), | |
| (sym::rsplit_terminator, 0), | |
| (sym::splitn, 1), | |
| (sym::rsplitn, 1), | |
| (sym::split_once, 0), | |
| (sym::rsplit_once, 0), | |
| (sym::matches, 0), | |
| (sym::rmatches, 0), | |
| (sym::match_indices, 0), | |
| (sym::rmatch_indices, 0), | |
| (sym::trim_start_matches, 0), | |
| (sym::trim_end_matches, 0), | |
| (sym::replace, 0), | |
| (sym::replacen, 0), | |
| ]; | |
| fn check_single_char_pattern_lint(cx: &LateContext<'_>, arg: &Expr<'_>) { | |
| let mut applicability = Applicability::MachineApplicable; | |
| if let Some(hint) = str_literal_to_char_literal(cx, arg, &mut applicability, true) { | |
| span_lint_and_sugg( | |
| cx, | |
| SINGLE_CHAR_PATTERN, | |
| arg.span, | |
| "single-character string constant used as pattern", | |
| "consider using a `char`", | |
| hint, | |
| applicability, | |
| ); | |
| } | |
| } | |
| fn get_char_span<'tcx>(cx: &'_ LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<Span> { | |
| if cx.typeck_results().expr_ty_adjusted(expr).is_char() | |
| && !expr.span.from_expansion() | |
| && switch_to_eager_eval(cx, expr) | |
| { | |
| Some(expr.span) | |
| } else { | |
| None | |
| } | |
| } | |
| fn check_manual_pattern_char_comparison(cx: &LateContext<'_>, method_arg: &Expr<'_>, msrv: Msrv) { | |
| if let ExprKind::Closure(closure) = method_arg.kind | |
| && let body = cx.tcx.hir_body(closure.body) | |
| && let Some(PatKind::Binding(_, binding, ..)) = body.params.first().map(|p| p.pat.kind) | |
| { | |
| let mut set_char_spans: Vec<Span> = Vec::new(); | |
| // We want to retrieve all the comparisons done. | |
| // They are ordered in a nested way and so we need to traverse the AST to collect them all. | |
| if for_each_expr(cx.tcx, body.value, |sub_expr| -> ControlFlow<(), Descend> { | |
| match sub_expr.kind { | |
| ExprKind::Binary(op, left, right) if op.node == BinOpKind::Eq => { | |
| if left.res_local_id() == Some(binding) | |
| && let Some(span) = get_char_span(cx, right) | |
| { | |
| set_char_spans.push(span); | |
| ControlFlow::Continue(Descend::No) | |
| } else if right.res_local_id() == Some(binding) | |
| && let Some(span) = get_char_span(cx, left) | |
| { | |
| set_char_spans.push(span); | |
| ControlFlow::Continue(Descend::No) | |
| } else { | |
| ControlFlow::Break(()) | |
| } | |
| }, | |
| ExprKind::Binary(op, _, _) if op.node == BinOpKind::Or => ControlFlow::Continue(Descend::Yes), | |
| ExprKind::Match(match_value, [arm, _], _) => { | |
| if arm.guard.is_some() | |
| || match_value.res_local_id() != Some(binding) | |
| || matching_root_macro_call(cx, sub_expr.span, sym::matches_macro).is_none() | |
| { | |
| return ControlFlow::Break(()); | |
| } | |
| if arm.pat.walk_short(|pat| match pat.kind { | |
| PatKind::Expr(expr) if let PatExprKind::Lit { lit, negated: false } = expr.kind => { | |
| if let LitKind::Char(_) = lit.node { | |
| set_char_spans.push(lit.span); | |
| } | |
| true | |
| }, | |
| PatKind::Or(_) => true, | |
| _ => false, | |
| }) { | |
| ControlFlow::Continue(Descend::No) | |
| } else { | |
| ControlFlow::Break(()) | |
| } | |
| }, | |
| _ => ControlFlow::Break(()), | |
| } | |
| }) | |
| .is_some() | |
| { | |
| return; | |
| } | |
| if set_char_spans.len() > 1 && !msrv.meets(cx, msrvs::PATTERN_TRAIT_CHAR_ARRAY) { | |
| return; | |
| } | |
| span_lint_and_then( | |
| cx, | |
| MANUAL_PATTERN_CHAR_COMPARISON, | |
| method_arg.span, | |
| "this manual char comparison can be written more succinctly", | |
| |diag| { | |
| if let [set_char_span] = set_char_spans[..] { | |
| diag.span_suggestion( | |
| method_arg.span, | |
| "consider using a `char`", | |
| snippet(cx, set_char_span, "c"), | |
| Applicability::MachineApplicable, | |
| ); | |
| } else { | |
| diag.span_suggestion( | |
| method_arg.span, | |
| "consider using an array of `char`", | |
| format!( | |
| "[{}]", | |
| set_char_spans.into_iter().map(|span| snippet(cx, span, "c")).join(", ") | |
| ), | |
| Applicability::MachineApplicable, | |
| ); | |
| } | |
| }, | |
| ); | |
| } | |
| } | |
| impl<'tcx> LateLintPass<'tcx> for StringPatterns { | |
| fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { | |
| if !expr.span.from_expansion() | |
| && let ExprKind::MethodCall(method, receiver, args, _) = expr.kind | |
| && let method_name = method.ident.name | |
| && let Some(&(_, pos)) = PATTERN_METHODS | |
| .iter() | |
| .find(|(array_method_name, _)| *array_method_name == method_name) | |
| && let Some(arg) = args.get(pos) | |
| && let ty::Ref(_, ty, _) = cx.typeck_results().expr_ty_adjusted(receiver).kind() | |
| && ty.is_str() | |
| { | |
| check_single_char_pattern_lint(cx, arg); | |
| check_manual_pattern_char_comparison(cx, arg, self.msrv); | |
| } | |
| } | |
| } |