mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-15 16:10:38 +00:00
Support variable keys in static dictionary key rule (#9411)
Closes https://github.com/astral-sh/ruff/issues/9410.
This commit is contained in:
parent
c2c9997682
commit
701697c37e
5 changed files with 114 additions and 39 deletions
|
@ -1,13 +1,17 @@
|
||||||
data = ["some", "Data"]
|
data = ["some", "Data"]
|
||||||
|
constant = 5
|
||||||
|
|
||||||
# Ok
|
# OK
|
||||||
{value: value.upper() for value in data}
|
{value: value.upper() for value in data}
|
||||||
{value.lower(): value.upper() for value in data}
|
{value.lower(): value.upper() for value in data}
|
||||||
{v: v*v for v in range(10)}
|
{v: v * v for v in range(10)}
|
||||||
{(0, "a", v): v*v for v in range(10)} # Tuple with variable
|
{(0, "a", v): v * v for v in range(10)} # Tuple with variable
|
||||||
|
{constant: value.upper() for value in data for constant in data}
|
||||||
|
|
||||||
# Errors
|
# Errors
|
||||||
{"key": value.upper() for value in data}
|
{"key": value.upper() for value in data}
|
||||||
{True: value.upper() for value in data}
|
{True: value.upper() for value in data}
|
||||||
{0: value.upper() for value in data}
|
{0: value.upper() for value in data}
|
||||||
{(1, "a"): value.upper() for value in data} # constant tuple
|
{(1, "a"): value.upper() for value in data} # Constant tuple
|
||||||
|
{constant: value.upper() for value in data}
|
||||||
|
{constant + constant: value.upper() for value in data}
|
||||||
|
|
|
@ -1396,12 +1396,14 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||||
refurb::rules::reimplemented_starmap(checker, &comp.into());
|
refurb::rules::reimplemented_starmap(checker, &comp.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::DictComp(ast::ExprDictComp {
|
Expr::DictComp(
|
||||||
|
dict_comp @ ast::ExprDictComp {
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
generators,
|
generators,
|
||||||
range: _,
|
range: _,
|
||||||
}) => {
|
},
|
||||||
|
) => {
|
||||||
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
|
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
|
||||||
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
|
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
|
||||||
}
|
}
|
||||||
|
@ -1422,7 +1424,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if checker.enabled(Rule::StaticKeyDictComprehension) {
|
if checker.enabled(Rule::StaticKeyDictComprehension) {
|
||||||
ruff::rules::static_key_dict_comprehension(checker, key);
|
ruff::rules::static_key_dict_comprehension(checker, dict_comp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::GeneratorExp(
|
Expr::GeneratorExp(
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
use ruff_python_ast::Expr;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::fix::snippet::SourceCodeSnippet;
|
|
||||||
use ruff_diagnostics::{Diagnostic, Violation};
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_ast::helpers::is_constant;
|
use ruff_python_ast::helpers::NameFinder;
|
||||||
|
use ruff_python_ast::visitor::Visitor;
|
||||||
|
use ruff_python_ast::{self as ast, Expr};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
|
use crate::fix::snippet::SourceCodeSnippet;
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for dictionary comprehensions that use a static key, like a string
|
/// Checks for dictionary comprehensions that use a static key, like a string
|
||||||
/// literal.
|
/// literal or a variable defined outside the comprehension.
|
||||||
///
|
///
|
||||||
/// ## Why is this bad?
|
/// ## Why is this bad?
|
||||||
/// Using a static key (like a string literal) in a dictionary comprehension
|
/// Using a static key (like a string literal) in a dictionary comprehension
|
||||||
|
@ -46,13 +48,40 @@ impl Violation for StaticKeyDictComprehension {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RUF011
|
/// RUF011
|
||||||
pub(crate) fn static_key_dict_comprehension(checker: &mut Checker, key: &Expr) {
|
pub(crate) fn static_key_dict_comprehension(checker: &mut Checker, dict_comp: &ast::ExprDictComp) {
|
||||||
if is_constant(key) {
|
// Collect the bound names in the comprehension's generators.
|
||||||
|
let names = {
|
||||||
|
let mut visitor = NameFinder::default();
|
||||||
|
for generator in &dict_comp.generators {
|
||||||
|
visitor.visit_expr(&generator.target);
|
||||||
|
}
|
||||||
|
visitor.names
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_constant(&dict_comp.key, &names) {
|
||||||
checker.diagnostics.push(Diagnostic::new(
|
checker.diagnostics.push(Diagnostic::new(
|
||||||
StaticKeyDictComprehension {
|
StaticKeyDictComprehension {
|
||||||
key: SourceCodeSnippet::from_str(checker.locator().slice(key)),
|
key: SourceCodeSnippet::from_str(checker.locator().slice(dict_comp.key.as_ref())),
|
||||||
},
|
},
|
||||||
key.range(),
|
dict_comp.key.range(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the given expression is a constant in the context of the dictionary
|
||||||
|
/// comprehension.
|
||||||
|
fn is_constant(key: &Expr, names: &FxHashMap<&str, &ast::ExprName>) -> bool {
|
||||||
|
match key {
|
||||||
|
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(|elt| is_constant(elt, names)),
|
||||||
|
Expr::Name(ast::ExprName { id, .. }) => !names.contains_key(id.as_str()),
|
||||||
|
Expr::BinOp(ast::ExprBinOp { left, right, .. }) => {
|
||||||
|
is_constant(left, names) && is_constant(right, names)
|
||||||
|
}
|
||||||
|
Expr::BoolOp(ast::ExprBoolOp { values, .. }) => {
|
||||||
|
values.iter().all(|value| is_constant(value, names))
|
||||||
|
}
|
||||||
|
Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => is_constant(operand, names),
|
||||||
|
expr if expr.is_literal_expr() => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,40 +1,60 @@
|
||||||
---
|
---
|
||||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||||
---
|
---
|
||||||
RUF011.py:10:2: RUF011 Dictionary comprehension uses static key: `"key"`
|
RUF011.py:12:2: RUF011 Dictionary comprehension uses static key: `"key"`
|
||||||
|
|
|
|
||||||
9 | # Errors
|
11 | # Errors
|
||||||
10 | {"key": value.upper() for value in data}
|
12 | {"key": value.upper() for value in data}
|
||||||
| ^^^^^ RUF011
|
| ^^^^^ RUF011
|
||||||
11 | {True: value.upper() for value in data}
|
13 | {True: value.upper() for value in data}
|
||||||
12 | {0: value.upper() for value in data}
|
14 | {0: value.upper() for value in data}
|
||||||
|
|
|
|
||||||
|
|
||||||
RUF011.py:11:2: RUF011 Dictionary comprehension uses static key: `True`
|
RUF011.py:13:2: RUF011 Dictionary comprehension uses static key: `True`
|
||||||
|
|
|
|
||||||
9 | # Errors
|
11 | # Errors
|
||||||
10 | {"key": value.upper() for value in data}
|
12 | {"key": value.upper() for value in data}
|
||||||
11 | {True: value.upper() for value in data}
|
13 | {True: value.upper() for value in data}
|
||||||
| ^^^^ RUF011
|
| ^^^^ RUF011
|
||||||
12 | {0: value.upper() for value in data}
|
14 | {0: value.upper() for value in data}
|
||||||
13 | {(1, "a"): value.upper() for value in data} # constant tuple
|
15 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
||||||
|
|
|
|
||||||
|
|
||||||
RUF011.py:12:2: RUF011 Dictionary comprehension uses static key: `0`
|
RUF011.py:14:2: RUF011 Dictionary comprehension uses static key: `0`
|
||||||
|
|
|
|
||||||
10 | {"key": value.upper() for value in data}
|
12 | {"key": value.upper() for value in data}
|
||||||
11 | {True: value.upper() for value in data}
|
13 | {True: value.upper() for value in data}
|
||||||
12 | {0: value.upper() for value in data}
|
14 | {0: value.upper() for value in data}
|
||||||
| ^ RUF011
|
| ^ RUF011
|
||||||
13 | {(1, "a"): value.upper() for value in data} # constant tuple
|
15 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
||||||
|
16 | {constant: value.upper() for value in data}
|
||||||
|
|
|
|
||||||
|
|
||||||
RUF011.py:13:2: RUF011 Dictionary comprehension uses static key: `(1, "a")`
|
RUF011.py:15:2: RUF011 Dictionary comprehension uses static key: `(1, "a")`
|
||||||
|
|
|
|
||||||
11 | {True: value.upper() for value in data}
|
13 | {True: value.upper() for value in data}
|
||||||
12 | {0: value.upper() for value in data}
|
14 | {0: value.upper() for value in data}
|
||||||
13 | {(1, "a"): value.upper() for value in data} # constant tuple
|
15 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
||||||
| ^^^^^^^^ RUF011
|
| ^^^^^^^^ RUF011
|
||||||
|
16 | {constant: value.upper() for value in data}
|
||||||
|
17 | {constant + constant: value.upper() for value in data}
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF011.py:16:2: RUF011 Dictionary comprehension uses static key: `constant`
|
||||||
|
|
|
||||||
|
14 | {0: value.upper() for value in data}
|
||||||
|
15 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
||||||
|
16 | {constant: value.upper() for value in data}
|
||||||
|
| ^^^^^^^^ RUF011
|
||||||
|
17 | {constant + constant: value.upper() for value in data}
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF011.py:17:2: RUF011 Dictionary comprehension uses static key: `constant + constant`
|
||||||
|
|
|
||||||
|
15 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
||||||
|
16 | {constant: value.upper() for value in data}
|
||||||
|
17 | {constant + constant: value.upper() for value in data}
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^ RUF011
|
||||||
|
|
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use ruff_python_trivia::CommentRanges;
|
use ruff_python_trivia::CommentRanges;
|
||||||
|
@ -891,6 +892,25 @@ pub fn resolve_imported_module_path<'a>(
|
||||||
Some(Cow::Owned(qualified_path))
|
Some(Cow::Owned(qualified_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A [`Visitor`] to collect all [`Expr::Name`] nodes in an AST.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct NameFinder<'a> {
|
||||||
|
/// A map from identifier to defining expression.
|
||||||
|
pub names: FxHashMap<&'a str, &'a ast::ExprName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> Visitor<'b> for NameFinder<'a>
|
||||||
|
where
|
||||||
|
'b: 'a,
|
||||||
|
{
|
||||||
|
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||||
|
if let Expr::Name(name) = expr {
|
||||||
|
self.names.insert(&name.id, name);
|
||||||
|
}
|
||||||
|
crate::visitor::walk_expr(self, expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A [`StatementVisitor`] that collects all `return` statements in a function or method.
|
/// A [`StatementVisitor`] that collects all `return` statements in a function or method.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ReturnStatementVisitor<'a> {
|
pub struct ReturnStatementVisitor<'a> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue