-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
[ruff
] Detect unnecessary dict
comprehensions for iterables (RUF025
)
#9613
Conversation
RemarkI'm getting the following error when running tests locally with ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Snapshot Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Snapshot file: crates/ruff_linter/src/rules/flake8_executable/snapshots/ruff_linter__rules__flake8_executable__tests__EXE001_1.py.snap
Snapshot: EXE001_1.py
Source: crates/ruff_linter/src/rules/flake8_executable/mod.rs:43
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
-old snapshot
+new results
────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0 │-EXE001_1.py:1:1: EXE001 Shebang is present but file is not executable␊
1 │- |␊
2 │-1 | #!/usr/bin/python␊
3 │- | ^^^^^^^^^^^^^^^^^ EXE001␊
4 │-2 | ␊
5 │-3 | if __name__ == '__main__':␊
6 │- |␊
────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
To update snapshots run `cargo insta review`
Stopped on the first failure. Run `cargo insta test` to run all snapshots.
thread 'rules::flake8_executable::tests::rules::path_new_exe001_1_py_expects' panicked at /home/mikko/.cargo/registry/src/index.crates.io-6f17d22bba15001f/insta-1.34.0/src/runtime.rs:563:9:
snapshot assertion for 'EXE001_1.py' failed in line 43
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
rules::flake8_executable::tests::rules::path_new_exe001_1_py_expects
rules::flake8_executable::tests::rules::path_new_exe002_1_py_expects Is this a known issue or should I do something differently? SuggestionHave you considered using cargo-make? It could be used to execute e.g. fmt, clippy and tests in one task. |
|
code | total | + violation | - violation | + fix | - fix |
---|---|---|---|---|---|
RUF025 | 28 | 28 | 0 | 0 | 0 |
…or key-value pair in comprehension is binded to a same name
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.
Looks pretty solid, thanks for working on this!
I've left some minor nits and one question about the fix generation but otherwise it looks good to go.
/// ``` | ||
#[violation] | ||
pub struct UnnecessaryDictComprehensionForIterable { | ||
is_value_none_literal: bool, |
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.
nit: we could use an enum instead of a boolean. A boolean value is hard to read until it is linked with some context like a variable name. An enum can prove to be readable in such cases.
enum Value {
// A `None` literal
NoneLiteral,
// Any value other than `None`
Other
}
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.
Yea, I thought about using an Enum but it kind of felt slightly over board for such a simple thing.
|
||
impl AlwaysFixableViolation for UnnecessaryDictComprehensionForIterable { | ||
#[derive_message_formats] | ||
#[allow(clippy::match_bool)] |
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 believe this must be a leftover and can be removed now?
} | ||
} | ||
|
||
#[allow(clippy::match_bool)] |
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 believe this must be a leftover and can be removed now?
// Don't suggest `dict.fromkeys` for async generator expressions, because `dict.fromkeys` is not async. | ||
// Don't suggest `dict.fromkeys` for nested generator expressions, because `dict.fromkeys` might be error-prone option at least for fixing. | ||
// Don't suggest `dict.fromkeys` for generator expressions with `if` clauses, because `dict.fromkeys` might not be valid option. |
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.
nit: can we combine these comments and use bullet points instead?
// Don't suggest `dict.fromkeys` for
// - ..., because ...
let Expr::Name(_) = &generator.target 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.
nit: we can use the is_
method on the node: generator.target.is_name_expr
/// Generate a [`Fix`] to replace `dict` comprehension with `dict.fromkeys`. | ||
/// (RUF025) Convert `{n: None for n in [1,2,3]}` to `dict.fromkeys([1,2,3])` or | ||
/// `{n: 1 for n in [1,2,3]}` to `dict.fromkeys([1,2,3], 1)`. | ||
fn fix_unnecessary_dict_comprehension( |
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 you choose to use libcst
nodes instead of the AST nodes to construct the fixed node? I might be wrong but I think it might be easier to construct the node using the AST nodes with the Generator
to generate the equivalent source code. I'm fine with either but just trying to understand 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.
No, specific reason. I was not quite sure what would be the correct way to construct the fixed node. Seems that using AST node with Generator
would be more reasonable.
@dhruvmanila thanks for the good comments! 👍 |
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.
Thanks!
return; | ||
} | ||
|
||
if !is_constant(dict_comp.value.as_ref()) { |
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.
@mikeleppane - I ended up using is_constant
here with doesn't include Expr::Name
, so that changed some of the test case results.
For example, we no longer flag this:
def func():
values = ["a", "b", "c"]
[{n: values for n in [1,2,3]}] # RUF025
But I think we're correct not to flag, since in that case, values
is actually mutable.
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.
Oh wait, hmm, but the user is already sharing it between elements. What you had is probably correct then.
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.
Refining back.
ruff
] Detect unnecessary dict
comprehensions for iterables (RUF025
)
Summary
Checks for unnecessary
dict
comprehension when creating a new dictionary from iterable. Suggest to replace withdict.fromkeys(iterable)
See: #9592
Test Plan
cargo test