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 unnecessary_to_owned lint #7978

Merged
merged 6 commits into from
Dec 15, 2021
Merged

Add unnecessary_to_owned lint #7978

merged 6 commits into from
Dec 15, 2021

Conversation

smoelius
Copy link
Contributor

This PR adds a lint to check for unnecessary calls to ToOwned::to_owned and other similar functions (e.g., Cow::into_owned, ToString::to_string, etc.).

The lint checks for expressions of the form &receiver.to_owned_like() used in a position requiring type &T where one of the following is true:

  • receiver's type is T exactly
  • receiver's type implements Deref<Target = T>
  • receiver's type implements AsRef<T>

The lint additionally checks for expressions of the form receiver.to_owned_like() used as arguments of type impl AsRef<T>.

It would be nice if the lint could also check for expressions used as arguments to functions like the following:

fn foo<T: AsRef<str>>(x: T) { ... }

However, I couldn't figure out how to determine whether a function input type was instantiated from a parameter with a trait bound.

If someone could offer me some guidance, I would be happy to add such functionality.

Closes #7933

changelog: Add [unnecessary_to_owned] lint

@rust-highfive
Copy link

r? @flip1995

(rust-highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Nov 16, 2021
@flip1995
Copy link
Member

flip1995 commented Nov 16, 2021

Oh this is a nice lint! I only skimmed the code. It'll probably take me a few days to do a in-depth review. In the mean time, could you add some documentation to the functions that makes it easier to understand what those functions are checking for, please? Especially the check_call_arg and the check_addr_of_expr functions.

However, I couldn't figure out how to determine whether a function input type was instantiated from a parameter with a trait bound.

Shouldn't implements_trait on the arg Ty return true for the AsRef trait? In other words: I don't think it should matter if you have x: impl AsRef or <T: AsRef>(x; T). But maybe that's a bug/shortcoming of the implements_trait utility 🤔

@smoelius
Copy link
Contributor Author

smoelius commented Nov 16, 2021

Oh this is a nice lint! ... In the mean time, could you add some documentation to the functions that makes it easier to understand what those functions are checking for, please?

Thank you. :) I will.

Shouldn't implements_trait on the arg Ty return true for the AsRef trait? In other words: I don't think it should matter if you have x: impl AsRef or <T: AsRef>(x; T). But maybe that's a bug/shortcoming of the implements_trait utility thinking

Sorry for not explaining well. Currently, I call fn_sig to get the function's signature. For these two functions:

fn foo(_: impl AsRef<str>) {}
fn bar<T: AsRef<str>>(_: T) {}

fn_sig gives you structures with the following debug representations:

([impl AsRef<str>]; c_variadic: false)->()
([T]; c_variadic: false)->()

Currently, the lint catches calls to the foo function, because it parses the ParamTy name, which is impl AsRef<str> in the first debug representation above. But for the bar case, the ParamTy name is just T, so there's not enough information in the name to recognize that the argument has to implement AsRef<str>.

Presumably, there is some rustc API that allows one to query this, e.g., "What are the bounds on the type parameters of the function with definition id def_id?" But I couldn't find that API.

@bors
Copy link
Contributor

bors commented Nov 17, 2021

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

@flip1995
Copy link
Member

The FnSig::inputs method gives you a slice of Tys. Calling implements_trait(cx, arg_ty, as_ref_did, &[???]) should return true for fn bar<T: AsRef<str>>(_: T) {}? Or am I completely misunderstanding something?

@smoelius
Copy link
Contributor Author

smoelius commented Nov 17, 2021

Well, it could be that I am misunderstanding something.

The FnSig::inputs method gives you a slice of Tys.

I think once you have Tys it is too late. Because what you really want to know is: what were the requirements when those Tys where chosen? Could we have chosen something else?

I'm not sure implements_trait(.., as_ref_id, ..) is of help here, because we can't be sure that AsRef<..> was a bound on the argument type.

That's sort of the crux of the issue: how do you find out what those bounds were?

@flip1995
Copy link
Member

I think once you have Tys it is too late.

The Ty must retain all type information, so also all the traits this type implements. Once Clippy has access to Tys, type resolution is fully done, so all relevant trait information for the type should be there.

So it doesn't really matter what bounds the argument type had in the code. After trait resolution and type checking, the generic type should implement the trait, no matter what actual type it is.

@smoelius
Copy link
Contributor Author

Sorry, I think I may have just found the answer: https://doc.rust-lang.org/beta/nightly-rustc/rustc_middle/ty/context/struct.TyCtxt.html#method.predicates_of

Please give me a few days to look into this and update the lint.

I apologize for my confusion.

@smoelius smoelius force-pushed the master branch 3 times, most recently from 1f2c732 to 24ac43c Compare November 21, 2021 22:51
@smoelius
Copy link
Contributor Author

@flip1995 I think this is working as it should and you are welcome to start reviewing whenever you have time.

@camsteffen
Copy link
Contributor

I just want to make sure we are careful with how this lint overlaps with implicit_clone and redundant_clone. This lint probably should share a list of methods with implicit_clone. I think this lint should just skip over cases of redundant_clone. Or you could make this lint pass responsible for those particular instances of redundant_clone if that simplifies the code overall.

@smoelius
Copy link
Contributor Author

I think this lint should just skip over cases of redundant_clone. Or you could make this lint pass responsible for those particular instances of redundant_clone if that simplifies the code overall.

How would you handle this, @camsteffen? Is there a way to, say, to call redundant_clone's check_fn and check whether it fired?

@camsteffen
Copy link
Contributor

Maybe you could do it the other way around - call the new lint implemention inside of redundant_clone.

@smoelius
Copy link
Contributor Author

Actually maybe a better strategy is the following: have this lint apply only when the receiver is copy-able, or when the to_owned-like function is Cow::into_owned.

I think that would eliminate the possibility of overlap between redundant_clone and this lint, but would otherwise handle all cases that this lint currently tests for.

An alternative might be to do just the first part and extend redundant_clone to handle Cow::into_owned. But I'm not sure if that would go too much against the grain of redundant_clone (i.e., because in a call to Cow::into_owned, the receiver is moved).

@camsteffen
Copy link
Contributor

I was just thinking of cases like path_buf.to_path_buf().

@smoelius
Copy link
Contributor Author

I was just thinking of cases like path_buf.to_path_buf().

OK. Then I will implement what I called a "better strategy" above. That should handle the case that you mentioned, and is less invasive than the other options (no changes to redundant_clone).

I will also implement the list of methods shared with implicit_clone.

@flip1995 Please give me a day or two to implement these changes.

@smoelius
Copy link
Contributor Author

@flip1995 I think the just pushed changes address all of @camsteffen's concerns.

* Share a list of methods with `implicit_clone`
* Ensure no overlap with `redundant_clone`
Comment on lines 1967 to 1995
NEEDLESS_SPLITN
NEEDLESS_SPLITN,
UNNECESSARY_TO_OWNED
Copy link
Contributor

@hellow554 hellow554 Dec 13, 2021

Choose a reason for hiding this comment

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

IMHO this cloud/should have a trailing , so that the diff is only one insertion, instead of 1 delete, 2 insertions

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

@@ -169,7 +169,7 @@ impl<'tcx> LateLintPass<'tcx> for UnitReturnExpectingOrd {
trait_name
),
Some(last_semi),
&"probably caused by this trailing semicolon".to_string(),
"probably caused by this trailing semicolon",
Copy link
Contributor

Choose a reason for hiding this comment

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

🥳

@hellow554
Copy link
Contributor

@smoelius can you please test the following code if the new lint triggers incorrectly?

#![feature(drain_filter)]

fn main() {
    let mut a = vec![3, 5, 7, -4, -6];
    for x in a.drain_filter(|x| *x < 0).collect::<Vec<_>>() {
        a.push(-x);
    }
}

The problem is, that I can't remove collect, because else I would get borrowchecker error.

@smoelius
Copy link
Contributor Author

smoelius commented Dec 13, 2021

For me, the new lint doesn't seem to fire on that example. Could you please tell me what you are seeing?

Please tell me if you observe something else.

@hellow554
Copy link
Contributor

For me, the new lint doesn't seem to fire on that example

that's great <3

Please tell me if you observe something else.

I haven't observed anything, because I didn't want to compile clippy on my machine ;) That's why I asked you to run that code for me and as it turns out, it looks fine :)
Thanks!

Copy link
Contributor

@llogiq llogiq left a comment

Choose a reason for hiding this comment

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

I'm impressed by the very comprehensive test suite. 👍 I only found a few small nits. With those picked, I think this should be deemed mergeable.

/// and other `to_owned`-like functions.
///
/// ### Why is this bad?
/// The unnecessary calls result in unnecessary allocations.
Copy link
Contributor

Choose a reason for hiding this comment

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

Style: "unnecessary" used twice. How about changing one to "needless" or "useless"?

/// ### Example
/// ```rust
/// let path = std::path::Path::new("x");
/// foo(&path.to_string_lossy().to_string());
Copy link
Contributor

Choose a reason for hiding this comment

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

Is to_string even caught here? I didn't find any mention of it in is_clone_like and wondered if that was covered by another lint.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

to_string is handled outside of is_clone_like:

|| is_to_string(cx, method_name, method_def_id)

fn is_to_string(cx: &LateContext<'_>, method_name: Symbol, method_def_id: DefId) -> bool {
method_name.as_str() == "to_string" && is_diag_trait_item(cx, method_def_id, sym::ToString)
}

The reason for this is that implicit_clone calls is_clone_like (as a consequence of this PR), and I didn't want to change implicit_clone's behavior:
if is_clone_like(cx, method_name, method_def_id);

Please let me know if you think something should be adjusted.

Copy link
Contributor

Choose a reason for hiding this comment

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

Then we should at least have a comment stating this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please tell me if what I wrote is inadequate.

then {
let (target_ty, n_target_refs) = peel_mid_ty_refs(target_ty);
let (receiver_ty, n_receiver_refs) = peel_mid_ty_refs(receiver_ty);
if_chain! {
Copy link
Contributor

Choose a reason for hiding this comment

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

This if_chain! has only two conditions that could simply be put together with &&, thus reducing rightwards drift.

@smoelius
Copy link
Contributor Author

@llogiq Thank for the review and for your kind words. Please let me know if you have any comments following the adjustments.

@llogiq
Copy link
Contributor

llogiq commented Dec 15, 2021

This looks good now. Thank you! @bors r+

@bors
Copy link
Contributor

bors commented Dec 15, 2021

📌 Commit b891389 has been approved by llogiq

@bors
Copy link
Contributor

bors commented Dec 15, 2021

⌛ Testing commit b891389 with merge 40fd785...

@bors
Copy link
Contributor

bors commented Dec 15, 2021

☀️ Test successful - checks-action_dev_test, checks-action_remark_test, checks-action_test
Approved by: llogiq
Pushing 40fd785 to master...

@bors bors merged commit 40fd785 into rust-lang:master Dec 15, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Unnecessary call to to_string when the underlying type implements AsRef<str>
7 participants