-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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 a lint for redundant references to type params #7844
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @xFrednet (or someone else) soon. Please see the contribution instructions for more information. |
737b0e7
to
7369ad1
Compare
Hey and welcome to Clippy 📎. I haven't looked at the code yet, as I first want to address your question if this is plausible. To me, it's surprising that they should be equivalent, I would expect rustc to handle them differently. Anyway, that could just be something I'm missing. I did a quick test, based on the lint documentation that you wrote, and came up with this piece of code: use std::io::{self, Read};
fn before<R: Read>(reader: &mut R) {
let mut buffer = [0; 10];
let _res = reader.read(&mut buffer);
}
fn after<R: Read>(reader: R) {
let mut buffer = [0; 10];
let _res = reader.read(&mut buffer);
}
fn main() {
let mut stdin = io::stdin();
before(&mut stdin);
// Still works with a reference due to `Read`
// having `impl<R: Read + ?Sized> Read for &mut R`
after(&mut stdin);
// But no longer requires one
after(stdin);
} However, this example doesn't compile (See Playground). Is there something I'm missing? Or would this be a case that wouldn't be linted? 🙃 If this is resolved, I would like to also ask the rest of the team what they think. It could be an option to add this lint and just make it allow-by-default. (Like it currently would be in the |
You're right, it should be including fn after<R: Read>(mut reader: R) {
let mut buffer = [0; 10];
let _res = reader.read(&mut buffer);
} This would be a case I'd expect it to lint |
☔ The latest upstream changes (presumably #7838) made this pull request unmergeable. Please resolve the merge conflicts. |
Alright, I missed the
I'm still not totally sure if this lint is entirely correct or if I'm also overlooking something. @rust-lang/clippy What are your thoughts on this? :) |
I don't understand this lint TBH. Isn't |
Yeah I think |
Actually looking at this again I think it does make more sense to not use |
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'm definitely open to this lint. It's similar to the lint, that suggests to use &str
over &String
or &[_]
over &Vec<_>
. As long as you don't have to change the call-sites this should be fine.
I can imagine, that it may be hard to generalize this lint without any FP though. So we should be careful here and be creative when it comes to writing tests.
@@ -59,6 +59,9 @@ pub const FILE: [&str; 3] = ["std", "fs", "File"]; | |||
pub const FILE_TYPE: [&str; 3] = ["std", "fs", "FileType"]; | |||
#[allow(clippy::invalid_paths)] // `check_path` does not seem to work for macros | |||
pub const FORMAT_ARGS_MACRO: [&str; 4] = ["core", "macros", "builtin", "format_args"]; | |||
pub const FMT_DEBUG: [&str; 3] = ["core", "fmt", "Debug"]; |
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.
Aren't there diagnostic items for this?
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.
For Debug there is, it was a few of the others that didn't have them. I think the fmt ones and Seek/BufRead
I thought of a tricky false positive, things like fn f<R: Read>(a: &mut R, b: &mut R) {
mem::swap(a, b);
} |
Couldn't this be fixed with fn f<R: Read>(mut a: R, mut b: R) {
std::mem::swap(&mut a, &mut b);
} producing an auto-applicable suggestion for this is nearly impossible, but that's why we have |
I think the name should mention "generic" or "type params", like |
So my concern with this lint is that |
Requiring a mutable reference could propagate through the rest of your code. That's bad. fn with_file(file: &mut File) { // this shouldn't have to be a mutable reference
with_read(file);
}
fn with_read(reader: &mut impl Read) {
todo!();
} |
That would work for within the function, but wouldn't swap them at the callsite (playground) use std::io::Read;
fn mut_ref<R: Read>(a: &mut R, b: &mut R) {
std::mem::swap(a, b);
}
fn owned<R: Read>(mut a: R, mut b: R) {
std::mem::swap(&mut a, &mut b);
}
fn main() {
let mut a = &[1, 2, 3][..];
let mut b = &[4, 5, 6][..];
println!("{:?} | {:?}", a, b);
owned(&mut a, &mut b);
println!("{:?} | {:?}", a, b);
mut_ref(&mut a, &mut b); // swaps our a & b
println!("{:?} | {:?}", a, b);
}
|
Oh right. But doesn't this apply to every other case as well? fn f<R: Read>(a: &mut R) {
a.taking_ref_mut_self();
}
So I now think the lint is only applicable if the argument isn't really mutated in the function body. Which brings me to a lint idea I had a few days ago: |
How? All that it can do is call use std::io::Read;
fn generic_read(mut reader: impl Read) {
let _ = reader.read_to_end(&mut Vec::new());
}
fn generic_mut_ref_read(reader: &mut impl Read) {
let _ = reader.read_to_end(&mut Vec::new());
}
fn main() {
{
// this will copy the slice ref and read it
let slice = &[1u8, 2, 3][..];
generic_read(slice);
}
{
// this will update the slice
let mut slice = &[1u8, 2, 3][..];
generic_mut_ref_read(&mut slice);
assert!(slice.is_empty());
}
{
// this also updates the slice, but I am not forced to do so
let mut slice = &[1u8, 2, 3][..];
generic_read(&mut slice);
assert!(slice.is_empty());
}
} |
I don't really see this as bad: the alternative being suggested is owned arguments, which is kinda worse? Owned arguments are more strict than mutable, you can always get |
I don't think that is really true looking at
|
I meant in the general case. But what I was thinking of was something like this: Playground use std::io::{self, Read};
#[derive(Default, Copy, Clone)]
struct S {
num_used: u32,
}
impl Read for S {
fn read(&mut self, _: &mut [u8]) -> io::Result<usize> {
self.num_used += 1;
Ok(0)
}
}
fn f<R: Read>(r: &mut R) {
let _ = r.read(&mut []);
}
fn g<R: Read>(mut r: R) {
let _ = r.read(&mut []);
}
fn main() {
let mut s = S::default();
f(&mut s);
g(&mut s);
g(s); // s is copied and therefore "2" is printed at the end
println!("{}", s.num_used);
} But that isn't really a problem, since the reference is copied and therefore But writing out this example gave me another finding: potential misuse of the API. With |
Sure, but in terms of the types you are "forced" to use, if you require My point here is that most people don't intuitively know about the fn do_something(r: BufReader) {
foo(r);
} without realizing that I think that the convention is to use |
The "used after move" case is unfortunate. IMO the solution to that is better rustc diagnostics. But without that, I can understand wanting avoid the footgun. I guess it's a matter of preference. |
One more thought. |
I'm mostly concerned about removing |
Having this as a |
Yes, thank you for the input everybody! I am convinced as well that the replacement is too unintuitive, and that For example I noticed that I'd be happy to just close this PR, but I can continue working on it to get it into |
Don't keep it open on my account 🙂 |
👍 |
changelog: Add a lint for redundant references to type parameters
This lints for things like
Which could be
I've seen it a couple times for different reasons now, some thinking it should be
&mut
because the methods on Read take&mut self
. Others wanting to pass&mut [something that impls Read]
who are unaware or forgot aboutimpl<R: Read + ?Sized> Read for &mut R
. So I thought it would be a good candidate to write my first lintThere's still some stuff unimplemented, it doesn't cover e.g.
&mut impl Read
and I haven't added many edge cases to the tests, but I wanted to ask for some feedback firstMainly -- is this lint plausible? I worry I'm overlooking some case where you do need that signature
And currently I'm using a few hardcoded traits to check against, I found
implements_trait
which looks ideal, but I wasn't sure what should be passed in asty_params