Skip to content

Commit 8689f77

Browse files
committed
Add lint assign_clone.
This lint reports when `*a = b.clone();` can be replaced with `a.clone_from(&b);`, and the same for `ToOwned::clone_into()`. Fixes #1709. Missing functionality: * Does not detect function call syntax in addition to method call syntax. * Does not correctly verify that the call is a call to `Clone::clone()` rather than some other similarly named method. * Does not lint on assignment to a local variable or `DerefMut` coercion, only on assignment to `*{some &mut T}`, because it does not know whether the local variable might be uninitialized.
1 parent 592ea39 commit 8689f77

File tree

7 files changed

+434
-0
lines changed

7 files changed

+434
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4396,6 +4396,7 @@ Released 2018-09-13
43964396
[`assertions_on_result_states`]: https://rust-lang.github.io/rust-clippy/master/index.html#assertions_on_result_states
43974397
[`assign_op_pattern`]: https://rust-lang.github.io/rust-clippy/master/index.html#assign_op_pattern
43984398
[`assign_ops`]: https://rust-lang.github.io/rust-clippy/master/index.html#assign_ops
4399+
[`assigning_clones`]: https://rust-lang.github.io/rust-clippy/master/index.html#assigning_clones
43994400
[`async_yields_async`]: https://rust-lang.github.io/rust-clippy/master/index.html#async_yields_async
44004401
[`await_holding_invalid_type`]: https://rust-lang.github.io/rust-clippy/master/index.html#await_holding_invalid_type
44014402
[`await_holding_lock`]: https://rust-lang.github.io/rust-clippy/master/index.html#await_holding_lock

clippy_lints/src/assigning_clones.rs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
use clippy_utils::{diagnostics::span_lint_and_then, sugg::Sugg, ty::implements_trait};
2+
use rustc_errors::Applicability;
3+
use rustc_hir::{self as hir, ExprKind};
4+
use rustc_lint::{LateContext, LateLintPass};
5+
use rustc_session::{declare_lint_pass, declare_tool_lint};
6+
use rustc_span::symbol::sym;
7+
8+
declare_clippy_lint! {
9+
/// ### What it does
10+
/// Checks for code like `foo = bar.clone();`
11+
///
12+
/// ### Why is this bad?
13+
/// If cloning `bar` allocates memory (or other resources), then this code will allocate
14+
/// new memory for the clone of `bar`, then drop `foo`, then overwrite `foo` with the clone.
15+
/// Instead, `Clone::clone_from()`, or `ToOwned::clone_into()`, may be able to update
16+
/// `foo` in-place, reusing existing memory.
17+
///
18+
/// Note that this only provides any actual improvement if the type has explicitly implemented
19+
/// the `clone_from()` trait method, since the trait-provided implementation will just call
20+
/// `clone()`.
21+
///
22+
/// ### Example
23+
/// ```rust
24+
/// #[derive(Clone)]
25+
/// struct Thing;
26+
///
27+
/// pub fn assign_to_ref(a: &mut Thing, b: Thing) {
28+
/// *a = b.clone();
29+
/// }
30+
/// ```
31+
/// Use instead:
32+
/// ```rust
33+
/// #[derive(Clone)]
34+
/// struct Thing;
35+
///
36+
/// pub fn assign_to_ref(a: &mut Thing, b: Thing) {
37+
/// a.clone_from(&b);
38+
/// }
39+
/// ```
40+
#[clippy::version = "1.70.0"]
41+
pub ASSIGNING_CLONES,
42+
perf,
43+
"assigning the result of cloning may be inefficient"
44+
}
45+
declare_lint_pass!(AssigningClones => [ASSIGNING_CLONES]);
46+
47+
impl<'tcx> LateLintPass<'tcx> for AssigningClones {
48+
fn check_expr(&mut self, cx: &LateContext<'tcx>, assign_expr: &'tcx hir::Expr<'_>) {
49+
let ExprKind::Assign(assign_target, clone_call, _span) = assign_expr.kind else { return };
50+
// TODO: Also look for `Clone::clone` function calls, not just method calls
51+
let ExprKind::MethodCall(method_name, clone_receiver, args, _span) = clone_call.kind else { return };
52+
53+
// Fast syntactic check: if it has args it can't be the call we are looking for,
54+
// so we don't even need to consult the types.
55+
if !args.is_empty() {
56+
return;
57+
}
58+
59+
let op = if method_name.ident.name == sym!(clone) {
60+
Op::Clone
61+
} else if method_name.ident.name == sym!(to_owned) {
62+
Op::ToOwned
63+
} else {
64+
return;
65+
};
66+
67+
if ok_to_suggest(cx, op, assign_target, clone_call).is_some() {
68+
suggest(cx, op, assign_expr, assign_target, clone_receiver);
69+
}
70+
}
71+
}
72+
73+
#[derive(Copy, Clone, Debug)]
74+
enum Op {
75+
Clone,
76+
ToOwned,
77+
}
78+
79+
// Return `Some(())` iff we have confirmed that the call is in fact one we want to replace.
80+
fn ok_to_suggest<'tcx>(
81+
cx: &LateContext<'tcx>,
82+
op: Op,
83+
assign_target: &hir::Expr<'tcx>,
84+
clone_call: &hir::Expr<'tcx>,
85+
) -> Option<()> {
86+
// Check that the call is actually a call to the trait.
87+
// TODO: Actually we are currently just checking that the result of the call is
88+
// a type that implements the trait, which is a bad proxy for it.
89+
let clone_result_type = cx.typeck_results().expr_ty_adjusted(clone_call);
90+
if !(implements_trait(cx, clone_result_type, op.expected_trait(cx)?, &[])) {
91+
return None;
92+
}
93+
94+
// If we're assigning to a dereferenced reference, then we know the place is already valid.
95+
// On the other hand, if the place is a variable or a Box, it might be uninitialized,
96+
// in which case the suggestion might be wrong.
97+
// TODO: Actually ask whether the place is uninitialized at this point, instead of
98+
// guessing based on the syntax and type.
99+
let ExprKind::Unary(hir::UnOp::Deref, derefed_target_expr) = assign_target.kind
100+
else { return None };
101+
if !cx.typeck_results().expr_ty(derefed_target_expr).is_ref() {
102+
return None;
103+
}
104+
105+
Some(())
106+
}
107+
108+
fn suggest<'tcx>(
109+
cx: &LateContext<'tcx>,
110+
op: Op,
111+
assign_expr: &hir::Expr<'tcx>,
112+
assign_target: &hir::Expr<'tcx>,
113+
clone_receiver: &hir::Expr<'tcx>,
114+
) {
115+
span_lint_and_then(cx, ASSIGNING_CLONES, assign_expr.span, op.message(), |diag| {
116+
// TODO: Make this MachineApplicable once we are more certain that the method being called
117+
// is what we think it is.
118+
let mut applicability = Applicability::MaybeIncorrect;
119+
120+
diag.span_suggestion(
121+
assign_expr.span,
122+
op.suggestion_msg(),
123+
op.suggested_replacement(cx, assign_target, clone_receiver, &mut applicability),
124+
applicability,
125+
);
126+
});
127+
}
128+
129+
impl Op {
130+
fn expected_trait(self, cx: &LateContext<'_>) -> Option<hir::def_id::DefId> {
131+
match self {
132+
Op::Clone => cx.tcx.lang_items().clone_trait(),
133+
Op::ToOwned => cx.tcx.get_diagnostic_item(sym::ToOwned),
134+
}
135+
}
136+
137+
fn message(self) -> &'static str {
138+
// TODO: Use the receiver type to say "is" instead of "may be" for types which
139+
// are known to have optimizations (e.g. `String`).
140+
match self {
141+
Op::Clone => "assigning the result of `Clone::clone()` may be inefficient",
142+
Op::ToOwned => "assigning the result of `ToOwned::to_owned()` may be inefficient",
143+
}
144+
}
145+
146+
fn suggestion_msg(self) -> &'static str {
147+
match self {
148+
Op::Clone => "use `clone_from()`",
149+
Op::ToOwned => "use `clone_into()`",
150+
}
151+
}
152+
153+
fn suggested_replacement<'tcx>(
154+
self,
155+
cx: &LateContext<'tcx>,
156+
assign_target: &hir::Expr<'tcx>,
157+
clone_receiver: &hir::Expr<'tcx>,
158+
applicability: &mut Applicability,
159+
) -> String {
160+
match self {
161+
Op::Clone => {
162+
// The assignment LHS, which will become the receiver of the `.clone_from()` call,
163+
// should lose one level of dereference operator since autoref takes care of that.
164+
let target_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = assign_target.kind {
165+
Sugg::hir_with_applicability(cx, ref_expr, "_", applicability)
166+
} else {
167+
Sugg::hir_with_applicability(cx, assign_target, "_", applicability)
168+
}
169+
.maybe_par();
170+
171+
// Determine whether we need to reference the argument to clone_from().
172+
let clone_receiver_type = cx.typeck_results().expr_ty(clone_receiver);
173+
let clone_receiver_adj_type = cx.typeck_results().expr_ty_adjusted(clone_receiver);
174+
let mut clone_source_sugg = Sugg::hir_with_applicability(cx, clone_receiver, "_", applicability);
175+
if clone_receiver_type != clone_receiver_adj_type {
176+
// The receiver may have been a value type, so we need to add an `&` to
177+
// be sure the argument to clone_from will be a reference.
178+
clone_source_sugg = clone_source_sugg.addr();
179+
};
180+
181+
format!("{target_sugg}.clone_from({clone_source_sugg})")
182+
},
183+
Op::ToOwned => {
184+
// If the assignment dereferences, we want the `&mut` that's getting dereferenced.
185+
// If it doesn't, then we need to *take* a `&mut`.
186+
// TODO: This doesn't yet handle `DerefMut` types (but it can't meet them)
187+
let target_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = assign_target.kind {
188+
Sugg::hir_with_applicability(cx, ref_expr, "_", applicability)
189+
} else {
190+
// TODO: there is not yet a test for this branch, and there cannot be
191+
// until we remove the assigning-to-a-variable restriction.
192+
Sugg::hir_with_applicability(cx, assign_target, "_", applicability).mut_addr()
193+
}
194+
.maybe_par();
195+
196+
// We are replacing `foo.to_owned()` with `foo.clone_into(...)`, so the receiver
197+
// can stay unchanged.
198+
let receiver_sugg = Sugg::hir_with_applicability(cx, clone_receiver, "_", applicability);
199+
200+
format!("{receiver_sugg}.clone_into({target_sugg})")
201+
},
202+
}
203+
}
204+
}

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
4343
crate::asm_syntax::INLINE_ASM_X86_INTEL_SYNTAX_INFO,
4444
crate::assertions_on_constants::ASSERTIONS_ON_CONSTANTS_INFO,
4545
crate::assertions_on_result_states::ASSERTIONS_ON_RESULT_STATES_INFO,
46+
crate::assigning_clones::ASSIGNING_CLONES_INFO,
4647
crate::async_yields_async::ASYNC_YIELDS_ASYNC_INFO,
4748
crate::attrs::ALLOW_ATTRIBUTES_WITHOUT_REASON_INFO,
4849
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
@@ -73,6 +73,7 @@ mod as_conversions;
7373
mod asm_syntax;
7474
mod assertions_on_constants;
7575
mod assertions_on_result_states;
76+
mod assigning_clones;
7677
mod async_yields_async;
7778
mod attrs;
7879
mod await_holding_invalid;
@@ -960,6 +961,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
960961
store.register_late_pass(|_| Box::new(tests_outside_test_module::TestsOutsideTestModule));
961962
store.register_late_pass(|_| Box::new(manual_slice_size_calculation::ManualSliceSizeCalculation));
962963
store.register_early_pass(|| Box::new(suspicious_doc_comments::SuspiciousDocComments));
964+
store.register_late_pass(|_| Box::new(assigning_clones::AssigningClones));
963965
// add lints here, do not remove this comment, it's used in `new_lint`
964966
}
965967

tests/ui/assigning_clones.fixed

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// run-rustfix
2+
#![allow(unused)]
3+
#![allow(clippy::redundant_clone)]
4+
#![allow(clippy::ptr_arg)] // https://github.com/rust-lang/rust-clippy/issues/10612
5+
#![warn(clippy::assigning_clones)]
6+
7+
use std::borrow::ToOwned;
8+
use std::ops::Add;
9+
10+
#[derive(Clone)]
11+
pub struct Thing;
12+
13+
pub fn val_to_mut_ref(mut_thing: &mut Thing, value_thing: Thing) {
14+
mut_thing.clone_from(&value_thing);
15+
}
16+
17+
pub fn ref_to_mut_ref(mut_thing: &mut Thing, ref_thing: &Thing) {
18+
mut_thing.clone_from(ref_thing);
19+
}
20+
21+
pub fn to_op(mut_thing: &mut Thing, ref_thing: &Thing) {
22+
// These parens should be kept as necessary for a receiver
23+
(mut_thing + &mut Thing).clone_from(ref_thing);
24+
}
25+
26+
pub fn from_op(mut_thing: &mut Thing, ref_thing: &Thing) {
27+
// These parens should be removed since they are not needed in a function argument
28+
mut_thing.clone_from(ref_thing + ref_thing);
29+
}
30+
31+
pub fn assign_to_var(b: Thing) -> Thing {
32+
let mut a = Thing;
33+
for _ in 1..10 {
34+
// TODO: We should rewrite this, but aren't checking whether `a` is initialized
35+
// to be certain it's okay.
36+
a = b.clone();
37+
}
38+
a
39+
}
40+
41+
pub fn assign_to_uninit_var(b: Thing) -> Thing {
42+
// This case CANNOT be rewritten as clone_from() and should not warn.
43+
let mut a;
44+
a = b.clone();
45+
a
46+
}
47+
48+
/// Test `ToOwned` as opposed to `Clone`.
49+
pub fn toowned_to_mut_ref(mut_string: &mut String, ref_str: &str) {
50+
ref_str.clone_into(mut_string);
51+
}
52+
53+
/// Don't remove the dereference operator here (as we would for clone_from); we need it.
54+
pub fn toowned_to_derefed(mut_box_string: &mut Box<String>, ref_str: &str) {
55+
// TODO: This is not actually linted, but it should be.
56+
**mut_box_string = ref_str.to_owned();
57+
}
58+
59+
struct FakeClone;
60+
impl FakeClone {
61+
/// This looks just like Clone::clone but has no clone_from()
62+
fn clone(&self) -> Self {
63+
FakeClone
64+
}
65+
}
66+
67+
pub fn test_fake_clone() {
68+
let mut a = FakeClone;
69+
let b = FakeClone;
70+
// This should not be changed because it is not Clone::clone
71+
a = b.clone();
72+
}
73+
74+
fn main() {}
75+
76+
/// Trait implementation to allow producing a `Thing` with a low-precedence expression.
77+
impl Add for Thing {
78+
type Output = Self;
79+
fn add(self, _: Thing) -> Self {
80+
self
81+
}
82+
}
83+
/// Trait implementation to allow producing a `&Thing` with a low-precedence expression.
84+
impl<'a> Add for &'a Thing {
85+
type Output = Self;
86+
fn add(self, _: &'a Thing) -> Self {
87+
self
88+
}
89+
}
90+
/// Trait implementation to allow producing a `&mut Thing` with a low-precedence expression.
91+
impl<'a> Add for &'a mut Thing {
92+
type Output = Self;
93+
fn add(self, _: &'a mut Thing) -> Self {
94+
self
95+
}
96+
}

0 commit comments

Comments
 (0)