Skip to content

Commit ceb2bf1

Browse files
[flake8-pyi] Ensure Literal[None,] | Literal[None,] is not autofixed to None | None (PYI061) (#17659)
Co-authored-by: Alex Waygood <[email protected]>
1 parent f521358 commit ceb2bf1

File tree

8 files changed

+56
-94
lines changed

8 files changed

+56
-94
lines changed

crates/ruff/tests/lint.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3475,7 +3475,7 @@ requires-python = ">= 3.11"
34753475
&inner_pyproject,
34763476
r#"
34773477
[tool.ruff]
3478-
target-version = "py310"
3478+
target-version = "py310"
34793479
"#,
34803480
)?;
34813481

@@ -4980,6 +4980,53 @@ fn flake8_import_convention_unused_aliased_import_no_conflict() {
49804980
.pass_stdin("1"));
49814981
}
49824982

4983+
// See: https://github.com/astral-sh/ruff/issues/16177
4984+
#[test]
4985+
fn flake8_pyi_redundant_none_literal() {
4986+
let snippet = r#"
4987+
from typing import Literal
4988+
4989+
# For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements
4990+
# but not both, as if both were autofixed it would result in `None | None`,
4991+
# which leads to a `TypeError` at runtime.
4992+
a: Literal[None,] | Literal[None,]
4993+
b: Literal[None] | Literal[None]
4994+
c: Literal[None] | Literal[None,]
4995+
d: Literal[None,] | Literal[None]
4996+
"#;
4997+
4998+
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
4999+
.args(STDIN_BASE_OPTIONS)
5000+
.args(["--select", "PYI061"])
5001+
.args(["--stdin-filename", "test.py"])
5002+
.arg("--preview")
5003+
.arg("--diff")
5004+
.arg("-")
5005+
.pass_stdin(snippet), @r"
5006+
success: false
5007+
exit_code: 1
5008+
----- stdout -----
5009+
--- test.py
5010+
+++ test.py
5011+
@@ -4,7 +4,7 @@
5012+
# For each of these expressions, Ruff provides a fix for one of the `Literal[None]` elements
5013+
# but not both, as if both were autofixed it would result in `None | None`,
5014+
# which leads to a `TypeError` at runtime.
5015+
-a: Literal[None,] | Literal[None,]
5016+
-b: Literal[None] | Literal[None]
5017+
-c: Literal[None] | Literal[None,]
5018+
-d: Literal[None,] | Literal[None]
5019+
+a: None | Literal[None,]
5020+
+b: None | Literal[None]
5021+
+c: None | Literal[None,]
5022+
+d: None | Literal[None]
5023+
5024+
5025+
----- stderr -----
5026+
Would fix 4 errors.
5027+
");
5028+
}
5029+
49835030
/// Test that private, old-style `TypeVar` generics
49845031
/// 1. Get replaced with PEP 695 type parameters (UP046, UP047)
49855032
/// 2. Get renamed to remove leading underscores (UP049)

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,3 @@ def good_func(arg1: Literal[int] | None):
7878
c: (None | Literal[None]) | None
7979
d: None | (Literal[None] | None)
8080
e: None | ((None | Literal[None]) | None) | None
81-
f: Literal[None] | Literal[None]

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,3 @@ b: None | Literal[None] | None
5353
c: (None | Literal[None]) | None
5454
d: None | (Literal[None] | None)
5555
e: None | ((None | Literal[None]) | None) | None
56-
f: Literal[None] | Literal[None]

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

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use ruff_python_ast::{
55
self as ast,
66
helpers::{pep_604_union, typing_optional},
77
name::Name,
8-
Expr, ExprBinOp, ExprContext, ExprNoneLiteral, ExprSubscript, Operator, PythonVersion,
8+
Expr, ExprBinOp, ExprContext, ExprNoneLiteral, Operator, PythonVersion,
99
};
1010
use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union};
1111
use ruff_text_size::{Ranged, TextRange};
@@ -130,6 +130,12 @@ pub(crate) fn redundant_none_literal<'a>(checker: &Checker, literal_expr: &'a Ex
130130
literal_elements.clone(),
131131
union_kind,
132132
)
133+
// Isolate the fix to ensure multiple fixes on the same expression (like
134+
// `Literal[None,] | Literal[None,]` -> `None | None`) happen across separate passes,
135+
// preventing the production of invalid code.
136+
.map(|fix| {
137+
fix.map(|fix| fix.isolate(Checker::isolation(semantic.current_statement_id())))
138+
})
133139
});
134140
checker.report_diagnostic(diagnostic);
135141
}
@@ -172,18 +178,9 @@ fn create_fix(
172178

173179
traverse_union(
174180
&mut |expr, _| {
175-
if matches!(expr, Expr::NoneLiteral(_)) {
181+
if expr.is_none_literal_expr() {
176182
is_fixable = false;
177183
}
178-
if expr != literal_expr {
179-
if let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr {
180-
if semantic.match_typing_expr(value, "Literal")
181-
&& matches!(**slice, Expr::NoneLiteral(_))
182-
{
183-
is_fixable = false;
184-
}
185-
}
186-
}
187184
},
188185
semantic,
189186
enclosing_pep604_union,

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

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,6 @@ PYI061.py:79:20: PYI061 Use `None` rather than `Literal[None]`
422422
79 | d: None | (Literal[None] | None)
423423
| ^^^^ PYI061
424424
80 | e: None | ((None | Literal[None]) | None) | None
425-
81 | f: Literal[None] | Literal[None]
426425
|
427426
= help: Replace with `None`
428427

@@ -432,24 +431,5 @@ PYI061.py:80:28: PYI061 Use `None` rather than `Literal[None]`
432431
79 | d: None | (Literal[None] | None)
433432
80 | e: None | ((None | Literal[None]) | None) | None
434433
| ^^^^ PYI061
435-
81 | f: Literal[None] | Literal[None]
436-
|
437-
= help: Replace with `None`
438-
439-
PYI061.py:81:12: PYI061 Use `None` rather than `Literal[None]`
440-
|
441-
79 | d: None | (Literal[None] | None)
442-
80 | e: None | ((None | Literal[None]) | None) | None
443-
81 | f: Literal[None] | Literal[None]
444-
| ^^^^ PYI061
445-
|
446-
= help: Replace with `None`
447-
448-
PYI061.py:81:28: PYI061 Use `None` rather than `Literal[None]`
449-
|
450-
79 | d: None | (Literal[None] | None)
451-
80 | e: None | ((None | Literal[None]) | None) | None
452-
81 | f: Literal[None] | Literal[None]
453-
| ^^^^ PYI061
454434
|
455435
= help: Replace with `None`

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

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,6 @@ PYI061.pyi:54:20: PYI061 Use `None` rather than `Literal[None]`
291291
54 | d: None | (Literal[None] | None)
292292
| ^^^^ PYI061
293293
55 | e: None | ((None | Literal[None]) | None) | None
294-
56 | f: Literal[None] | Literal[None]
295294
|
296295
= help: Replace with `None`
297296

@@ -301,24 +300,5 @@ PYI061.pyi:55:28: PYI061 Use `None` rather than `Literal[None]`
301300
54 | d: None | (Literal[None] | None)
302301
55 | e: None | ((None | Literal[None]) | None) | None
303302
| ^^^^ PYI061
304-
56 | f: Literal[None] | Literal[None]
305-
|
306-
= help: Replace with `None`
307-
308-
PYI061.pyi:56:12: PYI061 Use `None` rather than `Literal[None]`
309-
|
310-
54 | d: None | (Literal[None] | None)
311-
55 | e: None | ((None | Literal[None]) | None) | None
312-
56 | f: Literal[None] | Literal[None]
313-
| ^^^^ PYI061
314-
|
315-
= help: Replace with `None`
316-
317-
PYI061.pyi:56:28: PYI061 Use `None` rather than `Literal[None]`
318-
|
319-
54 | d: None | (Literal[None] | None)
320-
55 | e: None | ((None | Literal[None]) | None) | None
321-
56 | f: Literal[None] | Literal[None]
322-
| ^^^^ PYI061
323303
|
324304
= help: Replace with `None`

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

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,6 @@ PYI061.py:79:20: PYI061 Use `None` rather than `Literal[None]`
464464
79 | d: None | (Literal[None] | None)
465465
| ^^^^ PYI061
466466
80 | e: None | ((None | Literal[None]) | None) | None
467-
81 | f: Literal[None] | Literal[None]
468467
|
469468
= help: Replace with `None`
470469

@@ -474,24 +473,5 @@ PYI061.py:80:28: PYI061 Use `None` rather than `Literal[None]`
474473
79 | d: None | (Literal[None] | None)
475474
80 | e: None | ((None | Literal[None]) | None) | None
476475
| ^^^^ PYI061
477-
81 | f: Literal[None] | Literal[None]
478-
|
479-
= help: Replace with `None`
480-
481-
PYI061.py:81:12: PYI061 Use `None` rather than `Literal[None]`
482-
|
483-
79 | d: None | (Literal[None] | None)
484-
80 | e: None | ((None | Literal[None]) | None) | None
485-
81 | f: Literal[None] | Literal[None]
486-
| ^^^^ PYI061
487-
|
488-
= help: Replace with `None`
489-
490-
PYI061.py:81:28: PYI061 Use `None` rather than `Literal[None]`
491-
|
492-
79 | d: None | (Literal[None] | None)
493-
80 | e: None | ((None | Literal[None]) | None) | None
494-
81 | f: Literal[None] | Literal[None]
495-
| ^^^^ PYI061
496476
|
497477
= help: Replace with `None`

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

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,6 @@ PYI061.pyi:54:20: PYI061 Use `None` rather than `Literal[None]`
291291
54 | d: None | (Literal[None] | None)
292292
| ^^^^ PYI061
293293
55 | e: None | ((None | Literal[None]) | None) | None
294-
56 | f: Literal[None] | Literal[None]
295294
|
296295
= help: Replace with `None`
297296

@@ -301,24 +300,5 @@ PYI061.pyi:55:28: PYI061 Use `None` rather than `Literal[None]`
301300
54 | d: None | (Literal[None] | None)
302301
55 | e: None | ((None | Literal[None]) | None) | None
303302
| ^^^^ PYI061
304-
56 | f: Literal[None] | Literal[None]
305-
|
306-
= help: Replace with `None`
307-
308-
PYI061.pyi:56:12: PYI061 Use `None` rather than `Literal[None]`
309-
|
310-
54 | d: None | (Literal[None] | None)
311-
55 | e: None | ((None | Literal[None]) | None) | None
312-
56 | f: Literal[None] | Literal[None]
313-
| ^^^^ PYI061
314-
|
315-
= help: Replace with `None`
316-
317-
PYI061.pyi:56:28: PYI061 Use `None` rather than `Literal[None]`
318-
|
319-
54 | d: None | (Literal[None] | None)
320-
55 | e: None | ((None | Literal[None]) | None) | None
321-
56 | f: Literal[None] | Literal[None]
322-
| ^^^^ PYI061
323303
|
324304
= help: Replace with `None`

0 commit comments

Comments
 (0)