Skip to content
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

Closed
wants to merge 2 commits into from
Closed

Add lint assigning_clones. #10613

wants to merge 2 commits into from

Conversation

kpreid
Copy link
Contributor

@kpreid kpreid commented Apr 9, 2023

This lint reports when *a = b.clone(); can be replaced with a.clone_from(&b);, and the same for ToOwned::clone_into(). Fixes #1709.

This is my first attempt at writing code for clippy or rustc, and I could use help with these missing parts:

  • Does not correctly verify that the call is a call to 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.
  • Does not detect function call syntax in addition to method call syntax. I will fix this after the previous item.
  • 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. 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]

@rustbot
Copy link
Collaborator

rustbot commented Apr 9, 2023

r? @xFrednet

(rustbot has picked a reviewer for you, use r? to override)

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Apr 9, 2023
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.
@Noratrieb
Copy link
Member

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)

@kpreid
Copy link
Contributor Author

kpreid commented Apr 9, 2023

Note that the derive macro currently doesn't implement clone_from and it's unlikely to do so in the future

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 clone_from or not to decide whether to emit the lint (or how to phrase it). I also thought about mentioning the derive's behavior in the lint documentation, but it seems not great to do that when it might, in fact, be wrong in the future without being updated. (But maybe that's worth doing anyway.)

However, my hope here is that the lint will raise awareness of the existence of clone_from, and authors of types will more often implement it when it is applicable, thus expanding the performance gains. The rewrite this lint proposes should not significantly decrease performance even if it gains none, so the cost is only in “churn” (people seeing a new lint and fixing/allowing it), which is not to be ignored! But discussing whether this lint should not exist at all should probably happen on the issue, not the PR, yes?

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 clone_from() implementations.

Copy link
Member

@xFrednet xFrednet left a 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,
Copy link
Member

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:

  1. 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.
  2. Have the lint in an allow-by-default category, like pedantic or restriction
  3. 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.

Copy link
Contributor Author

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.

Copy link
Member

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:

https://rust-lang.zulipchat.com/#narrow/stream/257328-clippy/topic/Meeting.202023-05-02/near/355124756

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

Comment on lines +13 to +20
/// 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()`.
Copy link
Member

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:

Suggested change
/// 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.

Comment on lines +22 to +39
/// ### 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);
/// }
/// ```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// ### 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 };
Copy link
Member

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 :)

Suggested change
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
Copy link
Member

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) ...

Comment on lines +97 to +98
// TODO: Actually ask whether the place is uninitialized at this point, instead of
// guessing based on the syntax and type.
Copy link
Member

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 :)

Comment on lines +138 to +139
// TODO: Use the receiver type to say "is" instead of "may be" for types which
// are known to have optimizations (e.g. `String`).
Copy link
Member

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)
Copy link
Member

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 :)

@xFrednet
Copy link
Member

xFrednet commented Apr 11, 2023

The rewrite this lint proposes should not significantly decrease performance even if it gains none, so the cost is only in “churn” (people seeing a new lint and fixing/allowing it), which is not to be ignored!

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

But discussing whether this lint should not exist at all should probably happen on the issue, not the PR, yes?

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 :)

@rustbot rustbot added the I-nominated Issue: Nominated to be discussed at the next Clippy meeting label Apr 11, 2023
@bors
Copy link
Collaborator

bors commented Apr 23, 2023

☔ The latest upstream changes (presumably #10578) made this pull request unmergeable. Please resolve the merge conflicts.

@xFrednet xFrednet added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels May 2, 2023
@xFrednet xFrednet removed the I-nominated Issue: Nominated to be discussed at the next Clippy meeting label May 16, 2023
@xFrednet
Copy link
Member

xFrednet commented Jul 5, 2023

Hey @kpreid, this is a ping from triage. Do you need any further assistance with this PR? :)

@kpreid
Copy link
Contributor Author

kpreid commented Jul 5, 2023

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.

@xFrednet
Copy link
Member

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!

@xFrednet xFrednet closed this Nov 10, 2023
@xFrednet xFrednet added S-inactive-closed Status: Closed due to inactivity and removed S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) labels Nov 10, 2023
bors added a commit that referenced this pull request Mar 5, 2024
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`]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-inactive-closed Status: Closed due to inactivity
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Lint to suggest clone_from and clone_into
5 participants