blob: efce23d13a382cae0b1de44b18ad092b853de6fb [file] [log] [blame] [edit]
use clippy_config::Conf;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::mir::{PossibleBorrowerMap, enclosing_mir};
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::res::{MaybeDef, MaybeResPath};
use clippy_utils::sugg::Sugg;
use clippy_utils::{is_in_test, last_path_segment, local_is_initialized, sym};
use rustc_errors::Applicability;
use rustc_hir::{self as hir, Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::mir;
use rustc_middle::ty::{self, Instance, Mutability};
use rustc_session::impl_lint_pass;
use rustc_span::{Span, SyntaxContext};
declare_clippy_lint! {
/// ### What it does
/// Checks for code like `foo = bar.clone();`
///
/// ### Why is this bad?
/// Custom `Clone::clone_from()` or `ToOwned::clone_into` implementations allow the objects
/// to share resources and therefore avoid allocations.
///
/// ### Example
/// ```rust
/// struct Thing;
///
/// impl Clone for Thing {
/// fn clone(&self) -> Self { todo!() }
/// fn clone_from(&mut self, other: &Self) { todo!() }
/// }
///
/// pub fn assign_to_ref(a: &mut Thing, b: Thing) {
/// *a = b.clone();
/// }
/// ```
/// Use instead:
/// ```rust
/// struct Thing;
///
/// impl Clone for Thing {
/// fn clone(&self) -> Self { todo!() }
/// fn clone_from(&mut self, other: &Self) { todo!() }
/// }
///
/// pub fn assign_to_ref(a: &mut Thing, b: Thing) {
/// a.clone_from(&b);
/// }
/// ```
#[clippy::version = "1.78.0"]
pub ASSIGNING_CLONES,
pedantic,
"assigning the result of cloning may be inefficient"
}
pub struct AssigningClones {
msrv: Msrv,
}
impl AssigningClones {
pub fn new(conf: &'static Conf) -> Self {
Self { msrv: conf.msrv }
}
}
impl_lint_pass!(AssigningClones => [ASSIGNING_CLONES]);
impl<'tcx> LateLintPass<'tcx> for AssigningClones {
fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
if let ExprKind::Assign(lhs, rhs, _) = e.kind
&& let typeck = cx.typeck_results()
&& let (call_kind, fn_name, fn_def, fn_arg, fn_gen_args) = match rhs.kind {
ExprKind::Call(f, [arg])
if let ExprKind::Path(fn_path) = &f.kind
&& let Some(def) = typeck.qpath_res(fn_path, f.hir_id).opt_def(cx) =>
{
(CallKind::Ufcs, last_path_segment(fn_path).ident.name, def, arg, typeck.node_args(f.hir_id))
},
ExprKind::MethodCall(name, recv, [], _) if let Some(def) = typeck.type_dependent_def(rhs.hir_id) => {
(CallKind::Method, name.ident.name, def, recv, typeck.node_args(rhs.hir_id))
},
_ => return,
}
&& let ctxt = e.span.ctxt()
// Don't lint in macros.
&& ctxt.is_root()
&& let which_trait = match fn_name {
sym::clone if fn_def.assoc_fn_parent(cx).is_diag_item(cx, sym::Clone) => CloneTrait::Clone,
sym::to_owned
if fn_def.assoc_fn_parent(cx).is_diag_item(cx, sym::ToOwned)
&& self.msrv.meets(cx, msrvs::CLONE_INTO) =>
{
CloneTrait::ToOwned
},
_ => return,
}
&& let Ok(Some(resolved_fn)) = Instance::try_resolve(cx.tcx, cx.typing_env(), fn_def.1, fn_gen_args)
// TODO: This check currently bails if the local variable has no initializer.
// That is overly conservative - the lint should fire even if there was no initializer,
// but the variable has been initialized before `lhs` was evaluated.
&& lhs.res_local_id().is_none_or(|lhs| local_is_initialized(cx, lhs))
&& let Some(resolved_impl) = cx.tcx.impl_of_assoc(resolved_fn.def_id())
// Derived forms don't implement `clone_from`/`clone_into`.
// See https://github.com/rust-lang/rust/pull/98445#issuecomment-1190681305
&& !cx.tcx.is_builtin_derived(resolved_impl)
// Don't suggest calling a function we're implementing.
&& resolved_impl.as_local().is_none_or(|block_id| {
cx.tcx.hir_parent_owner_iter(e.hir_id).all(|(id, _)| id.def_id != block_id)
})
&& let resolved_assoc_items = cx.tcx.associated_items(resolved_impl)
// Only suggest if `clone_from`/`clone_into` is explicitly implemented
&& resolved_assoc_items.in_definition_order().any(|assoc|
match which_trait {
CloneTrait::Clone => assoc.name() == sym::clone_from,
CloneTrait::ToOwned => assoc.name() == sym::clone_into,
}
)
&& !clone_source_borrows_from_dest(cx, lhs, rhs.span)
&& !is_in_test(cx.tcx, e.hir_id)
{
span_lint_and_then(
cx,
ASSIGNING_CLONES,
e.span,
match which_trait {
CloneTrait::Clone => "assigning the result of `Clone::clone()` may be inefficient",
CloneTrait::ToOwned => "assigning the result of `ToOwned::to_owned()` may be inefficient",
},
|diag| {
let mut app = Applicability::Unspecified;
diag.span_suggestion(
e.span,
match which_trait {
CloneTrait::Clone => "use `clone_from()`",
CloneTrait::ToOwned => "use `clone_into()`",
},
build_sugg(cx, ctxt, lhs, fn_arg, which_trait, call_kind, &mut app),
app,
);
},
);
}
}
}
/// Checks if the data being cloned borrows from the place that is being assigned to:
///
/// ```
/// let mut s = String::new();
/// let s2 = &s;
/// s = s2.to_owned();
/// ```
///
/// This cannot be written `s2.clone_into(&mut s)` because it has conflicting borrows.
fn clone_source_borrows_from_dest(cx: &LateContext<'_>, lhs: &Expr<'_>, call_span: Span) -> bool {
let Some(mir) = enclosing_mir(cx.tcx, lhs.hir_id) else {
return false;
};
let PossibleBorrowerMap { map: borrow_map, .. } = PossibleBorrowerMap::new(cx, mir);
// The operation `dest = src.to_owned()` in MIR is split up across 3 blocks *if* the type has `Drop`
// code. For types that don't, the second basic block is simply skipped.
// For the doc example above that would be roughly:
//
// bb0:
// s2 = &s
// s_temp = ToOwned::to_owned(move s2) -> bb1
//
// bb1:
// drop(s) -> bb2 // drop the old string
//
// bb2:
// s = s_temp
if let Some(terminator) = mir.basic_blocks.iter()
.map(mir::BasicBlockData::terminator)
.find(|term| term.source_info.span == call_span)
&& let mir::TerminatorKind::Call { ref args, target: Some(assign_bb), .. } = terminator.kind
&& let [source] = &**args
&& let mir::Operand::Move(source) = &source.node
&& let assign_bb = &mir.basic_blocks[assign_bb]
&& let assign_bb = match assign_bb.terminator().kind {
// Skip the drop of the assignment's destination.
mir::TerminatorKind::Drop { target, .. } => &mir.basic_blocks[target],
_ => assign_bb,
}
// Skip any storage statements as they are just noise
&& let Some(assignment) = assign_bb.statements
.iter()
.find(|stmt| {
!matches!(stmt.kind, mir::StatementKind::StorageDead(_) | mir::StatementKind::StorageLive(_))
})
&& let mir::StatementKind::Assign(box (borrowed, _)) = &assignment.kind
&& let Some(borrowers) = borrow_map.get(&borrowed.local)
{
borrowers.contains(source.local)
} else {
false
}
}
#[derive(Clone, Copy)]
enum CloneTrait {
Clone,
ToOwned,
}
#[derive(Copy, Clone)]
enum CallKind {
Ufcs,
Method,
}
fn build_sugg<'tcx>(
cx: &LateContext<'tcx>,
ctxt: SyntaxContext,
lhs: &'tcx Expr<'_>,
fn_arg: &'tcx Expr<'_>,
which_trait: CloneTrait,
call_kind: CallKind,
app: &mut Applicability,
) -> String {
match which_trait {
CloneTrait::Clone => {
match call_kind {
CallKind::Method => {
let receiver_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = lhs.kind {
// If `ref_expr` is a reference, we can remove the dereference operator (`*`) to make
// the generated code a bit simpler. In other cases, we don't do this special case, to avoid
// having to deal with Deref (https://github.com/rust-lang/rust-clippy/issues/12437).
let ty = cx.typeck_results().expr_ty(ref_expr);
if ty.is_ref() {
// Apply special case, remove `*`
// `*lhs = self_expr.clone();` -> `lhs.clone_from(self_expr)`
Sugg::hir_with_applicability(cx, ref_expr, "_", app)
} else {
// Keep the original lhs
// `*lhs = self_expr.clone();` -> `(*lhs).clone_from(self_expr)`
Sugg::hir_with_applicability(cx, lhs, "_", app)
}
} else {
// Keep the original lhs
// `lhs = self_expr.clone();` -> `lhs.clone_from(self_expr)`
Sugg::hir_with_applicability(cx, lhs, "_", app)
}
.maybe_paren();
// Determine whether we need to reference the argument to clone_from().
let clone_receiver_type = cx.typeck_results().expr_ty(fn_arg);
let clone_receiver_adj_type = cx.typeck_results().expr_ty_adjusted(fn_arg);
let mut arg_sugg = Sugg::hir_with_context(cx, fn_arg, ctxt, "_", app);
if clone_receiver_type != clone_receiver_adj_type {
// The receiver may have been a value type, so we need to add an `&` to
// be sure the argument to clone_from will be a reference.
arg_sugg = arg_sugg.addr();
}
format!("{receiver_sugg}.clone_from({arg_sugg})")
},
CallKind::Ufcs => {
let self_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = lhs.kind {
// See special case of removing `*` in method handling above
let ty = cx.typeck_results().expr_ty(ref_expr);
if ty.is_ref() {
// `*lhs = Clone::clone(self_expr);` -> `Clone::clone_from(lhs, self_expr)`
Sugg::hir_with_applicability(cx, ref_expr, "_", app)
} else {
// `*lhs = Clone::clone(self_expr);` -> `Clone::clone_from(&mut *lhs, self_expr)`
// mut_addr_deref is used to avoid unnecessary parentheses around `*lhs`
Sugg::hir_with_applicability(cx, ref_expr, "_", app).mut_addr_deref()
}
} else {
// `lhs = Clone::clone(self_expr);` -> `Clone::clone_from(&mut lhs, self_expr)`
Sugg::hir_with_applicability(cx, lhs, "_", app).mut_addr()
};
// The RHS had to be exactly correct before the call, there is no auto-deref for function calls.
let rhs_sugg = Sugg::hir_with_context(cx, fn_arg, ctxt, "_", app);
format!("Clone::clone_from({self_sugg}, {rhs_sugg})")
},
}
},
CloneTrait::ToOwned => {
let rhs_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = lhs.kind {
// `*lhs = rhs.to_owned()` -> `rhs.clone_into(lhs)`
// `*lhs = ToOwned::to_owned(rhs)` -> `ToOwned::clone_into(rhs, lhs)`
let sugg = Sugg::hir_with_applicability(cx, ref_expr, "_", app).maybe_paren();
let inner_type = cx.typeck_results().expr_ty(ref_expr);
// If after unwrapping the dereference, the type is not a mutable reference, we add &mut to make it
// deref to a mutable reference.
if matches!(inner_type.kind(), ty::Ref(_, _, Mutability::Mut)) {
sugg
} else {
sugg.mut_addr()
}
} else {
// `lhs = rhs.to_owned()` -> `rhs.clone_into(&mut lhs)`
// `lhs = ToOwned::to_owned(rhs)` -> `ToOwned::clone_into(rhs, &mut lhs)`
Sugg::hir_with_applicability(cx, lhs, "_", app).maybe_paren().mut_addr()
};
match call_kind {
CallKind::Method => {
let receiver_sugg = Sugg::hir_with_context(cx, fn_arg, ctxt, "_", app);
format!("{receiver_sugg}.clone_into({rhs_sugg})")
},
CallKind::Ufcs => {
let self_sugg = Sugg::hir_with_context(cx, fn_arg, ctxt, "_", app);
format!("ToOwned::clone_into({self_sugg}, {rhs_sugg})")
},
}
},
}
}