-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New [suspicious_arguments]
lint for possibly swapped arguments
#9301
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
Conversation
r? @llogiq (rust-highfive has picked a reviewer for you, use r? to override) |
let defnames: Map<&str, usize> = ...;
let to_def_idx: Vec<Option<usize>> = vec![None; definition.len()];
for (i, _) in call.iter().enumerate() {
// set to_def_idx[i] to Some(_) if the name matches a defname
}
let mut cycle = Vec::new();
for callidx in 0..to_def_idx.len() {
cycle.clear();
let mut cur = callidx;
while let Some(defidx) = to_def_idx[cur].take() {
cycle.push(cur);
cur = callidx;
}
if Some(start) = cycle.iter().position(|e| *e == cur) {
// cycle detected at `cycle[start..]`, emit lint
}
}
A thought of a better algorithm that would detect Untested but worth considering |
☔ The latest upstream changes (presumably #9288) made this pull request unmergeable. Please resolve the merge conflicts. |
Here's a better version of the algorithm I sketched out yesterday on Discord, with bugs fixed and some explanatory comments: // Take care of degenerate cases.
if def_args.len() != call_args.len() || call_args.len() < 2 {
return;
}
// The mapping of argument names to their 'correct' position.
let def_names: Map<&str, usize> = ...;
// For each call argument, the index of the matching name in the definition.
let to_def_idx: Vec<Option<usize>> = def_args.iter().map(|arg| {
arg_to_ident_str(arg).and_then(|name| def_names.get(name))
});
let mut chain = Vec::new();
for call_idx in 0..to_def_idx.len() {
// Follow the 'chain' of indices until we get stuck.
// We remove the links as we traverse them to avoid infinite loops in cycles.
chain.clear();
let mut cur = call_idx;
while let Some(def_idx) = to_def_idx[cur].take() {
cycle.push(cur);
cur = def_idx;
}
// If the chain contains `cur`, then we have a cycle. Note that 1-cycles shouldn't be linted,
// as they correspond to well-positioned arguments, and so we ignore the last element.
let cycle_start = {
let mut iter = chain.iter();
iter.next_back();
iter.position(|e| *e == cur)
};
if Some(start) = cycle_start {
let cycle = &chain[start..];
// We have found a cyclic permutation of the arguments, emit the lint.
}
} |
☔ The latest upstream changes (presumably #9295) made this pull request unmergeable. Please resolve the merge conflicts. |
note: the arguments are defined here | ||
--> /home/jess/.cargo/registry/src/github.com-1ecc6299db9ec823/itertools-0.10.1/src/free.rs:151:18 | ||
| | ||
LL | pub fn all<I, F>(iterable: I, f: F) -> bool | ||
| ^^^^^^^^ ^ | ||
|
||
error: these arguments are possibly swapped | ||
--> $DIR/suspicious_arguments.rs:102:20 | ||
| | ||
LL | std::mem::swap(y, x); | ||
| ^ ^ | ||
| | ||
note: the arguments are defined here | ||
--> /home/jess/.rustup/toolchains/nightly-2022-07-28-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/mem/mod.rs:711:22 | ||
| | ||
LL | pub const fn swap<T>(x: &mut T, y: &mut T) { | ||
| ^ ^ |
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 an // aux-build
comment to test the separate crate version without running into these path issues
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.
Sorry for being absent for so long and thanks to @Alexendoo for taking over the review. I have a few nitpicks and one question, otherwise this looks mergeworthy.
@@ -0,0 +1,145 @@ | |||
// edition:2015 |
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.
Is there a specific reason for using the 2015 edition? We usually try to have all tests against the current edition, so if there is something specific to rust-2015, we should probably factor out a suspicious_argument_like_its_2015.rs
or something.
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.
Yeah, I was testing anonymous args, which you can only get with a trait in 2015. I'll move that to a different file.
if let Some((arg, call_span)) = arg_and_span { | ||
if let Some(&def_idx) = idxs.get(arg) { | ||
if call_idx != def_idx { | ||
if let Some((reverse_call, reverse_call_span)) = &call[def_idx] { | ||
let def_for_call = &definition[call_idx]; | ||
if reverse_call == &def_for_call.0 { |
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.
There is quite some rightwards drift here. We can use either the if_chain
macro or the recently reverted-on-stable-but-available-on-nightly let-chains feature (basically if let Some((arg, call_span)) = arg_and_span && let Some(&def_idx) = idxs.get(arg) && call_idx != def_idx && ..
etc.).
let mut def_args = Vec::new(); | ||
for ident in cx.tcx.fn_arg_names(def_id) { | ||
def_args.push((ident.to_string(), ident.span)); | ||
} |
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.
let mut def_args = Vec::new(); | |
for ident in cx.tcx.fn_arg_names(def_id) { | |
def_args.push((ident.to_string(), ident.span)); | |
} | |
let def_args = cx.tcx.fn_arg_names(def_id).map(|ident|{ | |
(ident.to_string(), ident.span) | |
}).collect::<Vec<_>>(); |
let mut call_args = Vec::new(); | ||
|
||
for call_arg in args { | ||
call_args.push(guess_ident(call_arg)); | ||
} |
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.
let mut call_args = Vec::new(); | |
for call_arg in args { | |
call_args.push(guess_ident(call_arg)); | |
} | |
let call_args = args.map(guess_ident).collect::<Vec<_>>(); |
Your call to |
It's not uncommon to have dummy parameter names which don't indicate anything, and thus cannot be reasonably used to guess accidental swaps. One example is arithmetic traits. It's common to implement unordered operation for different types based on a swap of some canonical order. E.g. impl std::ops::Add<Foo> for Bar { /* snip */ }
impl std::ops::Add<Bar> for Foo {
type Output = <Bar as Add<Foo>>::Output;
fn add(&self, other: &Bar) -> Self::Output {
std::ops::Add::add(other, self)
}
} I'm not sure if this specific lint will fire on this specific example ( It's hard to detect dummy identifiers in general, but the specific case of one-letter names is easy and common. It's unreasonable to expect that if I name my indexing variables Also, the lint ignores argument types (even has an explicit test for that with |
@5225225 I'm going to close this for now (365 days without activity), feel free to reopen if you still want to work on it! ❤️ PS: In this blog post, you can use SVG Filters instead of JS for better performance (letting the GPU do the rendering work instead of modifying the image with the CPU) ฅ՞•ﻌ•՞ฅ |
changelog: [
suspicious_arguments
]: add lint for probably accidentally swapped arguments