Skip to content

Commit 291413b

Browse files
[perflint] Fix PERF101 autofix creating a syntax error and mark autofix as unsafe if there are comments in the list call expr (#18803)
Co-authored-by: Micha Reiser <micha@reiser.io>
1 parent 7ec7853 commit 291413b

3 files changed

Lines changed: 100 additions & 7 deletions

File tree

crates/ruff_linter/resources/test/fixtures/perflint/PERF101.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,19 @@
8585

8686
for i in builtins.list(nested_tuple): # PERF101
8787
pass
88+
89+
# https://github.com/astral-sh/ruff/issues/18783
90+
items = (1, 2, 3)
91+
for i in(list)(items):
92+
print(i)
93+
94+
# https://github.com/astral-sh/ruff/issues/18784
95+
items = (1, 2, 3)
96+
for i in ( # 1
97+
list # 2
98+
# 3
99+
)( # 4
100+
items # 5
101+
# 6
102+
):
103+
print(i)

crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
use ruff_diagnostics::Applicability;
12
use ruff_macros::{ViolationMetadata, derive_message_formats};
23
use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt};
34
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
45
use ruff_python_semantic::analyze::typing::find_assigned_value;
56
use ruff_text_size::TextRange;
67

78
use crate::checkers::ast::Checker;
9+
use crate::fix::edits;
810
use crate::{AlwaysFixableViolation, Edit, Fix};
911

1012
/// ## What it does
@@ -35,6 +37,19 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
3537
/// for i in items:
3638
/// print(i)
3739
/// ```
40+
///
41+
/// ## Fix safety
42+
/// This rule's fix is marked as unsafe if there's comments in the
43+
/// `list()` call, as comments may be removed.
44+
///
45+
/// For example, the fix would be marked as unsafe in the following case:
46+
/// ```python
47+
/// items = (1, 2, 3)
48+
/// for i in list( # comment
49+
/// items
50+
/// ):
51+
/// print(i)
52+
/// ```
3853
#[derive(ViolationMetadata)]
3954
pub(crate) struct UnnecessaryListCast;
4055

@@ -89,7 +104,7 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt
89104
..
90105
}) => {
91106
let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, *list_range);
92-
diagnostic.set_fix(remove_cast(*list_range, *iterable_range));
107+
diagnostic.set_fix(remove_cast(checker, *list_range, *iterable_range));
93108
}
94109
Expr::Name(ast::ExprName {
95110
id,
@@ -116,18 +131,27 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt
116131
}
117132

118133
let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, *list_range);
119-
diagnostic.set_fix(remove_cast(*list_range, *iterable_range));
134+
diagnostic.set_fix(remove_cast(checker, *list_range, *iterable_range));
120135
}
121136
}
122137
_ => {}
123138
}
124139
}
125140

126141
/// Generate a [`Fix`] to remove a `list` cast from an expression.
127-
fn remove_cast(list_range: TextRange, iterable_range: TextRange) -> Fix {
128-
Fix::safe_edits(
129-
Edit::deletion(list_range.start(), iterable_range.start()),
130-
[Edit::deletion(iterable_range.end(), list_range.end())],
142+
fn remove_cast(checker: &Checker, list_range: TextRange, iterable_range: TextRange) -> Fix {
143+
let content = edits::pad(
144+
checker.locator().slice(iterable_range).to_string(),
145+
list_range,
146+
checker.locator(),
147+
);
148+
Fix::applicable_edit(
149+
Edit::range_replacement(content, list_range),
150+
if checker.comment_ranges().intersects(list_range) {
151+
Applicability::Unsafe
152+
} else {
153+
Applicability::Safe
154+
},
131155
)
132156
}
133157

crates/ruff_linter/src/rules/perflint/snapshots/ruff_linter__rules__perflint__tests__PERF101_PERF101.py.snap

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ PERF101.py:34:10: PERF101 [*] Do not cast an iterable to `list` before iterating
168168
|
169169
= help: Remove `list()` cast
170170

171-
Safe fix
171+
Unsafe fix
172172
31 31 | ):
173173
32 32 | pass
174174
33 33 |
@@ -238,3 +238,56 @@ PERF101.py:86:10: PERF101 [*] Do not cast an iterable to `list` before iterating
238238
86 |-for i in builtins.list(nested_tuple): # PERF101
239239
86 |+for i in nested_tuple: # PERF101
240240
87 87 | pass
241+
88 88 |
242+
89 89 | # https://github.com/astral-sh/ruff/issues/18783
243+
244+
PERF101.py:91:9: PERF101 [*] Do not cast an iterable to `list` before iterating over it
245+
|
246+
89 | # https://github.com/astral-sh/ruff/issues/18783
247+
90 | items = (1, 2, 3)
248+
91 | for i in(list)(items):
249+
| ^^^^^^^^^^^^^ PERF101
250+
92 | print(i)
251+
|
252+
= help: Remove `list()` cast
253+
254+
Safe fix
255+
88 88 |
256+
89 89 | # https://github.com/astral-sh/ruff/issues/18783
257+
90 90 | items = (1, 2, 3)
258+
91 |-for i in(list)(items):
259+
91 |+for i in items:
260+
92 92 | print(i)
261+
93 93 |
262+
94 94 | # https://github.com/astral-sh/ruff/issues/18784
263+
264+
PERF101.py:96:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it
265+
|
266+
94 | # https://github.com/astral-sh/ruff/issues/18784
267+
95 | items = (1, 2, 3)
268+
96 | for i in ( # 1
269+
| __________^
270+
97 | | list # 2
271+
98 | | # 3
272+
99 | | )( # 4
273+
100 | | items # 5
274+
101 | | # 6
275+
102 | | ):
276+
| |_^ PERF101
277+
103 | print(i)
278+
|
279+
= help: Remove `list()` cast
280+
281+
Unsafe fix
282+
93 93 |
283+
94 94 | # https://github.com/astral-sh/ruff/issues/18784
284+
95 95 | items = (1, 2, 3)
285+
96 |-for i in ( # 1
286+
97 |- list # 2
287+
98 |- # 3
288+
99 |-)( # 4
289+
100 |- items # 5
290+
101 |- # 6
291+
102 |-):
292+
96 |+for i in items:
293+
103 97 | print(i)

0 commit comments

Comments
 (0)