Skip to content

Commit 9558c11

Browse files
committed
Redirect RUF011 to B035
1 parent 4a3bb67 commit 9558c11

File tree

11 files changed

+118
-25
lines changed

11 files changed

+118
-25
lines changed

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1424,7 +1424,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
14241424
}
14251425
}
14261426
if checker.enabled(Rule::StaticKeyDictComprehension) {
1427-
ruff::rules::static_key_dict_comprehension(checker, dict_comp);
1427+
flake8_bugbear::rules::static_key_dict_comprehension(checker, dict_comp);
14281428
}
14291429
}
14301430
Expr::GeneratorExp(

crates/ruff_linter/src/codes.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
347347
(Flake8Bugbear, "032") => (RuleGroup::Stable, rules::flake8_bugbear::rules::UnintentionalTypeAnnotation),
348348
(Flake8Bugbear, "033") => (RuleGroup::Stable, rules::flake8_bugbear::rules::DuplicateValue),
349349
(Flake8Bugbear, "034") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ReSubPositionalArgs),
350+
(Flake8Bugbear, "035") => (RuleGroup::Stable, rules::flake8_bugbear::rules::StaticKeyDictComprehension),
350351
(Flake8Bugbear, "904") => (RuleGroup::Stable, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept),
351352
(Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict),
352353

@@ -910,7 +911,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
910911
(Ruff, "008") => (RuleGroup::Stable, rules::ruff::rules::MutableDataclassDefault),
911912
(Ruff, "009") => (RuleGroup::Stable, rules::ruff::rules::FunctionCallInDataclassDefaultArgument),
912913
(Ruff, "010") => (RuleGroup::Stable, rules::ruff::rules::ExplicitFStringTypeConversion),
913-
(Ruff, "011") => (RuleGroup::Stable, rules::ruff::rules::StaticKeyDictComprehension),
914914
(Ruff, "012") => (RuleGroup::Stable, rules::ruff::rules::MutableClassDefault),
915915
(Ruff, "013") => (RuleGroup::Stable, rules::ruff::rules::ImplicitOptional),
916916
#[cfg(feature = "unreachable-code")] // When removing this feature gate, also update rules_selector.rs

crates/ruff_linter/src/rule_redirects.rs

+1
Original file line numberDiff line numberDiff line change
@@ -98,5 +98,6 @@ static REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
9898
("T002", "FIX002"),
9999
("T003", "FIX003"),
100100
("T004", "FIX004"),
101+
("RUF011", "B035"),
101102
])
102103
});

crates/ruff_linter/src/rules/flake8_bugbear/mod.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ mod tests {
3333
#[test_case(Rule::GetAttrWithConstant, Path::new("B009_B010.py"))]
3434
#[test_case(Rule::JumpStatementInFinally, Path::new("B012.py"))]
3535
#[test_case(Rule::LoopVariableOverridesIterator, Path::new("B020.py"))]
36-
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))]
3736
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_1.py"))]
3837
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_2.py"))]
3938
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_3.py"))]
4039
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_4.py"))]
4140
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_5.py"))]
4241
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_6.py"))]
4342
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_7.py"))]
43+
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))]
4444
#[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))]
4545
#[test_case(Rule::RaiseLiteral, Path::new("B016.py"))]
4646
#[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"))]
@@ -49,16 +49,17 @@ mod tests {
4949
#[test_case(Rule::ReuseOfGroupbyGenerator, Path::new("B031.py"))]
5050
#[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"))]
5151
#[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"))]
52+
#[test_case(Rule::StaticKeyDictComprehension, Path::new("B035.py"))]
5253
#[test_case(Rule::StripWithMultiCharacters, Path::new("B005.py"))]
5354
#[test_case(Rule::UnaryPrefixIncrementDecrement, Path::new("B002.py"))]
5455
#[test_case(Rule::UnintentionalTypeAnnotation, Path::new("B032.py"))]
5556
#[test_case(Rule::UnreliableCallableCheck, Path::new("B004.py"))]
5657
#[test_case(Rule::UnusedLoopControlVariable, Path::new("B007.py"))]
57-
#[test_case(Rule::UselessComparison, Path::new("B015.py"))]
5858
#[test_case(Rule::UselessComparison, Path::new("B015.ipynb"))]
59+
#[test_case(Rule::UselessComparison, Path::new("B015.py"))]
5960
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))]
60-
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
6161
#[test_case(Rule::UselessExpression, Path::new("B018.ipynb"))]
62+
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
6263
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
6364
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
6465
let diagnostics = test_path(

crates/ruff_linter/src/rules/flake8_bugbear/rules/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub(crate) use redundant_tuple_in_exception_handler::*;
2222
pub(crate) use reuse_of_groupby_generator::*;
2323
pub(crate) use setattr_with_constant::*;
2424
pub(crate) use star_arg_unpacking_after_keyword_arg::*;
25+
pub(crate) use static_key_dict_comprehension::*;
2526
pub(crate) use strip_with_multi_characters::*;
2627
pub(crate) use unary_prefix_increment_decrement::*;
2728
pub(crate) use unintentional_type_annotation::*;
@@ -56,6 +57,7 @@ mod redundant_tuple_in_exception_handler;
5657
mod reuse_of_groupby_generator;
5758
mod setattr_with_constant;
5859
mod star_arg_unpacking_after_keyword_arg;
60+
mod static_key_dict_comprehension;
5961
mod strip_with_multi_characters;
6062
mod unary_prefix_increment_decrement;
6163
mod unintentional_type_annotation;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use rustc_hash::FxHashMap;
2+
3+
use ruff_diagnostics::{Diagnostic, Violation};
4+
use ruff_macros::{derive_message_formats, violation};
5+
use ruff_python_ast::helpers::NameFinder;
6+
use ruff_python_ast::visitor::Visitor;
7+
use ruff_python_ast::{self as ast, Expr};
8+
use ruff_text_size::Ranged;
9+
10+
use crate::checkers::ast::Checker;
11+
use crate::fix::snippet::SourceCodeSnippet;
12+
13+
/// ## What it does
14+
/// Checks for dictionary comprehensions that use a static key, like a string
15+
/// literal or a variable defined outside the comprehension.
16+
///
17+
/// ## Why is this bad?
18+
/// Using a static key (like a string literal) in a dictionary comprehension
19+
/// is usually a mistake, as it will result in a dictionary with only one key,
20+
/// despite the comprehension iterating over multiple values.
21+
///
22+
/// ## Example
23+
/// ```python
24+
/// data = ["some", "Data"]
25+
/// {"key": value.upper() for value in data}
26+
/// ```
27+
///
28+
/// Use instead:
29+
/// ```python
30+
/// data = ["some", "Data"]
31+
/// {value: value.upper() for value in data}
32+
/// ```
33+
#[violation]
34+
pub struct StaticKeyDictComprehension {
35+
key: SourceCodeSnippet,
36+
}
37+
38+
impl Violation for StaticKeyDictComprehension {
39+
#[derive_message_formats]
40+
fn message(&self) -> String {
41+
let StaticKeyDictComprehension { key } = self;
42+
if let Some(key) = key.full_display() {
43+
format!("Dictionary comprehension uses static key: `{key}`")
44+
} else {
45+
format!("Dictionary comprehension uses static key")
46+
}
47+
}
48+
}
49+
50+
/// RUF011
51+
pub(crate) fn static_key_dict_comprehension(checker: &mut Checker, dict_comp: &ast::ExprDictComp) {
52+
// Collect the bound names in the comprehension's generators.
53+
let names = {
54+
let mut visitor = NameFinder::default();
55+
for generator in &dict_comp.generators {
56+
visitor.visit_expr(&generator.target);
57+
}
58+
visitor.names
59+
};
60+
61+
if is_constant(&dict_comp.key, &names) {
62+
checker.diagnostics.push(Diagnostic::new(
63+
StaticKeyDictComprehension {
64+
key: SourceCodeSnippet::from_str(checker.locator().slice(dict_comp.key.as_ref())),
65+
},
66+
dict_comp.key.range(),
67+
));
68+
}
69+
}
70+
71+
/// Returns `true` if the given expression is a constant in the context of the dictionary
72+
/// comprehension.
73+
fn is_constant(key: &Expr, names: &FxHashMap<&str, &ast::ExprName>) -> bool {
74+
match key {
75+
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(|elt| is_constant(elt, names)),
76+
Expr::Name(ast::ExprName { id, .. }) => !names.contains_key(id.as_str()),
77+
Expr::Attribute(ast::ExprAttribute { value, .. }) => is_constant(value, names),
78+
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
79+
is_constant(value, names) && is_constant(slice, names)
80+
}
81+
Expr::BinOp(ast::ExprBinOp { left, right, .. }) => {
82+
is_constant(left, names) && is_constant(right, names)
83+
}
84+
Expr::BoolOp(ast::ExprBoolOp { values, .. }) => {
85+
values.iter().all(|value| is_constant(value, names))
86+
}
87+
Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => is_constant(operand, names),
88+
expr if expr.is_literal_expr() => true,
89+
_ => false,
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,80 @@
11
---
2-
source: crates/ruff_linter/src/rules/ruff/mod.rs
2+
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
33
---
4-
RUF011.py:15:2: RUF011 Dictionary comprehension uses static key: `"key"`
4+
B035.py:15:2: B035 Dictionary comprehension uses static key: `"key"`
55
|
66
14 | # Errors
77
15 | {"key": value.upper() for value in data}
8-
| ^^^^^ RUF011
8+
| ^^^^^ B035
99
16 | {True: value.upper() for value in data}
1010
17 | {0: value.upper() for value in data}
1111
|
1212

13-
RUF011.py:16:2: RUF011 Dictionary comprehension uses static key: `True`
13+
B035.py:16:2: B035 Dictionary comprehension uses static key: `True`
1414
|
1515
14 | # Errors
1616
15 | {"key": value.upper() for value in data}
1717
16 | {True: value.upper() for value in data}
18-
| ^^^^ RUF011
18+
| ^^^^ B035
1919
17 | {0: value.upper() for value in data}
2020
18 | {(1, "a"): value.upper() for value in data} # Constant tuple
2121
|
2222

23-
RUF011.py:17:2: RUF011 Dictionary comprehension uses static key: `0`
23+
B035.py:17:2: B035 Dictionary comprehension uses static key: `0`
2424
|
2525
15 | {"key": value.upper() for value in data}
2626
16 | {True: value.upper() for value in data}
2727
17 | {0: value.upper() for value in data}
28-
| ^ RUF011
28+
| ^ B035
2929
18 | {(1, "a"): value.upper() for value in data} # Constant tuple
3030
19 | {constant: value.upper() for value in data}
3131
|
3232

33-
RUF011.py:18:2: RUF011 Dictionary comprehension uses static key: `(1, "a")`
33+
B035.py:18:2: B035 Dictionary comprehension uses static key: `(1, "a")`
3434
|
3535
16 | {True: value.upper() for value in data}
3636
17 | {0: value.upper() for value in data}
3737
18 | {(1, "a"): value.upper() for value in data} # Constant tuple
38-
| ^^^^^^^^ RUF011
38+
| ^^^^^^^^ B035
3939
19 | {constant: value.upper() for value in data}
4040
20 | {constant + constant: value.upper() for value in data}
4141
|
4242

43-
RUF011.py:19:2: RUF011 Dictionary comprehension uses static key: `constant`
43+
B035.py:19:2: B035 Dictionary comprehension uses static key: `constant`
4444
|
4545
17 | {0: value.upper() for value in data}
4646
18 | {(1, "a"): value.upper() for value in data} # Constant tuple
4747
19 | {constant: value.upper() for value in data}
48-
| ^^^^^^^^ RUF011
48+
| ^^^^^^^^ B035
4949
20 | {constant + constant: value.upper() for value in data}
5050
21 | {constant.attribute: value.upper() for value in data}
5151
|
5252

53-
RUF011.py:20:2: RUF011 Dictionary comprehension uses static key: `constant + constant`
53+
B035.py:20:2: B035 Dictionary comprehension uses static key: `constant + constant`
5454
|
5555
18 | {(1, "a"): value.upper() for value in data} # Constant tuple
5656
19 | {constant: value.upper() for value in data}
5757
20 | {constant + constant: value.upper() for value in data}
58-
| ^^^^^^^^^^^^^^^^^^^ RUF011
58+
| ^^^^^^^^^^^^^^^^^^^ B035
5959
21 | {constant.attribute: value.upper() for value in data}
6060
22 | {constant[0]: value.upper() for value in data}
6161
|
6262

63-
RUF011.py:21:2: RUF011 Dictionary comprehension uses static key: `constant.attribute`
63+
B035.py:21:2: B035 Dictionary comprehension uses static key: `constant.attribute`
6464
|
6565
19 | {constant: value.upper() for value in data}
6666
20 | {constant + constant: value.upper() for value in data}
6767
21 | {constant.attribute: value.upper() for value in data}
68-
| ^^^^^^^^^^^^^^^^^^ RUF011
68+
| ^^^^^^^^^^^^^^^^^^ B035
6969
22 | {constant[0]: value.upper() for value in data}
7070
|
7171

72-
RUF011.py:22:2: RUF011 Dictionary comprehension uses static key: `constant[0]`
72+
B035.py:22:2: B035 Dictionary comprehension uses static key: `constant[0]`
7373
|
7474
20 | {constant + constant: value.upper() for value in data}
7575
21 | {constant.attribute: value.upper() for value in data}
7676
22 | {constant[0]: value.upper() for value in data}
77-
| ^^^^^^^^^^^ RUF011
77+
| ^^^^^^^^^^^ B035
7878
|
7979

8080

crates/ruff_linter/src/rules/ruff/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ mod tests {
3131
#[test_case(Rule::MutableClassDefault, Path::new("RUF012.py"))]
3232
#[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))]
3333
#[test_case(Rule::PairwiseOverZipped, Path::new("RUF007.py"))]
34-
#[test_case(Rule::StaticKeyDictComprehension, Path::new("RUF011.py"))]
3534
#[test_case(
3635
Rule::UnnecessaryIterableAllocationForFirstElement,
3736
Path::new("RUF015.py")

crates/ruff_linter/src/rules/ruff/rules/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ pub(crate) use never_union::*;
1313
pub(crate) use pairwise_over_zipped::*;
1414
pub(crate) use parenthesize_logical_operators::*;
1515
pub(crate) use quadratic_list_summation::*;
16-
pub(crate) use static_key_dict_comprehension::*;
1716
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
1817
pub(crate) use unnecessary_key_check::*;
1918
#[cfg(feature = "unreachable-code")]

ruff.schema.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)