diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs index e62d1f4ed2..1447b05322 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_rewritable_type_annotation.rs @@ -1,9 +1,11 @@ +use ruff_diagnostics::Fix; use ruff_python_ast::Expr; use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_semantic::{MemberNameImport, NameImport}; use ruff_text_size::Ranged; -use crate::Violation; +use crate::AlwaysFixableViolation; use crate::checkers::ast::Checker; /// ## What it does @@ -61,6 +63,10 @@ use crate::checkers::ast::Checker; /// def func(obj: dict[str, int | None]) -> None: ... /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe, as adding `from __future__ import annotations` +/// may change the semantics of the program. +/// /// ## Options /// - `target-version` #[derive(ViolationMetadata)] @@ -68,12 +74,16 @@ pub(crate) struct FutureRewritableTypeAnnotation { name: String, } -impl Violation for FutureRewritableTypeAnnotation { +impl AlwaysFixableViolation for FutureRewritableTypeAnnotation { #[derive_message_formats] fn message(&self) -> String { let FutureRewritableTypeAnnotation { name } = self; format!("Add `from __future__ import annotations` to simplify `{name}`") } + + fn fix_title(&self) -> String { + "Add `from __future__ import annotations`".to_string() + } } /// FA100 @@ -83,7 +93,17 @@ pub(crate) fn future_rewritable_type_annotation(checker: &Checker, expr: &Expr) .resolve_qualified_name(expr) .map(|binding| binding.to_string()); - if let Some(name) = name { - checker.report_diagnostic(FutureRewritableTypeAnnotation { name }, expr.range()); - } + let Some(name) = name else { return }; + + let import = &NameImport::ImportFrom(MemberNameImport::member( + "__future__".to_string(), + "annotations".to_string(), + )); + checker + .report_diagnostic(FutureRewritableTypeAnnotation { name }, expr.range()) + .set_fix(Fix::unsafe_edit( + checker + .importer() + .add_import(import, ruff_text_size::TextSize::default()), + )); } diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__edge_case.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__edge_case.py.snap index c3e38a6517..5f668c4700 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__edge_case.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__edge_case.py.snap @@ -1,19 +1,32 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs -snapshot_kind: text --- -edge_case.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +edge_case.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 5 | def main(_: List[int]) -> None: | ^^^^ FA100 6 | a_list: t.List[str] = [] 7 | a_list.append("hello") | + = help: Add `from __future__ import annotations` -edge_case.py:6:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | from typing import List +2 3 | import typing as t +3 4 | + +edge_case.py:6:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 5 | def main(_: List[int]) -> None: 6 | a_list: t.List[str] = [] | ^^^^^^ FA100 7 | a_list.append("hello") | + = help: Add `from __future__ import annotations` + +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | from typing import List +2 3 | import typing as t +3 4 | diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import.py.snap index d15966d4d2..9a32b4d1c9 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import.py.snap @@ -1,11 +1,17 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs -snapshot_kind: text --- -from_typing_import.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +from_typing_import.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 4 | def main() -> None: 5 | a_list: List[str] = [] | ^^^^ FA100 6 | a_list.append("hello") | + = help: Add `from __future__ import annotations` + +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | from typing import List +2 3 | +3 4 | diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap index e3426dc720..b2ae15f027 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap @@ -1,8 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs -snapshot_kind: text --- -from_typing_import_many.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +from_typing_import_many.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 4 | def main() -> None: 5 | a_list: List[Optional[str]] = [] @@ -10,8 +9,15 @@ from_typing_import_many.py:5:13: FA100 Add `from __future__ import annotations` 6 | a_list.append("hello") 7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) | + = help: Add `from __future__ import annotations` -from_typing_import_many.py:5:18: FA100 Add `from __future__ import annotations` to simplify `typing.Optional` +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | from typing import Dict, List, Optional, Set, Union, cast +2 3 | +3 4 | + +from_typing_import_many.py:5:18: FA100 [*] Add `from __future__ import annotations` to simplify `typing.Optional` | 4 | def main() -> None: 5 | a_list: List[Optional[str]] = [] @@ -19,3 +25,10 @@ from_typing_import_many.py:5:18: FA100 Add `from __future__ import annotations` 6 | a_list.append("hello") 7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) | + = help: Add `from __future__ import annotations` + +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | from typing import Dict, List, Optional, Set, Union, cast +2 3 | +3 4 | diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing.py.snap index afa5c4aa52..b42449261f 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing.py.snap @@ -1,11 +1,17 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs -snapshot_kind: text --- -import_typing.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +import_typing.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 4 | def main() -> None: 5 | a_list: typing.List[str] = [] | ^^^^^^^^^^^ FA100 6 | a_list.append("hello") | + = help: Add `from __future__ import annotations` + +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | import typing +2 3 | +3 4 | diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing_as.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing_as.py.snap index d2ec4a38fd..231038c3c4 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing_as.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__import_typing_as.py.snap @@ -1,11 +1,17 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs -snapshot_kind: text --- -import_typing_as.py:5:13: FA100 Add `from __future__ import annotations` to simplify `typing.List` +import_typing_as.py:5:13: FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` | 4 | def main() -> None: 5 | a_list: t.List[str] = [] | ^^^^^^ FA100 6 | a_list.append("hello") | + = help: Add `from __future__ import annotations` + +ℹ Unsafe fix + 1 |+from __future__ import annotations +1 2 | import typing as t +2 3 | +3 4 |