rust/clippy_lints/src/string_patterns.rs
Philipp Krones 3ab1da8bab
Formatting
2024-09-22 20:52:15 +02:00

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);
}