Skip to content

Commit b312b53

Browse files
authored
[flake8-pyi] Mark PYI030 fix unsafe when comments are deleted (astral-sh#16322)
1 parent c814745 commit b312b53

File tree

5 files changed

+150
-9
lines changed

5 files changed

+150
-9
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI030.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,15 @@ def func2() -> Literal[1] | Literal[2]: # Error
9191
from typing import IO, Literal
9292

9393
InlineOption = Literal["a"] | Literal["b"] | IO[str]
94+
95+
# Should use unsafe fix when comments are deleted
96+
field26: (
97+
# First comment
98+
Literal["a", "b"]
99+
# Second comment
100+
| Literal["c", "d"]
101+
)
102+
field27: (
103+
Literal["a", "b"] # First comment
104+
| Literal["c", "d"] # Second comment
105+
)

crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI030.pyi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,15 @@ field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[
8787

8888
# Should use the first literal subscript attribute when fixing
8989
field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error
90+
91+
# Should use unsafe fix when comments are deleted
92+
field26: (
93+
# First comment
94+
Literal["a", "b"]
95+
# Second comment
96+
| Literal["c", "d"]
97+
)
98+
field27: (
99+
Literal["a", "b"] # First comment
100+
| Literal["c", "d"] # Second comment
101+
)

crates/ruff_linter/src/rules/flake8_pyi/rules/unnecessary_literal_union.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ use crate::checkers::ast::Checker;
2828
/// field: Literal[1, 2] | str
2929
/// ```
3030
///
31+
/// ## Fix safety
32+
/// This fix is marked unsafe if it would delete any comments within the replacement range.
33+
///
34+
/// An example to illustrate where comments are preserved and where they are not:
35+
///
36+
/// ```pyi
37+
/// from typing import Literal
38+
///
39+
/// field: (
40+
/// # deleted comment
41+
/// Literal["a", "b"] # deleted comment
42+
/// # deleted comment
43+
/// | Literal["c", "d"] # preserved comment
44+
/// )
45+
/// ```
46+
///
3147
/// ## References
3248
/// - [Python documentation: `typing.Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)
3349
#[derive(ViolationMetadata)]
@@ -131,12 +147,8 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &Checker, expr: &'a Expr) {
131147
ctx: ExprContext::Load,
132148
});
133149

134-
if other_exprs.is_empty() {
135-
// if the union is only literals, we just replace the whole thing with a single literal
136-
Fix::safe_edit(Edit::range_replacement(
137-
checker.generator().expr(&literal),
138-
expr.range(),
139-
))
150+
let edit = if other_exprs.is_empty() {
151+
Edit::range_replacement(checker.generator().expr(&literal), expr.range())
140152
} else {
141153
let elts: Vec<Expr> = std::iter::once(literal)
142154
.chain(other_exprs.into_iter().cloned())
@@ -159,8 +171,13 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &Checker, expr: &'a Expr) {
159171
} else {
160172
checker.generator().expr(&pep_604_union(&elts))
161173
};
174+
Edit::range_replacement(content, expr.range())
175+
};
162176

163-
Fix::safe_edit(Edit::range_replacement(content, expr.range()))
177+
if checker.comment_ranges().intersects(expr.range()) {
178+
Fix::unsafe_edit(edit)
179+
} else {
180+
Fix::safe_edit(edit)
164181
}
165182
});
166183

crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI030_PYI030.py.snap

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ PYI030.py:63:10: PYI030 [*] Multiple literal members in a union. Use a single li
377377
|
378378
= help: Replace with a single `Literal`
379379

380-
Safe fix
380+
Unsafe fix
381381
60 60 | field19: typing.Literal[1] | typing.Literal[2] # Error
382382
61 61 |
383383
62 62 | # Should emit in cases with newlines
@@ -538,6 +538,8 @@ PYI030.py:93:16: PYI030 [*] Multiple literal members in a union. Use a single li
538538
92 |
539539
93 | InlineOption = Literal["a"] | Literal["b"] | IO[str]
540540
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
541+
94 |
542+
95 | # Should use unsafe fix when comments are deleted
541543
|
542544
= help: Replace with a single `Literal`
543545

@@ -547,3 +549,51 @@ PYI030.py:93:16: PYI030 [*] Multiple literal members in a union. Use a single li
547549
92 92 |
548550
93 |-InlineOption = Literal["a"] | Literal["b"] | IO[str]
549551
93 |+InlineOption = Literal["a", "b"] | IO[str]
552+
94 94 |
553+
95 95 | # Should use unsafe fix when comments are deleted
554+
96 96 | field26: (
555+
556+
PYI030.py:98:5: PYI030 [*] Multiple literal members in a union. Use a single literal, e.g. `Literal["a", "b", "c", "d"]`
557+
|
558+
96 | field26: (
559+
97 | # First comment
560+
98 | / Literal["a", "b"]
561+
99 | | # Second comment
562+
100 | | | Literal["c", "d"]
563+
| |_______________________^ PYI030
564+
101 | )
565+
102 | field27: (
566+
|
567+
= help: Replace with a single `Literal`
568+
569+
Unsafe fix
570+
95 95 | # Should use unsafe fix when comments are deleted
571+
96 96 | field26: (
572+
97 97 | # First comment
573+
98 |- Literal["a", "b"]
574+
99 |- # Second comment
575+
100 |- | Literal["c", "d"]
576+
98 |+ Literal["a", "b", "c", "d"]
577+
101 99 | )
578+
102 100 | field27: (
579+
103 101 | Literal["a", "b"] # First comment
580+
581+
PYI030.py:103:5: PYI030 [*] Multiple literal members in a union. Use a single literal, e.g. `Literal["a", "b", "c", "d"]`
582+
|
583+
101 | )
584+
102 | field27: (
585+
103 | / Literal["a", "b"] # First comment
586+
104 | | | Literal["c", "d"] # Second comment
587+
| |_______________________^ PYI030
588+
105 | )
589+
|
590+
= help: Replace with a single `Literal`
591+
592+
Unsafe fix
593+
100 100 | | Literal["c", "d"]
594+
101 101 | )
595+
102 102 | field27: (
596+
103 |- Literal["a", "b"] # First comment
597+
104 |- | Literal["c", "d"] # Second comment
598+
103 |+ Literal["a", "b", "c", "d"] # Second comment
599+
105 104 | )

crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI030_PYI030.pyi.snap

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ PYI030.pyi:63:10: PYI030 [*] Multiple literal members in a union. Use a single l
377377
|
378378
= help: Replace with a single `Literal`
379379

380-
Safe fix
380+
Unsafe fix
381381
60 60 | field19: typing.Literal[1] | typing.Literal[2] # Error
382382
61 61 |
383383
62 62 | # Should emit in cases with newlines
@@ -517,6 +517,8 @@ PYI030.pyi:89:10: PYI030 [*] Multiple literal members in a union. Use a single l
517517
88 | # Should use the first literal subscript attribute when fixing
518518
89 | field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error
519519
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
520+
90 |
521+
91 | # Should use unsafe fix when comments are deleted
520522
|
521523
= help: Replace with a single `Literal`
522524

@@ -526,3 +528,51 @@ PYI030.pyi:89:10: PYI030 [*] Multiple literal members in a union. Use a single l
526528
88 88 | # Should use the first literal subscript attribute when fixing
527529
89 |-field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error
528530
89 |+field25: typing.Union[typing_extensions.Literal[1, 2, 3, 4], str] # Error
531+
90 90 |
532+
91 91 | # Should use unsafe fix when comments are deleted
533+
92 92 | field26: (
534+
535+
PYI030.pyi:94:5: PYI030 [*] Multiple literal members in a union. Use a single literal, e.g. `Literal["a", "b", "c", "d"]`
536+
|
537+
92 | field26: (
538+
93 | # First comment
539+
94 | / Literal["a", "b"]
540+
95 | | # Second comment
541+
96 | | | Literal["c", "d"]
542+
| |_______________________^ PYI030
543+
97 | )
544+
98 | field27: (
545+
|
546+
= help: Replace with a single `Literal`
547+
548+
Unsafe fix
549+
91 91 | # Should use unsafe fix when comments are deleted
550+
92 92 | field26: (
551+
93 93 | # First comment
552+
94 |- Literal["a", "b"]
553+
95 |- # Second comment
554+
96 |- | Literal["c", "d"]
555+
94 |+ Literal["a", "b", "c", "d"]
556+
97 95 | )
557+
98 96 | field27: (
558+
99 97 | Literal["a", "b"] # First comment
559+
560+
PYI030.pyi:99:5: PYI030 [*] Multiple literal members in a union. Use a single literal, e.g. `Literal["a", "b", "c", "d"]`
561+
|
562+
97 | )
563+
98 | field27: (
564+
99 | / Literal["a", "b"] # First comment
565+
100 | | | Literal["c", "d"] # Second comment
566+
| |_______________________^ PYI030
567+
101 | )
568+
|
569+
= help: Replace with a single `Literal`
570+
571+
Unsafe fix
572+
96 96 | | Literal["c", "d"]
573+
97 97 | )
574+
98 98 | field27: (
575+
99 |- Literal["a", "b"] # First comment
576+
100 |- | Literal["c", "d"] # Second comment
577+
99 |+ Literal["a", "b", "c", "d"] # Second comment
578+
101 100 | )

0 commit comments

Comments
 (0)