247 lines
8.5 KiB
Rust
247 lines
8.5 KiB
Rust
use std::ops::ControlFlow;
|
|
|
|
use clippy_config::Conf;
|
|
use clippy_config::msrvs::{self, Msrv};
|
|
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::path_to_local_id;
|
|
use clippy_utils::source::{snippet, str_literal_to_char_literal};
|
|
use clippy_utils::visitors::{Descend, for_each_expr};
|
|
use itertools::Itertools;
|
|
use rustc_ast::{BinOpKind, LitKind};
|
|
use rustc_errors::Applicability;
|
|
use rustc_hir::{Expr, ExprKind, PatKind};
|
|
use rustc_lint::{LateContext, LateLintPass};
|
|
use rustc_middle::ty;
|
|
use rustc_session::impl_lint_pass;
|
|
use rustc_span::{Span, sym};
|
|
|
|
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\")`"
|
|
}
|
|
|
|
pub struct StringPatterns {
|
|
msrv: Msrv,
|
|
}
|
|
|
|
impl StringPatterns {
|
|
pub fn new(conf: &'static Conf) -> Self {
|
|
Self {
|
|
msrv: conf.msrv.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl_lint_pass!(StringPatterns => [MANUAL_PATTERN_CHAR_COMPARISON, SINGLE_CHAR_PATTERN]);
|
|
|
|
const PATTERN_METHODS: [(&str, usize); 22] = [
|
|
("contains", 0),
|
|
("starts_with", 0),
|
|
("ends_with", 0),
|
|
("find", 0),
|
|
("rfind", 0),
|
|
("split", 0),
|
|
("split_inclusive", 0),
|
|
("rsplit", 0),
|
|
("split_terminator", 0),
|
|
("rsplit_terminator", 0),
|
|
("splitn", 1),
|
|
("rsplitn", 1),
|
|
("split_once", 0),
|
|
("rsplit_once", 0),
|
|
("matches", 0),
|
|
("rmatches", 0),
|
|
("match_indices", 0),
|
|
("rmatch_indices", 0),
|
|
("trim_start_matches", 0),
|
|
("trim_end_matches", 0),
|
|
("replace", 0),
|
|
("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, body.value, |sub_expr| -> ControlFlow<(), Descend> {
|
|
match sub_expr.kind {
|
|
ExprKind::Binary(op, left, right) if op.node == BinOpKind::Eq => {
|
|
if path_to_local_id(left, binding)
|
|
&& let Some(span) = get_char_span(cx, right)
|
|
{
|
|
set_char_spans.push(span);
|
|
ControlFlow::Continue(Descend::No)
|
|
} else if path_to_local_id(right, 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 matching_root_macro_call(cx, sub_expr.span, sym::matches_macro).is_none()
|
|
|| arm.guard.is_some()
|
|
|| !path_to_local_id(match_value, binding)
|
|
{
|
|
return ControlFlow::Break(());
|
|
}
|
|
if arm.pat.walk_short(|pat| match pat.kind {
|
|
PatKind::Lit(expr) if let ExprKind::Lit(lit) = 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(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 ty::Ref(_, ty, _) = cx.typeck_results().expr_ty_adjusted(receiver).kind()
|
|
&& ty.is_str()
|
|
&& let method_name = method.ident.name.as_str()
|
|
&& let Some(&(_, pos)) = PATTERN_METHODS
|
|
.iter()
|
|
.find(|(array_method_name, _)| *array_method_name == method_name)
|
|
&& let Some(arg) = args.get(pos)
|
|
{
|
|
check_single_char_pattern_lint(cx, arg);
|
|
|
|
check_manual_pattern_char_comparison(cx, arg, &self.msrv);
|
|
}
|
|
}
|
|
|
|
extract_msrv_attr!(LateContext);
|
|
}
|