[perflint] Fix false negative in PERF401 (#18866)

This commit is contained in:
Victor Hugo Gomes 2025-06-25 05:44:32 -03:00 committed by GitHub
parent d2684a00c6
commit 66dbea90f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 79 additions and 35 deletions

View file

@ -4,7 +4,7 @@ use ruff_python_ast::{
};
use ruff_python_semantic::{Binding, analyze::typing::is_dict};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
use crate::preview::is_fix_manual_dict_comprehension_enabled;
@ -139,28 +139,11 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtF
};
// If any references to a target variable are after the loop,
// then removing the loop would cause a NameError
let any_references_after_for_loop = |target: &Expr| {
let target_binding = checker
.semantic()
.bindings
.iter()
.find(|binding| target.range() == binding.range);
debug_assert!(
target_binding.is_some(),
"for-loop target binding must exist"
);
let Some(target_binding) = target_binding else {
// All uses of this function will early-return if this returns true, so this must early-return the rule
return true;
};
target_binding
.references()
.map(|reference| checker.semantic().reference(reference))
.any(|other_reference| other_reference.start() > for_stmt.end())
};
// then removing the loop would cause a NameError. Make sure none
// of the variables are used outside the for loop.
if has_post_loop_references(checker, target, for_stmt.end()) {
return;
}
match target {
Expr::Tuple(tuple) => {
@ -176,10 +159,6 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtF
{
return;
}
// Make sure none of the variables are used outside the for loop
if tuple.iter().any(any_references_after_for_loop) {
return;
}
}
Expr::Name(_) => {
if ComparableExpr::from(key) != ComparableExpr::from(target) {
@ -188,12 +167,6 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtF
if ComparableExpr::from(value) != ComparableExpr::from(target) {
return;
}
// We know that `target` contains an ExprName, but closures can't take `&impl Ranged`,
// so we pass `target` itself instead of the inner ExprName
if any_references_after_for_loop(target) {
return;
}
}
_ => return,
}
@ -380,9 +353,18 @@ fn convert_to_dict_comprehension(
} else {
"for"
};
// Handles the case where `key` has a trailing comma, e.g, `dict[x,] = y`
let key_range = if let Expr::Tuple(ast::ExprTuple { elts, .. }) = key {
let [expr] = elts.as_slice() else {
return None;
};
expr.range()
} else {
key.range()
};
let elt_str = format!(
"{}: {}",
locator.slice(key.range()),
locator.slice(key_range),
locator.slice(value.range())
);
@ -473,3 +455,25 @@ enum DictComprehensionType {
Update,
Comprehension,
}
fn has_post_loop_references(checker: &Checker, expr: &Expr, loop_end: TextSize) -> bool {
any_over_expr(expr, &|expr| match expr {
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
.iter()
.any(|expr| has_post_loop_references(checker, expr, loop_end)),
Expr::Name(name) => {
let target_binding = checker
.semantic()
.bindings
.iter()
.find(|binding| name.range() == binding.range)
.expect("for-loop target binding must exist");
target_binding
.references()
.map(|reference| checker.semantic().reference(reference))
.any(|other_reference| other_reference.start() > loop_end)
}
_ => false,
})
}

View file

@ -146,5 +146,16 @@ PERF403.py:166:13: PERF403 Use a dictionary comprehension instead of a for-loop
165 | if lambda: 0:
166 | dst[k] = v
| ^^^^^^^^^^ PERF403
167 |
168 | # https://github.com/astral-sh/ruff/issues/18859
|
= help: Replace for loop with dict comprehension
PERF403.py:172:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
170 | v = {}
171 | for o,(x,)in():
172 | v[x,]=o
| ^^^^^^^ PERF403
|
= help: Replace for loop with dict comprehension

View file

@ -340,6 +340,8 @@ PERF403.py:166:13: PERF403 [*] Use `dict.update` instead of a for-loop
165 | if lambda: 0:
166 | dst[k] = v
| ^^^^^^^^^^ PERF403
167 |
168 | # https://github.com/astral-sh/ruff/issues/18859
|
= help: Replace for loop with `dict.update`
@ -351,3 +353,24 @@ PERF403.py:166:13: PERF403 [*] Use `dict.update` instead of a for-loop
165 |- if lambda: 0:
166 |- dst[k] = v
164 |+ dst.update({k: v for k, v in src if (lambda: 0)})
167 165 |
168 166 | # https://github.com/astral-sh/ruff/issues/18859
169 167 | def foo():
PERF403.py:172:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
170 | v = {}
171 | for o,(x,)in():
172 | v[x,]=o
| ^^^^^^^ PERF403
|
= help: Replace for loop with dict comprehension
Unsafe fix
167 167 |
168 168 | # https://github.com/astral-sh/ruff/issues/18859
169 169 | def foo():
170 |- v = {}
171 |- for o,(x,)in():
172 |- v[x,]=o
170 |+ v = {x: o for o,(x,) in ()}