From 9a55a6b849546d27ec7d934ff08e85a7e387d4fa Mon Sep 17 00:00:00 2001 From: Simon Brugman Date: Mon, 25 Nov 2024 14:59:26 +0100 Subject: [PATCH 1/8] Avoid syntax errors on fix. --- .../test/fixtures/flake8_pyi/PYI061.py | 16 +- .../test/fixtures/flake8_pyi/PYI061.pyi | 14 +- .../rules/redundant_none_literal.rs | 33 ++- ...__flake8_pyi__tests__PYI061_PYI061.py.snap | 231 ++++++++++++------ ..._flake8_pyi__tests__PYI061_PYI061.pyi.snap | 121 +++++++-- 5 files changed, 324 insertions(+), 91 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py index aa7d8d37345ff..87c4d427c2109 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py @@ -1,4 +1,4 @@ -from typing import Literal +from typing import Literal, Union def func1(arg1: Literal[None]): @@ -35,6 +35,14 @@ def func7(arg1: Literal[ ... +def func8(arg1: Literal[None] | None): + ... + + +def func9(arg1: Union[Literal[None], None]): + ... + + # OK def good_func(arg1: Literal[int] | None): ... @@ -58,3 +66,9 @@ def good_func(arg1: Literal[int] | None): # and there are no None members in the Literal[] slice, # only emit Y062: Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" + + +# Regression tests for https://github.com/astral-sh/ruff/issues/14567 +x: Literal[None] | None +y: None | Literal[None] +z: Union[Literal[None], None] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi index a43ab1460d844..38abfc51ae580 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi @@ -1,4 +1,4 @@ -from typing import Literal +from typing import Literal, Union def func1(arg1: Literal[None]): ... @@ -28,6 +28,12 @@ def func7(arg1: Literal[ ]): ... +def func8(arg1: Literal[None] | None):... + + +def func9(arg1: Union[Literal[None], None]): ... + + # OK def good_func(arg1: Literal[int] | None): ... @@ -35,3 +41,9 @@ def good_func(arg1: Literal[int] | None): ... # From flake8-pyi Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" + + +# Regression tests for https://github.com/astral-sh/ruff/issues/14567 +x: Literal[None] | None +y: None | Literal[None] +z: Union[Literal[None], None] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index da8066943f249..56ad4c99563f8 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -1,6 +1,6 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::{Expr, ExprNoneLiteral}; +use ruff_python_ast::{Expr, ExprBinOp, ExprNoneLiteral, Operator}; use ruff_python_semantic::analyze::typing::traverse_literal; use ruff_text_size::Ranged; @@ -31,6 +31,9 @@ use crate::checkers::ast::Checker; /// Literal[1, 2, 3, "foo", 5] | None /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as safe unless the literal contains comments. +/// /// ## References /// - [Typing documentation: Legal parameters for `Literal` at type check time](https://typing.readthedocs.io/en/latest/spec/literal.html#legal-parameters-for-literal-at-type-check-time) #[violation] @@ -87,8 +90,34 @@ pub(crate) fn redundant_none_literal<'a>(checker: &mut Checker, literal_expr: &' let fix = if other_literal_elements_seen { None } else { + // Avoid producing syntax errors when `Literal[None] | None` would be fixed to + // `None | None`. Instead fix to `None`. No action needed from `typing.Union`, + // as `Union[None, None]` is valid syntax. + // See https://github.com/astral-sh/ruff/issues/14567. + let replacement_range = if let Some(parent) = checker.semantic().current_expression_parent() + { + if let Expr::BinOp(ExprBinOp { + left, + op: Operator::BitOr, + right, + .. + }) = parent + { + if matches!(**left, Expr::NoneLiteral(_)) || matches!(**right, Expr::NoneLiteral(_)) + { + parent.range() + } else { + literal_expr.range() + } + } else { + literal_expr.range() + } + } else { + literal_expr.range() + }; + Some(Fix::applicable_edit( - Edit::range_replacement("None".to_string(), literal_expr.range()), + Edit::range_replacement("None".to_string(), replacement_range), if checker.comment_ranges().intersects(literal_expr.range()) { Applicability::Unsafe } else { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap index 41a074e440299..0d8d18445b0b4 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap @@ -10,7 +10,7 @@ PYI061.py:4:25: PYI061 [*] `Literal[None]` can be replaced with `None` = help: Replace with `None` ℹ Safe fix -1 1 | from typing import Literal +1 1 | from typing import Literal, Union 2 2 | 3 3 | 4 |-def func1(arg1: Literal[None]): @@ -132,112 +132,201 @@ PYI061.py:33:5: PYI061 [*] `Literal[None]` can be replaced with `None` 36 34 | 37 35 | -PYI061.py:44:9: PYI061 [*] `Literal[None]` can be replaced with `None` +PYI061.py:38:25: PYI061 [*] `Literal[None]` can be replaced with `None` | -43 | # From flake8-pyi -44 | Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None" - | ^^^^ PYI061 -45 | Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" +38 | def func8(arg1: Literal[None] | None): + | ^^^^ PYI061 +39 | ... | = help: Replace with `None` ℹ Safe fix +35 35 | ... +36 36 | +37 37 | +38 |-def func8(arg1: Literal[None] | None): + 38 |+def func8(arg1: None): +39 39 | ... +40 40 | 41 41 | -42 42 | -43 43 | # From flake8-pyi -44 |-Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None" - 44 |+None # Y061 None inside "Literal[]" expression. Replace with "None" -45 45 | Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" -46 46 | -47 47 | ### - -PYI061.py:45:15: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` - | -43 | # From flake8-pyi -44 | Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None" -45 | Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" + +PYI061.py:42:31: PYI061 [*] `Literal[None]` can be replaced with `None` + | +42 | def func9(arg1: Union[Literal[None], None]): + | ^^^^ PYI061 +43 | ... + | + = help: Replace with `None` + +ℹ Safe fix +39 39 | ... +40 40 | +41 41 | +42 |-def func9(arg1: Union[Literal[None], None]): + 42 |+def func9(arg1: Union[None, None]): +43 43 | ... +44 44 | +45 45 | + +PYI061.py:52:9: PYI061 [*] `Literal[None]` can be replaced with `None` + | +51 | # From flake8-pyi +52 | Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None" + | ^^^^ PYI061 +53 | Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" + | + = help: Replace with `None` + +ℹ Safe fix +49 49 | +50 50 | +51 51 | # From flake8-pyi +52 |-Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None" + 52 |+None # Y061 None inside "Literal[]" expression. Replace with "None" +53 53 | Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" +54 54 | +55 55 | ### + +PYI061.py:53:15: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` + | +51 | # From flake8-pyi +52 | Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None" +53 | Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" | ^^^^ PYI061 -46 | -47 | ### +54 | +55 | ### | = help: Replace with `Literal[...] | None` -PYI061.py:54:9: PYI061 [*] `Literal[None]` can be replaced with `None` +PYI061.py:62:9: PYI061 [*] `Literal[None]` can be replaced with `None` | -52 | # If Y061 and Y062 both apply, but all the duplicate members are None, -53 | # only emit Y061... -54 | Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" +60 | # If Y061 and Y062 both apply, but all the duplicate members are None, +61 | # only emit Y061... +62 | Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" | ^^^^ PYI061 -55 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" +63 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" | = help: Replace with `None` ℹ Safe fix -51 51 | -52 52 | # If Y061 and Y062 both apply, but all the duplicate members are None, -53 53 | # only emit Y061... -54 |-Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" - 54 |+None # Y061 None inside "Literal[]" expression. Replace with "None" -55 55 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" -56 56 | -57 57 | # ... but if Y061 and Y062 both apply - -PYI061.py:54:15: PYI061 [*] `Literal[None]` can be replaced with `None` - | -52 | # If Y061 and Y062 both apply, but all the duplicate members are None, -53 | # only emit Y061... -54 | Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" +59 59 | +60 60 | # If Y061 and Y062 both apply, but all the duplicate members are None, +61 61 | # only emit Y061... +62 |-Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" + 62 |+None # Y061 None inside "Literal[]" expression. Replace with "None" +63 63 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" +64 64 | +65 65 | # ... but if Y061 and Y062 both apply + +PYI061.py:62:15: PYI061 [*] `Literal[None]` can be replaced with `None` + | +60 | # If Y061 and Y062 both apply, but all the duplicate members are None, +61 | # only emit Y061... +62 | Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" | ^^^^ PYI061 -55 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" +63 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" | = help: Replace with `None` ℹ Safe fix -51 51 | -52 52 | # If Y061 and Y062 both apply, but all the duplicate members are None, -53 53 | # only emit Y061... -54 |-Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" - 54 |+None # Y061 None inside "Literal[]" expression. Replace with "None" -55 55 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" -56 56 | -57 57 | # ... but if Y061 and Y062 both apply - -PYI061.py:55:12: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` - | -53 | # only emit Y061... -54 | Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" -55 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" +59 59 | +60 60 | # If Y061 and Y062 both apply, but all the duplicate members are None, +61 61 | # only emit Y061... +62 |-Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" + 62 |+None # Y061 None inside "Literal[]" expression. Replace with "None" +63 63 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" +64 64 | +65 65 | # ... but if Y061 and Y062 both apply + +PYI061.py:63:12: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` + | +61 | # only emit Y061... +62 | Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" +63 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" | ^^^^ PYI061 -56 | -57 | # ... but if Y061 and Y062 both apply +64 | +65 | # ... but if Y061 and Y062 both apply | = help: Replace with `Literal[...] | None` -PYI061.py:55:25: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` +PYI061.py:63:25: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` | -53 | # only emit Y061... -54 | Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" -55 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" +61 | # only emit Y061... +62 | Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" +63 | Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" | ^^^^ PYI061 -56 | -57 | # ... but if Y061 and Y062 both apply +64 | +65 | # ... but if Y061 and Y062 both apply | = help: Replace with `Literal[...] | None` -PYI061.py:60:9: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` +PYI061.py:68:9: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` | -58 | # and there are no None members in the Literal[] slice, -59 | # only emit Y062: -60 | Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" +66 | # and there are no None members in the Literal[] slice, +67 | # only emit Y062: +68 | Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" | ^^^^ PYI061 | = help: Replace with `Literal[...] | None` -PYI061.py:60:21: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` +PYI061.py:68:21: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` | -58 | # and there are no None members in the Literal[] slice, -59 | # only emit Y062: -60 | Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" +66 | # and there are no None members in the Literal[] slice, +67 | # only emit Y062: +68 | Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" | ^^^^ PYI061 | = help: Replace with `Literal[...] | None` + +PYI061.py:72:12: PYI061 [*] `Literal[None]` can be replaced with `None` + | +71 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 +72 | x: Literal[None] | None + | ^^^^ PYI061 +73 | y: None | Literal[None] +74 | z: Union[Literal[None], None] + | + = help: Replace with `None` + +ℹ Safe fix +69 69 | +70 70 | +71 71 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 +72 |-x: Literal[None] | None + 72 |+x: None +73 73 | y: None | Literal[None] +74 74 | z: Union[Literal[None], None] + +PYI061.py:73:19: PYI061 [*] `Literal[None]` can be replaced with `None` + | +71 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 +72 | x: Literal[None] | None +73 | y: None | Literal[None] + | ^^^^ PYI061 +74 | z: Union[Literal[None], None] + | + = help: Replace with `None` + +ℹ Safe fix +70 70 | +71 71 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 +72 72 | x: Literal[None] | None +73 |-y: None | Literal[None] + 73 |+y: None +74 74 | z: Union[Literal[None], None] + +PYI061.py:74:18: PYI061 [*] `Literal[None]` can be replaced with `None` + | +72 | x: Literal[None] | None +73 | y: None | Literal[None] +74 | z: Union[Literal[None], None] + | ^^^^ PYI061 + | + = help: Replace with `None` + +ℹ Safe fix +71 71 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 +72 72 | x: Literal[None] | None +73 73 | y: None | Literal[None] +74 |-z: Union[Literal[None], None] + 74 |+z: Union[None, None] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap index e7ca825f64edd..01fe419b2de9c 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap @@ -9,7 +9,7 @@ PYI061.pyi:4:25: PYI061 [*] `Literal[None]` can be replaced with `None` = help: Replace with `None` ℹ Safe fix -1 1 | from typing import Literal +1 1 | from typing import Literal, Union 2 2 | 3 3 | 4 |-def func1(arg1: Literal[None]): ... @@ -123,30 +123,119 @@ PYI061.pyi:27:5: PYI061 [*] `Literal[None]` can be replaced with `None` 26 |+def func7(arg1: None): ... 29 27 | 30 28 | -31 29 | # OK +31 29 | def func8(arg1: Literal[None] | None):... -PYI061.pyi:36:9: PYI061 [*] `Literal[None]` can be replaced with `None` +PYI061.pyi:31:25: PYI061 [*] `Literal[None]` can be replaced with `None` | -35 | # From flake8-pyi -36 | Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" - | ^^^^ PYI061 -37 | Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" +31 | def func8(arg1: Literal[None] | None):... + | ^^^^ PYI061 + | + = help: Replace with `None` + +ℹ Safe fix +28 28 | ]): ... +29 29 | +30 30 | +31 |-def func8(arg1: Literal[None] | None):... + 31 |+def func8(arg1: None):... +32 32 | +33 33 | +34 34 | def func9(arg1: Union[Literal[None], None]): ... + +PYI061.pyi:34:31: PYI061 [*] `Literal[None]` can be replaced with `None` + | +34 | def func9(arg1: Union[Literal[None], None]): ... + | ^^^^ PYI061 | = help: Replace with `None` ℹ Safe fix +31 31 | def func8(arg1: Literal[None] | None):... +32 32 | 33 33 | -34 34 | -35 35 | # From flake8-pyi -36 |-Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" - 36 |+None # PYI061 None inside "Literal[]" expression. Replace with "None" -37 37 | Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" +34 |-def func9(arg1: Union[Literal[None], None]): ... + 34 |+def func9(arg1: Union[None, None]): ... +35 35 | +36 36 | +37 37 | # OK -PYI061.pyi:37:15: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` +PYI061.pyi:42:9: PYI061 [*] `Literal[None]` can be replaced with `None` + | +41 | # From flake8-pyi +42 | Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" + | ^^^^ PYI061 +43 | Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" | -35 | # From flake8-pyi -36 | Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" -37 | Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" + = help: Replace with `None` + +ℹ Safe fix +39 39 | +40 40 | +41 41 | # From flake8-pyi +42 |-Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" + 42 |+None # PYI061 None inside "Literal[]" expression. Replace with "None" +43 43 | Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" +44 44 | +45 45 | + +PYI061.pyi:43:15: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | None` + | +41 | # From flake8-pyi +42 | Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" +43 | Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" | ^^^^ PYI061 | = help: Replace with `Literal[...] | None` + +PYI061.pyi:47:12: PYI061 [*] `Literal[None]` can be replaced with `None` + | +46 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 +47 | x: Literal[None] | None + | ^^^^ PYI061 +48 | y: None | Literal[None] +49 | z: Union[Literal[None], None] + | + = help: Replace with `None` + +ℹ Safe fix +44 44 | +45 45 | +46 46 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 +47 |-x: Literal[None] | None + 47 |+x: None +48 48 | y: None | Literal[None] +49 49 | z: Union[Literal[None], None] + +PYI061.pyi:48:19: PYI061 [*] `Literal[None]` can be replaced with `None` + | +46 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 +47 | x: Literal[None] | None +48 | y: None | Literal[None] + | ^^^^ PYI061 +49 | z: Union[Literal[None], None] + | + = help: Replace with `None` + +ℹ Safe fix +45 45 | +46 46 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 +47 47 | x: Literal[None] | None +48 |-y: None | Literal[None] + 48 |+y: None +49 49 | z: Union[Literal[None], None] + +PYI061.pyi:49:18: PYI061 [*] `Literal[None]` can be replaced with `None` + | +47 | x: Literal[None] | None +48 | y: None | Literal[None] +49 | z: Union[Literal[None], None] + | ^^^^ PYI061 + | + = help: Replace with `None` + +ℹ Safe fix +46 46 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 +47 47 | x: Literal[None] | None +48 48 | y: None | Literal[None] +49 |-z: Union[Literal[None], None] + 49 |+z: Union[None, None] From 70f1f064d49d90b3018dd35179db8f54cd8c08d0 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 25 Nov 2024 14:06:35 +0000 Subject: [PATCH 2/8] comment nit --- .../src/rules/flake8_pyi/rules/redundant_none_literal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 56ad4c99563f8..7c56d097f572e 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -92,7 +92,7 @@ pub(crate) fn redundant_none_literal<'a>(checker: &mut Checker, literal_expr: &' } else { // Avoid producing syntax errors when `Literal[None] | None` would be fixed to // `None | None`. Instead fix to `None`. No action needed from `typing.Union`, - // as `Union[None, None]` is valid syntax. + // as `Union[None, None]` is valid Python. // See https://github.com/astral-sh/ruff/issues/14567. let replacement_range = if let Some(parent) = checker.semantic().current_expression_parent() { From 7ee67f6956f83d52bcdb2918f1e82f1a862698de Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 25 Nov 2024 14:07:06 +0000 Subject: [PATCH 3/8] Update crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs --- .../src/rules/flake8_pyi/rules/redundant_none_literal.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 7c56d097f572e..5356d27f526ba 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -90,7 +90,8 @@ pub(crate) fn redundant_none_literal<'a>(checker: &mut Checker, literal_expr: &' let fix = if other_literal_elements_seen { None } else { - // Avoid producing syntax errors when `Literal[None] | None` would be fixed to + // Avoid producing code that would raise an exception + // when `Literal[None] | None` would be fixed to // `None | None`. Instead fix to `None`. No action needed from `typing.Union`, // as `Union[None, None]` is valid Python. // See https://github.com/astral-sh/ruff/issues/14567. From 78bf0cf30505edc3877d5e52596e1a45dc353bd8 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 25 Nov 2024 14:07:51 +0000 Subject: [PATCH 4/8] Update crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs --- .../src/rules/flake8_pyi/rules/redundant_none_literal.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 5356d27f526ba..0e7afd71d5f4b 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -90,9 +90,9 @@ pub(crate) fn redundant_none_literal<'a>(checker: &mut Checker, literal_expr: &' let fix = if other_literal_elements_seen { None } else { - // Avoid producing code that would raise an exception - // when `Literal[None] | None` would be fixed to - // `None | None`. Instead fix to `None`. No action needed from `typing.Union`, + // Avoid producing code that would raise an exception when + // `Literal[None] | None` would be fixed to `None | None`. + // Instead fix to `None`. No action needed for `typing.Union`, // as `Union[None, None]` is valid Python. // See https://github.com/astral-sh/ruff/issues/14567. let replacement_range = if let Some(parent) = checker.semantic().current_expression_parent() From 175586f98d95684016b47b8160818e4221f56677 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 25 Nov 2024 14:11:20 +0000 Subject: [PATCH 5/8] simplify slightly --- .../rules/redundant_none_literal.rs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 0e7afd71d5f4b..29a62024feabf 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -90,26 +90,20 @@ pub(crate) fn redundant_none_literal<'a>(checker: &mut Checker, literal_expr: &' let fix = if other_literal_elements_seen { None } else { - // Avoid producing code that would raise an exception when + // Avoid producing code that would raise an exception when // `Literal[None] | None` would be fixed to `None | None`. // Instead fix to `None`. No action needed for `typing.Union`, // as `Union[None, None]` is valid Python. // See https://github.com/astral-sh/ruff/issues/14567. - let replacement_range = if let Some(parent) = checker.semantic().current_expression_parent() + let replacement_range = if let Some(Expr::BinOp(ExprBinOp { + left, + op: Operator::BitOr, + right, + range: parent_range + })) = checker.semantic().current_expression_parent() { - if let Expr::BinOp(ExprBinOp { - left, - op: Operator::BitOr, - right, - .. - }) = parent - { - if matches!(**left, Expr::NoneLiteral(_)) || matches!(**right, Expr::NoneLiteral(_)) - { - parent.range() - } else { - literal_expr.range() - } + if matches!(**left, Expr::NoneLiteral(_)) || matches!(**right, Expr::NoneLiteral(_)) { + *parent_range } else { literal_expr.range() } From df2683a4d2e57670528fc112fca105f5ccc5f48b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 25 Nov 2024 14:25:29 +0000 Subject: [PATCH 6/8] fmt --- .../src/rules/flake8_pyi/rules/redundant_none_literal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 29a62024feabf..1f8bfb480274c 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -99,7 +99,7 @@ pub(crate) fn redundant_none_literal<'a>(checker: &mut Checker, literal_expr: &' left, op: Operator::BitOr, right, - range: parent_range + range: parent_range, })) = checker.semantic().current_expression_parent() { if matches!(**left, Expr::NoneLiteral(_)) || matches!(**right, Expr::NoneLiteral(_)) { From 6a05263badd1032732e237242a6b96825c94bd7b Mon Sep 17 00:00:00 2001 From: Simon Brugman Date: Mon, 25 Nov 2024 18:04:12 +0100 Subject: [PATCH 7/8] Abstain from fixing when union contains bare None literal. --- .../test/fixtures/flake8_pyi/PYI061.py | 7 ++ .../test/fixtures/flake8_pyi/PYI061.pyi | 7 ++ .../rules/redundant_none_literal.rs | 96 ++++++++++----- ...__flake8_pyi__tests__PYI061_PYI061.py.snap | 109 +++++++++++++----- ..._flake8_pyi__tests__PYI061_PYI061.pyi.snap | 109 +++++++++++++----- 5 files changed, 237 insertions(+), 91 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py index 87c4d427c2109..4b0cf66f923ae 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py @@ -72,3 +72,10 @@ def good_func(arg1: Literal[int] | None): x: Literal[None] | None y: None | Literal[None] z: Union[Literal[None], None] + +a: int | Literal[None] | None +b: None | Literal[None] | None +c: (None | Literal[None]) | None +d: None | (Literal[None] | None) +e: None | ((None | Literal[None]) | None) | None +f: Literal[None] | Literal[None] diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi index 38abfc51ae580..ed879dd646e34 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi @@ -47,3 +47,10 @@ Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with " x: Literal[None] | None y: None | Literal[None] z: Union[Literal[None], None] + +a: int | Literal[None] | None +b: None | Literal[None] | None +c: (None | Literal[None]) | None +d: None | (Literal[None] | None) +e: None | ((None | Literal[None]) | None) | None +f: Literal[None] | Literal[None] diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 1f8bfb480274c..40d66fd1718fd 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -1,7 +1,10 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::{Expr, ExprBinOp, ExprNoneLiteral, Operator}; -use ruff_python_semantic::analyze::typing::traverse_literal; +use ruff_python_ast::{Expr, ExprBinOp, ExprNoneLiteral, ExprSubscript, Operator}; +use ruff_python_semantic::{ + analyze::typing::{traverse_literal, traverse_union}, + SemanticModel, +}; use ruff_text_size::Ranged; use smallvec::SmallVec; @@ -90,35 +93,16 @@ pub(crate) fn redundant_none_literal<'a>(checker: &mut Checker, literal_expr: &' let fix = if other_literal_elements_seen { None } else { - // Avoid producing code that would raise an exception when - // `Literal[None] | None` would be fixed to `None | None`. - // Instead fix to `None`. No action needed for `typing.Union`, - // as `Union[None, None]` is valid Python. - // See https://github.com/astral-sh/ruff/issues/14567. - let replacement_range = if let Some(Expr::BinOp(ExprBinOp { - left, - op: Operator::BitOr, - right, - range: parent_range, - })) = checker.semantic().current_expression_parent() - { - if matches!(**left, Expr::NoneLiteral(_)) || matches!(**right, Expr::NoneLiteral(_)) { - *parent_range - } else { - literal_expr.range() - } - } else { - literal_expr.range() - }; - - Some(Fix::applicable_edit( - Edit::range_replacement("None".to_string(), replacement_range), - if checker.comment_ranges().intersects(literal_expr.range()) { - Applicability::Unsafe - } else { - Applicability::Safe - }, - )) + create_fix_edit(checker.semantic(), literal_expr).map(|edit| { + Fix::applicable_edit( + edit, + if checker.comment_ranges().intersects(literal_expr.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + ) + }) }; for none_expr in none_exprs { @@ -134,3 +118,53 @@ pub(crate) fn redundant_none_literal<'a>(checker: &mut Checker, literal_expr: &' checker.diagnostics.push(diagnostic); } } + +fn create_fix_edit(semantic: &SemanticModel, literal_expr: &Expr) -> Option { + let mut enclosing_union = None; + let mut expression_ancestors = semantic.current_expressions().skip(1); + let mut parent_expr = expression_ancestors.next(); + while let Some(Expr::BinOp(ExprBinOp { + op: Operator::BitOr, + .. + })) = parent_expr + { + enclosing_union = parent_expr; + parent_expr = expression_ancestors.next(); + } + + let mut is_union_with_bare_none = false; + if let Some(enclosing_union) = enclosing_union { + traverse_union( + &mut |expr, _| { + if matches!(expr, Expr::NoneLiteral(_)) { + is_union_with_bare_none = true; + } + if expr != literal_expr { + if let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr { + if semantic.match_typing_expr(value, "Literal") + && matches!(**slice, Expr::NoneLiteral(_)) + { + is_union_with_bare_none = true; + } + } + } + }, + semantic, + enclosing_union, + ); + } + + // Avoid producing code that would raise an exception when + // `Literal[None] | None` would be fixed to `None | None`. + // Instead do not provide a fix. No action needed for `typing.Union`, + // as `Union[None, None]` is valid Python. + // See https://github.com/astral-sh/ruff/issues/14567. + if is_union_with_bare_none { + None + } else { + Some(Edit::range_replacement( + "None".to_string(), + literal_expr.range(), + )) + } +} diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap index 0d8d18445b0b4..ea36c6940e8dc 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.py.snap @@ -132,7 +132,7 @@ PYI061.py:33:5: PYI061 [*] `Literal[None]` can be replaced with `None` 36 34 | 37 35 | -PYI061.py:38:25: PYI061 [*] `Literal[None]` can be replaced with `None` +PYI061.py:38:25: PYI061 `Literal[None]` can be replaced with `None` | 38 | def func8(arg1: Literal[None] | None): | ^^^^ PYI061 @@ -140,16 +140,6 @@ PYI061.py:38:25: PYI061 [*] `Literal[None]` can be replaced with `None` | = help: Replace with `None` -ℹ Safe fix -35 35 | ... -36 36 | -37 37 | -38 |-def func8(arg1: Literal[None] | None): - 38 |+def func8(arg1: None): -39 39 | ... -40 40 | -41 41 | - PYI061.py:42:31: PYI061 [*] `Literal[None]` can be replaced with `None` | 42 | def func9(arg1: Union[Literal[None], None]): @@ -278,7 +268,7 @@ PYI061.py:68:21: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | = help: Replace with `Literal[...] | None` -PYI061.py:72:12: PYI061 [*] `Literal[None]` can be replaced with `None` +PYI061.py:72:12: PYI061 `Literal[None]` can be replaced with `None` | 71 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 72 | x: Literal[None] | None @@ -288,16 +278,7 @@ PYI061.py:72:12: PYI061 [*] `Literal[None]` can be replaced with `None` | = help: Replace with `None` -ℹ Safe fix -69 69 | -70 70 | -71 71 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 -72 |-x: Literal[None] | None - 72 |+x: None -73 73 | y: None | Literal[None] -74 74 | z: Union[Literal[None], None] - -PYI061.py:73:19: PYI061 [*] `Literal[None]` can be replaced with `None` +PYI061.py:73:19: PYI061 `Literal[None]` can be replaced with `None` | 71 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 72 | x: Literal[None] | None @@ -307,20 +288,14 @@ PYI061.py:73:19: PYI061 [*] `Literal[None]` can be replaced with `None` | = help: Replace with `None` -ℹ Safe fix -70 70 | -71 71 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 -72 72 | x: Literal[None] | None -73 |-y: None | Literal[None] - 73 |+y: None -74 74 | z: Union[Literal[None], None] - PYI061.py:74:18: PYI061 [*] `Literal[None]` can be replaced with `None` | 72 | x: Literal[None] | None 73 | y: None | Literal[None] 74 | z: Union[Literal[None], None] | ^^^^ PYI061 +75 | +76 | a: int | Literal[None] | None | = help: Replace with `None` @@ -330,3 +305,77 @@ PYI061.py:74:18: PYI061 [*] `Literal[None]` can be replaced with `None` 73 73 | y: None | Literal[None] 74 |-z: Union[Literal[None], None] 74 |+z: Union[None, None] +75 75 | +76 76 | a: int | Literal[None] | None +77 77 | b: None | Literal[None] | None + +PYI061.py:76:18: PYI061 `Literal[None]` can be replaced with `None` + | +74 | z: Union[Literal[None], None] +75 | +76 | a: int | Literal[None] | None + | ^^^^ PYI061 +77 | b: None | Literal[None] | None +78 | c: (None | Literal[None]) | None + | + = help: Replace with `None` + +PYI061.py:77:19: PYI061 `Literal[None]` can be replaced with `None` + | +76 | a: int | Literal[None] | None +77 | b: None | Literal[None] | None + | ^^^^ PYI061 +78 | c: (None | Literal[None]) | None +79 | d: None | (Literal[None] | None) + | + = help: Replace with `None` + +PYI061.py:78:20: PYI061 `Literal[None]` can be replaced with `None` + | +76 | a: int | Literal[None] | None +77 | b: None | Literal[None] | None +78 | c: (None | Literal[None]) | None + | ^^^^ PYI061 +79 | d: None | (Literal[None] | None) +80 | e: None | ((None | Literal[None]) | None) | None + | + = help: Replace with `None` + +PYI061.py:79:20: PYI061 `Literal[None]` can be replaced with `None` + | +77 | b: None | Literal[None] | None +78 | c: (None | Literal[None]) | None +79 | d: None | (Literal[None] | None) + | ^^^^ PYI061 +80 | e: None | ((None | Literal[None]) | None) | None +81 | f: Literal[None] | Literal[None] + | + = help: Replace with `None` + +PYI061.py:80:28: PYI061 `Literal[None]` can be replaced with `None` + | +78 | c: (None | Literal[None]) | None +79 | d: None | (Literal[None] | None) +80 | e: None | ((None | Literal[None]) | None) | None + | ^^^^ PYI061 +81 | f: Literal[None] | Literal[None] + | + = help: Replace with `None` + +PYI061.py:81:12: PYI061 `Literal[None]` can be replaced with `None` + | +79 | d: None | (Literal[None] | None) +80 | e: None | ((None | Literal[None]) | None) | None +81 | f: Literal[None] | Literal[None] + | ^^^^ PYI061 + | + = help: Replace with `None` + +PYI061.py:81:28: PYI061 `Literal[None]` can be replaced with `None` + | +79 | d: None | (Literal[None] | None) +80 | e: None | ((None | Literal[None]) | None) | None +81 | f: Literal[None] | Literal[None] + | ^^^^ PYI061 + | + = help: Replace with `None` diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap index 01fe419b2de9c..d1b695f73dbe1 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI061_PYI061.pyi.snap @@ -125,23 +125,13 @@ PYI061.pyi:27:5: PYI061 [*] `Literal[None]` can be replaced with `None` 30 28 | 31 29 | def func8(arg1: Literal[None] | None):... -PYI061.pyi:31:25: PYI061 [*] `Literal[None]` can be replaced with `None` +PYI061.pyi:31:25: PYI061 `Literal[None]` can be replaced with `None` | 31 | def func8(arg1: Literal[None] | None):... | ^^^^ PYI061 | = help: Replace with `None` -ℹ Safe fix -28 28 | ]): ... -29 29 | -30 30 | -31 |-def func8(arg1: Literal[None] | None):... - 31 |+def func8(arg1: None):... -32 32 | -33 33 | -34 34 | def func9(arg1: Union[Literal[None], None]): ... - PYI061.pyi:34:31: PYI061 [*] `Literal[None]` can be replaced with `None` | 34 | def func9(arg1: Union[Literal[None], None]): ... @@ -187,7 +177,7 @@ PYI061.pyi:43:15: PYI061 `Literal[None, ...]` can be replaced with `Literal[...] | = help: Replace with `Literal[...] | None` -PYI061.pyi:47:12: PYI061 [*] `Literal[None]` can be replaced with `None` +PYI061.pyi:47:12: PYI061 `Literal[None]` can be replaced with `None` | 46 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 47 | x: Literal[None] | None @@ -197,16 +187,7 @@ PYI061.pyi:47:12: PYI061 [*] `Literal[None]` can be replaced with `None` | = help: Replace with `None` -ℹ Safe fix -44 44 | -45 45 | -46 46 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 -47 |-x: Literal[None] | None - 47 |+x: None -48 48 | y: None | Literal[None] -49 49 | z: Union[Literal[None], None] - -PYI061.pyi:48:19: PYI061 [*] `Literal[None]` can be replaced with `None` +PYI061.pyi:48:19: PYI061 `Literal[None]` can be replaced with `None` | 46 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 47 | x: Literal[None] | None @@ -216,20 +197,14 @@ PYI061.pyi:48:19: PYI061 [*] `Literal[None]` can be replaced with `None` | = help: Replace with `None` -ℹ Safe fix -45 45 | -46 46 | # Regression tests for https://github.com/astral-sh/ruff/issues/14567 -47 47 | x: Literal[None] | None -48 |-y: None | Literal[None] - 48 |+y: None -49 49 | z: Union[Literal[None], None] - PYI061.pyi:49:18: PYI061 [*] `Literal[None]` can be replaced with `None` | 47 | x: Literal[None] | None 48 | y: None | Literal[None] 49 | z: Union[Literal[None], None] | ^^^^ PYI061 +50 | +51 | a: int | Literal[None] | None | = help: Replace with `None` @@ -239,3 +214,77 @@ PYI061.pyi:49:18: PYI061 [*] `Literal[None]` can be replaced with `None` 48 48 | y: None | Literal[None] 49 |-z: Union[Literal[None], None] 49 |+z: Union[None, None] +50 50 | +51 51 | a: int | Literal[None] | None +52 52 | b: None | Literal[None] | None + +PYI061.pyi:51:18: PYI061 `Literal[None]` can be replaced with `None` + | +49 | z: Union[Literal[None], None] +50 | +51 | a: int | Literal[None] | None + | ^^^^ PYI061 +52 | b: None | Literal[None] | None +53 | c: (None | Literal[None]) | None + | + = help: Replace with `None` + +PYI061.pyi:52:19: PYI061 `Literal[None]` can be replaced with `None` + | +51 | a: int | Literal[None] | None +52 | b: None | Literal[None] | None + | ^^^^ PYI061 +53 | c: (None | Literal[None]) | None +54 | d: None | (Literal[None] | None) + | + = help: Replace with `None` + +PYI061.pyi:53:20: PYI061 `Literal[None]` can be replaced with `None` + | +51 | a: int | Literal[None] | None +52 | b: None | Literal[None] | None +53 | c: (None | Literal[None]) | None + | ^^^^ PYI061 +54 | d: None | (Literal[None] | None) +55 | e: None | ((None | Literal[None]) | None) | None + | + = help: Replace with `None` + +PYI061.pyi:54:20: PYI061 `Literal[None]` can be replaced with `None` + | +52 | b: None | Literal[None] | None +53 | c: (None | Literal[None]) | None +54 | d: None | (Literal[None] | None) + | ^^^^ PYI061 +55 | e: None | ((None | Literal[None]) | None) | None +56 | f: Literal[None] | Literal[None] + | + = help: Replace with `None` + +PYI061.pyi:55:28: PYI061 `Literal[None]` can be replaced with `None` + | +53 | c: (None | Literal[None]) | None +54 | d: None | (Literal[None] | None) +55 | e: None | ((None | Literal[None]) | None) | None + | ^^^^ PYI061 +56 | f: Literal[None] | Literal[None] + | + = help: Replace with `None` + +PYI061.pyi:56:12: PYI061 `Literal[None]` can be replaced with `None` + | +54 | d: None | (Literal[None] | None) +55 | e: None | ((None | Literal[None]) | None) | None +56 | f: Literal[None] | Literal[None] + | ^^^^ PYI061 + | + = help: Replace with `None` + +PYI061.pyi:56:28: PYI061 `Literal[None]` can be replaced with `None` + | +54 | d: None | (Literal[None] | None) +55 | e: None | ((None | Literal[None]) | None) | None +56 | f: Literal[None] | Literal[None] + | ^^^^ PYI061 + | + = help: Replace with `None` From 5a3c3411b11e9036c634bef90e646bf7979abe3b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 25 Nov 2024 17:35:43 +0000 Subject: [PATCH 8/8] small cleanups --- .../test/fixtures/flake8_pyi/PYI061.py | 6 ++-- .../rules/redundant_none_literal.rs | 34 ++++++++----------- ...__flake8_pyi__tests__PYI061_PYI061.py.snap | 20 +++++------ 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py index 4b0cf66f923ae..51baabe892107 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py @@ -17,7 +17,7 @@ def func4(arg1: Literal[int, None, float]): ... -def func5(arg1: Literal[None, None]): +def func5(arg1: Literal[None, None]): ... @@ -25,13 +25,13 @@ def func6(arg1: Literal[ "hello", None # Comment 1 , "world" - ]): + ]): ... def func7(arg1: Literal[ None # Comment 1 - ]): + ]): ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 40d66fd1718fd..9a23fff1b8762 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -121,30 +121,31 @@ pub(crate) fn redundant_none_literal<'a>(checker: &mut Checker, literal_expr: &' fn create_fix_edit(semantic: &SemanticModel, literal_expr: &Expr) -> Option { let mut enclosing_union = None; - let mut expression_ancestors = semantic.current_expressions().skip(1); - let mut parent_expr = expression_ancestors.next(); - while let Some(Expr::BinOp(ExprBinOp { - op: Operator::BitOr, - .. - })) = parent_expr - { - enclosing_union = parent_expr; - parent_expr = expression_ancestors.next(); + for expr in semantic.current_expressions().skip(1).take_while(|expr| { + matches!( + expr, + Expr::BinOp(ExprBinOp { + op: Operator::BitOr, + .. + }) + ) + }) { + enclosing_union = Some(expr); } - let mut is_union_with_bare_none = false; + let mut is_fixable = true; if let Some(enclosing_union) = enclosing_union { traverse_union( &mut |expr, _| { if matches!(expr, Expr::NoneLiteral(_)) { - is_union_with_bare_none = true; + is_fixable = false; } if expr != literal_expr { if let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr { if semantic.match_typing_expr(value, "Literal") && matches!(**slice, Expr::NoneLiteral(_)) { - is_union_with_bare_none = true; + is_fixable = false; } } } @@ -159,12 +160,5 @@ fn create_fix_edit(semantic: &SemanticModel, literal_expr: &Expr) -> Option