-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add lint assigning_clones
.
#10613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add lint assigning_clones
.
#10613
Conversation
r? @xFrednet (rustbot has picked a reviewer for you, use r? to override) |
This lint reports when `*a = b.clone();` can be replaced with `a.clone_from(&b);`, and the same for `ToOwned::clone_into()`. Fixes rust-lang#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.
Note that the derive macro currently doesn't implement clone_from and it's unlikely to do so in the future: rust-lang/rust#98445 (comment) |
Indeed, that's why the note says “may be inefficient”. I could imagine the lint pass checking the implementing type to see whether it overrides However, my hope here is that the lint will raise awareness of the existence of Another idea: there could be a configuration option (or splitting this lint into two lints) to have it lint only on types known to have |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the PR. I left remarks regarding most todos. As suggested in a comment, it would probably be best to only lint cases, where a custom clone_from
implementation exists. I'm not 100% sure how to do that right now, but will look it up, until the next review. I hope that this gives you enough hints to continue on the implementation for now.
You're welcome to reach out if you have any questions :)
/// ``` | ||
#[clippy::version = "1.70.0"] | ||
pub ASSIGNING_CLONES, | ||
perf, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like perf
is too harsh for this lint. In part because I find a = XYZ.clone()
more readable. For instance, when I scan for the origin of a, I would find the example much faster than a.clone_from(XYZ)
. However, I do understand how this can affect performance. I see three options:
- Keep the lint in
perf
. This will most likely cause several lint issues in the next release and will annoy users, to the point where they'll just allow the lint. - Have the lint in an allow-by-default category, like
pedantic
orrestriction
- Restrict the lint, to only detect cases, where the type actually has a custom implementation of
clone_from()
I favor the third option, we can add a config value to detect all cases, but I'd guess that 99+% of the users will prefer this variant. I'll ask the other team members, what they think about it :)
Looking at the standard library, it looks like, most types that implement this function, mostly do so, to forward it to generic parameters, in case they have a custom implementation. COW
is one of the examples where this is not the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally like option 3 for now — focusing on giving definitely-useful advice. I haven't gotten back to working on this yet, but when I do I will look into that, and if doesn't seem feasible to do that then I'll change the category to pedantic
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've discussed the topic in the meeting 2 weeks ago, but I missed writing a comment then 😅. Here is a link to the discussion:
It was suggested to add a clippy attribute that crates can use, to mark a struct for this lint. Also restricting the lint to custom clone_from
suggestions was encouraged, IIRC
/// If cloning `bar` allocates memory (or other resources), then this code will allocate | ||
/// new memory for the clone of `bar`, then drop `foo`, then overwrite `foo` with the clone. | ||
/// Instead, `Clone::clone_from()`, or `ToOwned::clone_into()`, may be able to update | ||
/// `foo` in-place, reusing existing memory. | ||
/// | ||
/// Note that this only provides any actual improvement if the type has explicitly implemented | ||
/// the `clone_from()` trait method, since the trait-provided implementation will just call | ||
/// `clone()`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would write this much shorter, similar to the description in the Clone
trait:
/// If cloning `bar` allocates memory (or other resources), then this code will allocate | |
/// new memory for the clone of `bar`, then drop `foo`, then overwrite `foo` with the clone. | |
/// Instead, `Clone::clone_from()`, or `ToOwned::clone_into()`, may be able to update | |
/// `foo` in-place, reusing existing memory. | |
/// | |
/// Note that this only provides any actual improvement if the type has explicitly implemented | |
/// the `clone_from()` trait method, since the trait-provided implementation will just call | |
/// `clone()`. | |
/// Custom `clone_from()` implementations allow the objects to share resources | |
/// and therefore avoid allocations. |
/// ### Example | ||
/// ```rust | ||
/// #[derive(Clone)] | ||
/// struct Thing; | ||
/// | ||
/// pub fn assign_to_ref(a: &mut Thing, b: Thing) { | ||
/// *a = b.clone(); | ||
/// } | ||
/// ``` | ||
/// Use instead: | ||
/// ```rust | ||
/// #[derive(Clone)] | ||
/// struct Thing; | ||
/// | ||
/// pub fn assign_to_ref(a: &mut Thing, b: Thing) { | ||
/// a.clone_from(&b); | ||
/// } | ||
/// ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// ### Example | |
/// ```rust | |
/// #[derive(Clone)] | |
/// struct Thing; | |
/// | |
/// pub fn assign_to_ref(a: &mut Thing, b: Thing) { | |
/// *a = b.clone(); | |
/// } | |
/// ``` | |
/// Use instead: | |
/// ```rust | |
/// #[derive(Clone)] | |
/// struct Thing; | |
/// | |
/// pub fn assign_to_ref(a: &mut Thing, b: Thing) { | |
/// a.clone_from(&b); | |
/// } | |
/// ``` | |
/// ### Example | |
/// ```rust | |
/// # let src = "Cheesecake".to_string(); | |
/// let mut target = "Hello world".to_string(); | |
/// // [...] | |
/// target = src.clone(); | |
/// ``` | |
/// Use instead: | |
/// ```rust | |
/// # let src = "Cheesecake".to_string(); | |
/// let mut target = "Hello world".to_string(); | |
/// // [...] | |
/// target.clone_from(&src); | |
/// ``` |
fn check_expr(&mut self, cx: &LateContext<'tcx>, assign_expr: &'tcx hir::Expr<'_>) { | ||
let ExprKind::Assign(assign_target, clone_call, _span) = assign_expr.kind else { return }; | ||
// TODO: Also look for `Clone::clone` function calls, not just method calls | ||
let ExprKind::MethodCall(method_name, clone_receiver, args, _span) = clone_call.kind else { return }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tiny nit: You can check directly if args
are empty in this pattern :)
let ExprKind::MethodCall(method_name, clone_receiver, args, _span) = clone_call.kind else { return }; | |
let ExprKind::MethodCall(method_name, clone_receiver, [], _span) = clone_call.kind else { return }; |
impl<'tcx> LateLintPass<'tcx> for AssigningClones { | ||
fn check_expr(&mut self, cx: &LateContext<'tcx>, assign_expr: &'tcx hir::Expr<'_>) { | ||
let ExprKind::Assign(assign_target, clone_call, _span) = assign_expr.kind else { return }; | ||
// TODO: Also look for `Clone::clone` function calls, not just method calls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now to answer your first todo, from the PR description. Usually, we check if a type implements the trait in question, and then we check the function name. I believe, in this case, it would be:
if is_trait_method(cx, clone_call, sym::Clone) ...
// TODO: Actually ask whether the place is uninitialized at this point, instead of | ||
// guessing based on the syntax and type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finding the origin of a variable can be a bit difficult, when it comes to patterns, function parameters etc. Though, in this case, we only want to find the let <var>
statement if it's a local value. This is a rough sketch of how to check for this:
if let Some(id) = path_to_local(expr) {
if let Some(Node::Stmt(stmt)) = cx.tcx.hir().find(id) {
// if stmt is let, check for init expr
}
}
clippy_utils::expr_or_init
could also be interesting to look at :)
// TODO: Use the receiver type to say "is" instead of "may be" for types which | ||
// are known to have optimizations (e.g. `String`). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use type checking, to get the type of the clone call. If it's an ADT, you can use the DefId
to check for known types :)
See:
// The assignment LHS, which will become the receiver of the `.clone_from()` call, | ||
// should lose one level of dereference operator since autoref takes care of that. | ||
let target_sugg = if let ExprKind::Unary(hir::UnOp::Deref, ref_expr) = assign_target.kind { | ||
Sugg::hir_with_applicability(cx, ref_expr, "_", applicability) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice usage of hir_with_applicability
👍
You can resolve this directly :)
In some cases, this can actually be quite a big crunch. Which I why I suggested restricting the lint above. I think this is also worth a discussion with others, which is why I'm nominating this for the next meeting. @rustbot label +I-nominated
Ideally, yes. Though it's good to ask in PRs as well. We get so many notifications that we sadly cannot check every issue. These questions get noticed much faster in PRs. If you have a question in an issue, it's recommended to ping someone from the team, or ask on zulip for feedback :) |
☔ The latest upstream changes (presumably #10578) made this pull request unmergeable. Please resolve the merge conflicts. |
Hey @kpreid, this is a ping from triage. Do you need any further assistance with this PR? :) |
Not specifically; I just have not gotten back to it yet. I may have further questions when I do. |
Hey @kpreid, I'm closing this PR due to inactivity. If you want to continue this, you can add a comment for me to reopen this PR or simply create a new one. Thank you for the time you already put into this lint! |
Add `assigning_clones` lint This PR is a "revival" of #10613 (with `@kpreid's` permission). I tried to resolve most of the unresolved things from the mentioned PR: 1) The lint now checks properly if we indeed call the functions `std::clone::Clone::clone` or `std::borrow::ToOwned::to_owned`. 2) It now supports both method and function (UFCS) calls. 3) A heuristic has been added to decide if the lint should apply. It will only apply if the type on which the method is called has a custom implementation of `clone_from/clone_into`. Notably, it will not trigger for types that use `#[derive(Clone)]`. 4) `Deref` handling has been (hopefully) a bit improved, but I'm not sure if it's ideal yet. I also added a bunch of additional tests. There are a few things that could be improved, but shouldn't be blockers: 1) When the right-hand side is a function call, it is transformed into e.g. `::std::clone::Clone::clone(...)`. It would be nice to either auto-import the `Clone` trait or use the original path and modify it (e.g. `clone::Clone::clone` -> `clone::Clone::clone_from`). I don't know how to modify the `QPath` to do that though. 2) The lint currently does not trigger when the left-hand side is a local variable without an initializer. This is overly conservative, since it could trigger when the variable has no initializer, but it has been already initialized at the moment of the function call, e.g. ```rust let mut a; ... a = Foo; ... a = b.clone(); // Here the lint should trigger, but currently doesn't ``` These cases probably won't be super common, but it would be nice to make the lint more precise. I'm not sure how to do that though, I'd need access to some dataflow analytics or something like that. changelog: new lint [`assigning_clones`]
This lint reports when
*a = b.clone();
can be replaced witha.clone_from(&b);
, and the same forToOwned::clone_into()
. Fixes #1709.This is my first attempt at writing code for
clippy
orrustc
, and I could use help with these missing parts:Clone::clone()
rather than some other similarly named method. I need advice on how to ask rustc for this information; the lint isn't really correct without it.DerefMut
coercion, only on assignment to*{some &mut T}
, because it does not know whether the local variable might be uninitialized. I need advice on how to ask rustc for this information. (But if it's not feasible, then the lint will still work, just not apply to as many things.)changelog: new lint [
assigning_clones
]