Skip to content

Commit 0656d28

Browse files
committed
Add assigning_clones lint
1 parent 346b094 commit 0656d28

File tree

6 files changed

+844
-0
lines changed

6 files changed

+844
-0
lines changed

clippy_lints/src/assigning_clones.rs

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::sugg::Sugg;
3+
use clippy_utils::{is_trait_method, path_to_local};
4+
use rustc_errors::Applicability;
5+
use rustc_hir::{self as hir, Expr, ExprKind, Node};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_middle::ty;
8+
use rustc_middle::ty::{Instance, Mutability};
9+
use rustc_session::declare_lint_pass;
10+
use rustc_span::def_id::DefId;
11+
use rustc_span::symbol::sym;
12+
13+
declare_clippy_lint! {
14+
/// ### What it does
15+
/// Checks for code like `foo = bar.clone();`
16+
///
17+
/// ### Why is this bad?
18+
/// Custom `Clone::clone_from()` or `ToOwned::clone_into` implementations allow the objects
19+
/// to share resources and therefore avoid allocations.
20+
///
21+
/// ### Example
22+
/// ```rust
23+
/// struct Thing;
24+
///
25+
/// impl Clone for Thing {
26+
/// fn clone(&self) -> Self { todo!() }
27+
/// fn clone_from(&mut self, other: &Self) -> Self { todo!() }
28+
/// }
29+
///
30+
/// pub fn assign_to_ref(a: &mut Thing, b: Thing) {
31+
/// *a = b.clone();
32+
/// }
33+
/// ```
34+
/// Use instead:
35+
/// ```rust
36+
/// struct Thing;
37+
/// impl Clone for Thing {
38+
/// fn clone(&self) -> Self { todo!() }
39+
/// fn clone_from(&mut self, other: &Self) -> Self { todo!() }
40+
/// }
41+
///
42+
/// pub fn assign_to_ref(a: &mut Thing, b: Thing) {
43+
/// a.clone_from(&b);
44+
/// }
45+
/// ```
46+
#[clippy::version = "1.77.0"]
47+
pub ASSIGNING_CLONES,
48+
perf,
49+
"assigning the result of cloning may be inefficient"
50+
}
51+
declare_lint_pass!(AssigningClones => [ASSIGNING_CLONES]);
52+
53+
impl<'tcx> LateLintPass<'tcx> for AssigningClones {
54+
fn check_expr(&mut self, cx: &LateContext<'tcx>, assign_expr: &'tcx hir::Expr<'_>) {
55+
let ExprKind::Assign(lhs, rhs, _span) = assign_expr.kind else {
56+
return;
57+
};
58+
59+
let Some(call) = extract_call(cx, rhs) else {
60+
return;
61+
};
62+
63+
if is_ok_to_suggest(cx, lhs, &call) {
64+
suggest(cx, assign_expr, lhs, call);
65+
}
66+
}
67+
}
68+
69+
// Try to resolve the call to `Clone::clone` or `ToOwned::to_owned`.
70+
fn extract_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<CallCandidate<'tcx>> {
71+
let Some(fn_def_id) = clippy_utils::fn_def_id(cx, expr) else {
72+
return None;
73+
};
74+
75+
// Fast paths to only check method calls without arguments or function calls with a single argument
76+
let (target, kind, resolved_method) = match expr.kind {
77+
ExprKind::MethodCall(path, receiver, [], _span) => {
78+
let args = cx.typeck_results().node_args(expr.hir_id);
79+
let resolved_method = Instance::resolve(cx.tcx, cx.param_env, fn_def_id, args);
80+
if is_trait_method(cx, expr, sym::Clone) && path.ident.name == sym::clone {
81+
(TargetTrait::Clone, CallKind::MethodCall { receiver }, resolved_method)
82+
} else if is_trait_method(cx, expr, sym::ToOwned) && path.ident.name == sym!(to_owned) {
83+
(TargetTrait::ToOwned, CallKind::MethodCall { receiver }, resolved_method)
84+
} else {
85+
return None;
86+
}
87+
},
88+
ExprKind::Call(function, args) if args.len() == 1 => {
89+
let kind = cx.typeck_results().node_type(function.hir_id).kind();
90+
let resolved_method = match kind {
91+
ty::FnDef(_, args) => Instance::resolve(cx.tcx, cx.param_env, fn_def_id, args),
92+
_ => return None,
93+
};
94+
if cx.tcx.is_diagnostic_item(sym::to_owned_method, fn_def_id) {
95+
(
96+
TargetTrait::ToOwned,
97+
CallKind::FunctionCall { self_arg: &args[0] },
98+
resolved_method,
99+
)
100+
} else if let Some(trait_did) = cx.tcx.trait_of_item(fn_def_id)
101+
&& cx.tcx.is_diagnostic_item(sym::Clone, trait_did)
102+
{
103+
(
104+
TargetTrait::Clone,
105+
CallKind::FunctionCall { self_arg: &args[0] },
106+
resolved_method,
107+
)
108+
} else {
109+
return None;
110+
}
111+
},
112+
_ => return None,
113+
};
114+
let Ok(Some(resolved_method)) = resolved_method else {
115+
// If we could not resolve the method, don't apply the lint
116+
return None;
117+
};
118+
119+
Some(CallCandidate {
120+
target,
121+
kind,
122+
method_def_id: resolved_method.def_id(),
123+
})
124+
}
125+
126+
// Return true if we find that the called method has a custom implementation and isn't derived or
127+
// provided by default by the corresponding trait.
128+
fn is_ok_to_suggest<'tcx>(cx: &LateContext<'tcx>, lhs: &Expr<'tcx>, call: &CallCandidate<'tcx>) -> bool {
129+
// If the left-hand side is a local variable, it might be uninitialized at this point.
130+
// In that case we do not want to suggest the lint.
131+
if let Some(local) = path_to_local(lhs) {
132+
// TODO: This check currently bails if the local variable has no initializer.
133+
// That is overly conservative - the lint should fire even if there was no initializer,
134+
// but the variable has been initialized before `lhs` was evaluated.
135+
if let Some(Node::Local(local)) = cx.tcx.opt_hir_node(cx.tcx.hir().parent_id(local))
136+
&& local.init.is_none()
137+
{
138+
return false;
139+
}
140+
}
141+
142+
let Some(impl_block) = cx.tcx.impl_of_method(call.method_def_id) else {
143+
return false;
144+
};
145+
146+
// If the method implementation comes from #[derive(Clone)], then don't suggest the lint.
147+
// Automatically generated Clone impls do not override `clone_from`.
148+
// See e.g. https://github.com/rust-lang/rust/pull/98445#issuecomment-1190681305 for more details.
149+
if cx.tcx.is_builtin_derived(impl_block) {
150+
return false;
151+
}
152+
153+
// Find the function for which we want to check that it is implemented.
154+
let provided_fn = match call.target {
155+
TargetTrait::Clone => cx.tcx.get_diagnostic_item(sym::Clone).and_then(|clone| {
156+
cx.tcx
157+
.provided_trait_methods(clone)
158+
.find(|item| item.name == sym::clone_from)
159+
}),
160+
TargetTrait::ToOwned => cx.tcx.get_diagnostic_item(sym::ToOwned).and_then(|to_owned| {
161+
cx.tcx
162+
.provided_trait_methods(to_owned)
163+
.find(|item| item.name == sym!(clone_into))
164+
}),
165+
};
166+
let Some(provided_fn) = provided_fn else {
167+
return false;
168+
};
169+
170+
// Now take a look if the impl block defines an implementation for the method that we're interested
171+
// in. If not, then we're using a default implementation, which is not interesting, so we will
172+
// not suggest the lint.
173+
let implemented_fns = cx.tcx.impl_item_implementor_ids(impl_block);
174+
implemented_fns.contains_key(&provided_fn.def_id)
175+
}
176+
177+
fn suggest<'tcx>(
178+
cx: &LateContext<'tcx>,
179+
assign_expr: &hir::Expr<'tcx>,
180+
lhs: &hir::Expr<'tcx>,
181+
call: CallCandidate<'tcx>,
182+
) {
183+
span_lint_and_then(cx, ASSIGNING_CLONES, assign_expr.span, call.message(), |diag| {
184+
let mut applicability = Applicability::MachineApplicable;
185+
186+
diag.span_suggestion(
187+
assign_expr.span,
188+
call.suggestion_msg(),
189+
call.suggested_replacement(cx, lhs, &mut applicability),
190+
applicability,
191+
);
192+
});
193+
}
194+
195+
#[derive(Copy, Clone, Debug)]
196+
enum CallKind<'tcx> {
197+
MethodCall { receiver: &'tcx Expr<'tcx> },
198+
FunctionCall { self_arg: &'tcx Expr<'tcx> },
199+
}
200+
201+
#[derive(Copy, Clone, Debug)]
202+
enum TargetTrait {
203+
Clone,
204+
ToOwned,
205+
}
206+
207+
#[derive(Debug)]
208+
struct CallCandidate<'tcx> {
209+
target: TargetTrait,
210+
kind: CallKind<'tcx>,
211+
// DefId of the called method from an impl block that implements the target trait
212+
method_def_id: DefId,
213+
}
214+
215+
impl<'tcx> CallCandidate<'tcx> {
216+
fn message(&self) -> &'static str {
217+
match self.target {
218+
TargetTrait::Clone => "assigning the result of `Clone::clone()` may be inefficient",
219+
TargetTrait::ToOwned => "assigning the result of `ToOwned::to_owned()` may be inefficient",
220+
}
221+
}
222+
223+
fn suggestion_msg(&self) -> &'static str {
224+
match self.target {
225+
TargetTrait::Clone => "use `clone_from()`",
226+
TargetTrait::ToOwned => "use `clone_into()`",
227+
}
228+
}
229+
230+
fn suggested_replacement(
231+
&self,
232+
cx: &LateContext<'tcx>,
233+
lhs: &hir::Expr<'tcx>,
234+
applicability: &mut Applicability,
235+
) -> String {
236+
match self.target {
237+
TargetTrait::Clone => {
238+
match self.kind {
239+
CallKind::MethodCall { receiver } => {
240+
let receiver_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = lhs.kind {
241+
// `*lhs = self_expr.clone();` -> `lhs.clone_from(self_expr)`
242+
Sugg::hir_with_applicability(cx, ref_expr, "_", applicability)
243+
} else {
244+
// `lhs = self_expr.clone();` -> `lhs.clone_from(self_expr)`
245+
Sugg::hir_with_applicability(cx, lhs, "_", applicability)
246+
}
247+
.maybe_par();
248+
249+
// Determine whether we need to reference the argument to clone_from().
250+
let clone_receiver_type = cx.typeck_results().expr_ty(receiver);
251+
let clone_receiver_adj_type = cx.typeck_results().expr_ty_adjusted(receiver);
252+
let mut arg_sugg = Sugg::hir_with_applicability(cx, receiver, "_", applicability);
253+
if clone_receiver_type != clone_receiver_adj_type {
254+
// The receiver may have been a value type, so we need to add an `&` to
255+
// be sure the argument to clone_from will be a reference.
256+
arg_sugg = arg_sugg.addr();
257+
};
258+
259+
format!("{receiver_sugg}.clone_from({arg_sugg})")
260+
},
261+
CallKind::FunctionCall { self_arg, .. } => {
262+
let self_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = lhs.kind {
263+
// `*lhs = Clone::clone(self_expr);` -> `Clone::clone_from(lhs, self_expr)`
264+
Sugg::hir_with_applicability(cx, ref_expr, "_", applicability)
265+
} else {
266+
// `lhs = Clone::clone(self_expr);` -> `Clone::clone_from(&mut lhs, self_expr)`
267+
Sugg::hir_with_applicability(cx, lhs, "_", applicability).mut_addr()
268+
};
269+
// The RHS had to be exactly correct before the call, there is no auto-deref for function calls.
270+
let rhs_sugg = Sugg::hir_with_applicability(cx, self_arg, "_", applicability);
271+
272+
// TODO: how to get rid of the full path? Modify the function call function's (q)path? Or
273+
// auto-import it the trait?
274+
format!("::std::clone::Clone::clone_from({self_sugg}, {rhs_sugg})")
275+
},
276+
}
277+
},
278+
TargetTrait::ToOwned => {
279+
let rhs_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = lhs.kind {
280+
// `*lhs = rhs.to_owned()` -> `rhs.clone_into(lhs)`
281+
// `*lhs = ToOwned::to_owned(rhs)` -> `ToOwned::clone_into(rhs, lhs)`
282+
let sugg = Sugg::hir_with_applicability(cx, ref_expr, "_", applicability).maybe_par();
283+
let inner_type = cx.typeck_results().expr_ty(ref_expr);
284+
// If after unwrapping the dereference, the type is not a mutable reference, we add &mut to make it
285+
// deref to a mutable reference.
286+
if matches!(inner_type.kind(), ty::Ref(_, _, Mutability::Mut)) {
287+
sugg
288+
} else {
289+
sugg.mut_addr()
290+
}
291+
} else {
292+
// `lhs = rhs.to_owned()` -> `rhs.clone_into(&mut lhs)`
293+
// `lhs = ToOwned::to_owned(rhs)` -> `ToOwned::clone_into(rhs, &mut lhs)`
294+
Sugg::hir_with_applicability(cx, lhs, "_", applicability)
295+
.maybe_par()
296+
.mut_addr()
297+
};
298+
299+
match self.kind {
300+
CallKind::MethodCall { receiver } => {
301+
let receiver_sugg = Sugg::hir_with_applicability(cx, receiver, "_", applicability);
302+
format!("{receiver_sugg}.clone_into({rhs_sugg})")
303+
},
304+
CallKind::FunctionCall { self_arg, .. } => {
305+
let self_sugg = Sugg::hir_with_applicability(cx, self_arg, "_", applicability);
306+
format!("::std::borrow::ToOwned::clone_into({self_sugg}, {rhs_sugg})")
307+
},
308+
}
309+
},
310+
}
311+
}
312+
}

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
4747
crate::asm_syntax::INLINE_ASM_X86_INTEL_SYNTAX_INFO,
4848
crate::assertions_on_constants::ASSERTIONS_ON_CONSTANTS_INFO,
4949
crate::assertions_on_result_states::ASSERTIONS_ON_RESULT_STATES_INFO,
50+
crate::assigning_clones::ASSIGNING_CLONES_INFO,
5051
crate::async_yields_async::ASYNC_YIELDS_ASYNC_INFO,
5152
crate::attrs::ALLOW_ATTRIBUTES_WITHOUT_REASON_INFO,
5253
crate::attrs::BLANKET_CLIPPY_RESTRICTION_LINTS_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ mod as_conversions;
8080
mod asm_syntax;
8181
mod assertions_on_constants;
8282
mod assertions_on_result_states;
83+
mod assigning_clones;
8384
mod async_yields_async;
8485
mod attrs;
8586
mod await_holding_invalid;
@@ -1118,6 +1119,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
11181119
store.register_late_pass(move |_| Box::new(incompatible_msrv::IncompatibleMsrv::new(msrv())));
11191120
store.register_late_pass(|_| Box::new(to_string_trait_impl::ToStringTraitImpl));
11201121
store.register_early_pass(|| Box::new(multiple_bound_locations::MultipleBoundLocations));
1122+
store.register_late_pass(|_| Box::new(assigning_clones::AssigningClones));
11211123
// add lints here, do not remove this comment, it's used in `new_lint`
11221124
}
11231125

0 commit comments

Comments
 (0)