[refurb] Stabilize fix safety for readlines-in-for (FURB129) (#18496)

Note that the preview behavior was not documented (shame on us!) so the
documentation was not modified.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
Dylan 2025-06-06 12:01:35 -05:00 committed by Brent Westbrook
parent 913f136d33
commit 8aea383f29
6 changed files with 24 additions and 385 deletions

View file

@ -126,11 +126,9 @@ pub(crate) const fn is_check_file_level_directives_enabled(settings: &LinterSett
settings.preview.is_enabled() settings.preview.is_enabled()
} }
// https://github.com/astral-sh/ruff/pull/17644 // https://github.com/astral-sh/ruff/pull/18208
pub(crate) const fn is_readlines_in_for_fix_safe_enabled(settings: &LinterSettings) -> bool { pub(crate) const fn is_multiple_with_statements_fix_safe_enabled(
settings.preview.is_enabled() settings: &LinterSettings,
} ) -> bool {
pub(crate) const fn multiple_with_statements_fix_safe_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled() settings.preview.is_enabled()
} }

View file

@ -10,7 +10,7 @@ use super::fix_with;
use crate::Fix; use crate::Fix;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::fix::edits::fits; use crate::fix::edits::fits;
use crate::preview::multiple_with_statements_fix_safe_enabled; use crate::preview::is_multiple_with_statements_fix_safe_enabled;
use crate::{FixAvailability, Violation}; use crate::{FixAvailability, Violation};
/// ## What it does /// ## What it does
@ -195,7 +195,7 @@ pub(crate) fn multiple_with_statements(
checker.settings.tab_size, checker.settings.tab_size,
) )
}) { }) {
if multiple_with_statements_fix_safe_enabled(checker.settings) { if is_multiple_with_statements_fix_safe_enabled(checker.settings) {
Ok(Some(Fix::safe_edit(edit))) Ok(Some(Fix::safe_edit(edit)))
} else { } else {
Ok(Some(Fix::unsafe_edit(edit))) Ok(Some(Fix::unsafe_edit(edit)))

View file

@ -62,24 +62,6 @@ mod tests {
Ok(()) Ok(())
} }
#[test_case(Rule::ReadlinesInFor, Path::new("FURB129.py"))]
fn preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("refurb").join(path).as_path(),
&settings::LinterSettings {
preview: settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test] #[test]
fn write_whole_file_python_39() -> Result<()> { fn write_whole_file_python_39() -> Result<()> {
let diagnostics = test_path( let diagnostics = test_path(

View file

@ -7,7 +7,6 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::fix::edits::pad_end; use crate::fix::edits::pad_end;
use crate::preview::is_readlines_in_for_fix_safe_enabled;
use crate::{AlwaysFixableViolation, Edit, Fix}; use crate::{AlwaysFixableViolation, Edit, Fix};
/// ## What it does /// ## What it does
@ -106,9 +105,5 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) {
}; };
let mut diagnostic = checker.report_diagnostic(ReadlinesInFor, expr_call.range()); let mut diagnostic = checker.report_diagnostic(ReadlinesInFor, expr_call.range());
diagnostic.set_fix(if is_readlines_in_for_fix_safe_enabled(checker.settings) { diagnostic.set_fix(Fix::safe_edit(edit));
Fix::safe_edit(edit)
} else {
Fix::unsafe_edit(edit)
});
} }

View file

@ -12,7 +12,7 @@ FURB129.py:7:18: FURB129 [*] Instead of calling `readlines()`, iterate over file
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
4 4 | 4 4 |
5 5 | # Errors 5 5 | # Errors
6 6 | with open("FURB129.py") as f: 6 6 | with open("FURB129.py") as f:
@ -33,7 +33,7 @@ FURB129.py:9:35: FURB129 [*] Instead of calling `readlines()`, iterate over file
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
6 6 | with open("FURB129.py") as f: 6 6 | with open("FURB129.py") as f:
7 7 | for _line in f.readlines(): 7 7 | for _line in f.readlines():
8 8 | pass 8 8 | pass
@ -53,7 +53,7 @@ FURB129.py:10:35: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
7 7 | for _line in f.readlines(): 7 7 | for _line in f.readlines():
8 8 | pass 8 8 | pass
9 9 | a = [line.lower() for line in f.readlines()] 9 9 | a = [line.lower() for line in f.readlines()]
@ -74,7 +74,7 @@ FURB129.py:11:49: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
8 8 | pass 8 8 | pass
9 9 | a = [line.lower() for line in f.readlines()] 9 9 | a = [line.lower() for line in f.readlines()]
10 10 | b = {line.upper() for line in f.readlines()} 10 10 | b = {line.upper() for line in f.readlines()}
@ -93,7 +93,7 @@ FURB129.py:14:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
11 11 | c = {line.lower(): line.upper() for line in f.readlines()} 11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
12 12 | 12 12 |
13 13 | with Path("FURB129.py").open() as f: 13 13 | with Path("FURB129.py").open() as f:
@ -113,7 +113,7 @@ FURB129.py:17:14: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
14 14 | for _line in f.readlines(): 14 14 | for _line in f.readlines():
15 15 | pass 15 15 | pass
16 16 | 16 16 |
@ -133,7 +133,7 @@ FURB129.py:20:14: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
17 17 | for _line in open("FURB129.py").readlines(): 17 17 | for _line in open("FURB129.py").readlines():
18 18 | pass 18 18 | pass
19 19 | 19 19 |
@ -154,7 +154,7 @@ FURB129.py:26:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
23 23 | 23 23 |
24 24 | def func(): 24 24 | def func():
25 25 | f = Path("FURB129.py").open() 25 25 | f = Path("FURB129.py").open()
@ -173,7 +173,7 @@ FURB129.py:32:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
29 29 | 29 29 |
30 30 | 30 30 |
31 31 | def func(f: io.BytesIO): 31 31 | def func(f: io.BytesIO):
@ -194,7 +194,7 @@ FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
35 35 | 35 35 |
36 36 | def func(): 36 36 | def func():
37 37 | with (open("FURB129.py") as f, foo as bar): 37 37 | with (open("FURB129.py") as f, foo as bar):
@ -213,7 +213,7 @@ FURB129.py:47:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
44 44 | import builtins 44 44 | import builtins
45 45 | 45 45 |
46 46 | with builtins.open("FURB129.py") as f: 46 46 | with builtins.open("FURB129.py") as f:
@ -232,7 +232,7 @@ FURB129.py:54:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
51 51 | from builtins import open as o 51 51 | from builtins import open as o
52 52 | 52 52 |
53 53 | with o("FURB129.py") as f: 53 53 | with o("FURB129.py") as f:
@ -252,7 +252,7 @@ FURB129.py:93:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
90 90 | 90 90 |
91 91 | # https://github.com/astral-sh/ruff/issues/18231 91 91 | # https://github.com/astral-sh/ruff/issues/18231
92 92 | with open("furb129.py") as f: 92 92 | with open("furb129.py") as f:
@ -270,7 +270,7 @@ FURB129.py:97:23: FURB129 [*] Instead of calling `readlines()`, iterate over fil
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
94 94 | pass 94 94 | pass
95 95 | 95 95 |
96 96 | with open("furb129.py") as f: 96 96 | with open("furb129.py") as f:
@ -290,7 +290,7 @@ FURB129.py:101:17: FURB129 [*] Instead of calling `readlines()`, iterate over fi
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
98 98 | 98 98 |
99 99 | 99 99 |
100 100 | with open("furb129.py") as f: 100 100 | with open("furb129.py") as f:
@ -310,7 +310,7 @@ FURB129.py:103:16: FURB129 [*] Instead of calling `readlines()`, iterate over fi
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
100 100 | with open("furb129.py") as f: 100 100 | with open("furb129.py") as f:
101 101 | for line in (((f))).readlines(): 101 101 | for line in (((f))).readlines():
102 102 | pass 102 102 | pass
@ -328,7 +328,7 @@ FURB129.py:107:29: FURB129 [*] Instead of calling `readlines()`, iterate over fi
| |
= help: Remove `readlines()` = help: Remove `readlines()`
Unsafe fix Safe fix
104 104 | pass 104 104 | pass
105 105 | 105 105 |
106 106 | # Test case for issue #17683 (missing space before keyword) 106 106 | # Test case for issue #17683 (missing space before keyword)

View file

@ -1,336 +0,0 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB129.py:7:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
5 | # Errors
6 | with open("FURB129.py") as f:
7 | for _line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
8 | pass
9 | a = [line.lower() for line in f.readlines()]
|
= help: Remove `readlines()`
Safe fix
4 4 |
5 5 | # Errors
6 6 | with open("FURB129.py") as f:
7 |- for _line in f.readlines():
7 |+ for _line in f:
8 8 | pass
9 9 | a = [line.lower() for line in f.readlines()]
10 10 | b = {line.upper() for line in f.readlines()}
FURB129.py:9:35: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
7 | for _line in f.readlines():
8 | pass
9 | a = [line.lower() for line in f.readlines()]
| ^^^^^^^^^^^^^ FURB129
10 | b = {line.upper() for line in f.readlines()}
11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
= help: Remove `readlines()`
Safe fix
6 6 | with open("FURB129.py") as f:
7 7 | for _line in f.readlines():
8 8 | pass
9 |- a = [line.lower() for line in f.readlines()]
9 |+ a = [line.lower() for line in f]
10 10 | b = {line.upper() for line in f.readlines()}
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
12 12 |
FURB129.py:10:35: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
8 | pass
9 | a = [line.lower() for line in f.readlines()]
10 | b = {line.upper() for line in f.readlines()}
| ^^^^^^^^^^^^^ FURB129
11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
= help: Remove `readlines()`
Safe fix
7 7 | for _line in f.readlines():
8 8 | pass
9 9 | a = [line.lower() for line in f.readlines()]
10 |- b = {line.upper() for line in f.readlines()}
10 |+ b = {line.upper() for line in f}
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
12 12 |
13 13 | with Path("FURB129.py").open() as f:
FURB129.py:11:49: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
9 | a = [line.lower() for line in f.readlines()]
10 | b = {line.upper() for line in f.readlines()}
11 | c = {line.lower(): line.upper() for line in f.readlines()}
| ^^^^^^^^^^^^^ FURB129
12 |
13 | with Path("FURB129.py").open() as f:
|
= help: Remove `readlines()`
Safe fix
8 8 | pass
9 9 | a = [line.lower() for line in f.readlines()]
10 10 | b = {line.upper() for line in f.readlines()}
11 |- c = {line.lower(): line.upper() for line in f.readlines()}
11 |+ c = {line.lower(): line.upper() for line in f}
12 12 |
13 13 | with Path("FURB129.py").open() as f:
14 14 | for _line in f.readlines():
FURB129.py:14:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
13 | with Path("FURB129.py").open() as f:
14 | for _line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
15 | pass
|
= help: Remove `readlines()`
Safe fix
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
12 12 |
13 13 | with Path("FURB129.py").open() as f:
14 |- for _line in f.readlines():
14 |+ for _line in f:
15 15 | pass
16 16 |
17 17 | for _line in open("FURB129.py").readlines():
FURB129.py:17:14: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
15 | pass
16 |
17 | for _line in open("FURB129.py").readlines():
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB129
18 | pass
|
= help: Remove `readlines()`
Safe fix
14 14 | for _line in f.readlines():
15 15 | pass
16 16 |
17 |-for _line in open("FURB129.py").readlines():
17 |+for _line in open("FURB129.py"):
18 18 | pass
19 19 |
20 20 | for _line in Path("FURB129.py").open().readlines():
FURB129.py:20:14: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
18 | pass
19 |
20 | for _line in Path("FURB129.py").open().readlines():
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB129
21 | pass
|
= help: Remove `readlines()`
Safe fix
17 17 | for _line in open("FURB129.py").readlines():
18 18 | pass
19 19 |
20 |-for _line in Path("FURB129.py").open().readlines():
20 |+for _line in Path("FURB129.py").open():
21 21 | pass
22 22 |
23 23 |
FURB129.py:26:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
24 | def func():
25 | f = Path("FURB129.py").open()
26 | for _line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
27 | pass
28 | f.close()
|
= help: Remove `readlines()`
Safe fix
23 23 |
24 24 | def func():
25 25 | f = Path("FURB129.py").open()
26 |- for _line in f.readlines():
26 |+ for _line in f:
27 27 | pass
28 28 | f.close()
29 29 |
FURB129.py:32:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
31 | def func(f: io.BytesIO):
32 | for _line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
33 | pass
|
= help: Remove `readlines()`
Safe fix
29 29 |
30 30 |
31 31 | def func(f: io.BytesIO):
32 |- for _line in f.readlines():
32 |+ for _line in f:
33 33 | pass
34 34 |
35 35 |
FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
36 | def func():
37 | with (open("FURB129.py") as f, foo as bar):
38 | for _line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
39 | pass
40 | for _line in bar.readlines():
|
= help: Remove `readlines()`
Safe fix
35 35 |
36 36 | def func():
37 37 | with (open("FURB129.py") as f, foo as bar):
38 |- for _line in f.readlines():
38 |+ for _line in f:
39 39 | pass
40 40 | for _line in bar.readlines():
41 41 | pass
FURB129.py:47:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
46 | with builtins.open("FURB129.py") as f:
47 | for line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
48 | pass
|
= help: Remove `readlines()`
Safe fix
44 44 | import builtins
45 45 |
46 46 | with builtins.open("FURB129.py") as f:
47 |- for line in f.readlines():
47 |+ for line in f:
48 48 | pass
49 49 |
50 50 |
FURB129.py:54:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
53 | with o("FURB129.py") as f:
54 | for line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
55 | pass
|
= help: Remove `readlines()`
Safe fix
51 51 | from builtins import open as o
52 52 |
53 53 | with o("FURB129.py") as f:
54 |- for line in f.readlines():
54 |+ for line in f:
55 55 | pass
56 56 |
57 57 |
FURB129.py:93:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
91 | # https://github.com/astral-sh/ruff/issues/18231
92 | with open("furb129.py") as f:
93 | for line in (f).readlines():
| ^^^^^^^^^^^^^^^ FURB129
94 | pass
|
= help: Remove `readlines()`
Safe fix
90 90 |
91 91 | # https://github.com/astral-sh/ruff/issues/18231
92 92 | with open("furb129.py") as f:
93 |- for line in (f).readlines():
93 |+ for line in (f):
94 94 | pass
95 95 |
96 96 | with open("furb129.py") as f:
FURB129.py:97:23: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
96 | with open("furb129.py") as f:
97 | [line for line in (f).readlines()]
| ^^^^^^^^^^^^^^^ FURB129
|
= help: Remove `readlines()`
Safe fix
94 94 | pass
95 95 |
96 96 | with open("furb129.py") as f:
97 |- [line for line in (f).readlines()]
97 |+ [line for line in (f)]
98 98 |
99 99 |
100 100 | with open("furb129.py") as f:
FURB129.py:101:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
100 | with open("furb129.py") as f:
101 | for line in (((f))).readlines():
| ^^^^^^^^^^^^^^^^^^^ FURB129
102 | pass
103 | for line in(f).readlines():
|
= help: Remove `readlines()`
Safe fix
98 98 |
99 99 |
100 100 | with open("furb129.py") as f:
101 |- for line in (((f))).readlines():
101 |+ for line in (((f))):
102 102 | pass
103 103 | for line in(f).readlines():
104 104 | pass
FURB129.py:103:16: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
101 | for line in (((f))).readlines():
102 | pass
103 | for line in(f).readlines():
| ^^^^^^^^^^^^^^^ FURB129
104 | pass
|
= help: Remove `readlines()`
Safe fix
100 100 | with open("furb129.py") as f:
101 101 | for line in (((f))).readlines():
102 102 | pass
103 |- for line in(f).readlines():
103 |+ for line in(f):
104 104 | pass
105 105 |
106 106 | # Test case for issue #17683 (missing space before keyword)
FURB129.py:107:29: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
106 | # Test case for issue #17683 (missing space before keyword)
107 | print([line for line in f.readlines()if True])
| ^^^^^^^^^^^^^ FURB129
|
= help: Remove `readlines()`
Safe fix
104 104 | pass
105 105 |
106 106 | # Test case for issue #17683 (missing space before keyword)
107 |- print([line for line in f.readlines()if True])
107 |+ print([line for line in f if True])