mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 19:31:31 +00:00
[perflint] Fix false negative in PERF401 (#18866)
This commit is contained in:
parent
d2684a00c6
commit
66dbea90f1
4 changed files with 79 additions and 35 deletions
|
|
@ -163,4 +163,10 @@ def foo():
|
||||||
|
|
||||||
for k, v in src:
|
for k, v in src:
|
||||||
if lambda: 0:
|
if lambda: 0:
|
||||||
dst[k] = v
|
dst[k] = v
|
||||||
|
|
||||||
|
# https://github.com/astral-sh/ruff/issues/18859
|
||||||
|
def foo():
|
||||||
|
v = {}
|
||||||
|
for o,(x,)in():
|
||||||
|
v[x,]=o
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use ruff_python_ast::{
|
||||||
};
|
};
|
||||||
use ruff_python_semantic::{Binding, analyze::typing::is_dict};
|
use ruff_python_semantic::{Binding, analyze::typing::is_dict};
|
||||||
use ruff_source_file::LineRanges;
|
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::checkers::ast::Checker;
|
||||||
use crate::preview::is_fix_manual_dict_comprehension_enabled;
|
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,
|
// If any references to a target variable are after the loop,
|
||||||
// then removing the loop would cause a NameError
|
// then removing the loop would cause a NameError. Make sure none
|
||||||
let any_references_after_for_loop = |target: &Expr| {
|
// of the variables are used outside the for loop.
|
||||||
let target_binding = checker
|
if has_post_loop_references(checker, target, for_stmt.end()) {
|
||||||
.semantic()
|
return;
|
||||||
.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())
|
|
||||||
};
|
|
||||||
|
|
||||||
match target {
|
match target {
|
||||||
Expr::Tuple(tuple) => {
|
Expr::Tuple(tuple) => {
|
||||||
|
|
@ -176,10 +159,6 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtF
|
||||||
{
|
{
|
||||||
return;
|
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(_) => {
|
Expr::Name(_) => {
|
||||||
if ComparableExpr::from(key) != ComparableExpr::from(target) {
|
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) {
|
if ComparableExpr::from(value) != ComparableExpr::from(target) {
|
||||||
return;
|
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,
|
_ => return,
|
||||||
}
|
}
|
||||||
|
|
@ -380,9 +353,18 @@ fn convert_to_dict_comprehension(
|
||||||
} else {
|
} else {
|
||||||
"for"
|
"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!(
|
let elt_str = format!(
|
||||||
"{}: {}",
|
"{}: {}",
|
||||||
locator.slice(key.range()),
|
locator.slice(key_range),
|
||||||
locator.slice(value.range())
|
locator.slice(value.range())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -473,3 +455,25 @@ enum DictComprehensionType {
|
||||||
Update,
|
Update,
|
||||||
Comprehension,
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,5 +146,16 @@ PERF403.py:166:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
165 | if lambda: 0:
|
165 | if lambda: 0:
|
||||||
166 | dst[k] = v
|
166 | dst[k] = v
|
||||||
| ^^^^^^^^^^ PERF403
|
| ^^^^^^^^^^ 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
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,8 @@ PERF403.py:166:13: PERF403 [*] Use `dict.update` instead of a for-loop
|
||||||
165 | if lambda: 0:
|
165 | if lambda: 0:
|
||||||
166 | dst[k] = v
|
166 | dst[k] = v
|
||||||
| ^^^^^^^^^^ PERF403
|
| ^^^^^^^^^^ PERF403
|
||||||
|
167 |
|
||||||
|
168 | # https://github.com/astral-sh/ruff/issues/18859
|
||||||
|
|
|
|
||||||
= help: Replace for loop with `dict.update`
|
= 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:
|
165 |- if lambda: 0:
|
||||||
166 |- dst[k] = v
|
166 |- dst[k] = v
|
||||||
164 |+ dst.update({k: v for k, v in src if (lambda: 0)})
|
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 ()}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue