-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## Summary <!-- What's the purpose of the change? What does it do, and why? --> This PR implements the [consider dict items](https://pylint.pycqa.org/en/latest/user_guide/messages/convention/consider-using-dict-items.html) rule from Pylint. Enabling this rule flags: ```python ORCHESTRA = { "violin": "strings", "oboe": "woodwind", "tuba": "brass", "gong": "percussion", } for instrument in ORCHESTRA: print(f"{instrument}: {ORCHESTRA[instrument]}") for instrument in ORCHESTRA.keys(): print(f"{instrument}: {ORCHESTRA[instrument]}") for instrument in (inline_dict := {"foo": "bar"}): print(f"{instrument}: {inline_dict[instrument]}") ``` For not using `items()` to extract the value out of the dict. We ignore the case of an assignment, as you can't modify the underlying representation with the value in the list of tuples returned. ## Test Plan <!-- How was it tested? --> `cargo test`. --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
- Loading branch information
1 parent
084e546
commit 5a5a588
Showing
10 changed files
with
316 additions
and
3 deletions.
There are no files selected for viewing
55 changes: 55 additions & 0 deletions
55
crates/ruff_linter/resources/test/fixtures/pylint/dict_index_missing_items.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
ORCHESTRA = { | ||
"violin": "strings", | ||
"oboe": "woodwind", | ||
"tuba": "brass", | ||
"gong": "percussion", | ||
} | ||
|
||
# Errors | ||
for instrument in ORCHESTRA: | ||
print(f"{instrument}: {ORCHESTRA[instrument]}") | ||
|
||
for instrument in ORCHESTRA: | ||
ORCHESTRA[instrument] | ||
|
||
for instrument in ORCHESTRA.keys(): | ||
print(f"{instrument}: {ORCHESTRA[instrument]}") | ||
|
||
for instrument in ORCHESTRA.keys(): | ||
ORCHESTRA[instrument] | ||
|
||
for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}): | ||
print(f"{instrument}: {temp_orchestra[instrument]}") | ||
|
||
for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}): | ||
temp_orchestra[instrument] | ||
|
||
# # OK | ||
for instrument, section in ORCHESTRA.items(): | ||
print(f"{instrument}: {section}") | ||
|
||
for instrument, section in ORCHESTRA.items(): | ||
section | ||
|
||
for instrument, section in ( | ||
temp_orchestra := {"violin": "strings", "oboe": "woodwind"} | ||
).items(): | ||
print(f"{instrument}: {section}") | ||
|
||
for instrument, section in ( | ||
temp_orchestra := {"violin": "strings", "oboe": "woodwind"} | ||
).items(): | ||
section | ||
|
||
for instrument in ORCHESTRA: | ||
ORCHESTRA[instrument] = 3 | ||
|
||
|
||
# Shouldn't trigger for non-dict types | ||
items = {1, 2, 3, 4} | ||
for i in items: | ||
items[i] | ||
|
||
items = [1, 2, 3, 4] | ||
for i in items: | ||
items[i] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
crates/ruff_linter/src/rules/pylint/rules/dict_index_missing_items.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
use ruff_diagnostics::{Diagnostic, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::comparable::ComparableExpr; | ||
use ruff_python_ast::{ | ||
self as ast, | ||
visitor::{self, Visitor}, | ||
Expr, ExprContext, | ||
}; | ||
use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType}; | ||
use ruff_python_semantic::analyze::typing::is_dict; | ||
use ruff_python_semantic::SemanticModel; | ||
use ruff_text_size::Ranged; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Checks for dictionary iterations that extract the dictionary value | ||
/// via explicit indexing, instead of using `.items()`. | ||
/// | ||
/// ## Why is this bad? | ||
/// Iterating over a dictionary with `.items()` is semantically clearer | ||
/// and more efficient than extracting the value with the key. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// ORCHESTRA = { | ||
/// "violin": "strings", | ||
/// "oboe": "woodwind", | ||
/// "tuba": "brass", | ||
/// "gong": "percussion", | ||
/// } | ||
/// | ||
/// for instrument in ORCHESTRA: | ||
/// print(f"{instrument}: {ORCHESTRA[instrument]}") | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// ```python | ||
/// ORCHESTRA = { | ||
/// "violin": "strings", | ||
/// "oboe": "woodwind", | ||
/// "tuba": "brass", | ||
/// "gong": "percussion", | ||
/// } | ||
/// | ||
/// for instrument, section in ORCHESTRA.items(): | ||
/// print(f"{instrument}: {section}") | ||
/// ``` | ||
|
||
#[violation] | ||
pub struct DictIndexMissingItems; | ||
|
||
impl Violation for DictIndexMissingItems { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
format!("Extracting value from dictionary without calling `.items()`") | ||
} | ||
} | ||
|
||
/// PLC0206 | ||
pub(crate) fn dict_index_missing_items(checker: &mut Checker, stmt_for: &ast::StmtFor) { | ||
let ast::StmtFor { | ||
target, iter, body, .. | ||
} = stmt_for; | ||
|
||
// Extract the name of the iteration object (e.g., `obj` in `for key in obj:`). | ||
let Some(dict_name) = extract_dict_name(iter) else { | ||
return; | ||
}; | ||
|
||
// Determine if the right-hand side is a dictionary literal (i.e. `for key in (dict := {"a": 1}):`). | ||
let is_dict_literal = matches!( | ||
ResolvedPythonType::from(&**iter), | ||
ResolvedPythonType::Atom(PythonType::Dict), | ||
); | ||
|
||
if !is_dict_literal && !is_inferred_dict(dict_name, checker.semantic()) { | ||
return; | ||
} | ||
|
||
let has_violation = { | ||
let mut visitor = SubscriptVisitor::new(target, dict_name); | ||
for stmt in body { | ||
visitor.visit_stmt(stmt); | ||
} | ||
visitor.has_violation | ||
}; | ||
|
||
if has_violation { | ||
let diagnostic = Diagnostic::new(DictIndexMissingItems, stmt_for.range()); | ||
checker.diagnostics.push(diagnostic); | ||
} | ||
} | ||
|
||
/// A visitor to detect subscript operations on a target dictionary. | ||
struct SubscriptVisitor<'a> { | ||
/// The target of the for loop (e.g., `key` in `for key in obj:`). | ||
target: &'a Expr, | ||
/// The name of the iterated object (e.g., `obj` in `for key in obj:`). | ||
dict_name: &'a ast::ExprName, | ||
/// Whether a violation has been detected. | ||
has_violation: bool, | ||
} | ||
|
||
impl<'a> SubscriptVisitor<'a> { | ||
fn new(target: &'a Expr, dict_name: &'a ast::ExprName) -> Self { | ||
Self { | ||
target, | ||
dict_name, | ||
has_violation: false, | ||
} | ||
} | ||
} | ||
|
||
impl<'a> Visitor<'a> for SubscriptVisitor<'a> { | ||
fn visit_expr(&mut self, expr: &'a Expr) { | ||
// Given `obj[key]`, `value` must be `obj` and `slice` must be `key`. | ||
if let Expr::Subscript(ast::ExprSubscript { | ||
value, | ||
slice, | ||
ctx: ExprContext::Load, | ||
.. | ||
}) = expr | ||
{ | ||
let Expr::Name(name) = value.as_ref() else { | ||
return; | ||
}; | ||
|
||
// Check that the sliced dictionary name is the same as the iterated object name. | ||
if name.id != self.dict_name.id { | ||
return; | ||
} | ||
|
||
// Check that the sliced value is the same as the target of the `for` loop. | ||
if ComparableExpr::from(slice) != ComparableExpr::from(self.target) { | ||
return; | ||
} | ||
|
||
self.has_violation = true; | ||
} else { | ||
visitor::walk_expr(self, expr); | ||
} | ||
} | ||
} | ||
|
||
/// Extracts the name of the dictionary from the expression. | ||
fn extract_dict_name(expr: &Expr) -> Option<&ast::ExprName> { | ||
// Ex) `for key in obj:` | ||
if let Some(name_expr) = expr.as_name_expr() { | ||
return Some(name_expr); | ||
} | ||
|
||
// Ex) `for key in obj.keys():` | ||
if let Expr::Call(ast::ExprCall { func, .. }) = expr { | ||
if let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() { | ||
if attr == "keys" { | ||
if let Expr::Name(var_name) = value.as_ref() { | ||
return Some(var_name); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Ex) `for key in (my_dict := {"foo": "bar"}):` | ||
if let Expr::Named(ast::ExprNamed { target, value, .. }) = expr { | ||
if let Expr::Dict(ast::ExprDict { .. }) = value.as_ref() { | ||
if let Expr::Name(var_name) = target.as_ref() { | ||
return Some(var_name); | ||
} | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
/// Returns `true` if the binding is a dictionary, inferred from the type. | ||
fn is_inferred_dict(name: &ast::ExprName, semantic: &SemanticModel) -> bool { | ||
semantic | ||
.only_binding(name) | ||
.map(|id| semantic.binding(id)) | ||
.is_some_and(|binding| is_dict(binding, semantic)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
...int/snapshots/ruff_linter__rules__pylint__tests__PLC0206_dict_index_missing_items.py.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/pylint/mod.rs | ||
--- | ||
dict_index_missing_items.py:9:1: PLC0206 Extracting value from dictionary without calling `.items()` | ||
| | ||
8 | # Errors | ||
9 | / for instrument in ORCHESTRA: | ||
10 | | print(f"{instrument}: {ORCHESTRA[instrument]}") | ||
| |___________________________________________________^ PLC0206 | ||
11 | | ||
12 | for instrument in ORCHESTRA: | ||
| | ||
|
||
dict_index_missing_items.py:12:1: PLC0206 Extracting value from dictionary without calling `.items()` | ||
| | ||
10 | print(f"{instrument}: {ORCHESTRA[instrument]}") | ||
11 | | ||
12 | / for instrument in ORCHESTRA: | ||
13 | | ORCHESTRA[instrument] | ||
| |_________________________^ PLC0206 | ||
14 | | ||
15 | for instrument in ORCHESTRA.keys(): | ||
| | ||
|
||
dict_index_missing_items.py:15:1: PLC0206 Extracting value from dictionary without calling `.items()` | ||
| | ||
13 | ORCHESTRA[instrument] | ||
14 | | ||
15 | / for instrument in ORCHESTRA.keys(): | ||
16 | | print(f"{instrument}: {ORCHESTRA[instrument]}") | ||
| |___________________________________________________^ PLC0206 | ||
17 | | ||
18 | for instrument in ORCHESTRA.keys(): | ||
| | ||
|
||
dict_index_missing_items.py:18:1: PLC0206 Extracting value from dictionary without calling `.items()` | ||
| | ||
16 | print(f"{instrument}: {ORCHESTRA[instrument]}") | ||
17 | | ||
18 | / for instrument in ORCHESTRA.keys(): | ||
19 | | ORCHESTRA[instrument] | ||
| |_________________________^ PLC0206 | ||
20 | | ||
21 | for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}): | ||
| | ||
|
||
dict_index_missing_items.py:21:1: PLC0206 Extracting value from dictionary without calling `.items()` | ||
| | ||
19 | ORCHESTRA[instrument] | ||
20 | | ||
21 | / for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}): | ||
22 | | print(f"{instrument}: {temp_orchestra[instrument]}") | ||
| |________________________________________________________^ PLC0206 | ||
23 | | ||
24 | for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}): | ||
| | ||
|
||
dict_index_missing_items.py:24:1: PLC0206 Extracting value from dictionary without calling `.items()` | ||
| | ||
22 | print(f"{instrument}: {temp_orchestra[instrument]}") | ||
23 | | ||
24 | / for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}): | ||
25 | | temp_orchestra[instrument] | ||
| |______________________________^ PLC0206 | ||
26 | | ||
27 | # # OK | ||
| |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.