diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py index 904c1510ea..13be285aa0 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py @@ -42,3 +42,8 @@ class ServiceRefOrValue: # Regression test for: https://github.com/astral-sh/ruff/issues/7201 class ServiceRefOrValue: service_specification: Optional[str]is not True = None + + +# Test for: https://github.com/astral-sh/ruff/issues/18508 +# Optional[None] should not be offered a fix +foo: Optional[None] = None diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs index b1b888cf4c..bc49d8df08 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -100,8 +100,8 @@ impl Violation for NonPEP604AnnotationUnion { /// ``` /// /// ## Fix safety -/// This rule's fix is marked as unsafe, as it may lead to runtime errors when -/// alongside libraries that rely on runtime type annotations, like Pydantic, +/// This rule's fix is marked as unsafe, as it may lead to runtime errors +/// using libraries that rely on runtime type annotations, like Pydantic, /// on Python versions prior to Python 3.10. It may also lead to runtime errors /// in unusual and likely incorrect type annotations where the type does not /// support the `|` operator. @@ -138,7 +138,8 @@ pub(crate) fn non_pep604_annotation( // lead to invalid syntax. let fixable = checker.semantic().in_type_definition() && !checker.semantic().in_complex_string_type_definition() - && is_allowed_value(slice); + && is_allowed_value(slice) + && !is_optional_none(operator, slice); let applicability = if checker.target_version() >= PythonVersion::PY310 { Applicability::Safe @@ -276,3 +277,8 @@ fn is_allowed_value(expr: &Expr) -> bool { | Expr::IpyEscapeCommand(_) => false, } } + +/// Return `true` if this is an `Optional[None]` annotation. +fn is_optional_none(operator: Pep604Operator, slice: &Expr) -> bool { + matches!(operator, Pep604Operator::Optional) && matches!(slice, Expr::NoneLiteral(_)) +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap index 3579871395..9e78420b08 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs -snapshot_kind: text --- UP045.py:5:10: UP007 [*] Use `X | Y` for type annotations | @@ -149,3 +148,15 @@ UP045.py:44:28: UP007 [*] Use `X | Y` for type annotations 43 43 | class ServiceRefOrValue: 44 |- service_specification: Optional[str]is not True = None 44 |+ service_specification: str | None is not True = None +45 45 | +46 46 | +47 47 | # Test for: https://github.com/astral-sh/ruff/issues/18508 + +UP045.py:49:6: UP007 Use `X | Y` for type annotations + | +47 | # Test for: https://github.com/astral-sh/ruff/issues/18508 +48 | # Optional[None] should not be offered a fix +49 | foo: Optional[None] = None + | ^^^^^^^^^^^^^^ UP007 + | + = help: Convert to `X | Y`