rust/clippy_lints/src/manual_strip.rs
Nilstrieb ed0dfed24f Improve spans for indexing expressions
Indexing is similar to method calls in having an arbitrary
left-hand-side and then something on the right, which is the main part
of the expression. Method calls already have a span for that right part,
but indexing does not. This means that long method chains that use
indexing have really bad spans, especially when the indexing panics and
that span in coverted into a panic location.

This does the same thing as method calls for the AST and HIR, storing an
extra span which is then put into the `fn_span` field in THIR.
2023-08-04 13:17:39 +02:00

250 lines
8.9 KiB
Rust

use clippy_utils::consts::{constant, Constant};
use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then};
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::source::snippet;
use clippy_utils::usage::mutated_variables;
use clippy_utils::{eq_expr_value, higher, match_def_path, paths};
use if_chain::if_chain;
use rustc_ast::ast::LitKind;
use rustc_hir::def::Res;
use rustc_hir::intravisit::{walk_expr, Visitor};
use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty;
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::source_map::Spanned;
use rustc_span::Span;
declare_clippy_lint! {
/// ### What it does
/// Suggests using `strip_{prefix,suffix}` over `str::{starts,ends}_with` and slicing using
/// the pattern's length.
///
/// ### Why is this bad?
/// Using `str:strip_{prefix,suffix}` is safer and may have better performance as there is no
/// slicing which may panic and the compiler does not need to insert this panic code. It is
/// also sometimes more readable as it removes the need for duplicating or storing the pattern
/// used by `str::{starts,ends}_with` and in the slicing.
///
/// ### Example
/// ```rust
/// let s = "hello, world!";
/// if s.starts_with("hello, ") {
/// assert_eq!(s["hello, ".len()..].to_uppercase(), "WORLD!");
/// }
/// ```
/// Use instead:
/// ```rust
/// let s = "hello, world!";
/// if let Some(end) = s.strip_prefix("hello, ") {
/// assert_eq!(end.to_uppercase(), "WORLD!");
/// }
/// ```
#[clippy::version = "1.48.0"]
pub MANUAL_STRIP,
complexity,
"suggests using `strip_{prefix,suffix}` over `str::{starts,ends}_with` and slicing"
}
pub struct ManualStrip {
msrv: Msrv,
}
impl ManualStrip {
#[must_use]
pub fn new(msrv: Msrv) -> Self {
Self { msrv }
}
}
impl_lint_pass!(ManualStrip => [MANUAL_STRIP]);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StripKind {
Prefix,
Suffix,
}
impl<'tcx> LateLintPass<'tcx> for ManualStrip {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if !self.msrv.meets(msrvs::STR_STRIP_PREFIX) {
return;
}
if_chain! {
if let Some(higher::If { cond, then, .. }) = higher::If::hir(expr);
if let ExprKind::MethodCall(_, target_arg, [pattern], _) = cond.kind;
if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(cond.hir_id);
if let ExprKind::Path(target_path) = &target_arg.kind;
then {
let strip_kind = if match_def_path(cx, method_def_id, &paths::STR_STARTS_WITH) {
StripKind::Prefix
} else if match_def_path(cx, method_def_id, &paths::STR_ENDS_WITH) {
StripKind::Suffix
} else {
return;
};
let target_res = cx.qpath_res(target_path, target_arg.hir_id);
if target_res == Res::Err {
return;
};
if_chain! {
if let Res::Local(hir_id) = target_res;
if let Some(used_mutably) = mutated_variables(then, cx);
if used_mutably.contains(&hir_id);
then {
return;
}
}
let strippings = find_stripping(cx, strip_kind, target_res, pattern, then);
if !strippings.is_empty() {
let kind_word = match strip_kind {
StripKind::Prefix => "prefix",
StripKind::Suffix => "suffix",
};
let test_span = expr.span.until(then.span);
span_lint_and_then(cx, MANUAL_STRIP, strippings[0], &format!("stripping a {kind_word} manually"), |diag| {
diag.span_note(test_span, format!("the {kind_word} was tested here"));
multispan_sugg(
diag,
&format!("try using the `strip_{kind_word}` method"),
vec![(test_span,
format!("if let Some(<stripped>) = {}.strip_{kind_word}({}) ",
snippet(cx, target_arg.span, ".."),
snippet(cx, pattern.span, "..")))]
.into_iter().chain(strippings.into_iter().map(|span| (span, "<stripped>".into()))),
);
});
}
}
}
}
extract_msrv_attr!(LateContext);
}
// Returns `Some(arg)` if `expr` matches `arg.len()` and `None` otherwise.
fn len_arg<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> {
if_chain! {
if let ExprKind::MethodCall(_, arg, [], _) = expr.kind;
if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id);
if match_def_path(cx, method_def_id, &paths::STR_LEN);
then {
Some(arg)
} else {
None
}
}
}
// Returns the length of the `expr` if it's a constant string or char.
fn constant_length(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<u128> {
let value = constant(cx, cx.typeck_results(), expr)?;
match value {
Constant::Str(value) => Some(value.len() as u128),
Constant::Char(value) => Some(value.len_utf8() as u128),
_ => None,
}
}
// Tests if `expr` equals the length of the pattern.
fn eq_pattern_length<'tcx>(cx: &LateContext<'tcx>, pattern: &Expr<'_>, expr: &'tcx Expr<'_>) -> bool {
if let ExprKind::Lit(Spanned {
node: LitKind::Int(n, _),
..
}) = expr.kind
{
constant_length(cx, pattern).map_or(false, |length| length == *n)
} else {
len_arg(cx, expr).map_or(false, |arg| eq_expr_value(cx, pattern, arg))
}
}
// Tests if `expr` is a `&str`.
fn is_ref_str(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
match cx.typeck_results().expr_ty_adjusted(expr).kind() {
ty::Ref(_, ty, _) => ty.is_str(),
_ => false,
}
}
// Removes the outer `AddrOf` expression if needed.
fn peel_ref<'a>(expr: &'a Expr<'_>) -> &'a Expr<'a> {
if let ExprKind::AddrOf(BorrowKind::Ref, _, unref) = &expr.kind {
unref
} else {
expr
}
}
// Find expressions where `target` is stripped using the length of `pattern`.
// We'll suggest replacing these expressions with the result of the `strip_{prefix,suffix}`
// method.
fn find_stripping<'tcx>(
cx: &LateContext<'tcx>,
strip_kind: StripKind,
target: Res,
pattern: &'tcx Expr<'_>,
expr: &'tcx Expr<'_>,
) -> Vec<Span> {
struct StrippingFinder<'a, 'tcx> {
cx: &'a LateContext<'tcx>,
strip_kind: StripKind,
target: Res,
pattern: &'tcx Expr<'tcx>,
results: Vec<Span>,
}
impl<'a, 'tcx> Visitor<'tcx> for StrippingFinder<'a, 'tcx> {
fn visit_expr(&mut self, ex: &'tcx Expr<'_>) {
if_chain! {
if is_ref_str(self.cx, ex);
let unref = peel_ref(ex);
if let ExprKind::Index(indexed, index, _) = &unref.kind;
if let Some(higher::Range { start, end, .. }) = higher::Range::hir(index);
if let ExprKind::Path(path) = &indexed.kind;
if self.cx.qpath_res(path, ex.hir_id) == self.target;
then {
match (self.strip_kind, start, end) {
(StripKind::Prefix, Some(start), None) => {
if eq_pattern_length(self.cx, self.pattern, start) {
self.results.push(ex.span);
return;
}
},
(StripKind::Suffix, None, Some(end)) => {
if_chain! {
if let ExprKind::Binary(Spanned { node: BinOpKind::Sub, .. }, left, right) = end.kind;
if let Some(left_arg) = len_arg(self.cx, left);
if let ExprKind::Path(left_path) = &left_arg.kind;
if self.cx.qpath_res(left_path, left_arg.hir_id) == self.target;
if eq_pattern_length(self.cx, self.pattern, right);
then {
self.results.push(ex.span);
return;
}
}
},
_ => {}
}
}
}
walk_expr(self, ex);
}
}
let mut finder = StrippingFinder {
cx,
strip_kind,
target,
pattern,
results: vec![],
};
walk_expr(&mut finder, expr);
finder.results
}