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