Skip to content

[flake8-pyi] Ensure Literal[None,] | Literal[None,] is not autofixed to None | None (PYI061) #17659

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 28, 2025

Conversation

LaBatata101
Copy link
Contributor

Handles the case where the None literal inside Literal is part of a tuple, e.g. List[None,]. We don't create the fix for this case.

Fixes #16177.

Summary

Test Plan

Snapshot tests.

Handles the case where the `None` literal inside `Literal` is part of a
tuple, e.g. `List[None,]`. We don't create the fix for this case.
@AlexWaygood AlexWaygood added bug Something isn't working fixes Related to suggested fixes for violations labels Apr 27, 2025
Copy link
Contributor

github-actions bot commented Apr 27, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
@AlexWaygood
Copy link
Member

Looking at it again, I think this maybe fixes things the wrong way. If we have a foo.pyi file like this:

from typing import Literal

f: Literal[None,] | Literal[None,]
g: Literal[None,] | None
h: None | Literal[None,]

and I run cargo run -- check foo.pyi --select=PYI061 --preview --no-cache --diff from the main branch, Ruff says it would apply these fixes:

--- foo.pyi
+++ foo.pyi
@@ -1,5 +1,5 @@
 from typing import Literal
 
-f: Literal[None,] | Literal[None,]
+f: None | None
 g: Literal[None,] | None
 h: None | Literal[None,]

I.e., the second and third of these are fine, because this rule already knows that converting Literal[None,] | None to None | None will produce a TypeError. The reason why the first expression gets converted into one that will produce a TypeError is because there are actually two fixes being applied to the same expression as part of a single iteration of diagnostics-plus-fixes from Ruff. The second autofix isn't aware of the changes that the first autofix made (it isn't aware that the first element in the union has already been converted to None) so it thinks it's still safe to convert the second element in the union to None.

That indicates to me that the better fix here is to ensure that each element's autofix is forced to happen in a separate iteration of the diagnostics-plus-autofixes loop that Ruff does until there are no more autofixes to apply. I.e., this diff applied to main also fixes the issue for me, and feels to me like it more directly addresses the cause of the problem here:

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 16a48bc5f..e3fafac62 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
@@ -130,6 +130,9 @@ pub(crate) fn redundant_none_literal<'a>(checker: &Checker, literal_expr: &'a Ex
                 literal_elements.clone(),
                 union_kind,
             )
+            .map(|fix| {
+                fix.map(|fix| fix.isolate(Checker::isolation(semantic.current_statement_id())))
+            })
         });
         checker.report_diagnostic(diagnostic);
     }

@LaBatata101
Copy link
Contributor Author

Looking at it again, I think this maybe fixes things the wrong way. If we have a foo.pyi file like this:

from typing import Literal

f: Literal[None,] | Literal[None,]
g: Literal[None,] | None
h: None | Literal[None,]

and I run cargo run -- check foo.pyi --select=PYI061 --preview --no-cache --diff from the main branch, Ruff says it would apply these fixes:

--- foo.pyi
+++ foo.pyi
@@ -1,5 +1,5 @@
 from typing import Literal
 
-f: Literal[None,] | Literal[None,]
+f: None | None
 g: Literal[None,] | None
 h: None | Literal[None,]

I.e., the second and third of these are fine, because this rule already knows that converting Literal[None,] | None to None | None will produce a TypeError. The reason why the first expression gets converted into one that will produce a TypeError is because there are actually two fixes being applied to the same expression as part of a single iteration of diagnostics-plus-fixes from Ruff. The second autofix isn't aware of the changes that the first autofix made (it isn't aware that the first element in the union has already been converted to None) so it thinks it's still safe to convert the second element in the union to None.

That indicates to me that the better fix here is to ensure that each element's autofix is forced to happen in a separate iteration of the diagnostics-plus-autofixes loop that Ruff does until there are no more autofixes to apply. I.e., this diff applied to main also fixes the issue for me, and feels to me like it more directly addresses the cause of the problem here:

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 16a48bc5f..e3fafac62 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
@@ -130,6 +130,9 @@ pub(crate) fn redundant_none_literal<'a>(checker: &Checker, literal_expr: &'a Ex
                 literal_elements.clone(),
                 union_kind,
             )
+            .map(|fix| {
+                fix.map(|fix| fix.isolate(Checker::isolation(semantic.current_statement_id())))
+            })
         });
         checker.report_diagnostic(diagnostic);
     }

Makes sense, thanks!

@AlexWaygood AlexWaygood self-assigned this Apr 28, 2025
@AlexWaygood
Copy link
Member

I realised that we could simplify some of the logic added in #14583, because ensuring that the individual autofixes are isolated provides a more general fix! I pushed the change to your branch here

@AlexWaygood AlexWaygood changed the title [flake8-pyi] Improve autofix safety for PYI061 [flake8-pyi] Ensure Literal[None,] | Literal[None,] is not autofixed to None | None (PYI061) Apr 28, 2025
@AlexWaygood
Copy link
Member

Thank you!

@AlexWaygood AlexWaygood merged commit ceb2bf1 into astral-sh:main Apr 28, 2025
33 checks passed
dcreager added a commit that referenced this pull request Apr 28, 2025
* main: (37 commits)
  [red-knot] Revert blanket `clippy::too_many_arguments` allow (#17688)
  Add config option to disable `typing_extensions` imports  (#17611)
  ruff_db: render file paths in diagnostics as relative paths if possible
  Bump mypy_primer pin (#17685)
  red_knot_python_semantic: improve `not-iterable` diagnostic
  [red-knot] Allow all callables to be assignable to @Todo-signatures (#17680)
  [`refurb`] Mark fix as safe for `readlines-in-for` (`FURB129`) (#17644)
  Collect preview lint behaviors in separate module (#17646)
  Upgrade Salsa to a more recent commit (#17678)
  [red-knot] TypedDict: No errors for introspection dunder attributes (#17677)
  [`flake8-pyi`] Ensure `Literal[None,] | Literal[None,]` is not autofixed to `None | None` (`PYI061`) (#17659)
  [red-knot] No errors for definitions of `TypedDict`s (#17674)
  Update actions/download-artifact digest to d3f86a1 (#17664)
  [red-knot] Use 101 exit code when there's at least one diagnostic with severity 'fatal' (#17640)
  [`pycodestyle`] Fix duplicated diagnostic in `E712` (#17651)
  [airflow] fix typos `AIR312` (#17673)
  [red-knot] Don't ignore hidden files by default (#17655)
  Update pre-commit hook astral-sh/ruff-pre-commit to v0.11.7 (#17670)
  Update docker/build-push-action digest to 14487ce (#17665)
  Update taiki-e/install-action digest to ab3728c (#17666)
  ...
@LaBatata101 LaBatata101 deleted the fix-PYI061 branch April 30, 2025 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working fixes Related to suggested fixes for violations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PYI061 fixes Literal[None,] | Literal[None,] to None | None
2 participants