[flake8-future-annotations] Add autofix (FA100) (#18903)

Summary
--

This PR resolves the easiest part of
https://github.com/astral-sh/ruff/issues/18502 by adding an autofix that
just adds
`from __future__ import annotations` at the top of the file, in the same
way
as FA102, which already has an identical unsafe fix.

Test Plan
--

Existing snapshots, updated to add the fixes.
This commit is contained in:
Brent Westbrook 2025-06-25 08:37:18 -04:00 committed by GitHub
parent c1fed55d51
commit 7783cea14f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 81 additions and 17 deletions

View file

@ -1,9 +1,11 @@
use ruff_diagnostics::Fix;
use ruff_python_ast::Expr; use ruff_python_ast::Expr;
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::{MemberNameImport, NameImport};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::Violation; use crate::AlwaysFixableViolation;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
/// ## What it does /// ## What it does
@ -61,6 +63,10 @@ use crate::checkers::ast::Checker;
/// def func(obj: dict[str, int | None]) -> None: ... /// 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 /// ## Options
/// - `target-version` /// - `target-version`
#[derive(ViolationMetadata)] #[derive(ViolationMetadata)]
@ -68,12 +74,16 @@ pub(crate) struct FutureRewritableTypeAnnotation {
name: String, name: String,
} }
impl Violation for FutureRewritableTypeAnnotation { impl AlwaysFixableViolation for FutureRewritableTypeAnnotation {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let FutureRewritableTypeAnnotation { name } = self; let FutureRewritableTypeAnnotation { name } = self;
format!("Add `from __future__ import annotations` to simplify `{name}`") format!("Add `from __future__ import annotations` to simplify `{name}`")
} }
fn fix_title(&self) -> String {
"Add `from __future__ import annotations`".to_string()
}
} }
/// FA100 /// FA100
@ -83,7 +93,17 @@ pub(crate) fn future_rewritable_type_annotation(checker: &Checker, expr: &Expr)
.resolve_qualified_name(expr) .resolve_qualified_name(expr)
.map(|binding| binding.to_string()); .map(|binding| binding.to_string());
if let Some(name) = name { let Some(name) = name else { return };
checker.report_diagnostic(FutureRewritableTypeAnnotation { name }, expr.range());
} 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()),
));
} }

View file

@ -1,19 +1,32 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs 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: 5 | def main(_: List[int]) -> None:
| ^^^^ FA100 | ^^^^ FA100
6 | a_list: t.List[str] = [] 6 | a_list: t.List[str] = []
7 | a_list.append("hello") 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: 5 | def main(_: List[int]) -> None:
6 | a_list: t.List[str] = [] 6 | a_list: t.List[str] = []
| ^^^^^^ FA100 | ^^^^^^ FA100
7 | a_list.append("hello") 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 |

View file

@ -1,11 +1,17 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs 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: 4 | def main() -> None:
5 | a_list: List[str] = [] 5 | a_list: List[str] = []
| ^^^^ FA100 | ^^^^ FA100
6 | a_list.append("hello") 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 |

View file

@ -1,8 +1,7 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs 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: 4 | def main() -> None:
5 | a_list: List[Optional[str]] = [] 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") 6 | a_list.append("hello")
7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) 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: 4 | def main() -> None:
5 | a_list: List[Optional[str]] = [] 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") 6 | a_list.append("hello")
7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) 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 |

View file

@ -1,11 +1,17 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs 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: 4 | def main() -> None:
5 | a_list: typing.List[str] = [] 5 | a_list: typing.List[str] = []
| ^^^^^^^^^^^ FA100 | ^^^^^^^^^^^ FA100
6 | a_list.append("hello") 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 |

View file

@ -1,11 +1,17 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs 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: 4 | def main() -> None:
5 | a_list: t.List[str] = [] 5 | a_list: t.List[str] = []
| ^^^^^^ FA100 | ^^^^^^ FA100
6 | a_list.append("hello") 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 |