| use clippy_config::msrvs::{self, Msrv}; |
| use clippy_utils::diagnostics::span_lint_and_then; |
| use clippy_utils::macros::HirNode; |
| use clippy_utils::sugg::Sugg; |
| use clippy_utils::{is_trait_method, local_is_initialized, path_to_local}; |
| use rustc_errors::Applicability; |
| use rustc_hir::{self as hir, Expr, ExprKind}; |
| use rustc_lint::{LateContext, LateLintPass}; |
| use rustc_middle::ty::{self, Instance, Mutability}; |
| use rustc_session::impl_lint_pass; |
| use rustc_span::def_id::DefId; |
| use rustc_span::symbol::sym; |
| use rustc_span::{ExpnKind, 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 { |
| #[must_use] |
| pub fn new(msrv: Msrv) -> Self { |
| Self { msrv } |
| } |
| } |
| |
| impl_lint_pass!(AssigningClones => [ASSIGNING_CLONES]); |
| |
| impl<'tcx> LateLintPass<'tcx> for AssigningClones { |
| fn check_expr(&mut self, cx: &LateContext<'tcx>, assign_expr: &'tcx Expr<'_>) { |
| // Do not fire the lint in macros |
| let ctxt = assign_expr.span().ctxt(); |
| let expn_data = ctxt.outer_expn_data(); |
| match expn_data.kind { |
| ExpnKind::AstPass(_) | ExpnKind::Desugaring(_) | ExpnKind::Macro(..) => return, |
| ExpnKind::Root => {}, |
| } |
| |
| let ExprKind::Assign(lhs, rhs, _span) = assign_expr.kind else { |
| return; |
| }; |
| |
| let Some(call) = extract_call(cx, rhs) else { |
| return; |
| }; |
| |
| if is_ok_to_suggest(cx, lhs, &call, &self.msrv) { |
| suggest(cx, ctxt, assign_expr, lhs, &call); |
| } |
| } |
| |
| extract_msrv_attr!(LateContext); |
| } |
| |
| // Try to resolve the call to `Clone::clone` or `ToOwned::to_owned`. |
| fn extract_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<CallCandidate<'tcx>> { |
| let fn_def_id = clippy_utils::fn_def_id(cx, expr)?; |
| |
| // Fast paths to only check method calls without arguments or function calls with a single argument |
| let (target, kind, resolved_method) = match expr.kind { |
| ExprKind::MethodCall(path, receiver, [], _span) => { |
| let args = cx.typeck_results().node_args(expr.hir_id); |
| |
| // If we could not resolve the method, don't apply the lint |
| let Ok(Some(resolved_method)) = Instance::resolve(cx.tcx, cx.param_env, fn_def_id, args) else { |
| return None; |
| }; |
| if is_trait_method(cx, expr, sym::Clone) && path.ident.name == sym::clone { |
| (TargetTrait::Clone, CallKind::MethodCall { receiver }, resolved_method) |
| } else if is_trait_method(cx, expr, sym::ToOwned) && path.ident.name.as_str() == "to_owned" { |
| (TargetTrait::ToOwned, CallKind::MethodCall { receiver }, resolved_method) |
| } else { |
| return None; |
| } |
| }, |
| ExprKind::Call(function, [arg]) => { |
| let kind = cx.typeck_results().node_type(function.hir_id).kind(); |
| |
| // If we could not resolve the method, don't apply the lint |
| let Ok(Some(resolved_method)) = (match kind { |
| ty::FnDef(_, args) => Instance::resolve(cx.tcx, cx.param_env, fn_def_id, args), |
| _ => Ok(None), |
| }) else { |
| return None; |
| }; |
| if cx.tcx.is_diagnostic_item(sym::to_owned_method, fn_def_id) { |
| ( |
| TargetTrait::ToOwned, |
| CallKind::FunctionCall { self_arg: arg }, |
| resolved_method, |
| ) |
| } else if let Some(trait_did) = cx.tcx.trait_of_item(fn_def_id) |
| && cx.tcx.is_diagnostic_item(sym::Clone, trait_did) |
| { |
| ( |
| TargetTrait::Clone, |
| CallKind::FunctionCall { self_arg: arg }, |
| resolved_method, |
| ) |
| } else { |
| return None; |
| } |
| }, |
| _ => return None, |
| }; |
| |
| Some(CallCandidate { |
| target, |
| kind, |
| method_def_id: resolved_method.def_id(), |
| }) |
| } |
| |
| // Return true if we find that the called method has a custom implementation and isn't derived or |
| // provided by default by the corresponding trait. |
| fn is_ok_to_suggest<'tcx>(cx: &LateContext<'tcx>, lhs: &Expr<'tcx>, call: &CallCandidate<'tcx>, msrv: &Msrv) -> bool { |
| // For calls to .to_owned we suggest using .clone_into(), which was only stablilized in 1.63. |
| // If the current MSRV is below that, don't suggest the lint. |
| if !msrv.meets(msrvs::CLONE_INTO) && matches!(call.target, TargetTrait::ToOwned) { |
| return false; |
| } |
| |
| // If the left-hand side is a local variable, it might be uninitialized at this point. |
| // In that case we do not want to suggest the lint. |
| if let Some(local) = path_to_local(lhs) { |
| // 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. |
| if !local_is_initialized(cx, local) { |
| return false; |
| } |
| } |
| |
| let Some(impl_block) = cx.tcx.impl_of_method(call.method_def_id) else { |
| return false; |
| }; |
| |
| // If the method implementation comes from #[derive(Clone)], then don't suggest the lint. |
| // Automatically generated Clone impls do not currently override `clone_from`. |
| // See e.g. https://github.com/rust-lang/rust/pull/98445#issuecomment-1190681305 for more details. |
| if cx.tcx.is_builtin_derived(impl_block) { |
| return false; |
| } |
| |
| // If the call expression is inside an impl block that contains the method invoked by the |
| // call expression, we bail out to avoid suggesting something that could result in endless |
| // recursion. |
| if let Some(local_block_id) = impl_block.as_local() |
| && let Some(block) = cx.tcx.hir_node_by_def_id(local_block_id).as_owner() |
| { |
| let impl_block_owner = block.def_id(); |
| if cx |
| .tcx |
| .hir() |
| .parent_id_iter(lhs.hir_id) |
| .any(|parent| parent.owner == impl_block_owner) |
| { |
| return false; |
| } |
| } |
| |
| // Find the function for which we want to check that it is implemented. |
| let provided_fn = match call.target { |
| TargetTrait::Clone => cx.tcx.get_diagnostic_item(sym::Clone).and_then(|clone| { |
| cx.tcx |
| .provided_trait_methods(clone) |
| .find(|item| item.name == sym::clone_from) |
| }), |
| TargetTrait::ToOwned => cx.tcx.get_diagnostic_item(sym::ToOwned).and_then(|to_owned| { |
| cx.tcx |
| .provided_trait_methods(to_owned) |
| .find(|item| item.name.as_str() == "clone_into") |
| }), |
| }; |
| let Some(provided_fn) = provided_fn else { |
| return false; |
| }; |
| |
| // Now take a look if the impl block defines an implementation for the method that we're interested |
| // in. If not, then we're using a default implementation, which is not interesting, so we will |
| // not suggest the lint. |
| let implemented_fns = cx.tcx.impl_item_implementor_ids(impl_block); |
| implemented_fns.contains_key(&provided_fn.def_id) |
| } |
| |
| fn suggest<'tcx>( |
| cx: &LateContext<'tcx>, |
| ctxt: SyntaxContext, |
| assign_expr: &Expr<'tcx>, |
| lhs: &Expr<'tcx>, |
| call: &CallCandidate<'tcx>, |
| ) { |
| span_lint_and_then(cx, ASSIGNING_CLONES, assign_expr.span, call.message(), |diag| { |
| let mut applicability = Applicability::Unspecified; |
| |
| diag.span_suggestion( |
| assign_expr.span, |
| call.suggestion_msg(), |
| call.suggested_replacement(cx, ctxt, lhs, &mut applicability), |
| applicability, |
| ); |
| }); |
| } |
| |
| #[derive(Copy, Clone, Debug)] |
| enum CallKind<'tcx> { |
| MethodCall { receiver: &'tcx Expr<'tcx> }, |
| FunctionCall { self_arg: &'tcx Expr<'tcx> }, |
| } |
| |
| #[derive(Copy, Clone, Debug)] |
| enum TargetTrait { |
| Clone, |
| ToOwned, |
| } |
| |
| #[derive(Debug)] |
| struct CallCandidate<'tcx> { |
| target: TargetTrait, |
| kind: CallKind<'tcx>, |
| // DefId of the called method from an impl block that implements the target trait |
| method_def_id: DefId, |
| } |
| |
| impl<'tcx> CallCandidate<'tcx> { |
| #[inline] |
| fn message(&self) -> &'static str { |
| match self.target { |
| TargetTrait::Clone => "assigning the result of `Clone::clone()` may be inefficient", |
| TargetTrait::ToOwned => "assigning the result of `ToOwned::to_owned()` may be inefficient", |
| } |
| } |
| |
| #[inline] |
| fn suggestion_msg(&self) -> &'static str { |
| match self.target { |
| TargetTrait::Clone => "use `clone_from()`", |
| TargetTrait::ToOwned => "use `clone_into()`", |
| } |
| } |
| |
| fn suggested_replacement( |
| &self, |
| cx: &LateContext<'tcx>, |
| ctxt: SyntaxContext, |
| lhs: &Expr<'tcx>, |
| applicability: &mut Applicability, |
| ) -> String { |
| match self.target { |
| TargetTrait::Clone => { |
| match self.kind { |
| CallKind::MethodCall { receiver } => { |
| let receiver_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = lhs.kind { |
| // `*lhs = self_expr.clone();` -> `lhs.clone_from(self_expr)` |
| Sugg::hir_with_applicability(cx, ref_expr, "_", applicability) |
| } else { |
| // `lhs = self_expr.clone();` -> `lhs.clone_from(self_expr)` |
| Sugg::hir_with_applicability(cx, lhs, "_", applicability) |
| } |
| .maybe_par(); |
| |
| // Determine whether we need to reference the argument to clone_from(). |
| let clone_receiver_type = cx.typeck_results().expr_ty(receiver); |
| let clone_receiver_adj_type = cx.typeck_results().expr_ty_adjusted(receiver); |
| let mut arg_sugg = Sugg::hir_with_context(cx, receiver, ctxt, "_", applicability); |
| 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::FunctionCall { self_arg, .. } => { |
| let self_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = lhs.kind { |
| // `*lhs = Clone::clone(self_expr);` -> `Clone::clone_from(lhs, self_expr)` |
| Sugg::hir_with_applicability(cx, ref_expr, "_", applicability) |
| } else { |
| // `lhs = Clone::clone(self_expr);` -> `Clone::clone_from(&mut lhs, self_expr)` |
| Sugg::hir_with_applicability(cx, lhs, "_", applicability).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, self_arg, ctxt, "_", applicability); |
| |
| format!("Clone::clone_from({self_sugg}, {rhs_sugg})") |
| }, |
| } |
| }, |
| TargetTrait::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, "_", applicability).maybe_par(); |
| 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, "_", applicability) |
| .maybe_par() |
| .mut_addr() |
| }; |
| |
| match self.kind { |
| CallKind::MethodCall { receiver } => { |
| let receiver_sugg = Sugg::hir_with_context(cx, receiver, ctxt, "_", applicability); |
| format!("{receiver_sugg}.clone_into({rhs_sugg})") |
| }, |
| CallKind::FunctionCall { self_arg, .. } => { |
| let self_sugg = Sugg::hir_with_context(cx, self_arg, ctxt, "_", applicability); |
| format!("ToOwned::clone_into({self_sugg}, {rhs_sugg})") |
| }, |
| } |
| }, |
| } |
| } |
| } |