From fd8c38313f18e97828d2a0c5e8b13665c10ac543 Mon Sep 17 00:00:00 2001 From: Bhuminjay Date: Thu, 12 Feb 2026 12:38:08 +0530 Subject: [PATCH 01/17] remove dependency of FA100 from UP006 Signed-off-by: Bhuminjay --- .../test/fixtures/pyupgrade/UP006_future.py | 19 +++++ .../src/checkers/ast/analyze/expression.rs | 70 +++++-------------- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 15 ++++ .../pyupgrade/rules/use_pep585_annotation.rs | 51 ++++++++++---- 4 files changed, 90 insertions(+), 65 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_future.py diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_future.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_future.py new file mode 100644 index 00000000000000..ce2522800c959f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_future.py @@ -0,0 +1,19 @@ +from typing import List +import typing +import typing as t + + +def func1(a_list: List[str]) -> None: + pass + + +def func2(a_list: typing.List[str]) -> None: + pass + + +def func3(a_list: t.List[str]) -> None: + pass + + +def func4(_: List[int]) -> None: + a_list: t.List[str] = [] diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index e856ce0c6f5319..a9beaf649fd393 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -294,37 +294,21 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } // Ex) List[...] - if checker.any_rule_enabled(&[ - Rule::FutureRewritableTypeAnnotation, - Rule::NonPEP585Annotation, - ]) { + if checker.is_rule_enabled(Rule::NonPEP585Annotation) { if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) { - if checker.is_rule_enabled(Rule::FutureRewritableTypeAnnotation) { - if !checker.semantic.future_annotations_or_stub() - && checker.target_version() < PythonVersion::PY39 - && checker.target_version() >= PythonVersion::PY37 + if checker.source_type.is_stub() + || checker.target_version() >= PythonVersion::PY39 + || (checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() - && !checker.settings().pyupgrade.keep_runtime_typing - { - flake8_future_annotations::rules::future_rewritable_type_annotation(checker, expr); - } - } - if checker.is_rule_enabled(Rule::NonPEP585Annotation) { - if checker.source_type.is_stub() - || checker.target_version() >= PythonVersion::PY39 - || (checker.target_version() >= PythonVersion::PY37 - && checker.semantic.future_annotations_or_stub() - && checker.semantic.in_annotation() - && !checker.settings().pyupgrade.keep_runtime_typing) - { - pyupgrade::rules::use_pep585_annotation( - checker, - expr, - &replacement, - ); - } + && !checker.settings().pyupgrade.keep_runtime_typing) + { + pyupgrade::rules::use_pep585_annotation( + checker, + expr, + &replacement, + ); } } } @@ -403,33 +387,15 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } // Ex) typing.List[...] - if checker.any_rule_enabled(&[ - Rule::FutureRewritableTypeAnnotation, - Rule::NonPEP585Annotation, - ]) { + if checker.is_rule_enabled(Rule::NonPEP585Annotation) { if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) { - if checker.is_rule_enabled(Rule::FutureRewritableTypeAnnotation) { - if !checker.semantic.future_annotations_or_stub() - && checker.target_version() < PythonVersion::PY39 - && checker.target_version() >= PythonVersion::PY37 + if checker.source_type.is_stub() + || checker.target_version() >= PythonVersion::PY39 + || (checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() - && !checker.settings().pyupgrade.keep_runtime_typing - { - flake8_future_annotations::rules::future_rewritable_type_annotation( - checker, expr, - ); - } - } - if checker.is_rule_enabled(Rule::NonPEP585Annotation) { - if checker.source_type.is_stub() - || checker.target_version() >= PythonVersion::PY39 - || (checker.target_version() >= PythonVersion::PY37 - && checker.semantic.future_annotations_or_stub() - && checker.semantic.in_annotation() - && !checker.settings().pyupgrade.keep_runtime_typing) - { - pyupgrade::rules::use_pep585_annotation(checker, expr, &replacement); - } + && !checker.settings().pyupgrade.keep_runtime_typing) + { + pyupgrade::rules::use_pep585_annotation(checker, expr, &replacement); } } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 947aef26c632a6..bc99d6e35bab1a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -128,6 +128,21 @@ mod tests { Ok(()) } + #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_future.py"))] + fn rules_future(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = path.to_string_lossy().to_string(); + let diagnostics = test_path( + Path::new("pyupgrade").join(path).as_path(), + &settings::LinterSettings { + future_annotations: true, + unresolved_target_version: PythonVersion::PY38.into(), + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_2.py"))] #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_1.py"))] fn rules_not_applied_default_typevar_backported(rule_code: Rule, path: &Path) -> Result<()> { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 420768f1eda669..a82a6fa037d6ba 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -2,6 +2,7 @@ use ruff_python_ast::Expr; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::UnqualifiedName; +use ruff_python_parser::semantic_errors::SemanticSyntaxContext; use ruff_python_semantic::analyze::typing::ModuleMember; use ruff_text_size::Ranged; @@ -99,14 +100,17 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: checker.semantic(), )?; let binding_edit = Edit::range_replacement(binding, expr.range()); - let applicability = if checker.target_version() >= PythonVersion::PY310 { - Applicability::Safe - } else { - Applicability::Unsafe - }; + let (applicability, future_import) = fix_applicability(checker); + let mut secondary_edit = Vec::new(); + if let Some(import) = import_edit { + secondary_edit.push(import); + } + if let Some(future) = future_import { + secondary_edit.push(future); + } Ok(Fix::applicable_edits( binding_edit, - import_edit, + secondary_edit, applicability, )) }); @@ -120,17 +124,38 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: checker.semantic(), )?; let reference_edit = Edit::range_replacement(binding, expr.range()); + let (applicability, future_import) = fix_applicability(checker); + let mut secondary_edit = Vec::new(); + secondary_edit.push(import_edit); + if let Some(future) = future_import { + secondary_edit.push(future); + } Ok(Fix::applicable_edits( - import_edit, - [reference_edit], - if checker.target_version() >= PythonVersion::PY310 { - Applicability::Safe - } else { - Applicability::Unsafe - }, + reference_edit, + secondary_edit, + applicability, )) }); } } } } + +fn fix_applicability(checker: &Checker) -> (Applicability, Option) { + if checker.target_version() >= PythonVersion::PY39 { + return (Applicability::Safe, None); + } + if checker.settings().pyupgrade.keep_runtime_typing { + return (Applicability::Unsafe, None); + } + if checker.settings().future_annotations { + if checker.future_annotations_or_stub() { + (Applicability::Safe, None) + } else { + let future_import = checker.importer().add_future_import(); + (Applicability::Safe, Some(future_import)) + } + } else { + (Applicability::Unsafe, None) + } +} From 2c5ccb2f393f3bae0a5a7bc12510682db28ef5a8 Mon Sep 17 00:00:00 2001 From: Bhuminjay Date: Fri, 13 Feb 2026 12:03:38 +0530 Subject: [PATCH 02/17] update FA100 snaps Signed-off-by: Bhuminjay --- ...ture_annotations__tests__edge_case.py.snap | 28 ------------------- ...tations__tests__from_typing_import.py.snap | 15 +--------- ...ns__tests__from_typing_import_many.py.snap | 16 ----------- ..._annotations__tests__import_typing.py.snap | 15 +--------- ...notations__tests__import_typing_as.py.snap | 15 +--------- 5 files changed, 3 insertions(+), 86 deletions(-) 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 5d72a8965e7147..0e51cf4a6fa771 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,32 +1,4 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> edge_case.py:5:13 - | -5 | def main(_: List[int]) -> None: - | ^^^^ -6 | a_list: t.List[str] = [] -7 | a_list.append("hello") - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing as t -4 | -note: This is an unsafe fix and may change runtime behavior -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> edge_case.py:6:13 - | -5 | def main(_: List[int]) -> None: -6 | a_list: t.List[str] = [] - | ^^^^^^ -7 | a_list.append("hello") - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing as t -4 | -note: This is an unsafe fix and may change runtime behavior 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 cda1f547428bed..0e51cf4a6fa771 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,17 +1,4 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> from_typing_import.py:5:13 - | -4 | def main() -> None: -5 | a_list: List[str] = [] - | ^^^^ -6 | a_list.append("hello") - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | from typing import List -3 | -4 | -note: This is an unsafe fix and may change runtime behavior + 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 494953602015ab..5bd213ecb9d575 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,22 +1,6 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> from_typing_import_many.py:5:13 - | -4 | def main() -> None: -5 | a_list: List[Optional[str]] = [] - | ^^^^ -6 | a_list.append("hello") -7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | from typing import Dict, List, Optional, Set, Union, cast -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - FA100 [*] Add `from __future__ import annotations` to simplify `typing.Optional` --> from_typing_import_many.py:5:18 | 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 adaccdcc100726..0e51cf4a6fa771 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,17 +1,4 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> import_typing.py:5:13 - | -4 | def main() -> None: -5 | a_list: typing.List[str] = [] - | ^^^^^^^^^^^ -6 | a_list.append("hello") - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior + 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 ce51176f1e293a..0e51cf4a6fa771 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,17 +1,4 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> import_typing_as.py:5:13 - | -4 | def main() -> None: -5 | a_list: t.List[str] = [] - | ^^^^^^ -6 | a_list.append("hello") - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing as t -3 | -4 | -note: This is an unsafe fix and may change runtime behavior + From 458155a0196609a8f574388d31584452c92fedba Mon Sep 17 00:00:00 2001 From: Bhuminjay Date: Fri, 13 Feb 2026 12:16:49 +0530 Subject: [PATCH 03/17] add snapshot Signed-off-by: Bhuminjay --- ...es__pyupgrade__tests__UP006_future.py.snap | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py.snap diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py.snap new file mode 100644 index 00000000000000..b11a7fd6996c2a --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py.snap @@ -0,0 +1,105 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_future.py:6:19 + | +6 | def func1(a_list: List[str]) -> None: + | ^^^^ +7 | pass + | +help: Replace with `list` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing +4 | import typing as t +5 | +6 | + - def func1(a_list: List[str]) -> None: +7 + def func1(a_list: list[str]) -> None: +8 | pass +9 | +10 | + +UP006 [*] Use `list` instead of `typing.List` for type annotation + --> UP006_future.py:10:19 + | +10 | def func2(a_list: typing.List[str]) -> None: + | ^^^^^^^^^^^ +11 | pass + | +help: Replace with `list` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing +4 | import typing as t +-------------------------------------------------------------------------------- +8 | pass +9 | +10 | + - def func2(a_list: typing.List[str]) -> None: +11 + def func2(a_list: list[str]) -> None: +12 | pass +13 | +14 | + +UP006 [*] Use `list` instead of `t.List` for type annotation + --> UP006_future.py:14:19 + | +14 | def func3(a_list: t.List[str]) -> None: + | ^^^^^^ +15 | pass + | +help: Replace with `list` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing +4 | import typing as t +-------------------------------------------------------------------------------- +12 | pass +13 | +14 | + - def func3(a_list: t.List[str]) -> None: +15 + def func3(a_list: list[str]) -> None: +16 | pass +17 | +18 | + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_future.py:18:14 + | +18 | def func4(_: List[int]) -> None: + | ^^^^ +19 | a_list: t.List[str] = [] + | +help: Replace with `list` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing +4 | import typing as t +-------------------------------------------------------------------------------- +16 | pass +17 | +18 | + - def func4(_: List[int]) -> None: +19 + def func4(_: list[int]) -> None: +20 | a_list: t.List[str] = [] + +UP006 [*] Use `list` instead of `t.List` for type annotation + --> UP006_future.py:19:13 + | +18 | def func4(_: List[int]) -> None: +19 | a_list: t.List[str] = [] + | ^^^^^^ + | +help: Replace with `list` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing +4 | import typing as t +-------------------------------------------------------------------------------- +17 | +18 | +19 | def func4(_: List[int]) -> None: + - a_list: t.List[str] = [] +20 + a_list: list[str] = [] From df04058824ca8da5639b821a0c39e2cc754873f3 Mon Sep 17 00:00:00 2001 From: Bhuminjay Date: Fri, 13 Feb 2026 12:23:14 +0530 Subject: [PATCH 04/17] update a prev snap Signed-off-by: Bhuminjay --- crates/ruff/tests/cli/lint.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 12b697a5ddbdc6..71155647139a39 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -2043,10 +2043,12 @@ select = ["UP006"] .args(["--stdin-filename", "test.py"]) .arg("-") .pass_stdin(r#"from typing import List; foo: List[int]"#), @" - success: true - exit_code: 0 + success: false + exit_code: 1 ----- stdout ----- - All checks passed! + test.py:1:31: UP006 Use `list` instead of `List` for type annotation + Found 1 error. + No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option). ----- stderr ----- "); From b5b53ebfb4e7e9add1db5b51b967bb7146063a7e Mon Sep 17 00:00:00 2001 From: Bhuminjay Date: Thu, 5 Mar 2026 13:22:31 +0530 Subject: [PATCH 05/17] make it preview Signed-off-by: Bhuminjay --- .../src/checkers/ast/analyze/expression.rs | 68 ++++++++++++++----- crates/ruff_linter/src/preview.rs | 2 + ...ture_annotations__tests__edge_case.py.snap | 28 ++++++++ ...tations__tests__from_typing_import.py.snap | 15 +++- ...ns__tests__from_typing_import_many.py.snap | 16 +++++ ..._annotations__tests__import_typing.py.snap | 15 +++- ...notations__tests__import_typing_as.py.snap | 15 +++- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 5 +- .../pyupgrade/rules/use_pep585_annotation.rs | 6 +- 9 files changed, 146 insertions(+), 24 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index a9beaf649fd393..809ddd04568ecb 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -294,21 +294,36 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } // Ex) List[...] - if checker.is_rule_enabled(Rule::NonPEP585Annotation) { + if checker.any_rule_enabled(&[ + Rule::FutureRewritableTypeAnnotation, + Rule::NonPEP585Annotation, + ]) { if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) { - if checker.source_type.is_stub() - || checker.target_version() >= PythonVersion::PY39 - || (checker.target_version() >= PythonVersion::PY37 + if checker.is_rule_enabled(Rule::FutureRewritableTypeAnnotation) { + if !checker.semantic.future_annotations_or_stub() + && checker.target_version() < PythonVersion::PY39 + && checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() - && !checker.settings().pyupgrade.keep_runtime_typing) - { - pyupgrade::rules::use_pep585_annotation( - checker, - expr, - &replacement, - ); + && !checker.settings().pyupgrade.keep_runtime_typing + { + flake8_future_annotations::rules::future_rewritable_type_annotation(checker, expr); + } + } + if checker.is_rule_enabled(Rule::NonPEP585Annotation) { + if checker.source_type.is_stub() + || checker.target_version() >= PythonVersion::PY39 + || (checker.target_version() >= PythonVersion::PY37 + && checker.semantic.in_annotation() + && !checker.settings().pyupgrade.keep_runtime_typing) + { + pyupgrade::rules::use_pep585_annotation( + checker, + expr, + &replacement, + ); + } } } } @@ -387,15 +402,32 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { } // Ex) typing.List[...] - if checker.is_rule_enabled(Rule::NonPEP585Annotation) { + if checker.any_rule_enabled(&[ + Rule::FutureRewritableTypeAnnotation, + Rule::NonPEP585Annotation, + ]) { if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) { - if checker.source_type.is_stub() - || checker.target_version() >= PythonVersion::PY39 - || (checker.target_version() >= PythonVersion::PY37 + if checker.is_rule_enabled(Rule::FutureRewritableTypeAnnotation) { + if !checker.semantic.future_annotations_or_stub() + && checker.target_version() < PythonVersion::PY39 + && checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() - && !checker.settings().pyupgrade.keep_runtime_typing) - { - pyupgrade::rules::use_pep585_annotation(checker, expr, &replacement); + && !checker.settings().pyupgrade.keep_runtime_typing + { + flake8_future_annotations::rules::future_rewritable_type_annotation( + checker, expr, + ); + } + } + if checker.is_rule_enabled(Rule::NonPEP585Annotation) { + if checker.source_type.is_stub() + || checker.target_version() >= PythonVersion::PY39 + || (checker.target_version() >= PythonVersion::PY37 + && checker.semantic.in_annotation() + && !checker.settings().pyupgrade.keep_runtime_typing) + { + pyupgrade::rules::use_pep585_annotation(checker, expr, &replacement); + } } } } diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 0a4afc6bae0e43..4db712a44de7d1 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -312,5 +312,7 @@ pub(crate) const fn is_file_level_invalid_rule_code_enabled(settings: &LinterSet pub(crate) const fn is_incorrect_dict_iterator_comprehension_enabled( settings: &LinterSettings, ) -> bool { +// https://github.com/astral-sh/ruff/pull/23260 +pub(crate) const fn is_up006_future_annotations_fix_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } 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 0e51cf4a6fa771..5d72a8965e7147 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,4 +1,32 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> edge_case.py:5:13 + | +5 | def main(_: List[int]) -> None: + | ^^^^ +6 | a_list: t.List[str] = [] +7 | a_list.append("hello") + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing as t +4 | +note: This is an unsafe fix and may change runtime behavior +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> edge_case.py:6:13 + | +5 | def main(_: List[int]) -> None: +6 | a_list: t.List[str] = [] + | ^^^^^^ +7 | a_list.append("hello") + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing as t +4 | +note: This is an unsafe fix and may change runtime behavior 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 0e51cf4a6fa771..cda1f547428bed 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,4 +1,17 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- - +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> from_typing_import.py:5:13 + | +4 | def main() -> None: +5 | a_list: List[str] = [] + | ^^^^ +6 | a_list.append("hello") + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | from typing import List +3 | +4 | +note: This is an unsafe fix and may change runtime behavior 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 5bd213ecb9d575..494953602015ab 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,6 +1,22 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> from_typing_import_many.py:5:13 + | +4 | def main() -> None: +5 | a_list: List[Optional[str]] = [] + | ^^^^ +6 | a_list.append("hello") +7 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | from typing import Dict, List, Optional, Set, Union, cast +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + FA100 [*] Add `from __future__ import annotations` to simplify `typing.Optional` --> from_typing_import_many.py:5:18 | 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 0e51cf4a6fa771..adaccdcc100726 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,4 +1,17 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- - +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> import_typing.py:5:13 + | +4 | def main() -> None: +5 | a_list: typing.List[str] = [] + | ^^^^^^^^^^^ +6 | a_list.append("hello") + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior 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 0e51cf4a6fa771..ce51176f1e293a 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,4 +1,17 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- - +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> import_typing_as.py:5:13 + | +4 | def main() -> None: +5 | a_list: t.List[str] = [] + | ^^^^^^ +6 | a_list.append("hello") + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing as t +3 | +4 | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index bc99d6e35bab1a..567eb7baf30849 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -129,12 +129,13 @@ mod tests { } #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_future.py"))] - fn rules_future(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = path.to_string_lossy().to_string(); + fn up006_add_future_annotation_preview(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}__preview", path.to_string_lossy()); let diagnostics = test_path( Path::new("pyupgrade").join(path).as_path(), &settings::LinterSettings { future_annotations: true, + preview: PreviewMode::Enabled, unresolved_target_version: PythonVersion::PY38.into(), ..settings::LinterSettings::for_rule(rule_code) }, diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index a82a6fa037d6ba..d57d3f0b5c7b15 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -6,6 +6,8 @@ use ruff_python_parser::semantic_errors::SemanticSyntaxContext; use ruff_python_semantic::analyze::typing::ModuleMember; use ruff_text_size::Ranged; +use crate::preview::is_up006_future_annotations_fix_enabled; + use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; @@ -148,7 +150,9 @@ fn fix_applicability(checker: &Checker) -> (Applicability, Option) { if checker.settings().pyupgrade.keep_runtime_typing { return (Applicability::Unsafe, None); } - if checker.settings().future_annotations { + if checker.settings().future_annotations + && is_up006_future_annotations_fix_enabled(checker.settings()) + { if checker.future_annotations_or_stub() { (Applicability::Safe, None) } else { From ffadbdeff4406ad89043b62f3ea1da31e37773d9 Mon Sep 17 00:00:00 2001 From: Bhuminjay Date: Thu, 5 Mar 2026 13:32:26 +0530 Subject: [PATCH 06/17] fix preview Signed-off-by: Bhuminjay --- crates/ruff_linter/src/preview.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 4db712a44de7d1..f0be5463ca1ffe 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -312,6 +312,9 @@ pub(crate) const fn is_file_level_invalid_rule_code_enabled(settings: &LinterSet pub(crate) const fn is_incorrect_dict_iterator_comprehension_enabled( settings: &LinterSettings, ) -> bool { + settings.preview.is_enabled() +} + // https://github.com/astral-sh/ruff/pull/23260 pub(crate) const fn is_up006_future_annotations_fix_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() From 8463ef190733daf892137e72c3cc9d04a9952097 Mon Sep 17 00:00:00 2001 From: Bhuminjay Soni Date: Thu, 5 Mar 2026 13:47:56 +0530 Subject: [PATCH 07/17] Delete crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py.snap --- ...es__pyupgrade__tests__UP006_future.py.snap | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py.snap diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py.snap deleted file mode 100644 index b11a7fd6996c2a..00000000000000 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py.snap +++ /dev/null @@ -1,105 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyupgrade/mod.rs ---- -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_future.py:6:19 - | -6 | def func1(a_list: List[str]) -> None: - | ^^^^ -7 | pass - | -help: Replace with `list` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing -4 | import typing as t -5 | -6 | - - def func1(a_list: List[str]) -> None: -7 + def func1(a_list: list[str]) -> None: -8 | pass -9 | -10 | - -UP006 [*] Use `list` instead of `typing.List` for type annotation - --> UP006_future.py:10:19 - | -10 | def func2(a_list: typing.List[str]) -> None: - | ^^^^^^^^^^^ -11 | pass - | -help: Replace with `list` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing -4 | import typing as t --------------------------------------------------------------------------------- -8 | pass -9 | -10 | - - def func2(a_list: typing.List[str]) -> None: -11 + def func2(a_list: list[str]) -> None: -12 | pass -13 | -14 | - -UP006 [*] Use `list` instead of `t.List` for type annotation - --> UP006_future.py:14:19 - | -14 | def func3(a_list: t.List[str]) -> None: - | ^^^^^^ -15 | pass - | -help: Replace with `list` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing -4 | import typing as t --------------------------------------------------------------------------------- -12 | pass -13 | -14 | - - def func3(a_list: t.List[str]) -> None: -15 + def func3(a_list: list[str]) -> None: -16 | pass -17 | -18 | - -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_future.py:18:14 - | -18 | def func4(_: List[int]) -> None: - | ^^^^ -19 | a_list: t.List[str] = [] - | -help: Replace with `list` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing -4 | import typing as t --------------------------------------------------------------------------------- -16 | pass -17 | -18 | - - def func4(_: List[int]) -> None: -19 + def func4(_: list[int]) -> None: -20 | a_list: t.List[str] = [] - -UP006 [*] Use `list` instead of `t.List` for type annotation - --> UP006_future.py:19:13 - | -18 | def func4(_: List[int]) -> None: -19 | a_list: t.List[str] = [] - | ^^^^^^ - | -help: Replace with `list` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing -4 | import typing as t --------------------------------------------------------------------------------- -17 | -18 | -19 | def func4(_: List[int]) -> None: - - a_list: t.List[str] = [] -20 + a_list: list[str] = [] From 2deeb20908e49eacea06d85905a33c54c7b77598 Mon Sep 17 00:00:00 2001 From: Bhuminjay Date: Thu, 5 Mar 2026 13:48:16 +0530 Subject: [PATCH 08/17] add preview snap Signed-off-by: Bhuminjay --- ...rade__tests__UP006_future.py__preview.snap | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py__preview.snap diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py__preview.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py__preview.snap new file mode 100644 index 00000000000000..b11a7fd6996c2a --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py__preview.snap @@ -0,0 +1,105 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_future.py:6:19 + | +6 | def func1(a_list: List[str]) -> None: + | ^^^^ +7 | pass + | +help: Replace with `list` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing +4 | import typing as t +5 | +6 | + - def func1(a_list: List[str]) -> None: +7 + def func1(a_list: list[str]) -> None: +8 | pass +9 | +10 | + +UP006 [*] Use `list` instead of `typing.List` for type annotation + --> UP006_future.py:10:19 + | +10 | def func2(a_list: typing.List[str]) -> None: + | ^^^^^^^^^^^ +11 | pass + | +help: Replace with `list` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing +4 | import typing as t +-------------------------------------------------------------------------------- +8 | pass +9 | +10 | + - def func2(a_list: typing.List[str]) -> None: +11 + def func2(a_list: list[str]) -> None: +12 | pass +13 | +14 | + +UP006 [*] Use `list` instead of `t.List` for type annotation + --> UP006_future.py:14:19 + | +14 | def func3(a_list: t.List[str]) -> None: + | ^^^^^^ +15 | pass + | +help: Replace with `list` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing +4 | import typing as t +-------------------------------------------------------------------------------- +12 | pass +13 | +14 | + - def func3(a_list: t.List[str]) -> None: +15 + def func3(a_list: list[str]) -> None: +16 | pass +17 | +18 | + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_future.py:18:14 + | +18 | def func4(_: List[int]) -> None: + | ^^^^ +19 | a_list: t.List[str] = [] + | +help: Replace with `list` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing +4 | import typing as t +-------------------------------------------------------------------------------- +16 | pass +17 | +18 | + - def func4(_: List[int]) -> None: +19 + def func4(_: list[int]) -> None: +20 | a_list: t.List[str] = [] + +UP006 [*] Use `list` instead of `t.List` for type annotation + --> UP006_future.py:19:13 + | +18 | def func4(_: List[int]) -> None: +19 | a_list: t.List[str] = [] + | ^^^^^^ + | +help: Replace with `list` +1 + from __future__ import annotations +2 | from typing import List +3 | import typing +4 | import typing as t +-------------------------------------------------------------------------------- +17 | +18 | +19 | def func4(_: List[int]) -> None: + - a_list: t.List[str] = [] +20 + a_list: list[str] = [] From 7a21759c99259f568aecb0e7c4a5d56a2a499873 Mon Sep 17 00:00:00 2001 From: Bhuminjay Date: Fri, 6 Mar 2026 18:19:43 +0530 Subject: [PATCH 09/17] correct preview check Signed-off-by: Bhuminjay --- .../ruff_linter/src/checkers/ast/analyze/expression.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 809ddd04568ecb..893cf0d57deaf1 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -7,7 +7,9 @@ use ruff_python_semantic::analyze::typing; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::preview::is_future_required_preview_generics_enabled; +use crate::preview::{ + is_future_required_preview_generics_enabled, is_up006_future_annotations_fix_enabled, +}; use crate::registry::Rule; use crate::rules::{ airflow, flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear, @@ -315,6 +317,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.source_type.is_stub() || checker.target_version() >= PythonVersion::PY39 || (checker.target_version() >= PythonVersion::PY37 + && (checker.semantic.future_annotations_or_stub() + || is_up006_future_annotations_fix_enabled( + checker.settings(), + )) && checker.semantic.in_annotation() && !checker.settings().pyupgrade.keep_runtime_typing) { @@ -423,6 +429,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.source_type.is_stub() || checker.target_version() >= PythonVersion::PY39 || (checker.target_version() >= PythonVersion::PY37 + && (checker.semantic.future_annotations_or_stub() + || is_up006_future_annotations_fix_enabled(checker.settings())) && checker.semantic.in_annotation() && !checker.settings().pyupgrade.keep_runtime_typing) { From a6bd8da8f595e522d2107df9bddbb752186f173a Mon Sep 17 00:00:00 2001 From: Bhuminjay Date: Fri, 6 Mar 2026 18:27:55 +0530 Subject: [PATCH 10/17] revert prev snap Signed-off-by: Bhuminjay --- crates/ruff/tests/cli/lint.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 71155647139a39..12b697a5ddbdc6 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -2043,12 +2043,10 @@ select = ["UP006"] .args(["--stdin-filename", "test.py"]) .arg("-") .pass_stdin(r#"from typing import List; foo: List[int]"#), @" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- - test.py:1:31: UP006 Use `list` instead of `List` for type annotation - Found 1 error. - No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option). + All checks passed! ----- stderr ----- "); From 069164a973b8f0e92d8739ee0bd9939a76e9b4cd Mon Sep 17 00:00:00 2001 From: Bhuminjay Date: Mon, 9 Mar 2026 22:14:49 +0530 Subject: [PATCH 11/17] simplify Signed-off-by: Bhuminjay --- .../test/fixtures/pyupgrade/UP006_future.py | 19 - .../src/checkers/ast/analyze/expression.rs | 8 + crates/ruff_linter/src/rules/pyupgrade/mod.rs | 16 +- .../pyupgrade/rules/use_pep585_annotation.rs | 59 +-- ...UP006_0.py__preview_false__with_fa100.snap | 240 ++++++++++++ ..._UP006_0.py__preview_true__with_fa100.snap | 347 ++++++++++++++++++ ...tests__future_annotations_pep_585_p37.snap | 1 - 7 files changed, 625 insertions(+), 65 deletions(-) delete mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_future.py create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_false__with_fa100.snap create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_true__with_fa100.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_future.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_future.py deleted file mode 100644 index ce2522800c959f..00000000000000 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_future.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List -import typing -import typing as t - - -def func1(a_list: List[str]) -> None: - pass - - -def func2(a_list: typing.List[str]) -> None: - pass - - -def func3(a_list: t.List[str]) -> None: - pass - - -def func4(_: List[int]) -> None: - a_list: t.List[str] = [] diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 893cf0d57deaf1..ef382834a0e96a 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -309,6 +309,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { && checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() && !checker.settings().pyupgrade.keep_runtime_typing + && !(checker.is_rule_enabled(Rule::NonPEP585Annotation) + && is_up006_future_annotations_fix_enabled( + checker.settings(), + ) + && checker.settings().future_annotations) { flake8_future_annotations::rules::future_rewritable_type_annotation(checker, expr); } @@ -419,6 +424,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { && checker.target_version() >= PythonVersion::PY37 && checker.semantic.in_annotation() && !checker.settings().pyupgrade.keep_runtime_typing + && !(checker.is_rule_enabled(Rule::NonPEP585Annotation) + && is_up006_future_annotations_fix_enabled(checker.settings()) + && checker.settings().future_annotations) { flake8_future_annotations::rules::future_rewritable_type_annotation( checker, expr, diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 567eb7baf30849..d65b884d5b5d79 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -128,16 +128,22 @@ mod tests { Ok(()) } - #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_future.py"))] - fn up006_add_future_annotation_preview(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!("{}__preview", path.to_string_lossy()); + #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_0.py"), false; "stable_with_fa100")] + #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_0.py"), true; "preview_with_fa100")] + fn up006_future_annotations(rule_code: Rule, path: &Path, preview: bool) -> Result<()> { + let snapshot = format!("{}__preview_{preview}__with_fa100", path.to_string_lossy()); + let rules = vec![rule_code, Rule::FutureRewritableTypeAnnotation]; let diagnostics = test_path( Path::new("pyupgrade").join(path).as_path(), &settings::LinterSettings { future_annotations: true, - preview: PreviewMode::Enabled, + preview: if preview { + PreviewMode::Enabled + } else { + PreviewMode::Disabled + }, unresolved_target_version: PythonVersion::PY38.into(), - ..settings::LinterSettings::for_rule(rule_code) + ..settings::LinterSettings::for_rules(rules) }, )?; assert_diagnostics!(snapshot, diagnostics); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index d57d3f0b5c7b15..2b7e48a34f7235 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -1,13 +1,11 @@ use ruff_python_ast::Expr; +use crate::preview::is_up006_future_annotations_fix_enabled; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::name::UnqualifiedName; -use ruff_python_parser::semantic_errors::SemanticSyntaxContext; use ruff_python_semantic::analyze::typing::ModuleMember; use ruff_text_size::Ranged; -use crate::preview::is_up006_future_annotations_fix_enabled; - use crate::checkers::ast::Checker; use crate::importer::ImportRequest; use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; @@ -92,6 +90,22 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: expr.range(), ); if !checker.semantic().in_complex_string_type_definition() { + let future_import = if is_up006_future_annotations_fix_enabled(checker.settings()) + && checker.settings().future_annotations + && !checker.semantic().future_annotations_or_stub() + { + Some(checker.importer().add_future_import()) + } else { + None + }; + + let applicability = if checker.target_version() >= PythonVersion::PY310 + || checker.semantic().future_annotations_or_stub() + { + Applicability::Safe + } else { + Applicability::Unsafe + }; match replacement { ModuleMember::BuiltIn(name) => { // Built-in type, like `list`. @@ -102,17 +116,9 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: checker.semantic(), )?; let binding_edit = Edit::range_replacement(binding, expr.range()); - let (applicability, future_import) = fix_applicability(checker); - let mut secondary_edit = Vec::new(); - if let Some(import) = import_edit { - secondary_edit.push(import); - } - if let Some(future) = future_import { - secondary_edit.push(future); - } Ok(Fix::applicable_edits( binding_edit, - secondary_edit, + import_edit.into_iter().chain(future_import), applicability, )) }); @@ -126,15 +132,9 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: checker.semantic(), )?; let reference_edit = Edit::range_replacement(binding, expr.range()); - let (applicability, future_import) = fix_applicability(checker); - let mut secondary_edit = Vec::new(); - secondary_edit.push(import_edit); - if let Some(future) = future_import { - secondary_edit.push(future); - } Ok(Fix::applicable_edits( reference_edit, - secondary_edit, + std::iter::once(import_edit).chain(future_import), applicability, )) }); @@ -142,24 +142,3 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: } } } - -fn fix_applicability(checker: &Checker) -> (Applicability, Option) { - if checker.target_version() >= PythonVersion::PY39 { - return (Applicability::Safe, None); - } - if checker.settings().pyupgrade.keep_runtime_typing { - return (Applicability::Unsafe, None); - } - if checker.settings().future_annotations - && is_up006_future_annotations_fix_enabled(checker.settings()) - { - if checker.future_annotations_or_stub() { - (Applicability::Safe, None) - } else { - let future_import = checker.importer().add_future_import(); - (Applicability::Safe, Some(future_import)) - } - } else { - (Applicability::Unsafe, None) - } -} diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_false__with_fa100.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_false__with_fa100.snap new file mode 100644 index 00000000000000..2f990180b5eae9 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_false__with_fa100.snap @@ -0,0 +1,240 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:4:10 + | +4 | def f(x: typing.List[str]) -> None: + | ^^^^^^^^^^^ +5 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:11:10 + | +11 | def f(x: List[str]) -> None: + | ^^^^ +12 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:18:10 + | +18 | def f(x: t.List[str]) -> None: + | ^^^^^^ +19 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:25:10 + | +25 | def f(x: IList[str]) -> None: + | ^^^^^ +26 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:29:11 + | +29 | def f(x: "List[str]") -> None: + | ^^^^ +30 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:33:12 + | +33 | def f(x: r"List[str]") -> None: + | ^^^^ +34 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:37:11 + | +37 | def f(x: "List[str]") -> None: + | ^^^^ +38 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:41:13 + | +41 | def f(x: """List[str]""") -> None: + | ^^^^ +42 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:45:10 + | +45 | def f(x: "Li" "st[str]") -> None: + | ^^^^^^^^^^^^^^ +46 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:49:11 + | +49 | def f(x: "List['List[str]']") -> None: + | ^^^^ +50 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:49:17 + | +49 | def f(x: "List['List[str]']") -> None: + | ^^^^ +50 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:53:11 + | +53 | def f(x: "List['Li' 'st[str]']") -> None: + | ^^^^ +54 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:53:16 + | +53 | def f(x: "List['Li' 'st[str]']") -> None: + | ^^^^^^^^^^^^^^ +54 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:57:10 + | +57 | def f(x: "Li" "st['List[str]']") -> None: + | ^^^^^^^^^^^^^^^^^^^^^^ +58 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_0.py:57:10 + | +57 | def f(x: "Li" "st['List[str]']") -> None: + | ^^^^^^^^^^^^^^^^^^^^^^ +58 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.Deque` + --> UP006_0.py:61:10 + | +61 | def f(x: typing.Deque[str]) -> None: + | ^^^^^^^^^^^^ +62 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.DefaultDict` + --> UP006_0.py:65:10 + | +65 | def f(x: typing.DefaultDict[str, str]) -> None: + | ^^^^^^^^^^^^^^^^^^ +66 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_true__with_fa100.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_true__with_fa100.snap new file mode 100644 index 00000000000000..f565e04d8080b3 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_true__with_fa100.snap @@ -0,0 +1,347 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP006 [*] Use `list` instead of `typing.List` for type annotation + --> UP006_0.py:4:10 + | +4 | def f(x: typing.List[str]) -> None: + | ^^^^^^^^^^^ +5 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | + - def f(x: typing.List[str]) -> None: +5 + def f(x: list[str]) -> None: +6 | ... +7 | +8 | +note: This is an unsafe fix and may change runtime behavior + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_0.py:11:10 + | +11 | def f(x: List[str]) -> None: + | ^^^^ +12 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +9 | from typing import List +10 | +11 | + - def f(x: List[str]) -> None: +12 + def f(x: list[str]) -> None: +13 | ... +14 | +15 | +note: This is an unsafe fix and may change runtime behavior + +UP006 [*] Use `list` instead of `t.List` for type annotation + --> UP006_0.py:18:10 + | +18 | def f(x: t.List[str]) -> None: + | ^^^^^^ +19 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +16 | import typing as t +17 | +18 | + - def f(x: t.List[str]) -> None: +19 + def f(x: list[str]) -> None: +20 | ... +21 | +22 | +note: This is an unsafe fix and may change runtime behavior + +UP006 [*] Use `list` instead of `IList` for type annotation + --> UP006_0.py:25:10 + | +25 | def f(x: IList[str]) -> None: + | ^^^^^ +26 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +23 | from typing import List as IList +24 | +25 | + - def f(x: IList[str]) -> None: +26 + def f(x: list[str]) -> None: +27 | ... +28 | +29 | +note: This is an unsafe fix and may change runtime behavior + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_0.py:29:11 + | +29 | def f(x: "List[str]") -> None: + | ^^^^ +30 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +27 | ... +28 | +29 | + - def f(x: "List[str]") -> None: +30 + def f(x: "list[str]") -> None: +31 | ... +32 | +33 | +note: This is an unsafe fix and may change runtime behavior + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_0.py:33:12 + | +33 | def f(x: r"List[str]") -> None: + | ^^^^ +34 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +31 | ... +32 | +33 | + - def f(x: r"List[str]") -> None: +34 + def f(x: r"list[str]") -> None: +35 | ... +36 | +37 | +note: This is an unsafe fix and may change runtime behavior + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_0.py:37:11 + | +37 | def f(x: "List[str]") -> None: + | ^^^^ +38 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +35 | ... +36 | +37 | + - def f(x: "List[str]") -> None: +38 + def f(x: "list[str]") -> None: +39 | ... +40 | +41 | +note: This is an unsafe fix and may change runtime behavior + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_0.py:41:13 + | +41 | def f(x: """List[str]""") -> None: + | ^^^^ +42 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +39 | ... +40 | +41 | + - def f(x: """List[str]""") -> None: +42 + def f(x: """list[str]""") -> None: +43 | ... +44 | +45 | +note: This is an unsafe fix and may change runtime behavior + +UP006 Use `list` instead of `List` for type annotation + --> UP006_0.py:45:10 + | +45 | def f(x: "Li" "st[str]") -> None: + | ^^^^^^^^^^^^^^ +46 | ... + | +help: Replace with `list` + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_0.py:49:11 + | +49 | def f(x: "List['List[str]']") -> None: + | ^^^^ +50 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +47 | ... +48 | +49 | + - def f(x: "List['List[str]']") -> None: +50 + def f(x: "list['List[str]']") -> None: +51 | ... +52 | +53 | +note: This is an unsafe fix and may change runtime behavior + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_0.py:49:17 + | +49 | def f(x: "List['List[str]']") -> None: + | ^^^^ +50 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +47 | ... +48 | +49 | + - def f(x: "List['List[str]']") -> None: +50 + def f(x: "List['list[str]']") -> None: +51 | ... +52 | +53 | +note: This is an unsafe fix and may change runtime behavior + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_0.py:53:11 + | +53 | def f(x: "List['Li' 'st[str]']") -> None: + | ^^^^ +54 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +51 | ... +52 | +53 | + - def f(x: "List['Li' 'st[str]']") -> None: +54 + def f(x: "list['Li' 'st[str]']") -> None: +55 | ... +56 | +57 | +note: This is an unsafe fix and may change runtime behavior + +UP006 Use `list` instead of `List` for type annotation + --> UP006_0.py:53:16 + | +53 | def f(x: "List['Li' 'st[str]']") -> None: + | ^^^^^^^^^^^^^^ +54 | ... + | +help: Replace with `list` + +UP006 Use `list` instead of `List` for type annotation + --> UP006_0.py:57:10 + | +57 | def f(x: "Li" "st['List[str]']") -> None: + | ^^^^^^^^^^^^^^^^^^^^^^ +58 | ... + | +help: Replace with `list` + +UP006 Use `list` instead of `List` for type annotation + --> UP006_0.py:57:10 + | +57 | def f(x: "Li" "st['List[str]']") -> None: + | ^^^^^^^^^^^^^^^^^^^^^^ +58 | ... + | +help: Replace with `list` + +UP006 [*] Use `collections.deque` instead of `typing.Deque` for type annotation + --> UP006_0.py:61:10 + | +61 | def f(x: typing.Deque[str]) -> None: + | ^^^^^^^^^^^^ +62 | ... + | +help: Replace with `collections.deque` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +21 | +22 | +23 | from typing import List as IList +24 + from collections import deque +25 | +26 | +27 | def f(x: IList[str]) -> None: +-------------------------------------------------------------------------------- +60 | ... +61 | +62 | + - def f(x: typing.Deque[str]) -> None: +63 + def f(x: deque[str]) -> None: +64 | ... +65 | +66 | +note: This is an unsafe fix and may change runtime behavior + +UP006 [*] Use `collections.defaultdict` instead of `typing.DefaultDict` for type annotation + --> UP006_0.py:65:10 + | +65 | def f(x: typing.DefaultDict[str, str]) -> None: + | ^^^^^^^^^^^^^^^^^^ +66 | ... + | +help: Replace with `collections.defaultdict` +1 + from __future__ import annotations +2 | import typing +3 | +4 | +-------------------------------------------------------------------------------- +21 | +22 | +23 | from typing import List as IList +24 + from collections import defaultdict +25 | +26 | +27 | def f(x: IList[str]) -> None: +-------------------------------------------------------------------------------- +64 | ... +65 | +66 | + - def f(x: typing.DefaultDict[str, str]) -> None: +67 + def f(x: defaultdict[str, str]) -> None: +68 | ... +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_p37.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_p37.snap index 45d9d11dabee0a..8e3f9637e723ff 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_p37.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_p37.snap @@ -18,4 +18,3 @@ help: Replace with `list` 35 | y = List[int]() 36 | y.append(x) 37 | return y -note: This is an unsafe fix and may change runtime behavior From 2d85f5089fd6159ef4a8c8ed27dc4facdc7bdcf1 Mon Sep 17 00:00:00 2001 From: Bhuminjay Soni Date: Mon, 9 Mar 2026 22:15:57 +0530 Subject: [PATCH 12/17] Delete crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py__preview.snap --- ...rade__tests__UP006_future.py__preview.snap | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py__preview.snap diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py__preview.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py__preview.snap deleted file mode 100644 index b11a7fd6996c2a..00000000000000 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_future.py__preview.snap +++ /dev/null @@ -1,105 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyupgrade/mod.rs ---- -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_future.py:6:19 - | -6 | def func1(a_list: List[str]) -> None: - | ^^^^ -7 | pass - | -help: Replace with `list` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing -4 | import typing as t -5 | -6 | - - def func1(a_list: List[str]) -> None: -7 + def func1(a_list: list[str]) -> None: -8 | pass -9 | -10 | - -UP006 [*] Use `list` instead of `typing.List` for type annotation - --> UP006_future.py:10:19 - | -10 | def func2(a_list: typing.List[str]) -> None: - | ^^^^^^^^^^^ -11 | pass - | -help: Replace with `list` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing -4 | import typing as t --------------------------------------------------------------------------------- -8 | pass -9 | -10 | - - def func2(a_list: typing.List[str]) -> None: -11 + def func2(a_list: list[str]) -> None: -12 | pass -13 | -14 | - -UP006 [*] Use `list` instead of `t.List` for type annotation - --> UP006_future.py:14:19 - | -14 | def func3(a_list: t.List[str]) -> None: - | ^^^^^^ -15 | pass - | -help: Replace with `list` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing -4 | import typing as t --------------------------------------------------------------------------------- -12 | pass -13 | -14 | - - def func3(a_list: t.List[str]) -> None: -15 + def func3(a_list: list[str]) -> None: -16 | pass -17 | -18 | - -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_future.py:18:14 - | -18 | def func4(_: List[int]) -> None: - | ^^^^ -19 | a_list: t.List[str] = [] - | -help: Replace with `list` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing -4 | import typing as t --------------------------------------------------------------------------------- -16 | pass -17 | -18 | - - def func4(_: List[int]) -> None: -19 + def func4(_: list[int]) -> None: -20 | a_list: t.List[str] = [] - -UP006 [*] Use `list` instead of `t.List` for type annotation - --> UP006_future.py:19:13 - | -18 | def func4(_: List[int]) -> None: -19 | a_list: t.List[str] = [] - | ^^^^^^ - | -help: Replace with `list` -1 + from __future__ import annotations -2 | from typing import List -3 | import typing -4 | import typing as t --------------------------------------------------------------------------------- -17 | -18 | -19 | def func4(_: List[int]) -> None: - - a_list: t.List[str] = [] -20 + a_list: list[str] = [] From 21b4bf167067e0f878ed9b44d3a8f4a09100d4b6 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 9 Mar 2026 18:07:50 -0400 Subject: [PATCH 13/17] fixes --- .../src/checkers/ast/analyze/expression.rs | 8 +++++--- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 16 ++++++++++++++++ .../pyupgrade/rules/use_pep585_annotation.rs | 16 +++++++++------- ...e__tests__future_annotations_pep_585_p37.snap | 1 + ...06_preview_no_future_annotations_setting.snap | 4 ++++ 5 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_no_future_annotations_setting.snap diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index ef382834a0e96a..26472001fec9ef 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -323,9 +323,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { || checker.target_version() >= PythonVersion::PY39 || (checker.target_version() >= PythonVersion::PY37 && (checker.semantic.future_annotations_or_stub() - || is_up006_future_annotations_fix_enabled( + || (is_up006_future_annotations_fix_enabled( checker.settings(), - )) + ) && checker.settings().future_annotations)) && checker.semantic.in_annotation() && !checker.settings().pyupgrade.keep_runtime_typing) { @@ -438,7 +438,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { || checker.target_version() >= PythonVersion::PY39 || (checker.target_version() >= PythonVersion::PY37 && (checker.semantic.future_annotations_or_stub() - || is_up006_future_annotations_fix_enabled(checker.settings())) + || (is_up006_future_annotations_fix_enabled( + checker.settings(), + ) && checker.settings().future_annotations)) && checker.semantic.in_annotation() && !checker.settings().pyupgrade.keep_runtime_typing) { diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index d65b884d5b5d79..210f329dbd7a2c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -150,6 +150,22 @@ mod tests { Ok(()) } + /// Test that `UP006` does not fire on 3.8 when `future-annotations` is disabled, even in + /// preview mode. + #[test] + fn up006_preview_no_future_annotations_setting() -> Result<()> { + let diagnostics = test_path( + Path::new("pyupgrade/UP006_0.py"), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + unresolved_target_version: PythonVersion::PY38.into(), + ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_2.py"))] #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_1.py"))] fn rules_not_applied_default_typevar_backported(rule_code: Rule, path: &Path) -> Result<()> { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 2b7e48a34f7235..0e2ed8aea850d0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -99,13 +99,15 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: None }; - let applicability = if checker.target_version() >= PythonVersion::PY310 - || checker.semantic().future_annotations_or_stub() - { - Applicability::Safe - } else { - Applicability::Unsafe - }; + // Adding `from __future__ import annotations` changes runtime behavior for all + // annotations in the file, so those fixes are always unsafe. Without a future + // import, the original applicability applies: safe only on Python 3.10+. + let applicability = + if future_import.is_none() && checker.target_version() >= PythonVersion::PY310 { + Applicability::Safe + } else { + Applicability::Unsafe + }; match replacement { ModuleMember::BuiltIn(name) => { // Built-in type, like `list`. diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_p37.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_p37.snap index 8e3f9637e723ff..45d9d11dabee0a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_p37.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_p37.snap @@ -18,3 +18,4 @@ help: Replace with `list` 35 | y = List[int]() 36 | y.append(x) 37 | return y +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_no_future_annotations_setting.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_no_future_annotations_setting.snap new file mode 100644 index 00000000000000..2bacb5d540775c --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_no_future_annotations_setting.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- + From de70f3641a94fae1d5b07c5ac4a7b2a50887624d Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 9 Mar 2026 19:31:06 -0400 Subject: [PATCH 14/17] simplify tests --- .../test/fixtures/pyupgrade/UP006_4.py | 10 + crates/ruff_linter/src/rules/pyupgrade/mod.rs | 56 +-- ...UP006_0.py__preview_false__with_fa100.snap | 240 ------------ ..._UP006_0.py__preview_true__with_fa100.snap | 347 ------------------ ...preview_no_future_annotations_setting.snap | 4 - ...rade__tests__up006_preview_with_fa100.snap | 85 +++++ ...h_fa100_no_future_annotations_setting.snap | 40 ++ 7 files changed, 169 insertions(+), 613 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_4.py delete mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_false__with_fa100.snap delete mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_true__with_fa100.snap delete mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_no_future_annotations_setting.snap create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100.snap create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100_no_future_annotations_setting.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_4.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_4.py new file mode 100644 index 00000000000000..782ef95f5ed92a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP006_4.py @@ -0,0 +1,10 @@ +import typing +from typing import List + + +def f(x: typing.List[str]) -> None: + ... + + +def g(x: List[str]) -> None: + ... diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 210f329dbd7a2c..34c2c208b682a8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -128,41 +128,53 @@ mod tests { Ok(()) } - #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_0.py"), false; "stable_with_fa100")] - #[test_case(Rule::NonPEP585Annotation, Path::new("UP006_0.py"), true; "preview_with_fa100")] - fn up006_future_annotations(rule_code: Rule, path: &Path, preview: bool) -> Result<()> { - let snapshot = format!("{}__preview_{preview}__with_fa100", path.to_string_lossy()); - let rules = vec![rule_code, Rule::FutureRewritableTypeAnnotation]; - let diagnostics = test_path( - Path::new("pyupgrade").join(path).as_path(), + /// Test that enabling preview switches from `FA100` to `UP006` when `future-annotations` is on. + #[test] + fn up006_preview_with_fa100() -> Result<()> { + assert_diagnostics_diff!( + Path::new("pyupgrade/UP006_4.py"), &settings::LinterSettings { future_annotations: true, - preview: if preview { - PreviewMode::Enabled - } else { - PreviewMode::Disabled - }, + preview: PreviewMode::Disabled, unresolved_target_version: PythonVersion::PY38.into(), - ..settings::LinterSettings::for_rules(rules) + ..settings::LinterSettings::for_rules(vec![ + Rule::NonPEP585Annotation, + Rule::FutureRewritableTypeAnnotation + ]) }, - )?; - assert_diagnostics!(snapshot, diagnostics); + &settings::LinterSettings { + future_annotations: true, + preview: PreviewMode::Enabled, + unresolved_target_version: PythonVersion::PY38.into(), + ..settings::LinterSettings::for_rules(vec![ + Rule::NonPEP585Annotation, + Rule::FutureRewritableTypeAnnotation + ]) + }, + ); Ok(()) } - /// Test that `UP006` does not fire on 3.8 when `future-annotations` is disabled, even in - /// preview mode. + /// Test that `FA100` fires when added alongside `UP006` in preview on 3.8 with + /// `future-annotations` disabled. #[test] - fn up006_preview_no_future_annotations_setting() -> Result<()> { - let diagnostics = test_path( - Path::new("pyupgrade/UP006_0.py"), + fn up006_preview_with_fa100_no_future_annotations_setting() -> Result<()> { + assert_diagnostics_diff!( + Path::new("pyupgrade/UP006_4.py"), &settings::LinterSettings { preview: PreviewMode::Enabled, unresolved_target_version: PythonVersion::PY38.into(), ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) }, - )?; - assert_diagnostics!(diagnostics); + &settings::LinterSettings { + preview: PreviewMode::Enabled, + unresolved_target_version: PythonVersion::PY38.into(), + ..settings::LinterSettings::for_rules(vec![ + Rule::NonPEP585Annotation, + Rule::FutureRewritableTypeAnnotation, + ]) + }, + ); Ok(()) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_false__with_fa100.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_false__with_fa100.snap deleted file mode 100644 index 2f990180b5eae9..00000000000000 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_false__with_fa100.snap +++ /dev/null @@ -1,240 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyupgrade/mod.rs ---- -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:4:10 - | -4 | def f(x: typing.List[str]) -> None: - | ^^^^^^^^^^^ -5 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:11:10 - | -11 | def f(x: List[str]) -> None: - | ^^^^ -12 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:18:10 - | -18 | def f(x: t.List[str]) -> None: - | ^^^^^^ -19 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:25:10 - | -25 | def f(x: IList[str]) -> None: - | ^^^^^ -26 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:29:11 - | -29 | def f(x: "List[str]") -> None: - | ^^^^ -30 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:33:12 - | -33 | def f(x: r"List[str]") -> None: - | ^^^^ -34 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:37:11 - | -37 | def f(x: "List[str]") -> None: - | ^^^^ -38 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:41:13 - | -41 | def f(x: """List[str]""") -> None: - | ^^^^ -42 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:45:10 - | -45 | def f(x: "Li" "st[str]") -> None: - | ^^^^^^^^^^^^^^ -46 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:49:11 - | -49 | def f(x: "List['List[str]']") -> None: - | ^^^^ -50 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:49:17 - | -49 | def f(x: "List['List[str]']") -> None: - | ^^^^ -50 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:53:11 - | -53 | def f(x: "List['Li' 'st[str]']") -> None: - | ^^^^ -54 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:53:16 - | -53 | def f(x: "List['Li' 'st[str]']") -> None: - | ^^^^^^^^^^^^^^ -54 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:57:10 - | -57 | def f(x: "Li" "st['List[str]']") -> None: - | ^^^^^^^^^^^^^^^^^^^^^^ -58 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` - --> UP006_0.py:57:10 - | -57 | def f(x: "Li" "st['List[str]']") -> None: - | ^^^^^^^^^^^^^^^^^^^^^^ -58 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.Deque` - --> UP006_0.py:61:10 - | -61 | def f(x: typing.Deque[str]) -> None: - | ^^^^^^^^^^^^ -62 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior - -FA100 [*] Add `from __future__ import annotations` to simplify `typing.DefaultDict` - --> UP006_0.py:65:10 - | -65 | def f(x: typing.DefaultDict[str, str]) -> None: - | ^^^^^^^^^^^^^^^^^^ -66 | ... - | -help: Add `from __future__ import annotations` -1 + from __future__ import annotations -2 | import typing -3 | -4 | -note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_true__with_fa100.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_true__with_fa100.snap deleted file mode 100644 index f565e04d8080b3..00000000000000 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP006_0.py__preview_true__with_fa100.snap +++ /dev/null @@ -1,347 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyupgrade/mod.rs ---- -UP006 [*] Use `list` instead of `typing.List` for type annotation - --> UP006_0.py:4:10 - | -4 | def f(x: typing.List[str]) -> None: - | ^^^^^^^^^^^ -5 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | - - def f(x: typing.List[str]) -> None: -5 + def f(x: list[str]) -> None: -6 | ... -7 | -8 | -note: This is an unsafe fix and may change runtime behavior - -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_0.py:11:10 - | -11 | def f(x: List[str]) -> None: - | ^^^^ -12 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -9 | from typing import List -10 | -11 | - - def f(x: List[str]) -> None: -12 + def f(x: list[str]) -> None: -13 | ... -14 | -15 | -note: This is an unsafe fix and may change runtime behavior - -UP006 [*] Use `list` instead of `t.List` for type annotation - --> UP006_0.py:18:10 - | -18 | def f(x: t.List[str]) -> None: - | ^^^^^^ -19 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -16 | import typing as t -17 | -18 | - - def f(x: t.List[str]) -> None: -19 + def f(x: list[str]) -> None: -20 | ... -21 | -22 | -note: This is an unsafe fix and may change runtime behavior - -UP006 [*] Use `list` instead of `IList` for type annotation - --> UP006_0.py:25:10 - | -25 | def f(x: IList[str]) -> None: - | ^^^^^ -26 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -23 | from typing import List as IList -24 | -25 | - - def f(x: IList[str]) -> None: -26 + def f(x: list[str]) -> None: -27 | ... -28 | -29 | -note: This is an unsafe fix and may change runtime behavior - -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_0.py:29:11 - | -29 | def f(x: "List[str]") -> None: - | ^^^^ -30 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -27 | ... -28 | -29 | - - def f(x: "List[str]") -> None: -30 + def f(x: "list[str]") -> None: -31 | ... -32 | -33 | -note: This is an unsafe fix and may change runtime behavior - -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_0.py:33:12 - | -33 | def f(x: r"List[str]") -> None: - | ^^^^ -34 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -31 | ... -32 | -33 | - - def f(x: r"List[str]") -> None: -34 + def f(x: r"list[str]") -> None: -35 | ... -36 | -37 | -note: This is an unsafe fix and may change runtime behavior - -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_0.py:37:11 - | -37 | def f(x: "List[str]") -> None: - | ^^^^ -38 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -35 | ... -36 | -37 | - - def f(x: "List[str]") -> None: -38 + def f(x: "list[str]") -> None: -39 | ... -40 | -41 | -note: This is an unsafe fix and may change runtime behavior - -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_0.py:41:13 - | -41 | def f(x: """List[str]""") -> None: - | ^^^^ -42 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -39 | ... -40 | -41 | - - def f(x: """List[str]""") -> None: -42 + def f(x: """list[str]""") -> None: -43 | ... -44 | -45 | -note: This is an unsafe fix and may change runtime behavior - -UP006 Use `list` instead of `List` for type annotation - --> UP006_0.py:45:10 - | -45 | def f(x: "Li" "st[str]") -> None: - | ^^^^^^^^^^^^^^ -46 | ... - | -help: Replace with `list` - -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_0.py:49:11 - | -49 | def f(x: "List['List[str]']") -> None: - | ^^^^ -50 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -47 | ... -48 | -49 | - - def f(x: "List['List[str]']") -> None: -50 + def f(x: "list['List[str]']") -> None: -51 | ... -52 | -53 | -note: This is an unsafe fix and may change runtime behavior - -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_0.py:49:17 - | -49 | def f(x: "List['List[str]']") -> None: - | ^^^^ -50 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -47 | ... -48 | -49 | - - def f(x: "List['List[str]']") -> None: -50 + def f(x: "List['list[str]']") -> None: -51 | ... -52 | -53 | -note: This is an unsafe fix and may change runtime behavior - -UP006 [*] Use `list` instead of `List` for type annotation - --> UP006_0.py:53:11 - | -53 | def f(x: "List['Li' 'st[str]']") -> None: - | ^^^^ -54 | ... - | -help: Replace with `list` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -51 | ... -52 | -53 | - - def f(x: "List['Li' 'st[str]']") -> None: -54 + def f(x: "list['Li' 'st[str]']") -> None: -55 | ... -56 | -57 | -note: This is an unsafe fix and may change runtime behavior - -UP006 Use `list` instead of `List` for type annotation - --> UP006_0.py:53:16 - | -53 | def f(x: "List['Li' 'st[str]']") -> None: - | ^^^^^^^^^^^^^^ -54 | ... - | -help: Replace with `list` - -UP006 Use `list` instead of `List` for type annotation - --> UP006_0.py:57:10 - | -57 | def f(x: "Li" "st['List[str]']") -> None: - | ^^^^^^^^^^^^^^^^^^^^^^ -58 | ... - | -help: Replace with `list` - -UP006 Use `list` instead of `List` for type annotation - --> UP006_0.py:57:10 - | -57 | def f(x: "Li" "st['List[str]']") -> None: - | ^^^^^^^^^^^^^^^^^^^^^^ -58 | ... - | -help: Replace with `list` - -UP006 [*] Use `collections.deque` instead of `typing.Deque` for type annotation - --> UP006_0.py:61:10 - | -61 | def f(x: typing.Deque[str]) -> None: - | ^^^^^^^^^^^^ -62 | ... - | -help: Replace with `collections.deque` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -21 | -22 | -23 | from typing import List as IList -24 + from collections import deque -25 | -26 | -27 | def f(x: IList[str]) -> None: --------------------------------------------------------------------------------- -60 | ... -61 | -62 | - - def f(x: typing.Deque[str]) -> None: -63 + def f(x: deque[str]) -> None: -64 | ... -65 | -66 | -note: This is an unsafe fix and may change runtime behavior - -UP006 [*] Use `collections.defaultdict` instead of `typing.DefaultDict` for type annotation - --> UP006_0.py:65:10 - | -65 | def f(x: typing.DefaultDict[str, str]) -> None: - | ^^^^^^^^^^^^^^^^^^ -66 | ... - | -help: Replace with `collections.defaultdict` -1 + from __future__ import annotations -2 | import typing -3 | -4 | --------------------------------------------------------------------------------- -21 | -22 | -23 | from typing import List as IList -24 + from collections import defaultdict -25 | -26 | -27 | def f(x: IList[str]) -> None: --------------------------------------------------------------------------------- -64 | ... -65 | -66 | - - def f(x: typing.DefaultDict[str, str]) -> None: -67 + def f(x: defaultdict[str, str]) -> None: -68 | ... -note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_no_future_annotations_setting.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_no_future_annotations_setting.snap deleted file mode 100644 index 2bacb5d540775c..00000000000000 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_no_future_annotations_setting.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyupgrade/mod.rs ---- - diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100.snap new file mode 100644 index 00000000000000..f5f0189802f653 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100.snap @@ -0,0 +1,85 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +--- Linter settings --- +-linter.preview = disabled ++linter.preview = enabled + +--- Summary --- +Removed: 2 +Added: 2 + +--- Removed --- +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_4.py:5:10 + | +5 | def f(x: typing.List[str]) -> None: + | ^^^^^^^^^^^ +6 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | from typing import List +4 | +note: This is an unsafe fix and may change runtime behavior + + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_4.py:9:10 + | + 9 | def g(x: List[str]) -> None: + | ^^^^ +10 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | from typing import List +4 | +note: This is an unsafe fix and may change runtime behavior + + + +--- Added --- +UP006 [*] Use `list` instead of `typing.List` for type annotation + --> UP006_4.py:5:10 + | +5 | def f(x: typing.List[str]) -> None: + | ^^^^^^^^^^^ +6 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | from typing import List +4 | +5 | + - def f(x: typing.List[str]) -> None: +6 + def f(x: list[str]) -> None: +7 | ... +8 | +9 | +note: This is an unsafe fix and may change runtime behavior + + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_4.py:9:10 + | + 9 | def g(x: List[str]) -> None: + | ^^^^ +10 | ... + | +help: Replace with `list` +1 + from __future__ import annotations +2 | import typing +3 | from typing import List +4 | +-------------------------------------------------------------------------------- +7 | ... +8 | +9 | + - def g(x: List[str]) -> None: +10 + def g(x: list[str]) -> None: +11 | ... +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100_no_future_annotations_setting.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100_no_future_annotations_setting.snap new file mode 100644 index 00000000000000..34241a23a3aee9 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100_no_future_annotations_setting.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +--- Linter settings --- ++ future-rewritable-type-annotation (FA100), ++ future-rewritable-type-annotation (FA100), + +--- Summary --- +Removed: 0 +Added: 2 + +--- Added --- +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_4.py:5:10 + | +5 | def f(x: typing.List[str]) -> None: + | ^^^^^^^^^^^ +6 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | from typing import List +4 | +note: This is an unsafe fix and may change runtime behavior + + +FA100 [*] Add `from __future__ import annotations` to simplify `typing.List` + --> UP006_4.py:9:10 + | + 9 | def g(x: List[str]) -> None: + | ^^^^ +10 | ... + | +help: Add `from __future__ import annotations` +1 + from __future__ import annotations +2 | import typing +3 | from typing import List +4 | +note: This is an unsafe fix and may change runtime behavior From cb4b8881d538a9f1445d551627d8b3941359fde5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 9 Mar 2026 19:48:26 -0400 Subject: [PATCH 15/17] tidy imports --- .../src/rules/pyupgrade/rules/use_pep585_annotation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 0e2ed8aea850d0..e92908bdd5a393 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -1,15 +1,15 @@ use ruff_python_ast::Expr; -use crate::preview::is_up006_future_annotations_fix_enabled; use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::PythonVersion; use ruff_python_ast::name::UnqualifiedName; use ruff_python_semantic::analyze::typing::ModuleMember; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::importer::ImportRequest; +use crate::preview::is_up006_future_annotations_fix_enabled; use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; -use ruff_python_ast::PythonVersion; /// ## What it does /// Checks for the use of generics that can be replaced with standard library From f9a43d208cb18a5e3ee17a40b7712b3c885cccfb Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 9 Mar 2026 20:04:09 -0400 Subject: [PATCH 16/17] fix missing version check this should fully preserve the stable behavior of a safe fix for Python versions after 3.10 --- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 20 +++++++++++ .../pyupgrade/rules/use_pep585_annotation.rs | 3 +- ...ith_fa100_and_future_annotations_py39.snap | 34 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100_and_future_annotations_py39.snap diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 34c2c208b682a8..a4340bd604e711 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -178,6 +178,26 @@ mod tests { Ok(()) } + /// On 3.10, the `__future__` import is unnecessary from either the `future-annotations` setting + /// or from FA100. + #[test] + fn up006_preview_with_fa100_and_future_annotations_py39() -> Result<()> { + let diagnostics = test_path( + Path::new("pyupgrade/UP006_4.py"), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + future_annotations: true, + unresolved_target_version: PythonVersion::PY310.into(), + ..settings::LinterSettings::for_rules(vec![ + Rule::NonPEP585Annotation, + Rule::FutureRewritableTypeAnnotation, + ]) + }, + )?; + assert_diagnostics!(diagnostics); + Ok(()) + } + #[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_2.py"))] #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_1.py"))] fn rules_not_applied_default_typevar_backported(rule_code: Rule, path: &Path) -> Result<()> { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index e92908bdd5a393..1e3c945ae703f2 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -90,7 +90,8 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement: expr.range(), ); if !checker.semantic().in_complex_string_type_definition() { - let future_import = if is_up006_future_annotations_fix_enabled(checker.settings()) + let future_import = if checker.target_version() < PythonVersion::PY310 + && is_up006_future_annotations_fix_enabled(checker.settings()) && checker.settings().future_annotations && !checker.semantic().future_annotations_or_stub() { diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100_and_future_annotations_py39.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100_and_future_annotations_py39.snap new file mode 100644 index 00000000000000..99f188efaaf3a3 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__up006_preview_with_fa100_and_future_annotations_py39.snap @@ -0,0 +1,34 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP006 [*] Use `list` instead of `typing.List` for type annotation + --> UP006_4.py:5:10 + | +5 | def f(x: typing.List[str]) -> None: + | ^^^^^^^^^^^ +6 | ... + | +help: Replace with `list` +2 | from typing import List +3 | +4 | + - def f(x: typing.List[str]) -> None: +5 + def f(x: list[str]) -> None: +6 | ... +7 | +8 | + +UP006 [*] Use `list` instead of `List` for type annotation + --> UP006_4.py:9:10 + | + 9 | def g(x: List[str]) -> None: + | ^^^^ +10 | ... + | +help: Replace with `list` +6 | ... +7 | +8 | + - def g(x: List[str]) -> None: +9 + def g(x: list[str]) -> None: +10 | ... From 0bf4a22f39cd3f4d2f933b9424e58c70ddb04524 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 9 Mar 2026 20:11:34 -0400 Subject: [PATCH 17/17] add preview note to docs --- .../src/rules/pyupgrade/rules/use_pep585_annotation.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs index 1e3c945ae703f2..458be3099031ff 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep585_annotation.rs @@ -50,11 +50,17 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// alongside libraries that rely on runtime type annotations, like Pydantic, /// on Python versions prior to Python 3.9. /// +/// In [preview], this rule can also add its own `__future__` import on Python +/// 3.9 and earlier, if the [`lint.future-annotations`] setting is enabled. This +/// also makes the fix unsafe. +/// /// ## Options /// - `target-version` /// - `lint.pyupgrade.keep-runtime-typing` +/// - `lint.future-annotations` /// /// [PEP 585]: https://peps.python.org/pep-0585/ +/// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.155")] pub(crate) struct NonPEP585Annotation {