diff --git a/README.md b/README.md index 0579d377ea..116ecd7812 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,7 @@ quality tools, including: - [flake8-eradicate](https://pypi.org/project/flake8-eradicate/) - [flake8-errmsg](https://pypi.org/project/flake8-errmsg/) - [flake8-executable](https://pypi.org/project/flake8-executable/) +- [flake8-future-annotations](https://pypi.org/project/flake8-future-annotations/) - [flake8-gettext](https://pypi.org/project/flake8-gettext/) - [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/) - [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions) diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/edge_case.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/edge_case.py new file mode 100644 index 0000000000..f98adefaf4 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/edge_case.py @@ -0,0 +1,7 @@ +from typing import List +import typing as t + + +def main(_: List[int]) -> None: + a_list: t.List[str] = [] + a_list.append("hello") diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import.py new file mode 100644 index 0000000000..a8229aca12 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import.py @@ -0,0 +1,6 @@ +from typing import List + + +def main() -> None: + a_list: List[str] = [] + a_list.append("hello") diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import_many.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import_many.py new file mode 100644 index 0000000000..28ccc2e4c3 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import_many.py @@ -0,0 +1,8 @@ +from typing import Dict, List, Optional, Set, Union, cast + + +def main() -> None: + a_list: List[Optional[str]] = [] + a_list.append("hello") + a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) + a_dict[1] = {True, False} diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing.py new file mode 100644 index 0000000000..fccfe30aa2 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing.py @@ -0,0 +1,6 @@ +import typing + + +def main() -> None: + a_list: typing.List[str] = [] + a_list.append("hello") diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing_as.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing_as.py new file mode 100644 index 0000000000..5f634a0334 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing_as.py @@ -0,0 +1,6 @@ +import typing as t + + +def main() -> None: + a_list: t.List[str] = [] + a_list.append("hello") diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_lowercase.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_lowercase.py new file mode 100644 index 0000000000..a573432cd5 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_lowercase.py @@ -0,0 +1,7 @@ +def main() -> None: + a_list: list[str] = [] + a_list.append("hello") + + +def hello(y: dict[str, int]) -> None: + del y diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_union.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_union.py new file mode 100644 index 0000000000..50206192f1 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_union.py @@ -0,0 +1,7 @@ +def main() -> None: + a_list: list[str] | None = [] + a_list.append("hello") + + +def hello(y: dict[str, int] | None) -> None: + del y diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_union_inner.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_union_inner.py new file mode 100644 index 0000000000..9f9b5bd574 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_union_inner.py @@ -0,0 +1,8 @@ +def main() -> None: + a_list: list[str | None] = [] + a_list.append("hello") + + +def hello(y: dict[str | None, int]) -> None: + z: tuple[str, str | None, str] = tuple(y) + del z diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_no_types.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_no_types.py new file mode 100644 index 0000000000..54fff80906 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_no_types.py @@ -0,0 +1,3 @@ +def main() -> str: + a_str = "hello" + return a_str diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_non_simplifiable_types.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_non_simplifiable_types.py new file mode 100644 index 0000000000..b5121a9297 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_non_simplifiable_types.py @@ -0,0 +1,10 @@ +from typing import NamedTuple + + +class Stuff(NamedTuple): + x: int + + +def main() -> None: + a_list = Stuff(5) + print(a_list) diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_uses_future.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_uses_future.py new file mode 100644 index 0000000000..281b96d393 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_uses_future.py @@ -0,0 +1,6 @@ +from __future__ import annotations + + +def main() -> None: + a_list: list[str] = [] + a_list.append("hello") diff --git a/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_variable_name.py b/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_variable_name.py new file mode 100644 index 0000000000..a1a8febe42 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_variable_name.py @@ -0,0 +1,8 @@ +import typing + +IRRELEVANT = typing.TypeVar + + +def main() -> None: + List: list[str] = [] + List.append("hello") diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 63899c91be..53bab09e27 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -44,12 +44,12 @@ use crate::registry::{AsRule, Rule}; use crate::rules::{ flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger, - flake8_django, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, - flake8_import_conventions, flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, - flake8_pytest_style, flake8_raise, flake8_return, flake8_self, flake8_simplify, - flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, flake8_use_pathlib, flynt, - mccabe, numpy, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, - pylint, pyupgrade, ruff, tryceratops, + flake8_django, flake8_errmsg, flake8_future_annotations, flake8_gettext, + flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format, flake8_pie, + flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_self, + flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, + flake8_use_pathlib, flynt, mccabe, numpy, pandas_vet, pep8_naming, pycodestyle, pydocstyle, + pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops, }; use crate::settings::types::PythonVersion; use crate::settings::{flags, Settings}; @@ -1077,7 +1077,6 @@ where pyupgrade::rules::unnecessary_builtin_import(self, stmt, module, names); } } - if self.settings.rules.enabled(Rule::BannedApi) { if let Some(module) = helpers::resolve_imported_module_path(level, module, self.module_path) @@ -2258,12 +2257,25 @@ where match &expr.node { ExprKind::Subscript(ast::ExprSubscript { value, slice, .. }) => { // Ex) Optional[...], Union[...] + if self + .settings + .rules + .enabled(Rule::MissingFutureAnnotationsImport) + && (self.settings.target_version < PythonVersion::Py310 + && (self.settings.target_version >= PythonVersion::Py37 + && !self.ctx.future_annotations() + && self.ctx.in_annotation())) + && analyze::typing::is_pep604_builtin(value, &self.ctx) + { + flake8_future_annotations::rules::missing_future_annotations(self, value); + } if !self.settings.pyupgrade.keep_runtime_typing && self.settings.rules.enabled(Rule::NonPEP604Annotation) && (self.settings.target_version >= PythonVersion::Py310 || (self.settings.target_version >= PythonVersion::Py37 && self.ctx.future_annotations() && self.ctx.in_annotation())) + && analyze::typing::is_pep604_builtin(value, &self.ctx) { pyupgrade::rules::use_pep604_annotation(self, expr, value, slice); } @@ -2321,6 +2333,20 @@ where } // Ex) List[...] + if self + .settings + .rules + .enabled(Rule::MissingFutureAnnotationsImport) + && (self.settings.target_version < PythonVersion::Py39 + && (self.settings.target_version >= PythonVersion::Py37 + && !self.ctx.future_annotations() + && self.ctx.in_annotation())) + && analyze::typing::is_pep585_builtin(expr, &self.ctx) + { + flake8_future_annotations::rules::missing_future_annotations( + self, expr, + ); + } if !self.settings.pyupgrade.keep_runtime_typing && self.settings.rules.enabled(Rule::NonPEP585Annotation) && (self.settings.target_version >= PythonVersion::Py39 @@ -2372,6 +2398,18 @@ where } ExprKind::Attribute(ast::ExprAttribute { attr, value, .. }) => { // Ex) typing.List[...] + if self + .settings + .rules + .enabled(Rule::MissingFutureAnnotationsImport) + && (self.settings.target_version < PythonVersion::Py39 + && (self.settings.target_version >= PythonVersion::Py37 + && !self.ctx.future_annotations() + && self.ctx.in_annotation())) + && analyze::typing::is_pep585_builtin(expr, &self.ctx) + { + flake8_future_annotations::rules::missing_future_annotations(self, expr); + } if !self.settings.pyupgrade.keep_runtime_typing && self.settings.rules.enabled(Rule::NonPEP585Annotation) && (self.settings.target_version >= PythonVersion::Py39 diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index a712e224b4..ef7926af69 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -331,6 +331,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Flake8Annotations, "206") => Rule::MissingReturnTypeClassMethod, (Flake8Annotations, "401") => Rule::AnyType, + // flake8-future-annotations + (Flake8FutureAnnotations, "100") => Rule::MissingFutureAnnotationsImport, + // flake8-2020 (Flake82020, "101") => Rule::SysVersionSlice3, (Flake82020, "102") => Rule::SysVersion2, diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 59af691058..7954301501 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -290,6 +290,8 @@ ruff_macros::register_rules!( rules::flake8_annotations::rules::MissingReturnTypeStaticMethod, rules::flake8_annotations::rules::MissingReturnTypeClassMethod, rules::flake8_annotations::rules::AnyType, + // flake8-future-annotations + rules::flake8_future_annotations::rules::MissingFutureAnnotationsImport, // flake8-2020 rules::flake8_2020::rules::SysVersionSlice3, rules::flake8_2020::rules::SysVersion2, @@ -769,6 +771,9 @@ pub enum Linter { /// [flake8-executable](https://pypi.org/project/flake8-executable/) #[prefix = "EXE"] Flake8Executable, + /// [flake8-future-annotations](https://pypi.org/project/flake8-future-annotations/) + #[prefix = "FA"] + Flake8FutureAnnotations, /// [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/) #[prefix = "ISC"] Flake8ImplicitStrConcat, diff --git a/crates/ruff/src/rules/flake8_future_annotations/mod.rs b/crates/ruff/src/rules/flake8_future_annotations/mod.rs new file mode 100644 index 0000000000..49d6ca57ab --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/mod.rs @@ -0,0 +1,40 @@ +//! Rules from [flake8-future-annotations](https://pypi.org/project/flake8-future-annotations/). +pub(crate) mod rules; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + use test_case::test_case; + + use crate::registry::Rule; + use crate::settings::types::PythonVersion; + use crate::test::test_path; + use crate::{assert_messages, settings}; + + #[test_case(Path::new("edge_case.py"); "edge_case")] + #[test_case(Path::new("from_typing_import.py"); "from_typing_import")] + #[test_case(Path::new("from_typing_import_many.py"); "from_typing_import_many")] + #[test_case(Path::new("import_typing.py"); "import_typing")] + #[test_case(Path::new("import_typing_as.py"); "import_typing_as")] + #[test_case(Path::new("no_future_import_uses_lowercase.py"); "no_future_import_uses_lowercase")] + #[test_case(Path::new("no_future_import_uses_union.py"); "no_future_import_uses_union")] + #[test_case(Path::new("no_future_import_uses_union_inner.py"); "no_future_import_uses_union_inner")] + #[test_case(Path::new("ok_no_types.py"); "ok_no_types")] + #[test_case(Path::new("ok_non_simplifiable_types.py"); "ok_non_simplifiable_types")] + #[test_case(Path::new("ok_uses_future.py"); "ok_uses_future")] + #[test_case(Path::new("ok_variable_name.py"); "ok_variable_name")] + fn rules(path: &Path) -> Result<()> { + let snapshot = path.to_string_lossy().into_owned(); + let diagnostics = test_path( + Path::new("flake8_future_annotations").join(path).as_path(), + &settings::Settings { + target_version: PythonVersion::Py37, + ..settings::Settings::for_rule(Rule::MissingFutureAnnotationsImport) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } +} diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules.rs b/crates/ruff/src/rules/flake8_future_annotations/rules.rs new file mode 100644 index 0000000000..f7086d2d2c --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/rules.rs @@ -0,0 +1,78 @@ +use rustpython_parser::ast::Expr; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::call_path::format_call_path; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for missing `from __future__ import annotations` imports upon +/// detecting type annotations that can be written more succinctly under +/// PEP 563. +/// +/// ## Why is this bad? +/// PEP 563 enabled the use of a number of convenient type annotations, such as +/// `list[str]` instead of `List[str]`, or `str | None` instead of +/// `Optional[str]`. However, these annotations are only available on Python +/// 3.9 and higher, _unless_ the `from __future__ import annotations` import is present. +/// +/// By adding the `__future__` import, the pyupgrade rules can automatically +/// migrate existing code to use the new syntax, even for older Python versions. +/// This rule thus pairs well with pyupgrade and with Ruff's pyupgrade rules. +/// +/// ## Example +/// ```python +/// from typing import List, Dict, Optional +/// +/// +/// def function(a_dict: Dict[str, Optional[int]]) -> None: +/// a_list: List[str] = [] +/// a_list.append("hello") +/// ``` +/// +/// Use instead: +/// ```python +/// from __future__ import annotations +/// +/// from typing import List, Dict, Optional +/// +/// +/// def function(a_dict: Dict[str, Optional[int]]) -> None: +/// a_list: List[str] = [] +/// a_list.append("hello") +/// ``` +/// +/// After running the additional pyupgrade rules: +/// ```python +/// from __future__ import annotations +/// +/// +/// def function(a_dict: dict[str, int | None]) -> None: +/// a_list: list[str] = [] +/// a_list.append("hello") +/// ``` +#[violation] +pub struct MissingFutureAnnotationsImport { + name: String, +} + +impl Violation for MissingFutureAnnotationsImport { + #[derive_message_formats] + fn message(&self) -> String { + let MissingFutureAnnotationsImport { name } = self; + format!("Missing `from __future__ import annotations`, but uses `{name}`") + } +} + +/// FA100 +pub(crate) fn missing_future_annotations(checker: &mut Checker, expr: &Expr) { + if let Some(binding) = checker.ctx.resolve_call_path(expr) { + checker.diagnostics.push(Diagnostic::new( + MissingFutureAnnotationsImport { + name: format_call_path(&binding), + }, + expr.range(), + )); + } +} diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap new file mode 100644 index 0000000000..9cde96fc8e --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- +edge_case.py:6:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` + | +6 | def main(_: List[int]) -> None: +7 | a_list: t.List[str] = [] + | ^^^^^^ FA100 +8 | a_list.append("hello") + | + + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__from_typing_import.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__from_typing_import.py.snap new file mode 100644 index 0000000000..0f4171346c --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__from_typing_import.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- +from_typing_import.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` + | +5 | def main() -> None: +6 | a_list: List[str] = [] + | ^^^^ FA100 +7 | a_list.append("hello") + | + + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap new file mode 100644 index 0000000000..ca9f715c13 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- +from_typing_import_many.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` + | +5 | def main() -> None: +6 | a_list: List[Optional[str]] = [] + | ^^^^ FA100 +7 | a_list.append("hello") +8 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) + | + +from_typing_import_many.py:5:18: FA100 Missing `from __future__ import annotations`, but uses `typing.Optional` + | +5 | def main() -> None: +6 | a_list: List[Optional[str]] = [] + | ^^^^^^^^ FA100 +7 | a_list.append("hello") +8 | a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) + | + + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__import_typing.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__import_typing.py.snap new file mode 100644 index 0000000000..de648de0f5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__import_typing.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- +import_typing.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` + | +5 | def main() -> None: +6 | a_list: typing.List[str] = [] + | ^^^^^^^^^^^ FA100 +7 | a_list.append("hello") + | + + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__import_typing_as.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__import_typing_as.py.snap new file mode 100644 index 0000000000..4241b2e940 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__import_typing_as.py.snap @@ -0,0 +1,12 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- +import_typing_as.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` + | +5 | def main() -> None: +6 | a_list: t.List[str] = [] + | ^^^^^^ FA100 +7 | a_list.append("hello") + | + + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_lowercase.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_lowercase.py.snap new file mode 100644 index 0000000000..681d4be5e5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_lowercase.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_union.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_union.py.snap new file mode 100644 index 0000000000..681d4be5e5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_union.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_union_inner.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_union_inner.py.snap new file mode 100644 index 0000000000..681d4be5e5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_union_inner.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_no_types.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_no_types.py.snap new file mode 100644 index 0000000000..681d4be5e5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_no_types.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_non_simplifiable_types.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_non_simplifiable_types.py.snap new file mode 100644 index 0000000000..681d4be5e5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_non_simplifiable_types.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_uses_future.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_uses_future.py.snap new file mode 100644 index 0000000000..681d4be5e5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_uses_future.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_variable_name.py.snap b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_variable_name.py.snap new file mode 100644 index 0000000000..681d4be5e5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_variable_name.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- + diff --git a/crates/ruff/src/rules/mod.rs b/crates/ruff/src/rules/mod.rs index fd268d02b9..a2fc2cfe75 100644 --- a/crates/ruff/src/rules/mod.rs +++ b/crates/ruff/src/rules/mod.rs @@ -14,6 +14,7 @@ pub mod flake8_debugger; pub mod flake8_django; pub mod flake8_errmsg; pub mod flake8_executable; +pub mod flake8_future_annotations; pub mod flake8_gettext; pub mod flake8_implicit_str_concat; pub mod flake8_import_conventions; diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 586f9f2359..a248c08cf6 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -70,6 +70,15 @@ pub fn is_pep585_builtin(expr: &Expr, context: &Context) -> bool { }) } +/// Returns `true` if `Expr` represents a reference to a typing object with a +/// PEP 603 built-in. +pub fn is_pep604_builtin(expr: &Expr, context: &Context) -> bool { + context.resolve_call_path(expr).map_or(false, |call_path| { + context.match_typing_call_path(&call_path, "Optional") + || context.match_typing_call_path(&call_path, "Union") + }) +} + pub fn is_immutable_annotation(context: &Context, expr: &Expr) -> bool { match &expr.node { ExprKind::Name(_) | ExprKind::Attribute(_) => { diff --git a/ruff.schema.json b/ruff.schema.json index caa6e56a73..c523f3913e 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1816,6 +1816,10 @@ "F9", "F90", "F901", + "FA", + "FA1", + "FA10", + "FA100", "FBT", "FBT0", "FBT00", diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index 44cecd824b..cc1e741933 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -5,6 +5,7 @@ Example usage: scripts/check_ecosystem.py """ +from __future__ import annotations import argparse import asyncio @@ -18,7 +19,7 @@ import time from asyncio.subprocess import PIPE, create_subprocess_exec from contextlib import asynccontextmanager, nullcontext from pathlib import Path -from typing import TYPE_CHECKING, NamedTuple, Optional, Self +from typing import TYPE_CHECKING, NamedTuple, Self if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator, Sequence @@ -31,13 +32,13 @@ class Repository(NamedTuple): org: str repo: str - ref: Optional[str] + ref: str | None select: str = "" ignore: str = "" exclude: str = "" @asynccontextmanager - async def clone(self: Self, checkout_dir: Path) -> "AsyncIterator[Path]": + async def clone(self: Self, checkout_dir: Path) -> AsyncIterator[Path]: """Shallow clone this repository to a temporary directory.""" if checkout_dir.exists(): logger.debug(f"Reusing {self.org}/{self.repo}") @@ -101,7 +102,7 @@ async def check( select: str = "", ignore: str = "", exclude: str = "", -) -> "Sequence[str]": +) -> Sequence[str]: """Run the given ruff binary against the specified path.""" logger.debug(f"Checking {name} with {ruff}") ruff_args = ["check", "--no-cache", "--exit-zero"] @@ -148,7 +149,7 @@ class Diff(NamedTuple): """Return true if this diff is non-empty.""" return bool(self.removed or self.added) - def __iter__(self: Self) -> "Iterator[str]": + def __iter__(self: Self) -> Iterator[str]: """Iterate through the changed lines in diff format.""" for line in heapq.merge(sorted(self.removed), sorted(self.added)): if line in self.removed: @@ -161,7 +162,7 @@ async def compare( ruff1: Path, ruff2: Path, repo: Repository, - checkouts: Optional[Path] = None, + checkouts: Path | None = None, ) -> Diff | None: """Check a specific repository against two versions of ruff.""" removed, added = set(), set() @@ -254,8 +255,8 @@ async def main( *, ruff1: Path, ruff2: Path, - projects_jsonl: Optional[Path], - checkouts: Optional[Path] = None, + projects_jsonl: Path | None, + checkouts: Path | None = None, ) -> None: """Check two versions of ruff against a corpus of open-source code.""" if projects_jsonl: