mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 22:31:23 +00:00
[perflint
] Implement fix for manual-dict-comprehension
(PERF403
) (#16719)
## Summary This change adds an auto-fix for manual dict comprehensions. It also copies many of the improvements from #13919 (and associated PRs fixing issues with it), and moves some of the utility functions from `manual_list_comprehension.rs` into a separate `helpers.rs` to be used in both. ## Test Plan I added a preview test case to showcase the new fix and added a test case in `PERF403.py` to make sure lines with semicolons function. I didn't yet make similar tests to the ones I added earlier to `PERF401.py`, but the logic is the same, so it might be good to add those to make sure they work.
This commit is contained in:
parent
5fec1039ed
commit
08221454f6
10 changed files with 908 additions and 123 deletions
|
@ -18,7 +18,9 @@ def foo():
|
||||||
result = {}
|
result = {}
|
||||||
for idx, name in enumerate(fruit):
|
for idx, name in enumerate(fruit):
|
||||||
if idx % 2:
|
if idx % 2:
|
||||||
result[idx] = name # Ok (false negative: edge case where `else` is same as `if`)
|
result[idx] = (
|
||||||
|
name # Ok (false negative: edge case where `else` is same as `if`)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
result[idx] = name
|
result[idx] = name
|
||||||
|
|
||||||
|
@ -85,7 +87,67 @@ def foo():
|
||||||
|
|
||||||
def foo():
|
def foo():
|
||||||
from builtins import dict as SneakyDict
|
from builtins import dict as SneakyDict
|
||||||
|
|
||||||
fruit = ["apple", "pear", "orange"]
|
fruit = ["apple", "pear", "orange"]
|
||||||
result = SneakyDict()
|
result = SneakyDict()
|
||||||
for idx, name in enumerate(fruit):
|
for idx, name in enumerate(fruit):
|
||||||
result[name] = idx # PERF403
|
result[name] = idx # PERF403
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
fruit = ["apple", "pear", "orange"]
|
||||||
|
result: dict[str, int] = {
|
||||||
|
# comment 1
|
||||||
|
}
|
||||||
|
for idx, name in enumerate(
|
||||||
|
fruit # comment 2
|
||||||
|
):
|
||||||
|
# comment 3
|
||||||
|
result[
|
||||||
|
name # comment 4
|
||||||
|
] = idx # PERF403
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
fruit = ["apple", "pear", "orange"]
|
||||||
|
a = 1; result = {}; b = 2
|
||||||
|
for idx, name in enumerate(fruit):
|
||||||
|
result[name] = idx # PERF403
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
fruit = ["apple", "pear", "orange"]
|
||||||
|
result = {"kiwi": 3}
|
||||||
|
for idx, name in enumerate(fruit):
|
||||||
|
result[name] = idx # PERF403
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
fruit = ["apple", "pear", "orange"]
|
||||||
|
(_, result) = (None, {"kiwi": 3})
|
||||||
|
for idx, name in enumerate(fruit):
|
||||||
|
result[name] = idx # PERF403
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
fruit = ["apple", "pear", "orange"]
|
||||||
|
result = {}
|
||||||
|
print(len(result))
|
||||||
|
for idx, name in enumerate(fruit):
|
||||||
|
result[name] = idx # PERF403
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
fruit = ["apple", "pear", "orange"]
|
||||||
|
result = {}
|
||||||
|
for idx, name in enumerate(fruit):
|
||||||
|
if last_idx := idx % 3:
|
||||||
|
result[name] = idx # PERF403
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
fruit = ["apple", "pear", "orange"]
|
||||||
|
indices = [0, 1, 2]
|
||||||
|
result = {}
|
||||||
|
for idx, name in indices, fruit:
|
||||||
|
result[name] = idx # PERF403
|
||||||
|
|
|
@ -35,6 +35,9 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::DictIndexMissingItems) {
|
if checker.enabled(Rule::DictIndexMissingItems) {
|
||||||
pylint::rules::dict_index_missing_items(checker, stmt_for);
|
pylint::rules::dict_index_missing_items(checker, stmt_for);
|
||||||
}
|
}
|
||||||
|
if checker.enabled(Rule::ManualDictComprehension) {
|
||||||
|
perflint::rules::manual_dict_comprehension(checker, stmt_for);
|
||||||
|
}
|
||||||
if checker.enabled(Rule::ManualListComprehension) {
|
if checker.enabled(Rule::ManualListComprehension) {
|
||||||
perflint::rules::manual_list_comprehension(checker, stmt_for);
|
perflint::rules::manual_list_comprehension(checker, stmt_for);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1319,6 +1319,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||||
Rule::UnnecessaryEnumerate,
|
Rule::UnnecessaryEnumerate,
|
||||||
Rule::UnusedLoopControlVariable,
|
Rule::UnusedLoopControlVariable,
|
||||||
Rule::YieldInForLoop,
|
Rule::YieldInForLoop,
|
||||||
|
Rule::ManualDictComprehension,
|
||||||
Rule::ManualListComprehension,
|
Rule::ManualListComprehension,
|
||||||
]) {
|
]) {
|
||||||
checker.analyze.for_loops.push(checker.semantic.snapshot());
|
checker.analyze.for_loops.push(checker.semantic.snapshot());
|
||||||
|
@ -1347,9 +1348,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::ManualListCopy) {
|
if checker.enabled(Rule::ManualListCopy) {
|
||||||
perflint::rules::manual_list_copy(checker, for_stmt);
|
perflint::rules::manual_list_copy(checker, for_stmt);
|
||||||
}
|
}
|
||||||
if checker.enabled(Rule::ManualDictComprehension) {
|
|
||||||
perflint::rules::manual_dict_comprehension(checker, target, body);
|
|
||||||
}
|
|
||||||
if checker.enabled(Rule::ModifiedIteratingSet) {
|
if checker.enabled(Rule::ModifiedIteratingSet) {
|
||||||
pylint::rules::modified_iterating_set(checker, for_stmt);
|
pylint::rules::modified_iterating_set(checker, for_stmt);
|
||||||
}
|
}
|
||||||
|
|
98
crates/ruff_linter/src/rules/perflint/helpers.rs
Normal file
98
crates/ruff_linter/src/rules/perflint/helpers.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use ruff_python_trivia::{
|
||||||
|
BackwardsTokenizer, PythonWhitespace, SimpleToken, SimpleTokenKind, SimpleTokenizer,
|
||||||
|
};
|
||||||
|
use ruff_source_file::LineRanges;
|
||||||
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
|
pub(super) fn comment_strings_in_range<'a>(
|
||||||
|
checker: &'a Checker,
|
||||||
|
range: TextRange,
|
||||||
|
ranges_to_ignore: &[TextRange],
|
||||||
|
) -> Vec<&'a str> {
|
||||||
|
checker
|
||||||
|
.comment_ranges()
|
||||||
|
.comments_in_range(range)
|
||||||
|
.iter()
|
||||||
|
// Ignore comments inside of the append or iterator, since these are preserved
|
||||||
|
.filter(|comment| {
|
||||||
|
!ranges_to_ignore
|
||||||
|
.iter()
|
||||||
|
.any(|to_ignore| to_ignore.contains_range(**comment))
|
||||||
|
})
|
||||||
|
.map(|range| checker.locator().slice(range).trim_whitespace_start())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn semicolon_before_and_after(
|
||||||
|
checker: &Checker,
|
||||||
|
statement: TextRange,
|
||||||
|
) -> (Option<SimpleToken>, Option<SimpleToken>) {
|
||||||
|
// determine whether there's a semicolon either before or after the binding statement.
|
||||||
|
// Since it's a binding statement, we can just check whether there's a semicolon immediately
|
||||||
|
// after the whitespace in front of or behind it
|
||||||
|
let mut after_tokenizer =
|
||||||
|
SimpleTokenizer::starts_at(statement.end(), checker.locator().contents()).skip_trivia();
|
||||||
|
|
||||||
|
let after_semicolon = if after_tokenizer
|
||||||
|
.next()
|
||||||
|
.is_some_and(|token| token.kind() == SimpleTokenKind::Semi)
|
||||||
|
{
|
||||||
|
after_tokenizer.next()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let semicolon_before = BackwardsTokenizer::up_to(
|
||||||
|
statement.start(),
|
||||||
|
checker.locator().contents(),
|
||||||
|
checker.comment_ranges(),
|
||||||
|
)
|
||||||
|
.skip_trivia()
|
||||||
|
.next()
|
||||||
|
.filter(|token| token.kind() == SimpleTokenKind::Semi);
|
||||||
|
|
||||||
|
(semicolon_before, after_semicolon)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the range necessary to delete a statement (including any semicolons around it).
|
||||||
|
/// Returns the range and whether there were multiple statements on the line
|
||||||
|
pub(super) fn statement_deletion_range(
|
||||||
|
checker: &Checker,
|
||||||
|
statement_range: TextRange,
|
||||||
|
) -> (TextRange, bool) {
|
||||||
|
let locator = checker.locator();
|
||||||
|
// If the binding has multiple statements on its line, the fix would be substantially more complicated
|
||||||
|
let (semicolon_before, after_semicolon) = semicolon_before_and_after(checker, statement_range);
|
||||||
|
|
||||||
|
// If there are multiple binding statements in one line, we don't want to accidentally delete them
|
||||||
|
// Instead, we just delete the binding statement and leave any comments where they are
|
||||||
|
|
||||||
|
match (semicolon_before, after_semicolon) {
|
||||||
|
// ```python
|
||||||
|
// a = []
|
||||||
|
// ```
|
||||||
|
(None, None) => (locator.full_lines_range(statement_range), false),
|
||||||
|
|
||||||
|
// ```python
|
||||||
|
// a = 1; b = []
|
||||||
|
// ^^^^^^^^
|
||||||
|
// a = 1; b = []; c = 3
|
||||||
|
// ^^^^^^^^
|
||||||
|
// ```
|
||||||
|
(Some(semicolon_before), Some(_) | None) => (
|
||||||
|
TextRange::new(semicolon_before.start(), statement_range.end()),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
|
||||||
|
// ```python
|
||||||
|
// a = []; b = 3
|
||||||
|
// ^^^^^^^
|
||||||
|
// ```
|
||||||
|
(None, Some(after_semicolon)) => (
|
||||||
|
TextRange::new(statement_range.start(), after_semicolon.start()),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
//! Rules from [perflint](https://pypi.org/project/perflint/).
|
//! Rules from [perflint](https://pypi.org/project/perflint/).
|
||||||
|
mod helpers;
|
||||||
pub(crate) mod rules;
|
pub(crate) mod rules;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -31,7 +31,8 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove this test case when the fix for `perf401` is stabilized
|
// TODO: remove this test case when the fixes for `perf401` and `perf403` are stabilized
|
||||||
|
#[test_case(Rule::ManualDictComprehension, Path::new("PERF403.py"))]
|
||||||
#[test_case(Rule::ManualListComprehension, Path::new("PERF401.py"))]
|
#[test_case(Rule::ManualListComprehension, Path::new("PERF401.py"))]
|
||||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
let snapshot = format!(
|
let snapshot = format!(
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
use ruff_diagnostics::{Diagnostic, Violation};
|
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||||
use ruff_python_ast::comparable::ComparableExpr;
|
use ruff_python_ast::{
|
||||||
use ruff_python_ast::helpers::any_over_expr;
|
self as ast, comparable::ComparableExpr, helpers::any_over_expr, Expr, Stmt,
|
||||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
};
|
||||||
use ruff_python_semantic::analyze::typing::is_dict;
|
use ruff_python_semantic::{analyze::typing::is_dict, Binding};
|
||||||
|
use ruff_source_file::LineRanges;
|
||||||
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
|
use crate::rules::perflint::helpers::{comment_strings_in_range, statement_deletion_range};
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for `for` loops that can be replaced by a dictionary comprehension.
|
/// Checks for `for` loops that can be replaced by a dictionary comprehension.
|
||||||
|
@ -42,17 +45,45 @@ use crate::checkers::ast::Checker;
|
||||||
/// result.update({x: y for x, y in pairs if y % 2})
|
/// result.update({x: y for x, y in pairs if y % 2})
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(ViolationMetadata)]
|
#[derive(ViolationMetadata)]
|
||||||
pub(crate) struct ManualDictComprehension;
|
pub(crate) struct ManualDictComprehension {
|
||||||
|
fix_type: DictComprehensionType,
|
||||||
|
is_async: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl Violation for ManualDictComprehension {
|
impl Violation for ManualDictComprehension {
|
||||||
|
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||||
|
|
||||||
#[derive_message_formats]
|
#[derive_message_formats]
|
||||||
fn message(&self) -> String {
|
fn message(&self) -> String {
|
||||||
"Use a dictionary comprehension instead of a for-loop".to_string()
|
let modifier = if self.is_async { "an async" } else { "a" };
|
||||||
|
|
||||||
|
match self.fix_type {
|
||||||
|
DictComprehensionType::Comprehension => {
|
||||||
|
format!("Use a dictionary comprehension instead of {modifier} for-loop")
|
||||||
|
}
|
||||||
|
DictComprehensionType::Update => {
|
||||||
|
format!("Use `dict.update` instead of {modifier} for-loop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn fix_title(&self) -> Option<String> {
|
||||||
|
let modifier = if self.is_async { "async " } else { "" };
|
||||||
|
match self.fix_type {
|
||||||
|
DictComprehensionType::Comprehension => Some(format!(
|
||||||
|
"Replace {modifier}for loop with dict comprehension"
|
||||||
|
)),
|
||||||
|
DictComprehensionType::Update => {
|
||||||
|
Some(format!("Replace {modifier}for loop with `dict.update`"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PERF403
|
/// PERF403
|
||||||
pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body: &[Stmt]) {
|
pub(crate) fn manual_dict_comprehension(checker: &Checker, for_stmt: &ast::StmtFor) {
|
||||||
|
let ast::StmtFor { body, target, .. } = for_stmt;
|
||||||
|
let body = body.as_slice();
|
||||||
|
let target = target.as_ref();
|
||||||
let (stmt, if_test) = match body {
|
let (stmt, if_test) = match body {
|
||||||
// ```python
|
// ```python
|
||||||
// for idx, name in enumerate(names):
|
// for idx, name in enumerate(names):
|
||||||
|
@ -94,18 +125,42 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body:
|
||||||
|
|
||||||
let [Expr::Subscript(ast::ExprSubscript {
|
let [Expr::Subscript(ast::ExprSubscript {
|
||||||
value: subscript_value,
|
value: subscript_value,
|
||||||
slice,
|
slice: key,
|
||||||
..
|
..
|
||||||
})] = targets.as_slice()
|
})] = targets.as_slice()
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
};
|
||||||
|
|
||||||
match target {
|
match target {
|
||||||
Expr::Tuple(tuple) => {
|
Expr::Tuple(tuple) => {
|
||||||
if !tuple
|
if !tuple
|
||||||
.iter()
|
.iter()
|
||||||
.any(|element| ComparableExpr::from(slice) == ComparableExpr::from(element))
|
.any(|element| ComparableExpr::from(key) == ComparableExpr::from(element))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -115,14 +170,24 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body:
|
||||||
{
|
{
|
||||||
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(slice) != ComparableExpr::from(target) {
|
if ComparableExpr::from(key) != ComparableExpr::from(target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
@ -164,5 +229,242 @@ pub(crate) fn manual_dict_comprehension(checker: &Checker, target: &Expr, body:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
checker.report_diagnostic(Diagnostic::new(ManualDictComprehension, *range));
|
if checker.settings.preview.is_enabled() {
|
||||||
|
let binding_stmt = binding.statement(checker.semantic());
|
||||||
|
let binding_value = binding_stmt.and_then(|binding_stmt| match binding_stmt {
|
||||||
|
ast::Stmt::AnnAssign(assign) => assign.value.as_deref(),
|
||||||
|
ast::Stmt::Assign(assign) => Some(&assign.value),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the variable is an empty dict literal, then we might be able to replace it with a full dict comprehension.
|
||||||
|
// otherwise, it has to be replaced with a `dict.update`
|
||||||
|
let binding_is_empty_dict =
|
||||||
|
binding_value.is_some_and(|binding_value| match binding_value {
|
||||||
|
// value = {}
|
||||||
|
Expr::Dict(dict_expr) => dict_expr.is_empty(),
|
||||||
|
// value = dict()
|
||||||
|
Expr::Call(call) => {
|
||||||
|
checker
|
||||||
|
.semantic()
|
||||||
|
.resolve_builtin_symbol(&call.func)
|
||||||
|
.is_some_and(|name| name == "dict")
|
||||||
|
&& call.arguments.is_empty()
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let assignment_in_same_statement = binding.source.is_some_and(|binding_source| {
|
||||||
|
let for_loop_parent = checker.semantic().current_statement_parent_id();
|
||||||
|
let binding_parent = checker.semantic().parent_statement_id(binding_source);
|
||||||
|
for_loop_parent == binding_parent
|
||||||
|
});
|
||||||
|
// If the binding is not a single name expression, it could be replaced with a dict comprehension,
|
||||||
|
// but not necessarily, so this needs to be manually fixed. This does not apply when using an update.
|
||||||
|
let binding_has_one_target = binding_stmt.is_some_and(|binding_stmt| match binding_stmt {
|
||||||
|
ast::Stmt::AnnAssign(_) => true,
|
||||||
|
ast::Stmt::Assign(assign) => assign.targets.len() == 1,
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
// If the binding gets used in between the assignment and the for loop, a comprehension is no longer safe
|
||||||
|
|
||||||
|
// If the binding is after the for loop, then it can't be fixed, and this check would panic,
|
||||||
|
// so we check that they are in the same statement first
|
||||||
|
let binding_unused_between = assignment_in_same_statement
|
||||||
|
&& binding_stmt.is_some_and(|binding_stmt| {
|
||||||
|
let from_assign_to_loop = TextRange::new(binding_stmt.end(), for_stmt.start());
|
||||||
|
// Test if there's any reference to the result dictionary between its definition and the for loop.
|
||||||
|
// If there's at least one, then it's been accessed in the middle somewhere, so it's not safe to change into a comprehension
|
||||||
|
!binding
|
||||||
|
.references()
|
||||||
|
.map(|ref_id| checker.semantic().reference(ref_id).range())
|
||||||
|
.any(|text_range| from_assign_to_loop.contains_range(text_range))
|
||||||
|
});
|
||||||
|
// A dict update works in every context, while a dict comprehension only works when all the criteria are true
|
||||||
|
let fix_type = if binding_is_empty_dict
|
||||||
|
&& assignment_in_same_statement
|
||||||
|
&& binding_has_one_target
|
||||||
|
&& binding_unused_between
|
||||||
|
{
|
||||||
|
DictComprehensionType::Comprehension
|
||||||
|
} else {
|
||||||
|
DictComprehensionType::Update
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
ManualDictComprehension {
|
||||||
|
fix_type,
|
||||||
|
is_async: for_stmt.is_async,
|
||||||
|
},
|
||||||
|
*range,
|
||||||
|
);
|
||||||
|
diagnostic.try_set_optional_fix(|| {
|
||||||
|
Ok(convert_to_dict_comprehension(
|
||||||
|
fix_type,
|
||||||
|
binding,
|
||||||
|
for_stmt,
|
||||||
|
if_test.map(std::convert::AsRef::as_ref),
|
||||||
|
key.as_ref(),
|
||||||
|
value.as_ref(),
|
||||||
|
checker,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
|
checker.report_diagnostic(diagnostic);
|
||||||
|
} else {
|
||||||
|
checker.report_diagnostic(Diagnostic::new(
|
||||||
|
ManualDictComprehension {
|
||||||
|
fix_type: DictComprehensionType::Comprehension,
|
||||||
|
is_async: for_stmt.is_async,
|
||||||
|
},
|
||||||
|
*range,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_to_dict_comprehension(
|
||||||
|
fix_type: DictComprehensionType,
|
||||||
|
binding: &Binding,
|
||||||
|
for_stmt: &ast::StmtFor,
|
||||||
|
if_test: Option<&ast::Expr>,
|
||||||
|
key: &Expr,
|
||||||
|
value: &Expr,
|
||||||
|
checker: &Checker,
|
||||||
|
) -> Option<Fix> {
|
||||||
|
let locator = checker.locator();
|
||||||
|
|
||||||
|
let if_str = match if_test {
|
||||||
|
Some(test) => {
|
||||||
|
// If the test is an assignment expression,
|
||||||
|
// we must parenthesize it when it appears
|
||||||
|
// inside the comprehension to avoid a syntax error.
|
||||||
|
//
|
||||||
|
// Notice that we do not need `any_over_expr` here,
|
||||||
|
// since if the assignment expression appears
|
||||||
|
// internally (e.g. as an operand in a boolean
|
||||||
|
// operation) then it will already be parenthesized.
|
||||||
|
if test.is_named_expr() {
|
||||||
|
format!(" if ({})", locator.slice(test.range()))
|
||||||
|
} else {
|
||||||
|
format!(" if {}", locator.slice(test.range()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// if the loop target was an implicit tuple, add parentheses around it
|
||||||
|
// ```python
|
||||||
|
// for i in a, b:
|
||||||
|
// ...
|
||||||
|
// ```
|
||||||
|
// becomes
|
||||||
|
// {... for i in (a, b)}
|
||||||
|
let iter_str = if let Expr::Tuple(ast::ExprTuple {
|
||||||
|
parenthesized: false,
|
||||||
|
..
|
||||||
|
}) = &*for_stmt.iter
|
||||||
|
{
|
||||||
|
format!("({})", locator.slice(for_stmt.iter.range()))
|
||||||
|
} else {
|
||||||
|
locator.slice(for_stmt.iter.range()).to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_str = locator.slice(for_stmt.target.range());
|
||||||
|
let for_type = if for_stmt.is_async {
|
||||||
|
"async for"
|
||||||
|
} else {
|
||||||
|
"for"
|
||||||
|
};
|
||||||
|
let elt_str = format!(
|
||||||
|
"{}: {}",
|
||||||
|
locator.slice(key.range()),
|
||||||
|
locator.slice(value.range())
|
||||||
|
);
|
||||||
|
|
||||||
|
let comprehension_str = format!("{{{elt_str} {for_type} {target_str} in {iter_str}{if_str}}}");
|
||||||
|
|
||||||
|
let for_loop_inline_comments = comment_strings_in_range(
|
||||||
|
checker,
|
||||||
|
for_stmt.range,
|
||||||
|
&[key.range(), value.range(), for_stmt.iter.range()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let newline = checker.stylist().line_ending().as_str();
|
||||||
|
|
||||||
|
let indent = locator.slice(TextRange::new(
|
||||||
|
locator.line_start(for_stmt.range.start()),
|
||||||
|
for_stmt.range.start(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let variable_name = locator.slice(binding);
|
||||||
|
match fix_type {
|
||||||
|
DictComprehensionType::Update => {
|
||||||
|
let indentation = if for_loop_inline_comments.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("{newline}{indent}")
|
||||||
|
};
|
||||||
|
|
||||||
|
let comprehension_body = format!("{variable_name}.update({comprehension_str})");
|
||||||
|
|
||||||
|
let text_to_replace = format!(
|
||||||
|
"{}{indentation}{comprehension_body}",
|
||||||
|
for_loop_inline_comments.join(&indentation)
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(Fix::unsafe_edit(Edit::range_replacement(
|
||||||
|
text_to_replace,
|
||||||
|
for_stmt.range,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
DictComprehensionType::Comprehension => {
|
||||||
|
let binding_stmt = binding.statement(checker.semantic());
|
||||||
|
debug_assert!(
|
||||||
|
binding_stmt.is_some(),
|
||||||
|
"must be passed a binding with a statement"
|
||||||
|
);
|
||||||
|
let binding_stmt = binding_stmt?;
|
||||||
|
|
||||||
|
let binding_stmt_range = binding_stmt.range();
|
||||||
|
|
||||||
|
let annotations = match binding_stmt.as_ann_assign_stmt() {
|
||||||
|
Some(assign) => format!(": {}", locator.slice(assign.annotation.range())),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If there are multiple binding statements in one line, we don't want to accidentally delete them
|
||||||
|
// Instead, we just delete the binding statement and leave any comments where they are
|
||||||
|
let (binding_stmt_deletion_range, binding_is_multiple_stmts) =
|
||||||
|
statement_deletion_range(checker, binding_stmt_range);
|
||||||
|
|
||||||
|
let comments_to_move = if binding_is_multiple_stmts {
|
||||||
|
for_loop_inline_comments
|
||||||
|
} else {
|
||||||
|
let mut new_comments =
|
||||||
|
comment_strings_in_range(checker, binding_stmt_deletion_range, &[]);
|
||||||
|
new_comments.extend(for_loop_inline_comments);
|
||||||
|
new_comments
|
||||||
|
};
|
||||||
|
|
||||||
|
let indentation = if comments_to_move.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("{newline}{indent}")
|
||||||
|
};
|
||||||
|
let leading_comments = format!("{}{indentation}", comments_to_move.join(&indentation));
|
||||||
|
|
||||||
|
let comprehension_body =
|
||||||
|
format!("{leading_comments}{variable_name}{annotations} = {comprehension_str}");
|
||||||
|
Some(Fix::unsafe_edits(
|
||||||
|
Edit::range_deletion(binding_stmt_deletion_range),
|
||||||
|
[Edit::range_replacement(comprehension_body, for_stmt.range)],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum DictComprehensionType {
|
||||||
|
Update,
|
||||||
|
Comprehension,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
use ruff_python_ast::{self as ast, Arguments, Expr};
|
use ruff_python_ast::{self as ast, Arguments, Expr};
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::{checkers::ast::Checker, rules::perflint::helpers::statement_deletion_range};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||||
|
|
||||||
|
use crate::rules::perflint::helpers::comment_strings_in_range;
|
||||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||||
use ruff_python_ast::helpers::any_over_expr;
|
use ruff_python_ast::helpers::any_over_expr;
|
||||||
use ruff_python_semantic::{analyze::typing::is_list, Binding};
|
use ruff_python_semantic::{analyze::typing::is_list, Binding};
|
||||||
use ruff_python_trivia::{BackwardsTokenizer, PythonWhitespace, SimpleTokenKind, SimpleTokenizer};
|
|
||||||
use ruff_source_file::LineRanges;
|
use ruff_source_file::LineRanges;
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for `for` loops that can be replaced by a list comprehension.
|
/// Checks for `for` loops that can be replaced by a list comprehension.
|
||||||
///
|
///
|
||||||
|
@ -264,12 +263,14 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF
|
||||||
ast::Stmt::Assign(assign) => Some(&assign.value),
|
ast::Stmt::Assign(assign) => Some(&assign.value),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If the variable is an empty list literal, then we might be able to replace it with a full list comprehension
|
// If the variable is an empty list literal, then we might be able to replace it with a full list comprehension
|
||||||
// otherwise, it has to be replaced with a `list.extend`
|
// otherwise, it has to be replaced with a `list.extend`.
|
||||||
let binding_is_empty_list =
|
let binding_is_empty_list =
|
||||||
list_binding_value.is_some_and(|binding_value| match binding_value.as_list_expr() {
|
list_binding_value.is_some_and(|binding_value| match binding_value {
|
||||||
Some(list_expr) => list_expr.elts.is_empty(),
|
// `value = []`
|
||||||
None => false,
|
Expr::List(list_expr) => list_expr.is_empty(),
|
||||||
|
_ => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If the for loop does not have the same parent element as the binding, then it cannot always be
|
// If the for loop does not have the same parent element as the binding, then it cannot always be
|
||||||
|
@ -397,22 +398,12 @@ fn convert_to_list_extend(
|
||||||
let elt_str = locator.slice(to_append);
|
let elt_str = locator.slice(to_append);
|
||||||
let generator_str = format!("{elt_str} {for_type} {target_str} in {for_iter_str}{if_str}");
|
let generator_str = format!("{elt_str} {for_type} {target_str} in {for_iter_str}{if_str}");
|
||||||
|
|
||||||
let comment_strings_in_range = |range| {
|
|
||||||
checker
|
|
||||||
.comment_ranges()
|
|
||||||
.comments_in_range(range)
|
|
||||||
.iter()
|
|
||||||
// Ignore comments inside of the append or iterator, since these are preserved
|
|
||||||
.filter(|comment| {
|
|
||||||
!to_append.range().contains_range(**comment)
|
|
||||||
&& !for_stmt.iter.range().contains_range(**comment)
|
|
||||||
})
|
|
||||||
.map(|range| locator.slice(range).trim_whitespace_start())
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let variable_name = locator.slice(binding);
|
let variable_name = locator.slice(binding);
|
||||||
let for_loop_inline_comments: Vec<&str> = comment_strings_in_range(for_stmt.range);
|
let for_loop_inline_comments = comment_strings_in_range(
|
||||||
|
checker,
|
||||||
|
for_stmt.range,
|
||||||
|
&[to_append.range(), for_stmt.iter.range()],
|
||||||
|
);
|
||||||
|
|
||||||
let newline = checker.stylist().line_ending().as_str();
|
let newline = checker.stylist().line_ending().as_str();
|
||||||
|
|
||||||
|
@ -457,74 +448,25 @@ fn convert_to_list_extend(
|
||||||
.ok_or(anyhow!(
|
.ok_or(anyhow!(
|
||||||
"Binding must have a statement to convert into a list comprehension"
|
"Binding must have a statement to convert into a list comprehension"
|
||||||
))?;
|
))?;
|
||||||
// If the binding has multiple statements on its line, the fix would be substantially more complicated
|
|
||||||
let (semicolon_before, after_semicolon) = {
|
|
||||||
// determine whether there's a semicolon either before or after the binding statement.
|
|
||||||
// Since it's a binding statement, we can just check whether there's a semicolon immediately
|
|
||||||
// after the whitespace in front of or behind it
|
|
||||||
let mut after_tokenizer =
|
|
||||||
SimpleTokenizer::starts_at(binding_stmt_range.end(), locator.contents())
|
|
||||||
.skip_trivia();
|
|
||||||
|
|
||||||
let after_semicolon = if after_tokenizer
|
|
||||||
.next()
|
|
||||||
.is_some_and(|token| token.kind() == SimpleTokenKind::Semi)
|
|
||||||
{
|
|
||||||
after_tokenizer.next()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let semicolon_before = BackwardsTokenizer::up_to(
|
|
||||||
binding_stmt_range.start(),
|
|
||||||
locator.contents(),
|
|
||||||
checker.comment_ranges(),
|
|
||||||
)
|
|
||||||
.skip_trivia()
|
|
||||||
.next()
|
|
||||||
.filter(|token| token.kind() == SimpleTokenKind::Semi);
|
|
||||||
|
|
||||||
(semicolon_before, after_semicolon)
|
|
||||||
};
|
|
||||||
// If there are multiple binding statements in one line, we don't want to accidentally delete them
|
// If there are multiple binding statements in one line, we don't want to accidentally delete them
|
||||||
// Instead, we just delete the binding statement and leave any comments where they are
|
// Instead, we just delete the binding statement and leave any comments where they are
|
||||||
let (binding_stmt_deletion_range, binding_is_multiple_stmts) =
|
let (binding_stmt_deletion_range, binding_is_multiple_stmts) =
|
||||||
match (semicolon_before, after_semicolon) {
|
statement_deletion_range(checker, binding_stmt_range);
|
||||||
// ```python
|
|
||||||
// a = []
|
|
||||||
// ```
|
|
||||||
(None, None) => (locator.full_lines_range(binding_stmt_range), false),
|
|
||||||
|
|
||||||
// ```python
|
|
||||||
// a = 1; b = []
|
|
||||||
// ^^^^^^^^
|
|
||||||
// a = 1; b = []; c = 3
|
|
||||||
// ^^^^^^^^
|
|
||||||
// ```
|
|
||||||
(Some(semicolon_before), Some(_) | None) => (
|
|
||||||
TextRange::new(semicolon_before.start(), binding_stmt_range.end()),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
|
|
||||||
// ```python
|
|
||||||
// a = []; b = 3
|
|
||||||
// ^^^^^^^
|
|
||||||
// ```
|
|
||||||
(None, Some(after_semicolon)) => (
|
|
||||||
TextRange::new(binding_stmt_range.start(), after_semicolon.start()),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let annotations = match binding_stmt.and_then(|stmt| stmt.as_ann_assign_stmt()) {
|
let annotations = match binding_stmt.and_then(|stmt| stmt.as_ann_assign_stmt()) {
|
||||||
Some(assign) => format!(": {}", locator.slice(assign.annotation.range())),
|
Some(assign) => format!(": {}", locator.slice(assign.annotation.range())),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut comments_to_move = for_loop_inline_comments;
|
let comments_to_move = if binding_is_multiple_stmts {
|
||||||
if !binding_is_multiple_stmts {
|
for_loop_inline_comments
|
||||||
comments_to_move.extend(comment_strings_in_range(binding_stmt_deletion_range));
|
} else {
|
||||||
}
|
let mut new_comments =
|
||||||
|
comment_strings_in_range(checker, binding_stmt_deletion_range, &[]);
|
||||||
|
new_comments.extend(for_loop_inline_comments);
|
||||||
|
new_comments
|
||||||
|
};
|
||||||
|
|
||||||
let indentation = if comments_to_move.is_empty() {
|
let indentation = if comments_to_move.is_empty() {
|
||||||
String::new()
|
String::new()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
source: crates/ruff_linter/src/rules/perflint/mod.rs
|
source: crates/ruff_linter/src/rules/perflint/mod.rs
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
PERF403.py:5:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
PERF403.py:5:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
|
||||||
|
@ -9,6 +8,7 @@ PERF403.py:5:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
5 | result[idx] = name # PERF403
|
5 | result[idx] = name # PERF403
|
||||||
| ^^^^^^^^^^^^^^^^^^ PERF403
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
PERF403.py:13:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
PERF403.py:13:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
|
||||||
|
@ -17,43 +17,114 @@ PERF403.py:13:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
13 | result[idx] = name # PERF403
|
13 | result[idx] = name # PERF403
|
||||||
| ^^^^^^^^^^^^^^^^^^ PERF403
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
PERF403.py:31:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
PERF403.py:33:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
|
||||||
29 | for idx, name in enumerate(fruit):
|
31 | for idx, name in enumerate(fruit):
|
||||||
30 | if idx % 2:
|
32 | if idx % 2:
|
||||||
31 | result[idx] = name # PERF403
|
33 | result[idx] = name # PERF403
|
||||||
| ^^^^^^^^^^^^^^^^^^ PERF403
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
PERF403.py:61:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
PERF403.py:63:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
|
||||||
59 | for idx, name in enumerate(fruit):
|
61 | for idx, name in enumerate(fruit):
|
||||||
60 | if idx % 2:
|
62 | if idx % 2:
|
||||||
61 | result[idx] = name # PERF403
|
63 | result[idx] = name # PERF403
|
||||||
| ^^^^^^^^^^^^^^^^^^ PERF403
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
PERF403.py:76:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
PERF403.py:78:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
|
||||||
74 | result = {}
|
76 | result = {}
|
||||||
75 | for name in fruit:
|
77 | for name in fruit:
|
||||||
76 | result[name] = name # PERF403
|
78 | result[name] = name # PERF403
|
||||||
| ^^^^^^^^^^^^^^^^^^^ PERF403
|
| ^^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
PERF403.py:83:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
PERF403.py:85:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
|
||||||
81 | result = {}
|
83 | result = {}
|
||||||
82 | for idx, name in enumerate(fruit):
|
84 | for idx, name in enumerate(fruit):
|
||||||
83 | result[name] = idx # PERF403
|
85 | result[name] = idx # PERF403
|
||||||
| ^^^^^^^^^^^^^^^^^^ PERF403
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
PERF403.py:91:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
PERF403.py:94:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
|
||||||
89 | result = SneakyDict()
|
92 | result = SneakyDict()
|
||||||
90 | for idx, name in enumerate(fruit):
|
93 | for idx, name in enumerate(fruit):
|
||||||
91 | result[name] = idx # PERF403
|
94 | result[name] = idx # PERF403
|
||||||
| ^^^^^^^^^^^^^^^^^^ PERF403
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
PERF403.py:106:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
104 | ):
|
||||||
|
105 | # comment 3
|
||||||
|
106 | / result[
|
||||||
|
107 | | name # comment 4
|
||||||
|
108 | | ] = idx # PERF403
|
||||||
|
| |_______________^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
PERF403.py:115:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
113 | a = 1; result = {}; b = 2
|
||||||
|
114 | for idx, name in enumerate(fruit):
|
||||||
|
115 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
PERF403.py:122:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
120 | result = {"kiwi": 3}
|
||||||
|
121 | for idx, name in enumerate(fruit):
|
||||||
|
122 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
PERF403.py:129:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
127 | (_, result) = (None, {"kiwi": 3})
|
||||||
|
128 | for idx, name in enumerate(fruit):
|
||||||
|
129 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
PERF403.py:137:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
135 | print(len(result))
|
||||||
|
136 | for idx, name in enumerate(fruit):
|
||||||
|
137 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
PERF403.py:145:13: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
143 | for idx, name in enumerate(fruit):
|
||||||
|
144 | if last_idx := idx % 3:
|
||||||
|
145 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
PERF403.py:153:9: PERF403 Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
151 | result = {}
|
||||||
|
152 | for idx, name in indices, fruit:
|
||||||
|
153 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
|
@ -169,10 +169,10 @@ PERF401.py:119:13: PERF401 [*] Use a list comprehension to create a transformed
|
||||||
117 |- # single-line comment 2 should be protected
|
117 |- # single-line comment 2 should be protected
|
||||||
118 |- if i % 2: # single-line comment 3 should be protected
|
118 |- if i % 2: # single-line comment 3 should be protected
|
||||||
119 |- result.append(i) # PERF401
|
119 |- result.append(i) # PERF401
|
||||||
115 |+ # single-line comment 1 should be protected
|
115 |+ # comment after assignment should be protected
|
||||||
116 |+ # single-line comment 2 should be protected
|
116 |+ # single-line comment 1 should be protected
|
||||||
117 |+ # single-line comment 3 should be protected
|
117 |+ # single-line comment 2 should be protected
|
||||||
118 |+ # comment after assignment should be protected
|
118 |+ # single-line comment 3 should be protected
|
||||||
119 |+ result = [i for i in range(10) if i % 2] # PERF401
|
119 |+ result = [i for i in range(10) if i % 2] # PERF401
|
||||||
120 120 |
|
120 120 |
|
||||||
121 121 |
|
121 121 |
|
||||||
|
|
|
@ -0,0 +1,307 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/perflint/mod.rs
|
||||||
|
---
|
||||||
|
PERF403.py:5:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
3 | result = {}
|
||||||
|
4 | for idx, name in enumerate(fruit):
|
||||||
|
5 | result[idx] = name # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
1 1 | def foo():
|
||||||
|
2 2 | fruit = ["apple", "pear", "orange"]
|
||||||
|
3 |- result = {}
|
||||||
|
4 |- for idx, name in enumerate(fruit):
|
||||||
|
5 |- result[idx] = name # PERF403
|
||||||
|
3 |+ result = {idx: name for idx, name in enumerate(fruit)} # PERF403
|
||||||
|
6 4 |
|
||||||
|
7 5 |
|
||||||
|
8 6 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:13:13: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
11 | for idx, name in enumerate(fruit):
|
||||||
|
12 | if idx % 2:
|
||||||
|
13 | result[idx] = name # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
7 7 |
|
||||||
|
8 8 | def foo():
|
||||||
|
9 9 | fruit = ["apple", "pear", "orange"]
|
||||||
|
10 |- result = {}
|
||||||
|
11 |- for idx, name in enumerate(fruit):
|
||||||
|
12 |- if idx % 2:
|
||||||
|
13 |- result[idx] = name # PERF403
|
||||||
|
10 |+ result = {idx: name for idx, name in enumerate(fruit) if idx % 2} # PERF403
|
||||||
|
14 11 |
|
||||||
|
15 12 |
|
||||||
|
16 13 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:33:13: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
31 | for idx, name in enumerate(fruit):
|
||||||
|
32 | if idx % 2:
|
||||||
|
33 | result[idx] = name # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
26 26 |
|
||||||
|
27 27 |
|
||||||
|
28 28 | def foo():
|
||||||
|
29 |- result = {}
|
||||||
|
30 29 | fruit = ["apple", "pear", "orange"]
|
||||||
|
31 |- for idx, name in enumerate(fruit):
|
||||||
|
32 |- if idx % 2:
|
||||||
|
33 |- result[idx] = name # PERF403
|
||||||
|
30 |+ result = {idx: name for idx, name in enumerate(fruit) if idx % 2} # PERF403
|
||||||
|
34 31 |
|
||||||
|
35 32 |
|
||||||
|
36 33 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:63:13: PERF403 [*] Use `dict.update` instead of a for-loop
|
||||||
|
|
|
||||||
|
61 | for idx, name in enumerate(fruit):
|
||||||
|
62 | if idx % 2:
|
||||||
|
63 | result[idx] = name # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with `dict.update`
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
58 58 | def foo():
|
||||||
|
59 59 | result = {1: "banana"}
|
||||||
|
60 60 | fruit = ["apple", "pear", "orange"]
|
||||||
|
61 |- for idx, name in enumerate(fruit):
|
||||||
|
62 |- if idx % 2:
|
||||||
|
63 |- result[idx] = name # PERF403
|
||||||
|
61 |+ result.update({idx: name for idx, name in enumerate(fruit) if idx % 2}) # PERF403
|
||||||
|
64 62 |
|
||||||
|
65 63 |
|
||||||
|
66 64 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:78:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
76 | result = {}
|
||||||
|
77 | for name in fruit:
|
||||||
|
78 | result[name] = name # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
73 73 |
|
||||||
|
74 74 | def foo():
|
||||||
|
75 75 | fruit = ["apple", "pear", "orange"]
|
||||||
|
76 |- result = {}
|
||||||
|
77 |- for name in fruit:
|
||||||
|
78 |- result[name] = name # PERF403
|
||||||
|
76 |+ result = {name: name for name in fruit} # PERF403
|
||||||
|
79 77 |
|
||||||
|
80 78 |
|
||||||
|
81 79 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:85:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
83 | result = {}
|
||||||
|
84 | for idx, name in enumerate(fruit):
|
||||||
|
85 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
80 80 |
|
||||||
|
81 81 | def foo():
|
||||||
|
82 82 | fruit = ["apple", "pear", "orange"]
|
||||||
|
83 |- result = {}
|
||||||
|
84 |- for idx, name in enumerate(fruit):
|
||||||
|
85 |- result[name] = idx # PERF403
|
||||||
|
83 |+ result = {name: idx for idx, name in enumerate(fruit)} # PERF403
|
||||||
|
86 84 |
|
||||||
|
87 85 |
|
||||||
|
88 86 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:94:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
92 | result = SneakyDict()
|
||||||
|
93 | for idx, name in enumerate(fruit):
|
||||||
|
94 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
89 89 | from builtins import dict as SneakyDict
|
||||||
|
90 90 |
|
||||||
|
91 91 | fruit = ["apple", "pear", "orange"]
|
||||||
|
92 |- result = SneakyDict()
|
||||||
|
93 |- for idx, name in enumerate(fruit):
|
||||||
|
94 |- result[name] = idx # PERF403
|
||||||
|
92 |+ result = {name: idx for idx, name in enumerate(fruit)} # PERF403
|
||||||
|
95 93 |
|
||||||
|
96 94 |
|
||||||
|
97 95 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:106:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
104 | ):
|
||||||
|
105 | # comment 3
|
||||||
|
106 | / result[
|
||||||
|
107 | | name # comment 4
|
||||||
|
108 | | ] = idx # PERF403
|
||||||
|
| |_______________^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
96 96 |
|
||||||
|
97 97 | def foo():
|
||||||
|
98 98 | fruit = ["apple", "pear", "orange"]
|
||||||
|
99 |- result: dict[str, int] = {
|
||||||
|
100 |- # comment 1
|
||||||
|
101 |- }
|
||||||
|
102 |- for idx, name in enumerate(
|
||||||
|
99 |+ # comment 1
|
||||||
|
100 |+ # comment 3
|
||||||
|
101 |+ # comment 4
|
||||||
|
102 |+ result: dict[str, int] = {name: idx for idx, name in enumerate(
|
||||||
|
103 103 | fruit # comment 2
|
||||||
|
104 |- ):
|
||||||
|
105 |- # comment 3
|
||||||
|
106 |- result[
|
||||||
|
107 |- name # comment 4
|
||||||
|
108 |- ] = idx # PERF403
|
||||||
|
104 |+ )} # PERF403
|
||||||
|
109 105 |
|
||||||
|
110 106 |
|
||||||
|
111 107 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:115:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
113 | a = 1; result = {}; b = 2
|
||||||
|
114 | for idx, name in enumerate(fruit):
|
||||||
|
115 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
110 110 |
|
||||||
|
111 111 | def foo():
|
||||||
|
112 112 | fruit = ["apple", "pear", "orange"]
|
||||||
|
113 |- a = 1; result = {}; b = 2
|
||||||
|
114 |- for idx, name in enumerate(fruit):
|
||||||
|
115 |- result[name] = idx # PERF403
|
||||||
|
113 |+ a = 1; b = 2
|
||||||
|
114 |+ result = {name: idx for idx, name in enumerate(fruit)} # PERF403
|
||||||
|
116 115 |
|
||||||
|
117 116 |
|
||||||
|
118 117 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:122:9: PERF403 [*] Use `dict.update` instead of a for-loop
|
||||||
|
|
|
||||||
|
120 | result = {"kiwi": 3}
|
||||||
|
121 | for idx, name in enumerate(fruit):
|
||||||
|
122 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with `dict.update`
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
118 118 | def foo():
|
||||||
|
119 119 | fruit = ["apple", "pear", "orange"]
|
||||||
|
120 120 | result = {"kiwi": 3}
|
||||||
|
121 |- for idx, name in enumerate(fruit):
|
||||||
|
122 |- result[name] = idx # PERF403
|
||||||
|
121 |+ result.update({name: idx for idx, name in enumerate(fruit)}) # PERF403
|
||||||
|
123 122 |
|
||||||
|
124 123 |
|
||||||
|
125 124 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:129:9: PERF403 [*] Use `dict.update` instead of a for-loop
|
||||||
|
|
|
||||||
|
127 | (_, result) = (None, {"kiwi": 3})
|
||||||
|
128 | for idx, name in enumerate(fruit):
|
||||||
|
129 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with `dict.update`
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
125 125 | def foo():
|
||||||
|
126 126 | fruit = ["apple", "pear", "orange"]
|
||||||
|
127 127 | (_, result) = (None, {"kiwi": 3})
|
||||||
|
128 |- for idx, name in enumerate(fruit):
|
||||||
|
129 |- result[name] = idx # PERF403
|
||||||
|
128 |+ result.update({name: idx for idx, name in enumerate(fruit)}) # PERF403
|
||||||
|
130 129 |
|
||||||
|
131 130 |
|
||||||
|
132 131 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:137:9: PERF403 [*] Use `dict.update` instead of a for-loop
|
||||||
|
|
|
||||||
|
135 | print(len(result))
|
||||||
|
136 | for idx, name in enumerate(fruit):
|
||||||
|
137 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with `dict.update`
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
133 133 | fruit = ["apple", "pear", "orange"]
|
||||||
|
134 134 | result = {}
|
||||||
|
135 135 | print(len(result))
|
||||||
|
136 |- for idx, name in enumerate(fruit):
|
||||||
|
137 |- result[name] = idx # PERF403
|
||||||
|
136 |+ result.update({name: idx for idx, name in enumerate(fruit)}) # PERF403
|
||||||
|
138 137 |
|
||||||
|
139 138 |
|
||||||
|
140 139 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:145:13: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
143 | for idx, name in enumerate(fruit):
|
||||||
|
144 | if last_idx := idx % 3:
|
||||||
|
145 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
139 139 |
|
||||||
|
140 140 | def foo():
|
||||||
|
141 141 | fruit = ["apple", "pear", "orange"]
|
||||||
|
142 |- result = {}
|
||||||
|
143 |- for idx, name in enumerate(fruit):
|
||||||
|
144 |- if last_idx := idx % 3:
|
||||||
|
145 |- result[name] = idx # PERF403
|
||||||
|
142 |+ result = {name: idx for idx, name in enumerate(fruit) if (last_idx := idx % 3)} # PERF403
|
||||||
|
146 143 |
|
||||||
|
147 144 |
|
||||||
|
148 145 | def foo():
|
||||||
|
|
||||||
|
PERF403.py:153:9: PERF403 [*] Use a dictionary comprehension instead of a for-loop
|
||||||
|
|
|
||||||
|
151 | result = {}
|
||||||
|
152 | for idx, name in indices, fruit:
|
||||||
|
153 | result[name] = idx # PERF403
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ PERF403
|
||||||
|
|
|
||||||
|
= help: Replace for loop with dict comprehension
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
148 148 | def foo():
|
||||||
|
149 149 | fruit = ["apple", "pear", "orange"]
|
||||||
|
150 150 | indices = [0, 1, 2]
|
||||||
|
151 |- result = {}
|
||||||
|
152 |- for idx, name in indices, fruit:
|
||||||
|
153 |- result[name] = idx # PERF403
|
||||||
|
151 |+ result = {name: idx for idx, name in (indices, fruit)} # PERF403
|
Loading…
Add table
Add a link
Reference in a new issue