blob: a7c1d1bd6cd36491835859038bfb69f873ad1cd8 [file] [log] [blame]
use super::{IncrementVisitor, InitializeVisitor, MANUAL_MEMCPY};
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet;
use clippy_utils::sugg::Sugg;
use clippy_utils::ty::is_copy;
use clippy_utils::usage::local_used_in;
use clippy_utils::{get_enclosing_block, higher, path_to_local, sugg};
use rustc_ast::ast;
use rustc_errors::Applicability;
use rustc_hir::intravisit::walk_block;
use rustc_hir::{BinOpKind, Block, Expr, ExprKind, HirId, Pat, PatKind, StmtKind};
use rustc_lint::LateContext;
use rustc_middle::ty::{self, Ty};
use rustc_span::symbol::sym;
use std::fmt::Display;
/// Checks for `for` loops that sequentially copy items from one slice-like
/// object to another.
pub(super) fn check<'tcx>(
cx: &LateContext<'tcx>,
pat: &'tcx Pat<'_>,
arg: &'tcx Expr<'_>,
body: &'tcx Expr<'_>,
expr: &'tcx Expr<'_>,
) -> bool {
if let Some(higher::Range {
start: Some(start),
end: Some(end),
limits,
}) = higher::Range::hir(arg)
{
// the var must be a single name
if let PatKind::Binding(_, canonical_id, _, _) = pat.kind {
let mut starts = vec![Start {
id: canonical_id,
kind: StartKind::Range,
}];
// This is one of few ways to return different iterators
// derived from: https://stackoverflow.com/questions/29760668/conditionally-iterate-over-one-of-several-possible-iterators/52064434#52064434
let mut iter_a = None;
let mut iter_b = None;
if let ExprKind::Block(block, _) = body.kind {
if let Some(loop_counters) = get_loop_counters(cx, block, expr) {
starts.extend(loop_counters);
}
iter_a = Some(get_assignments(block, &starts));
} else {
iter_b = Some(get_assignment(body));
}
let assignments = iter_a.into_iter().flatten().chain(iter_b);
let big_sugg = assignments
// The only statements in the for loops can be indexed assignments from
// indexed retrievals (except increments of loop counters).
.map(|o| {
o.and_then(|(lhs, rhs)| {
let rhs = fetch_cloned_expr(rhs);
if let ExprKind::Index(base_left, idx_left, _) = lhs.kind
&& let ExprKind::Index(base_right, idx_right, _) = rhs.kind
&& let Some(ty) = get_slice_like_element_ty(cx, cx.typeck_results().expr_ty(base_left))
&& get_slice_like_element_ty(cx, cx.typeck_results().expr_ty(base_right)).is_some()
&& let Some((start_left, offset_left)) = get_details_from_idx(cx, idx_left, &starts)
&& let Some((start_right, offset_right)) = get_details_from_idx(cx, idx_right, &starts)
&& !local_used_in(cx, canonical_id, base_left)
&& !local_used_in(cx, canonical_id, base_right)
// Source and destination must be different
&& path_to_local(base_left) != path_to_local(base_right)
{
Some((
ty,
IndexExpr {
base: base_left,
idx: start_left,
idx_offset: offset_left,
},
IndexExpr {
base: base_right,
idx: start_right,
idx_offset: offset_right,
},
))
} else {
None
}
})
})
.map(|o| o.map(|(ty, dst, src)| build_manual_memcpy_suggestion(cx, start, end, limits, ty, &dst, &src)))
.collect::<Option<Vec<_>>>()
.filter(|v| !v.is_empty())
.map(|v| v.join("\n "));
if let Some(big_sugg) = big_sugg {
span_lint_and_sugg(
cx,
MANUAL_MEMCPY,
expr.span,
"it looks like you're manually copying between slices",
"try replacing the loop by",
big_sugg,
Applicability::Unspecified,
);
return true;
}
}
}
false
}
fn build_manual_memcpy_suggestion<'tcx>(
cx: &LateContext<'tcx>,
start: &Expr<'_>,
end: &Expr<'_>,
limits: ast::RangeLimits,
elem_ty: Ty<'tcx>,
dst: &IndexExpr<'_>,
src: &IndexExpr<'_>,
) -> String {
fn print_offset(offset: MinifyingSugg<'static>) -> MinifyingSugg<'static> {
if offset.to_string() == "0" {
sugg::EMPTY.into()
} else {
offset
}
}
let print_limit = |end: &Expr<'_>, end_str: &str, base: &Expr<'_>, sugg: MinifyingSugg<'static>| {
if let ExprKind::MethodCall(method, recv, [], _) = end.kind
&& method.ident.name == sym::len
&& path_to_local(recv) == path_to_local(base)
{
if sugg.to_string() == end_str {
sugg::EMPTY.into()
} else {
sugg
}
} else {
match limits {
ast::RangeLimits::Closed => sugg + &sugg::ONE.into(),
ast::RangeLimits::HalfOpen => sugg,
}
}
};
let start_str = Sugg::hir(cx, start, "").into();
let end_str: MinifyingSugg<'_> = Sugg::hir(cx, end, "").into();
let print_offset_and_limit = |idx_expr: &IndexExpr<'_>| match idx_expr.idx {
StartKind::Range => (
print_offset(apply_offset(&start_str, &idx_expr.idx_offset)).into_sugg(),
print_limit(
end,
end_str.to_string().as_str(),
idx_expr.base,
apply_offset(&end_str, &idx_expr.idx_offset),
)
.into_sugg(),
),
StartKind::Counter { initializer } => {
let counter_start = Sugg::hir(cx, initializer, "").into();
(
print_offset(apply_offset(&counter_start, &idx_expr.idx_offset)).into_sugg(),
print_limit(
end,
end_str.to_string().as_str(),
idx_expr.base,
apply_offset(&end_str, &idx_expr.idx_offset) + &counter_start - &start_str,
)
.into_sugg(),
)
},
};
let (dst_offset, dst_limit) = print_offset_and_limit(dst);
let (src_offset, src_limit) = print_offset_and_limit(src);
let dst_base_str = snippet(cx, dst.base.span, "???");
let src_base_str = snippet(cx, src.base.span, "???");
let dst = if (dst_offset == sugg::EMPTY && dst_limit == sugg::EMPTY)
|| is_array_length_equal_to_range(cx, start, end, dst.base)
{
dst_base_str
} else {
format!("{dst_base_str}[{}..{}]", dst_offset.maybe_par(), dst_limit.maybe_par()).into()
};
let method_str = if is_copy(cx, elem_ty) {
"copy_from_slice"
} else {
"clone_from_slice"
};
let src = if is_array_length_equal_to_range(cx, start, end, src.base) {
src_base_str
} else {
format!("{src_base_str}[{}..{}]", src_offset.maybe_par(), src_limit.maybe_par()).into()
};
format!("{dst}.{method_str}(&{src});")
}
/// a wrapper of `Sugg`. Besides what `Sugg` do, this removes unnecessary `0`;
/// and also, it avoids subtracting a variable from the same one by replacing it with `0`.
/// it exists for the convenience of the overloaded operators while normal functions can do the
/// same.
#[derive(Clone)]
struct MinifyingSugg<'a>(Sugg<'a>);
impl<'a> Display for MinifyingSugg<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<'a> MinifyingSugg<'a> {
fn into_sugg(self) -> Sugg<'a> {
self.0
}
}
impl<'a> From<Sugg<'a>> for MinifyingSugg<'a> {
fn from(sugg: Sugg<'a>) -> Self {
Self(sugg)
}
}
impl std::ops::Add for &MinifyingSugg<'static> {
type Output = MinifyingSugg<'static>;
fn add(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> {
match (self.to_string().as_str(), rhs.to_string().as_str()) {
("0", _) => rhs.clone(),
(_, "0") => self.clone(),
(_, _) => (&self.0 + &rhs.0).into(),
}
}
}
impl std::ops::Sub for &MinifyingSugg<'static> {
type Output = MinifyingSugg<'static>;
fn sub(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> {
match (self.to_string().as_str(), rhs.to_string().as_str()) {
(_, "0") => self.clone(),
("0", _) => (-rhs.0.clone()).into(),
(x, y) if x == y => sugg::ZERO.into(),
(_, _) => (&self.0 - &rhs.0).into(),
}
}
}
impl std::ops::Add<&MinifyingSugg<'static>> for MinifyingSugg<'static> {
type Output = MinifyingSugg<'static>;
fn add(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> {
match (self.to_string().as_str(), rhs.to_string().as_str()) {
("0", _) => rhs.clone(),
(_, "0") => self,
(_, _) => (self.0 + &rhs.0).into(),
}
}
}
impl std::ops::Sub<&MinifyingSugg<'static>> for MinifyingSugg<'static> {
type Output = MinifyingSugg<'static>;
fn sub(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> {
match (self.to_string().as_str(), rhs.to_string().as_str()) {
(_, "0") => self,
("0", _) => (-rhs.0.clone()).into(),
(x, y) if x == y => sugg::ZERO.into(),
(_, _) => (self.0 - &rhs.0).into(),
}
}
}
/// a wrapper around `MinifyingSugg`, which carries an operator like currying
/// so that the suggested code become more efficient (e.g. `foo + -bar` `foo - bar`).
struct Offset {
value: MinifyingSugg<'static>,
sign: OffsetSign,
}
#[derive(Clone, Copy)]
enum OffsetSign {
Positive,
Negative,
}
impl Offset {
fn negative(value: Sugg<'static>) -> Self {
Self {
value: value.into(),
sign: OffsetSign::Negative,
}
}
fn positive(value: Sugg<'static>) -> Self {
Self {
value: value.into(),
sign: OffsetSign::Positive,
}
}
fn empty() -> Self {
Self::positive(sugg::ZERO)
}
}
fn apply_offset(lhs: &MinifyingSugg<'static>, rhs: &Offset) -> MinifyingSugg<'static> {
match rhs.sign {
OffsetSign::Positive => lhs + &rhs.value,
OffsetSign::Negative => lhs - &rhs.value,
}
}
#[derive(Debug, Clone, Copy)]
enum StartKind<'hir> {
Range,
Counter { initializer: &'hir Expr<'hir> },
}
struct IndexExpr<'hir> {
base: &'hir Expr<'hir>,
idx: StartKind<'hir>,
idx_offset: Offset,
}
struct Start<'hir> {
id: HirId,
kind: StartKind<'hir>,
}
fn get_slice_like_element_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> {
match ty.kind() {
ty::Adt(adt, subs) if cx.tcx.is_diagnostic_item(sym::Vec, adt.did()) => Some(subs.type_at(0)),
ty::Ref(_, subty, _) => get_slice_like_element_ty(cx, *subty),
ty::Slice(ty) | ty::Array(ty, _) => Some(*ty),
_ => None,
}
}
fn fetch_cloned_expr<'tcx>(expr: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
if let ExprKind::MethodCall(method, arg, [], _) = expr.kind
&& method.ident.name == sym::clone
{
arg
} else {
expr
}
}
fn get_details_from_idx<'tcx>(
cx: &LateContext<'tcx>,
idx: &Expr<'_>,
starts: &[Start<'tcx>],
) -> Option<(StartKind<'tcx>, Offset)> {
fn get_start<'tcx>(e: &Expr<'_>, starts: &[Start<'tcx>]) -> Option<StartKind<'tcx>> {
let id = path_to_local(e)?;
starts.iter().find(|start| start.id == id).map(|start| start.kind)
}
fn get_offset<'tcx>(cx: &LateContext<'tcx>, e: &Expr<'_>, starts: &[Start<'tcx>]) -> Option<Sugg<'static>> {
match &e.kind {
ExprKind::Lit(l) => match l.node {
ast::LitKind::Int(x, _ty) => Some(Sugg::NonParen(x.to_string().into())),
_ => None,
},
ExprKind::Path(..) if get_start(e, starts).is_none() => Some(Sugg::hir(cx, e, "???")),
_ => None,
}
}
match idx.kind {
ExprKind::Binary(op, lhs, rhs) => match op.node {
BinOpKind::Add => {
let offset_opt = get_start(lhs, starts)
.and_then(|s| get_offset(cx, rhs, starts).map(|o| (s, o)))
.or_else(|| get_start(rhs, starts).and_then(|s| get_offset(cx, lhs, starts).map(|o| (s, o))));
offset_opt.map(|(s, o)| (s, Offset::positive(o)))
},
BinOpKind::Sub => {
get_start(lhs, starts).and_then(|s| get_offset(cx, rhs, starts).map(|o| (s, Offset::negative(o))))
},
_ => None,
},
ExprKind::Path(..) => get_start(idx, starts).map(|s| (s, Offset::empty())),
_ => None,
}
}
fn get_assignment<'tcx>(e: &'tcx Expr<'tcx>) -> Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)> {
if let ExprKind::Assign(lhs, rhs, _) = e.kind {
Some((lhs, rhs))
} else {
None
}
}
/// Get assignments from the given block.
/// The returned iterator yields `None` if no assignment expressions are there,
/// filtering out the increments of the given whitelisted loop counters;
/// because its job is to make sure there's nothing other than assignments and the increments.
fn get_assignments<'a, 'tcx>(
Block { stmts, expr, .. }: &'tcx Block<'tcx>,
loop_counters: &'a [Start<'tcx>],
) -> impl Iterator<Item = Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)>> + 'a {
// As the `filter` and `map` below do different things, I think putting together
// just increases complexity. (cc #3188 and #4193)
stmts
.iter()
.filter_map(move |stmt| match stmt.kind {
StmtKind::Let(..) | StmtKind::Item(..) => None,
StmtKind::Expr(e) | StmtKind::Semi(e) => Some(e),
})
.chain(*expr)
.filter(move |e| {
if let ExprKind::AssignOp(_, place, _) = e.kind {
path_to_local(place).map_or(false, |id| {
!loop_counters
.iter()
// skip the first item which should be `StartKind::Range`
// this makes it possible to use the slice with `StartKind::Range` in the same iterator loop.
.skip(1)
.any(|counter| counter.id == id)
})
} else {
true
}
})
.map(get_assignment)
}
fn get_loop_counters<'a, 'tcx>(
cx: &'a LateContext<'tcx>,
body: &'tcx Block<'tcx>,
expr: &'tcx Expr<'_>,
) -> Option<impl Iterator<Item = Start<'tcx>> + 'a> {
// Look for variables that are incremented once per loop iteration.
let mut increment_visitor = IncrementVisitor::new(cx);
walk_block(&mut increment_visitor, body);
// For each candidate, check the parent block to see if
// it's initialized to zero at the start of the loop.
get_enclosing_block(cx, expr.hir_id).and_then(|block| {
increment_visitor
.into_results()
.filter_map(move |var_id| {
let mut initialize_visitor = InitializeVisitor::new(cx, expr, var_id);
walk_block(&mut initialize_visitor, block);
initialize_visitor.get_result().map(|(_, _, initializer)| Start {
id: var_id,
kind: StartKind::Counter { initializer },
})
})
.into()
})
}
fn is_array_length_equal_to_range(cx: &LateContext<'_>, start: &Expr<'_>, end: &Expr<'_>, arr: &Expr<'_>) -> bool {
fn extract_lit_value(expr: &Expr<'_>) -> Option<u128> {
if let ExprKind::Lit(lit) = expr.kind
&& let ast::LitKind::Int(value, _) = lit.node
{
Some(value.get())
} else {
None
}
}
let arr_ty = cx.typeck_results().expr_ty(arr).peel_refs();
if let ty::Array(_, s) = arr_ty.kind() {
let size: u128 = if let Some(size) = s.try_eval_target_usize(cx.tcx, cx.param_env) {
size.into()
} else {
return false;
};
let range = match (extract_lit_value(start), extract_lit_value(end)) {
(Some(start_value), Some(end_value)) => end_value - start_value,
_ => return false,
};
size == range
} else {
false
}
}