-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Changes from 9 commits
0c91ac8
9e7b476
4105716
c4b16aa
cdb3e96
1e3e24b
99a57f4
10da052
0b4584d
5de6750
d6e1395
ebf534a
3cca228
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# Violation cases: RUF025 | ||
|
||
def func(): | ||
numbers = [1,2,3] | ||
{n: None for n in numbers} # RUF025 | ||
|
||
def func(): | ||
for key, value in {n: 1 for n in [1,2,3]}.items(): # RUF025 | ||
pass | ||
|
||
def func(): | ||
{n: 1.1 for n in [1,2,3]} # RUF025 | ||
|
||
def func(): | ||
{n: complex(3, 5) for n in [1,2,3]} # RUF025 | ||
|
||
def func(): | ||
def f(data): | ||
return data | ||
f({c: "a" for c in "12345"}) # RUF025 | ||
|
||
def func(): | ||
{n: True for n in [1,2,2]} # RUF025 | ||
|
||
def func(): | ||
{n: b'hello' for n in (1,2,2)} # RUF025 | ||
|
||
def func(): | ||
{n: ... for n in [1,2,3]} # RUF025 | ||
|
||
def func(): | ||
{n: False for n in {1: "a", 2: "b"}} # RUF025 | ||
|
||
def func(): | ||
{(a, b): 1 for (a, b) in [(1,2), (3, 4)]} # RUF025 | ||
|
||
|
||
# Non-violation cases: RUF025 | ||
|
||
def func(): | ||
{n: 1 for n in [1,2,3,4,5] if n < 3} # OK | ||
|
||
def func(): | ||
{n: 1 for c in [1,2,3,4,5] for n in [1,2,3] if c < 3} # OK | ||
|
||
def func(): | ||
def f(): | ||
pass | ||
{n: f() for n in [1,2,3]} # OK | ||
|
||
def func(): | ||
{n: n for n in [1,2,3,4,5]} # OK | ||
|
||
def func(): | ||
def f(): | ||
return {n: 1 for c in [1,2,3,4,5] for n in [1,2,3]} # OK | ||
|
||
f() | ||
|
||
def func(): | ||
def f(): | ||
return 1 | ||
|
||
a = f() | ||
{n: a for n in [1, 2, 3]} # RUF025 | ||
|
||
def func(): | ||
values = ["a", "b", "c"] | ||
[{n: values for n in [1,2,3]}] # RUF025 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,4 +9,3 @@ EXE001_1.py:1:1: EXE001 Shebang is present but file is not executable | |
3 | if __name__ == '__main__': | ||
| | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
use ast::ExprName; | ||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::comparable::ComparableExpr; | ||
use ruff_python_ast::helpers::is_constant; | ||
use ruff_python_ast::{self as ast, Arguments, Comprehension, Expr, ExprCall, ExprContext}; | ||
use ruff_text_size::{Ranged, TextRange}; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Checks for unnecessary `dict` comprehension when creating a dictionary from | ||
/// an iterable. | ||
/// | ||
/// ## Why is this bad? | ||
/// It's unnecessary to use a `dict` comprehension to build a dictionary from | ||
/// an iterable when the value is static. | ||
/// | ||
/// Prefer `dict.fromkeys(iterable)` over `{value: None for value in iterable}`, | ||
/// as `dict.fromkeys` is more readable and efficient. | ||
/// | ||
/// ## Examples | ||
/// ```python | ||
/// {a: None for a in iterable} | ||
/// {a: 1 for a in iterable} | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// ```python | ||
/// dict.fromkeys(iterable) | ||
/// dict.fromkeys(iterable, 1) | ||
/// ``` | ||
#[violation] | ||
pub struct UnnecessaryDictComprehensionForIterable { | ||
is_value_none_literal: bool, | ||
} | ||
|
||
impl Violation for UnnecessaryDictComprehensionForIterable { | ||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; | ||
|
||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
format!("Unnecessary dict comprehension for iterable; use `dict.fromkeys` instead") | ||
} | ||
|
||
fn fix_title(&self) -> Option<String> { | ||
if self.is_value_none_literal { | ||
Some(format!("Replace with `dict.fromkeys(iterable, value)`)")) | ||
} else { | ||
Some(format!("Replace with `dict.fromkeys(iterable)`)")) | ||
} | ||
} | ||
} | ||
|
||
/// RUF025 | ||
pub(crate) fn unnecessary_dict_comprehension_for_iterable( | ||
checker: &mut Checker, | ||
dict_comp: &ast::ExprDictComp, | ||
) { | ||
let [generator] = dict_comp.generators.as_slice() else { | ||
return; | ||
}; | ||
|
||
// Don't suggest `dict.fromkeys` for: | ||
// - async generator expressions, because `dict.fromkeys` is not async. | ||
// - nested generator expressions, because `dict.fromkeys` might be error-prone option at least for fixing. | ||
// - generator expressions with `if` clauses, because `dict.fromkeys` might not be valid option. | ||
if !generator.ifs.is_empty() && generator.is_async { | ||
return; | ||
} | ||
|
||
// Don't suggest `dict.keys` if the target is not the same as the key. | ||
if ComparableExpr::from(&generator.target) != ComparableExpr::from(dict_comp.key.as_ref()) { | ||
return; | ||
} | ||
|
||
if !is_constant(dict_comp.value.as_ref()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mikeleppane - I ended up using 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, There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Refining back. |
||
return; | ||
} | ||
|
||
let mut diagnostic = Diagnostic::new( | ||
UnnecessaryDictComprehensionForIterable { | ||
is_value_none_literal: dict_comp.value.is_none_literal_expr(), | ||
}, | ||
dict_comp.range(), | ||
); | ||
|
||
if checker.semantic().is_builtin("dict") { | ||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( | ||
checker | ||
.generator() | ||
.expr(&fix_unnecessary_dict_comprehension( | ||
dict_comp.value.as_ref(), | ||
generator, | ||
)), | ||
dict_comp.range(), | ||
))); | ||
} | ||
|
||
checker.diagnostics.push(diagnostic); | ||
} | ||
|
||
/// Generate a [`Fix`] to replace `dict` comprehension with `dict.fromkeys`. | ||
/// | ||
/// For example: | ||
/// - Given `{n: None for n in [1,2,3]}`, generate `dict.fromkeys([1,2,3])`. | ||
/// - Given `{n: 1 for n in [1,2,3]}`, generate `dict.fromkeys([1,2,3], 1)`. | ||
fn fix_unnecessary_dict_comprehension(value: &Expr, generator: &Comprehension) -> Expr { | ||
let iterable = generator.iter.clone(); | ||
let args = Arguments { | ||
args: if value.is_none_literal_expr() { | ||
vec![iterable] | ||
} else { | ||
vec![iterable, value.clone()] | ||
}, | ||
keywords: vec![], | ||
range: TextRange::default(), | ||
}; | ||
Expr::Call(ExprCall { | ||
func: Box::new(Expr::Name(ExprName { | ||
id: "dict.fromkeys".into(), | ||
ctx: ExprContext::Load, | ||
range: TextRange::default(), | ||
})), | ||
arguments: args, | ||
range: TextRange::default(), | ||
}) | ||
} |
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.
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.