From 04566116283ab31e78d2789226aaa4c75ed8a3a9 Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Sun, 12 Mar 2023 12:37:24 -0700 Subject: [PATCH 01/15] Implement flake8-future-annotations --- .python-version | 1 + README.md | 1 + .../flake8_future_annotations/edge_case.py | 7 + .../from_typing_import.py | 6 + .../from_typing_import_many.py | 8 ++ .../import_typing.py | 6 + .../import_typing_as.py | 6 + .../no_future_import_uses_lowercase.py | 7 + .../no_future_import_uses_union.py | 7 + .../no_future_import_uses_union_inner.py | 8 ++ .../flake8_future_annotations/ok_no_types.py | 3 + .../ok_non_simplifiable_types.py | 10 ++ .../ok_uses_future.py | 6 + .../ok_variable_name.py | 8 ++ crates/ruff/src/checkers/ast/mod.rs | 41 +++++- crates/ruff/src/codes.rs | 3 + crates/ruff/src/registry.rs | 5 + .../rules/flake8_future_annotations/mod.rs | 36 +++++ .../rules/flake8_future_annotations/rules.rs | 127 ++++++++++++++++++ ...ture_annotations__tests__edge_case.py.snap | 21 +++ ...tations__tests__from_typing_import.py.snap | 11 ++ ...ns__tests__from_typing_import_many.py.snap | 11 ++ ..._annotations__tests__import_typing.py.snap | 13 ++ ...notations__tests__import_typing_as.py.snap | 13 ++ ...s__no_future_import_uses_lowercase.py.snap | 4 + ...tests__no_future_import_uses_union.py.snap | 4 + ..._no_future_import_uses_union_inner.py.snap | 4 + ...re_annotations__tests__ok_no_types.py.snap | 4 + ...__tests__ok_non_simplifiable_types.py.snap | 4 + ...annotations__tests__ok_uses_future.py.snap | 4 + ...notations__tests__ok_variable_name.py.snap | 4 + crates/ruff/src/rules/mod.rs | 1 + ruff.schema.json | 4 + scripts/check_ecosystem.py | 13 +- 34 files changed, 398 insertions(+), 13 deletions(-) create mode 100644 .python-version create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/edge_case.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/from_typing_import_many.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/import_typing_as.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_lowercase.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_union.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/no_future_import_uses_union_inner.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_no_types.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_non_simplifiable_types.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_uses_future.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_future_annotations/ok_variable_name.py create mode 100644 crates/ruff/src/rules/flake8_future_annotations/mod.rs create mode 100644 crates/ruff/src/rules/flake8_future_annotations/rules.rs create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__from_typing_import.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__from_typing_import_many.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__import_typing.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__import_typing_as.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_lowercase.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_union.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__no_future_import_uses_union_inner.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_no_types.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_non_simplifiable_types.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_uses_future.py.snap create mode 100644 crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__ok_variable_name.py.snap diff --git a/.python-version b/.python-version new file mode 100644 index 00000000000000..e0af1238f7c6bf --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11.0 \ No newline at end of file diff --git a/README.md b/README.md index f531d5a6d6c124..08bcb692a6ecea 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,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 00000000000000..f98adefaf4cea9 --- /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 00000000000000..a8229aca124389 --- /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 00000000000000..28ccc2e4c3c3cd --- /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 00000000000000..fccfe30aa26123 --- /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 00000000000000..5f634a0334bc19 --- /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 00000000000000..a573432cd5aad5 --- /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 00000000000000..50206192f181b1 --- /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 00000000000000..9f9b5bd574b354 --- /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 00000000000000..54fff8090690bd --- /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 00000000000000..b5121a9297ee67 --- /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 00000000000000..281b96d393c331 --- /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 00000000000000..a1a8febe427d77 --- /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 d6293006563b90..17592708d1cff0 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -43,12 +43,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, 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, 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}; @@ -1157,7 +1157,20 @@ where pyupgrade::rules::unnecessary_builtin_import(self, stmt, module, names); } } - + if self + .settings + .rules + .enabled(Rule::MissingFutureAnnotationsWithImports) + { + if let Some(module) = module.as_deref() { + flake8_future_annotations::rules::check_missing_future_annotations_from_typing_import( + self, + stmt, + module, + names, + ); + } + } if self.settings.rules.enabled(Rule::BannedApi) { if let Some(module) = module { for name in names { @@ -2373,6 +2386,16 @@ where if self.settings.rules.enabled(Rule::BannedApi) { flake8_tidy_imports::banned_api::banned_attribute_access(self, expr); } + if self + .settings + .rules + .enabled(Rule::MissingFutureAnnotationsWithImports) + && analyze::typing::is_pep585_builtin(expr, &self.ctx) + { + flake8_future_annotations::rules::check_missing_future_annotations_import( + self, expr, + ); + } if self.settings.rules.enabled(Rule::PrivateMemberAccess) { flake8_self::rules::private_member_access(self, expr); } @@ -4106,6 +4129,10 @@ where self.ctx.body = prev_body; self.ctx.body_index = prev_body_index; + + // if self.settings.rules.enabled(&Rule::MissingFutureAnnotationsWithImports) { + // flake8_future_annotations::rules::check_missing_future_annotations_import(self, body); + // } } } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index c4072ceca2a0be..a7dd5869ffd74f 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -319,6 +319,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::MissingFutureAnnotationsWithImports, + // 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 9fda460a5f70d1..dceca17ddc30b9 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -286,6 +286,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::MissingFutureAnnotationsWithImports, // flake8-2020 rules::flake8_2020::rules::SysVersionSlice3, rules::flake8_2020::rules::SysVersion2, @@ -749,6 +751,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 00000000000000..06ef658be76ed7 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/mod.rs @@ -0,0 +1,36 @@ +//! 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::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::for_rules(vec![Rule::MissingFutureAnnotationsWithImports]), + )?; + 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 00000000000000..b50d9c4f890665 --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/rules.rs @@ -0,0 +1,127 @@ +use crate::checkers::ast::Checker; +use itertools::Itertools; +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::types::Range; +use rustpython_parser::ast::{AliasData, Expr, Located, Stmt}; + +/// ## What it does +/// Checks for missing `from __future__ import annotations` import if a type used in the +/// module can be rewritten using PEP 563. +/// +/// ## Why is this bad? +/// +/// Pairs well with pyupgrade with the --py37-plus flag or higher, since pyupgrade only +/// replaces type annotations with the PEP 563 rules if `from __future__ import annotations` +/// is present. +/// +/// ## Example +/// ```python +/// import typing as t +/// from typing import List +/// +/// def function(a_dict: t.Dict[str, t.Optional[int]]) -> None: +/// a_list: List[str] = [] +/// a_list.append("hello") +/// ``` +/// +/// To fix the lint error: +/// ```python +/// from __future__ import annotations +/// +/// import typing as t +/// from typing import List +/// +/// def function(a_dict: t.Dict[str, t.Optional[int]]) -> None: +/// a_list: List[str] = [] +/// a_list.append("hello") +/// ``` +/// +/// After running additional pyupgrade autofixes: +/// ```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 MissingFutureAnnotationsWithImports { + pub names: Vec, +} + +impl AlwaysAutofixableViolation for MissingFutureAnnotationsWithImports { + #[derive_message_formats] + fn message(&self) -> String { + let MissingFutureAnnotationsWithImports { names } = self; + let names = names.iter().map(|name| format!("`{name}`")).join(", "); + format!("Missing from __future__ import annotations but imports: {names}") + } + + fn autofix_title(&self) -> String { + "Add `from __future__ import annotations` import".to_string() + } +} + +// PEP_593_SUBSCRIPTS +pub const FUTURE_ANNOTATIONS_REWRITE_ELIGIBLE: &[&[&str]] = &[ + &["typing", "DefaultDict"], + &["typing", "Deque"], + &["typing", "Dict"], + &["typing", "FrozenSet"], + &["typing", "List"], + &["typing", "Optional"], + &["typing", "Set"], + &["typing", "Tuple"], + &["typing", "Type"], + &["typing", "Union"], + &["typing_extensions", "Type"], +]; + +/// FA100 +pub fn check_missing_future_annotations_from_typing_import( + checker: &mut Checker, + stmt: &Stmt, + module: &str, + names: &[Located], +) { + let result: Vec = names + .iter() + .map(|name| name.node.name.as_str()) + .filter(|alias| FUTURE_ANNOTATIONS_REWRITE_ELIGIBLE.contains(&[module, alias].as_slice())) + .map(std::string::ToString::to_string) + .sorted() + .collect(); + + if !checker.ctx.annotations_future_enabled && !result.is_empty() { + checker.diagnostics.push(Diagnostic::new( + MissingFutureAnnotationsWithImports { names: result }, + Range::from(stmt), + )); + } +} + +pub fn check_missing_future_annotations_import(checker: &mut Checker, expr: &Expr) { + if let Some(binding) = checker.ctx.resolve_call_path(expr) { + if !checker.ctx.annotations_future_enabled + && FUTURE_ANNOTATIONS_REWRITE_ELIGIBLE.contains(&binding.as_slice()) + { + checker.diagnostics.push(Diagnostic::new( + MissingFutureAnnotationsWithImports { + names: vec![binding.iter().join(".")], + }, + Range::from(expr), + )); + } + } + + // let mut diagnostic = Diagnostic::new(TypingTextStrAlias, Range::from(expr)); + // if checker.patch(diagnostic.kind.rule()) { + // diagnostic.amend(Fix::replacement( + // "str".to_string(), + // expr.location, + // expr.end_location.unwrap(), + // )); + // } + // checker.diagnostics.push(diagnostic); +} 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 00000000000000..505638ac2378ec --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__edge_case.py.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- +edge_case.py:1:1: FA100 [*] Missing from __future__ import annotations but imports: `List` + | +1 | from typing import List + | ^^^^^^^^^^^^^^^^^^^^^^^ FA100 +2 | import typing as t + | + = help: Add `from future import annotations` import + +edge_case.py:6:13: FA100 [*] Missing from __future__ import annotations but imports: `typing.List` + | +6 | def main(_: List[int]) -> None: +7 | a_list: t.List[str] = [] + | ^^^^^^ FA100 +8 | a_list.append("hello") + | + = help: Add `from future import annotations` import + + 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 00000000000000..b05c9c621314cb --- /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,11 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- +from_typing_import.py:1:1: FA100 [*] Missing from __future__ import annotations but imports: `List` + | +1 | from typing import List + | ^^^^^^^^^^^^^^^^^^^^^^^ FA100 + | + = help: Add `from future import annotations` import + + 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 00000000000000..20345a780de374 --- /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,11 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- +from_typing_import_many.py:1:1: FA100 [*] Missing from __future__ import annotations but imports: `Dict`, `List`, `Optional`, `Set`, `Union` + | +1 | from typing import Dict, List, Optional, Set, Union, cast + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA100 + | + = help: Add `from future import annotations` import + + 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 00000000000000..85c3e86bc4714b --- /dev/null +++ b/crates/ruff/src/rules/flake8_future_annotations/snapshots/ruff__rules__flake8_future_annotations__tests__import_typing.py.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- +import_typing.py:5:13: FA100 [*] Missing from __future__ import annotations but imports: `typing.List` + | +5 | def main() -> None: +6 | a_list: typing.List[str] = [] + | ^^^^^^^^^^^ FA100 +7 | a_list.append("hello") + | + = help: Add `from future import annotations` import + + 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 00000000000000..c9eb5651b87a60 --- /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,13 @@ +--- +source: crates/ruff/src/rules/flake8_future_annotations/mod.rs +--- +import_typing_as.py:5:13: FA100 [*] Missing from __future__ import annotations but imports: `typing.List` + | +5 | def main() -> None: +6 | a_list: t.List[str] = [] + | ^^^^^^ FA100 +7 | a_list.append("hello") + | + = help: Add `from future import annotations` import + + 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 00000000000000..681d4be5e53381 --- /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 00000000000000..681d4be5e53381 --- /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 00000000000000..681d4be5e53381 --- /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 00000000000000..681d4be5e53381 --- /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 00000000000000..681d4be5e53381 --- /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 00000000000000..681d4be5e53381 --- /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 00000000000000..681d4be5e53381 --- /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 418693c883687c..e97bcfe61d3c56 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/ruff.schema.json b/ruff.schema.json index 37320b5cbf940b..9af52cdbce08cd 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1812,6 +1812,10 @@ "F9", "F90", "F901", + "FA", + "FA1", + "FA10", + "FA100", "FBT", "FBT0", "FBT00", diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index 1a68bb917ebe1f..47da629212d65b 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -5,6 +5,7 @@ scripts/check_ecosystem.py """ +from __future__ import annotations import argparse import asyncio @@ -17,7 +18,7 @@ from asyncio.subprocess import PIPE, create_subprocess_exec from contextlib import asynccontextmanager 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 @@ -30,13 +31,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) -> "AsyncIterator[Path]": + async def clone(self: Self) -> AsyncIterator[Path]: """Shallow clone this repository to a temporary directory.""" with tempfile.TemporaryDirectory() as tmpdir: logger.debug(f"Cloning {self.org}/{self.repo}") @@ -96,7 +97,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"] @@ -141,7 +142,7 @@ def __bool__(self: Self) -> bool: """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: @@ -226,7 +227,7 @@ def read_projects_jsonl(projects_jsonl: Path) -> dict[str, Repository]: return repositories -async def main(*, ruff1: Path, ruff2: Path, projects_jsonl: Optional[Path]) -> None: +async def main(*, ruff1: Path, ruff2: Path, projects_jsonl: Path | None) -> None: """Check two versions of ruff against a corpus of open-source code.""" if projects_jsonl: repositories = read_projects_jsonl(projects_jsonl) From d5c81de5fb0c2be6b48d927a6d4876cea3104041 Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Sat, 15 Apr 2023 12:16:22 -0700 Subject: [PATCH 02/15] Remove commented code, remove autofix for now --- crates/ruff/src/checkers/ast/mod.rs | 4 ---- .../rules/flake8_future_annotations/rules.rs | 18 ++---------------- ...uture_annotations__tests__edge_case.py.snap | 6 ++---- ...otations__tests__from_typing_import.py.snap | 3 +-- ...ons__tests__from_typing_import_many.py.snap | 3 +-- ...e_annotations__tests__import_typing.py.snap | 3 +-- ...nnotations__tests__import_typing_as.py.snap | 3 +-- 7 files changed, 8 insertions(+), 32 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 17592708d1cff0..2787c485ef48fc 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4129,10 +4129,6 @@ where self.ctx.body = prev_body; self.ctx.body_index = prev_body_index; - - // if self.settings.rules.enabled(&Rule::MissingFutureAnnotationsWithImports) { - // flake8_future_annotations::rules::check_missing_future_annotations_import(self, body); - // } } } diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules.rs b/crates/ruff/src/rules/flake8_future_annotations/rules.rs index b50d9c4f890665..8fe2cdc73cc9f5 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules.rs @@ -1,6 +1,6 @@ use crate::checkers::ast::Checker; use itertools::Itertools; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic}; +use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::types::Range; use rustpython_parser::ast::{AliasData, Expr, Located, Stmt}; @@ -50,17 +50,13 @@ pub struct MissingFutureAnnotationsWithImports { pub names: Vec, } -impl AlwaysAutofixableViolation for MissingFutureAnnotationsWithImports { +impl Violation for MissingFutureAnnotationsWithImports { #[derive_message_formats] fn message(&self) -> String { let MissingFutureAnnotationsWithImports { names } = self; let names = names.iter().map(|name| format!("`{name}`")).join(", "); format!("Missing from __future__ import annotations but imports: {names}") } - - fn autofix_title(&self) -> String { - "Add `from __future__ import annotations` import".to_string() - } } // PEP_593_SUBSCRIPTS @@ -114,14 +110,4 @@ pub fn check_missing_future_annotations_import(checker: &mut Checker, expr: &Exp )); } } - - // let mut diagnostic = Diagnostic::new(TypingTextStrAlias, Range::from(expr)); - // if checker.patch(diagnostic.kind.rule()) { - // diagnostic.amend(Fix::replacement( - // "str".to_string(), - // expr.location, - // expr.end_location.unwrap(), - // )); - // } - // checker.diagnostics.push(diagnostic); } 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 index 505638ac2378ec..8639111c7844a7 100644 --- 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 @@ -1,21 +1,19 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -edge_case.py:1:1: FA100 [*] Missing from __future__ import annotations but imports: `List` +edge_case.py:1:1: FA100 Missing from __future__ import annotations but imports: `List` | 1 | from typing import List | ^^^^^^^^^^^^^^^^^^^^^^^ FA100 2 | import typing as t | - = help: Add `from future import annotations` import -edge_case.py:6:13: FA100 [*] Missing from __future__ import annotations but imports: `typing.List` +edge_case.py:6:13: FA100 Missing from __future__ import annotations but imports: `typing.List` | 6 | def main(_: List[int]) -> None: 7 | a_list: t.List[str] = [] | ^^^^^^ FA100 8 | a_list.append("hello") | - = help: Add `from future import annotations` import 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 index b05c9c621314cb..ac0d7ab61406cb 100644 --- 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 @@ -1,11 +1,10 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -from_typing_import.py:1:1: FA100 [*] Missing from __future__ import annotations but imports: `List` +from_typing_import.py:1:1: FA100 Missing from __future__ import annotations but imports: `List` | 1 | from typing import List | ^^^^^^^^^^^^^^^^^^^^^^^ FA100 | - = help: Add `from future import annotations` import 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 index 20345a780de374..ba22429607f146 100644 --- 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 @@ -1,11 +1,10 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -from_typing_import_many.py:1:1: FA100 [*] Missing from __future__ import annotations but imports: `Dict`, `List`, `Optional`, `Set`, `Union` +from_typing_import_many.py:1:1: FA100 Missing from __future__ import annotations but imports: `Dict`, `List`, `Optional`, `Set`, `Union` | 1 | from typing import Dict, List, Optional, Set, Union, cast | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA100 | - = help: Add `from future import annotations` import 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 index 85c3e86bc4714b..efaf0ff846b4bb 100644 --- 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 @@ -1,13 +1,12 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -import_typing.py:5:13: FA100 [*] Missing from __future__ import annotations but imports: `typing.List` +import_typing.py:5:13: FA100 Missing from __future__ import annotations but imports: `typing.List` | 5 | def main() -> None: 6 | a_list: typing.List[str] = [] | ^^^^^^^^^^^ FA100 7 | a_list.append("hello") | - = help: Add `from future import annotations` import 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 index c9eb5651b87a60..c76c99b2eb0b46 100644 --- 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 @@ -1,13 +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 imports: `typing.List` +import_typing_as.py:5:13: FA100 Missing from __future__ import annotations but imports: `typing.List` | 5 | def main() -> None: 6 | a_list: t.List[str] = [] | ^^^^^^ FA100 7 | a_list.append("hello") | - = help: Add `from future import annotations` import From cc9e9ce5bbf37e78d8d159d52f2be68973219271 Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Sat, 15 Apr 2023 12:42:48 -0700 Subject: [PATCH 03/15] Remove .python-version --- .python-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .python-version diff --git a/.python-version b/.python-version deleted file mode 100644 index e0af1238f7c6bf..00000000000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11.0 \ No newline at end of file From 73b4653ab158fb8527a304eb9f70a7462bfd6134 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 4 May 2023 12:36:11 -0400 Subject: [PATCH 04/15] Defer context checks if future is enabled --- crates/ruff/src/checkers/ast/mod.rs | 4 +-- .../rules/flake8_future_annotations/rules.rs | 35 +++++++++++-------- ...ture_annotations__tests__edge_case.py.snap | 4 +-- ...tations__tests__from_typing_import.py.snap | 2 +- ...ns__tests__from_typing_import_many.py.snap | 2 +- ..._annotations__tests__import_typing.py.snap | 2 +- ...notations__tests__import_typing_as.py.snap | 2 +- 7 files changed, 29 insertions(+), 22 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 3997d02186ea71..56f81fd7d193ef 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1173,7 +1173,7 @@ where .enabled(Rule::MissingFutureAnnotationsWithImports) { if let Some(module) = module.as_deref() { - flake8_future_annotations::rules::check_missing_future_annotations_from_typing_import( + flake8_future_annotations::rules::missing_future_annotations_from_typing_import( self, stmt, module, @@ -2435,7 +2435,7 @@ where .enabled(Rule::MissingFutureAnnotationsWithImports) && analyze::typing::is_pep585_builtin(expr, &self.ctx) { - flake8_future_annotations::rules::check_missing_future_annotations_import( + flake8_future_annotations::rules::missing_future_annotations_from_typing_usage( self, expr, ); } diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules.rs b/crates/ruff/src/rules/flake8_future_annotations/rules.rs index 8fe2cdc73cc9f5..c97102bb4de308 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules.rs @@ -1,16 +1,16 @@ -use crate::checkers::ast::Checker; use itertools::Itertools; +use rustpython_parser::ast::{Alias, Expr, Stmt}; + use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::types::Range; -use rustpython_parser::ast::{AliasData, Expr, Located, Stmt}; + +use crate::checkers::ast::Checker; /// ## What it does /// Checks for missing `from __future__ import annotations` import if a type used in the /// module can be rewritten using PEP 563. /// /// ## Why is this bad? -/// /// Pairs well with pyupgrade with the --py37-plus flag or higher, since pyupgrade only /// replaces type annotations with the PEP 563 rules if `from __future__ import annotations` /// is present. @@ -55,7 +55,7 @@ impl Violation for MissingFutureAnnotationsWithImports { fn message(&self) -> String { let MissingFutureAnnotationsWithImports { names } = self; let names = names.iter().map(|name| format!("`{name}`")).join(", "); - format!("Missing from __future__ import annotations but imports: {names}") + format!("Missing `from __future__ import annotations`, but imports: {names}") } } @@ -75,12 +75,16 @@ pub const FUTURE_ANNOTATIONS_REWRITE_ELIGIBLE: &[&[&str]] = &[ ]; /// FA100 -pub fn check_missing_future_annotations_from_typing_import( +pub fn missing_future_annotations_from_typing_import( checker: &mut Checker, stmt: &Stmt, module: &str, - names: &[Located], + names: &[Alias], ) { + if checker.ctx.annotations_future_enabled { + return; + } + let result: Vec = names .iter() .map(|name| name.node.name.as_str()) @@ -89,24 +93,27 @@ pub fn check_missing_future_annotations_from_typing_import( .sorted() .collect(); - if !checker.ctx.annotations_future_enabled && !result.is_empty() { + if !result.is_empty() { checker.diagnostics.push(Diagnostic::new( MissingFutureAnnotationsWithImports { names: result }, - Range::from(stmt), + stmt.range(), )); } } -pub fn check_missing_future_annotations_import(checker: &mut Checker, expr: &Expr) { +/// FA100 +pub fn missing_future_annotations_from_typing_usage(checker: &mut Checker, expr: &Expr) { + if checker.ctx.annotations_future_enabled { + return; + } + if let Some(binding) = checker.ctx.resolve_call_path(expr) { - if !checker.ctx.annotations_future_enabled - && FUTURE_ANNOTATIONS_REWRITE_ELIGIBLE.contains(&binding.as_slice()) - { + if FUTURE_ANNOTATIONS_REWRITE_ELIGIBLE.contains(&binding.as_slice()) { checker.diagnostics.push(Diagnostic::new( MissingFutureAnnotationsWithImports { names: vec![binding.iter().join(".")], }, - Range::from(expr), + 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 index 8639111c7844a7..a089d99fd50fdd 100644 --- 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 @@ -1,14 +1,14 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -edge_case.py:1:1: FA100 Missing from __future__ import annotations but imports: `List` +edge_case.py:1:1: FA100 Missing `from __future__ import annotations`, but imports: `List` | 1 | from typing import List | ^^^^^^^^^^^^^^^^^^^^^^^ FA100 2 | import typing as t | -edge_case.py:6:13: FA100 Missing from __future__ import annotations but imports: `typing.List` +edge_case.py:6:13: FA100 Missing `from __future__ import annotations`, but imports: `typing.List` | 6 | def main(_: List[int]) -> None: 7 | a_list: t.List[str] = [] 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 index ac0d7ab61406cb..7ac9a898cc5c9b 100644 --- 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 @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -from_typing_import.py:1:1: FA100 Missing from __future__ import annotations but imports: `List` +from_typing_import.py:1:1: FA100 Missing `from __future__ import annotations`, but imports: `List` | 1 | from typing import List | ^^^^^^^^^^^^^^^^^^^^^^^ FA100 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 index ba22429607f146..a89490e5c5dbd0 100644 --- 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 @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -from_typing_import_many.py:1:1: FA100 Missing from __future__ import annotations but imports: `Dict`, `List`, `Optional`, `Set`, `Union` +from_typing_import_many.py:1:1: FA100 Missing `from __future__ import annotations`, but imports: `Dict`, `List`, `Optional`, `Set`, `Union` | 1 | from typing import Dict, List, Optional, Set, Union, cast | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA100 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 index efaf0ff846b4bb..ac87b7e3acb157 100644 --- 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 @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -import_typing.py:5:13: FA100 Missing from __future__ import annotations but imports: `typing.List` +import_typing.py:5:13: FA100 Missing `from __future__ import annotations`, but imports: `typing.List` | 5 | def main() -> None: 6 | a_list: typing.List[str] = [] 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 index c76c99b2eb0b46..d479143c2110df 100644 --- 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 @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -import_typing_as.py:5:13: FA100 Missing from __future__ import annotations but imports: `typing.List` +import_typing_as.py:5:13: FA100 Missing `from __future__ import annotations`, but imports: `typing.List` | 5 | def main() -> None: 6 | a_list: t.List[str] = [] From 69abeb0cfc889132941b15643def36181a744144 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 4 May 2023 12:44:42 -0400 Subject: [PATCH 05/15] Move list into typing --- .../rules/flake8_future_annotations/rules.rs | 26 +++++-------------- crates/ruff_python_stdlib/src/typing.rs | 15 +++++++++++ 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules.rs b/crates/ruff/src/rules/flake8_future_annotations/rules.rs index c97102bb4de308..21b3999fbcc3ec 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules.rs @@ -3,6 +3,7 @@ use rustpython_parser::ast::{Alias, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_stdlib::typing::PEP_585_SUBSCRIPT_ELIGIBLE; use crate::checkers::ast::Checker; @@ -59,21 +60,6 @@ impl Violation for MissingFutureAnnotationsWithImports { } } -// PEP_593_SUBSCRIPTS -pub const FUTURE_ANNOTATIONS_REWRITE_ELIGIBLE: &[&[&str]] = &[ - &["typing", "DefaultDict"], - &["typing", "Deque"], - &["typing", "Dict"], - &["typing", "FrozenSet"], - &["typing", "List"], - &["typing", "Optional"], - &["typing", "Set"], - &["typing", "Tuple"], - &["typing", "Type"], - &["typing", "Union"], - &["typing_extensions", "Type"], -]; - /// FA100 pub fn missing_future_annotations_from_typing_import( checker: &mut Checker, @@ -85,17 +71,17 @@ pub fn missing_future_annotations_from_typing_import( return; } - let result: Vec = names + let names: Vec = names .iter() .map(|name| name.node.name.as_str()) - .filter(|alias| FUTURE_ANNOTATIONS_REWRITE_ELIGIBLE.contains(&[module, alias].as_slice())) + .filter(|alias| PEP_585_SUBSCRIPT_ELIGIBLE.contains(&[module, alias].as_slice())) .map(std::string::ToString::to_string) .sorted() .collect(); - if !result.is_empty() { + if !names.is_empty() { checker.diagnostics.push(Diagnostic::new( - MissingFutureAnnotationsWithImports { names: result }, + MissingFutureAnnotationsWithImports { names }, stmt.range(), )); } @@ -108,7 +94,7 @@ pub fn missing_future_annotations_from_typing_usage(checker: &mut Checker, expr: } if let Some(binding) = checker.ctx.resolve_call_path(expr) { - if FUTURE_ANNOTATIONS_REWRITE_ELIGIBLE.contains(&binding.as_slice()) { + if PEP_585_SUBSCRIPT_ELIGIBLE.contains(&binding.as_slice()) { checker.diagnostics.push(Diagnostic::new( MissingFutureAnnotationsWithImports { names: vec![binding.iter().join(".")], diff --git a/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index 313ff0a8fd2511..a16dd2ea838c38 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -198,6 +198,21 @@ pub const PEP_585_BUILTINS_ELIGIBLE: &[&[&str]] = &[ &["typing_extensions", "Type"], ]; +// See: https://peps.python.org/pep-0585/ +pub const PEP_585_SUBSCRIPT_ELIGIBLE: &[&[&str]] = &[ + &["typing", "DefaultDict"], + &["typing", "Deque"], + &["typing", "Dict"], + &["typing", "FrozenSet"], + &["typing", "List"], + &["typing", "Optional"], + &["typing", "Set"], + &["typing", "Tuple"], + &["typing", "Type"], + &["typing", "Union"], + &["typing_extensions", "Type"], +]; + // See: https://github.com/JelleZijlstra/autotyping/blob/0adba5ba0eee33c1de4ad9d0c79acfd737321dd9/autotyping/autotyping.py#L69-L91 pub static SIMPLE_MAGIC_RETURN_TYPES: Lazy> = Lazy::new(|| { From 4373751df0a828ef4714560237293bc108876404 Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Thu, 4 May 2023 18:12:49 +0000 Subject: [PATCH 06/15] Remove Deque and DefaultDict --- crates/ruff_python_stdlib/src/typing.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index a16dd2ea838c38..4d219d0de6165f 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -200,8 +200,6 @@ pub const PEP_585_BUILTINS_ELIGIBLE: &[&[&str]] = &[ // See: https://peps.python.org/pep-0585/ pub const PEP_585_SUBSCRIPT_ELIGIBLE: &[&[&str]] = &[ - &["typing", "DefaultDict"], - &["typing", "Deque"], &["typing", "Dict"], &["typing", "FrozenSet"], &["typing", "List"], From 8bdd0c1583bf54369bf4f502a2033afd3ce0210c Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Tue, 9 May 2023 07:01:51 +0000 Subject: [PATCH 07/15] Fix fmt --- crates/ruff/src/checkers/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 70dc7023573837..0d112029401fa0 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -46,7 +46,7 @@ use crate::rules::{ 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, + flake8_use_pathlib, flynt, mccabe, numpy, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops, }; use crate::settings::types::PythonVersion; From 4569ac64b06f5b8e1e359c0ed0c74d5ef7ee4a34 Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Tue, 9 May 2023 07:02:52 +0000 Subject: [PATCH 08/15] Revert "Remove Deque and DefaultDict" This reverts commit 4373751df0a828ef4714560237293bc108876404. --- crates/ruff_python_stdlib/src/typing.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index 4d219d0de6165f..a16dd2ea838c38 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -200,6 +200,8 @@ pub const PEP_585_BUILTINS_ELIGIBLE: &[&[&str]] = &[ // See: https://peps.python.org/pep-0585/ pub const PEP_585_SUBSCRIPT_ELIGIBLE: &[&[&str]] = &[ + &["typing", "DefaultDict"], + &["typing", "Deque"], &["typing", "Dict"], &["typing", "FrozenSet"], &["typing", "List"], From e6775ea94ac75bcb81ea5c3157da420f12edd4a2 Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Tue, 9 May 2023 16:00:18 +0000 Subject: [PATCH 09/15] Fix mkdocs --- .../src/rules/flake8_future_annotations/rules.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules.rs b/crates/ruff/src/rules/flake8_future_annotations/rules.rs index 21b3999fbcc3ec..168435f63d155f 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules.rs @@ -7,20 +7,12 @@ use ruff_python_stdlib::typing::PEP_585_SUBSCRIPT_ELIGIBLE; use crate::checkers::ast::Checker; -/// ## What it does -/// Checks for missing `from __future__ import annotations` import if a type used in the -/// module can be rewritten using PEP 563. -/// -/// ## Why is this bad? -/// Pairs well with pyupgrade with the --py37-plus flag or higher, since pyupgrade only -/// replaces type annotations with the PEP 563 rules if `from __future__ import annotations` -/// is present. -/// /// ## Example /// ```python /// import typing as t /// from typing import List /// +/// /// def function(a_dict: t.Dict[str, t.Optional[int]]) -> None: /// a_list: List[str] = [] /// a_list.append("hello") @@ -33,6 +25,7 @@ use crate::checkers::ast::Checker; /// import typing as t /// from typing import List /// +/// /// def function(a_dict: t.Dict[str, t.Optional[int]]) -> None: /// a_list: List[str] = [] /// a_list.append("hello") @@ -42,6 +35,7 @@ use crate::checkers::ast::Checker; /// ```python /// from __future__ import annotations /// +/// /// def function(a_dict: dict[str, int | None]) -> None: /// a_list: list[str] = [] /// a_list.append("hello") From d872a25db4674e87c05033b2e053ce5fb4eeb712 Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Tue, 9 May 2023 17:08:11 +0000 Subject: [PATCH 10/15] Fix mkdocs --- crates/ruff/src/rules/flake8_future_annotations/rules.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules.rs b/crates/ruff/src/rules/flake8_future_annotations/rules.rs index 168435f63d155f..158e24475a8334 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules.rs @@ -7,6 +7,15 @@ use ruff_python_stdlib::typing::PEP_585_SUBSCRIPT_ELIGIBLE; use crate::checkers::ast::Checker; +/// ## What it does +/// Checks for missing `from __future__ import annotations` import if a type used in the +/// module can be rewritten using PEP 563. +/// +/// ## Why is this bad? +/// Pairs well with pyupgrade with the --py37-plus flag or higher, since pyupgrade only +/// replaces type annotations with the PEP 563 rules if `from __future__ import annotations` +/// is present. +/// /// ## Example /// ```python /// import typing as t From 92cc016daced4bedfb231eb899650cefa23e907c Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Fri, 12 May 2023 04:47:12 +0000 Subject: [PATCH 11/15] Rebase onto main --- crates/ruff/src/checkers/ast/mod.rs | 2 +- crates/ruff/src/rules/flake8_future_annotations/rules.rs | 4 ++-- scripts/check_ecosystem.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 19e686ca6c57ba..e76faa6c1bf5dd 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1102,7 +1102,7 @@ where .rules .enabled(Rule::MissingFutureAnnotationsWithImports) { - if let Some(module) = module.as_deref() { + if let Some(module) = module { flake8_future_annotations::rules::missing_future_annotations_from_typing_import( self, stmt, diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules.rs b/crates/ruff/src/rules/flake8_future_annotations/rules.rs index 158e24475a8334..eb2a0aad7d4db1 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules.rs @@ -64,7 +64,7 @@ impl Violation for MissingFutureAnnotationsWithImports { } /// FA100 -pub fn missing_future_annotations_from_typing_import( +pub(crate) fn missing_future_annotations_from_typing_import( checker: &mut Checker, stmt: &Stmt, module: &str, @@ -91,7 +91,7 @@ pub fn missing_future_annotations_from_typing_import( } /// FA100 -pub fn missing_future_annotations_from_typing_usage(checker: &mut Checker, expr: &Expr) { +pub(crate) fn missing_future_annotations_from_typing_usage(checker: &mut Checker, expr: &Expr) { if checker.ctx.annotations_future_enabled { return; } diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index 263f129576d542..4740bbebf7a0c3 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -161,7 +161,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() From 64e201261f1a4099be784c4010c0e7c2ed0a2fe5 Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Fri, 12 May 2023 05:01:22 +0000 Subject: [PATCH 12/15] Fix lint error --- scripts/check_ecosystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index 4740bbebf7a0c3..08b0f55d0556aa 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -17,7 +17,7 @@ import tempfile import time from asyncio.subprocess import PIPE, create_subprocess_exec -from contextlib import asynccontextmanager, nullcontext +from contextlib import nullcontext from pathlib import Path from typing import TYPE_CHECKING, NamedTuple, Self From af9be693369f5243a997a3487b20c1a4d767abdb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 12 May 2023 14:16:05 -0400 Subject: [PATCH 13/15] Fix rebased future_annotations() check --- .../ruff/src/rules/flake8_future_annotations/rules.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/ruff/src/rules/flake8_future_annotations/rules.rs b/crates/ruff/src/rules/flake8_future_annotations/rules.rs index eb2a0aad7d4db1..d44049abfab32c 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules.rs @@ -8,8 +8,9 @@ use ruff_python_stdlib::typing::PEP_585_SUBSCRIPT_ELIGIBLE; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for missing `from __future__ import annotations` import if a type used in the -/// module can be rewritten using PEP 563. +/// 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? /// Pairs well with pyupgrade with the --py37-plus flag or higher, since pyupgrade only @@ -51,7 +52,7 @@ use crate::checkers::ast::Checker; /// ``` #[violation] pub struct MissingFutureAnnotationsWithImports { - pub names: Vec, + names: Vec, } impl Violation for MissingFutureAnnotationsWithImports { @@ -70,7 +71,7 @@ pub(crate) fn missing_future_annotations_from_typing_import( module: &str, names: &[Alias], ) { - if checker.ctx.annotations_future_enabled { + if checker.ctx.future_annotations() { return; } @@ -92,7 +93,7 @@ pub(crate) fn missing_future_annotations_from_typing_import( /// FA100 pub(crate) fn missing_future_annotations_from_typing_usage(checker: &mut Checker, expr: &Expr) { - if checker.ctx.annotations_future_enabled { + if checker.ctx.future_annotations() { return; } From 33a73b9e27c028f82c81ad21fab70224264dd2df Mon Sep 17 00:00:00 2001 From: Tyler Yep Date: Sat, 13 May 2023 22:34:56 +0000 Subject: [PATCH 14/15] Fix bad rebase --- scripts/check_ecosystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/check_ecosystem.py b/scripts/check_ecosystem.py index 08b0f55d0556aa..cc1e741933c286 100755 --- a/scripts/check_ecosystem.py +++ b/scripts/check_ecosystem.py @@ -17,7 +17,7 @@ import tempfile import time from asyncio.subprocess import PIPE, create_subprocess_exec -from contextlib import nullcontext +from contextlib import asynccontextmanager, nullcontext from pathlib import Path from typing import TYPE_CHECKING, NamedTuple, Self @@ -37,6 +37,7 @@ class Repository(NamedTuple): ignore: str = "" exclude: str = "" + @asynccontextmanager async def clone(self: Self, checkout_dir: Path) -> AsyncIterator[Path]: """Shallow clone this repository to a temporary directory.""" if checkout_dir.exists(): From 41f65c46bc41074c7d777aaed28123912825b18e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 13 May 2023 22:53:56 -0400 Subject: [PATCH 15/15] Include PEP 604 --- crates/ruff/src/checkers/ast/mod.rs | 63 ++++++++------ crates/ruff/src/codes.rs | 2 +- crates/ruff/src/registry.rs | 2 +- .../rules/flake8_future_annotations/mod.rs | 6 +- .../rules/flake8_future_annotations/rules.rs | 86 ++++++------------- ...ture_annotations__tests__edge_case.py.snap | 9 +- ...tations__tests__from_typing_import.py.snap | 8 +- ...ns__tests__from_typing_import_many.py.snap | 18 +++- ..._annotations__tests__import_typing.py.snap | 2 +- ...notations__tests__import_typing_as.py.snap | 2 +- .../src/analyze/typing.rs | 9 ++ crates/ruff_python_stdlib/src/typing.rs | 15 ---- 12 files changed, 105 insertions(+), 117 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index d5bc1a0f31e7b0..53bab09e27cac6 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1077,20 +1077,6 @@ where pyupgrade::rules::unnecessary_builtin_import(self, stmt, module, names); } } - if self - .settings - .rules - .enabled(Rule::MissingFutureAnnotationsWithImports) - { - if let Some(module) = module { - flake8_future_annotations::rules::missing_future_annotations_from_typing_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) @@ -2271,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); } @@ -2334,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 @@ -2385,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 @@ -2415,16 +2440,6 @@ where if self.settings.rules.enabled(Rule::BannedApi) { flake8_tidy_imports::banned_api::banned_attribute_access(self, expr); } - if self - .settings - .rules - .enabled(Rule::MissingFutureAnnotationsWithImports) - && analyze::typing::is_pep585_builtin(expr, &self.ctx) - { - flake8_future_annotations::rules::missing_future_annotations_from_typing_usage( - self, expr, - ); - } if self.settings.rules.enabled(Rule::PrivateMemberAccess) { flake8_self::rules::private_member_access(self, expr); } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 6699a78a9ab5d4..ef7926af69fe58 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -332,7 +332,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option { (Flake8Annotations, "401") => Rule::AnyType, // flake8-future-annotations - (Flake8FutureAnnotations, "100") => Rule::MissingFutureAnnotationsWithImports, + (Flake8FutureAnnotations, "100") => Rule::MissingFutureAnnotationsImport, // flake8-2020 (Flake82020, "101") => Rule::SysVersionSlice3, diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index 9748a9dd53e503..79543015017d7d 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -291,7 +291,7 @@ ruff_macros::register_rules!( rules::flake8_annotations::rules::MissingReturnTypeClassMethod, rules::flake8_annotations::rules::AnyType, // flake8-future-annotations - rules::flake8_future_annotations::rules::MissingFutureAnnotationsWithImports, + rules::flake8_future_annotations::rules::MissingFutureAnnotationsImport, // flake8-2020 rules::flake8_2020::rules::SysVersionSlice3, rules::flake8_2020::rules::SysVersion2, diff --git a/crates/ruff/src/rules/flake8_future_annotations/mod.rs b/crates/ruff/src/rules/flake8_future_annotations/mod.rs index 06ef658be76ed7..49d6ca57abcd0a 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/mod.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/mod.rs @@ -9,6 +9,7 @@ mod tests { use test_case::test_case; use crate::registry::Rule; + use crate::settings::types::PythonVersion; use crate::test::test_path; use crate::{assert_messages, settings}; @@ -28,7 +29,10 @@ mod tests { let snapshot = path.to_string_lossy().into_owned(); let diagnostics = test_path( Path::new("flake8_future_annotations").join(path).as_path(), - &settings::Settings::for_rules(vec![Rule::MissingFutureAnnotationsWithImports]), + &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 index d44049abfab32c..f7086d2d2cc087 100644 --- a/crates/ruff/src/rules/flake8_future_annotations/rules.rs +++ b/crates/ruff/src/rules/flake8_future_annotations/rules.rs @@ -1,9 +1,8 @@ -use itertools::Itertools; -use rustpython_parser::ast::{Alias, Expr, Stmt}; +use rustpython_parser::ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_stdlib::typing::PEP_585_SUBSCRIPT_ELIGIBLE; +use ruff_python_ast::call_path::format_call_path; use crate::checkers::ast::Checker; @@ -13,35 +12,38 @@ use crate::checkers::ast::Checker; /// PEP 563. /// /// ## Why is this bad? -/// Pairs well with pyupgrade with the --py37-plus flag or higher, since pyupgrade only -/// replaces type annotations with the PEP 563 rules if `from __future__ import annotations` -/// is present. +/// 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 -/// import typing as t -/// from typing import List +/// from typing import List, Dict, Optional /// /// -/// def function(a_dict: t.Dict[str, t.Optional[int]]) -> None: +/// def function(a_dict: Dict[str, Optional[int]]) -> None: /// a_list: List[str] = [] /// a_list.append("hello") /// ``` /// -/// To fix the lint error: +/// Use instead: /// ```python /// from __future__ import annotations /// -/// import typing as t -/// from typing import List +/// from typing import List, Dict, Optional /// /// -/// def function(a_dict: t.Dict[str, t.Optional[int]]) -> None: +/// def function(a_dict: Dict[str, Optional[int]]) -> None: /// a_list: List[str] = [] /// a_list.append("hello") /// ``` /// -/// After running additional pyupgrade autofixes: +/// After running the additional pyupgrade rules: /// ```python /// from __future__ import annotations /// @@ -51,60 +53,26 @@ use crate::checkers::ast::Checker; /// a_list.append("hello") /// ``` #[violation] -pub struct MissingFutureAnnotationsWithImports { - names: Vec, +pub struct MissingFutureAnnotationsImport { + name: String, } -impl Violation for MissingFutureAnnotationsWithImports { +impl Violation for MissingFutureAnnotationsImport { #[derive_message_formats] fn message(&self) -> String { - let MissingFutureAnnotationsWithImports { names } = self; - let names = names.iter().map(|name| format!("`{name}`")).join(", "); - format!("Missing `from __future__ import annotations`, but imports: {names}") + let MissingFutureAnnotationsImport { name } = self; + format!("Missing `from __future__ import annotations`, but uses `{name}`") } } /// FA100 -pub(crate) fn missing_future_annotations_from_typing_import( - checker: &mut Checker, - stmt: &Stmt, - module: &str, - names: &[Alias], -) { - if checker.ctx.future_annotations() { - return; - } - - let names: Vec = names - .iter() - .map(|name| name.node.name.as_str()) - .filter(|alias| PEP_585_SUBSCRIPT_ELIGIBLE.contains(&[module, alias].as_slice())) - .map(std::string::ToString::to_string) - .sorted() - .collect(); - - if !names.is_empty() { +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( - MissingFutureAnnotationsWithImports { names }, - stmt.range(), + MissingFutureAnnotationsImport { + name: format_call_path(&binding), + }, + expr.range(), )); } } - -/// FA100 -pub(crate) fn missing_future_annotations_from_typing_usage(checker: &mut Checker, expr: &Expr) { - if checker.ctx.future_annotations() { - return; - } - - if let Some(binding) = checker.ctx.resolve_call_path(expr) { - if PEP_585_SUBSCRIPT_ELIGIBLE.contains(&binding.as_slice()) { - checker.diagnostics.push(Diagnostic::new( - MissingFutureAnnotationsWithImports { - names: vec![binding.iter().join(".")], - }, - 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 index a089d99fd50fdd..9cde96fc8eccf0 100644 --- 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 @@ -1,14 +1,7 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -edge_case.py:1:1: FA100 Missing `from __future__ import annotations`, but imports: `List` - | -1 | from typing import List - | ^^^^^^^^^^^^^^^^^^^^^^^ FA100 -2 | import typing as t - | - -edge_case.py:6:13: FA100 Missing `from __future__ import annotations`, but imports: `typing.List` +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] = [] 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 index 7ac9a898cc5c9b..0f4171346cd710 100644 --- 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 @@ -1,10 +1,12 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -from_typing_import.py:1:1: FA100 Missing `from __future__ import annotations`, but imports: `List` +from_typing_import.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` | -1 | from typing import List - | ^^^^^^^^^^^^^^^^^^^^^^^ FA100 +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 index a89490e5c5dbd0..ca9f715c131df6 100644 --- 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 @@ -1,10 +1,22 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -from_typing_import_many.py:1:1: FA100 Missing `from __future__ import annotations`, but imports: `Dict`, `List`, `Optional`, `Set`, `Union` +from_typing_import_many.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List` | -1 | from typing import Dict, List, Optional, Set, Union, cast - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA100 +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 index ac87b7e3acb157..de648de0f5450a 100644 --- 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 @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -import_typing.py:5:13: FA100 Missing `from __future__ import annotations`, but imports: `typing.List` +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] = [] 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 index d479143c2110df..4241b2e94031b2 100644 --- 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 @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_future_annotations/mod.rs --- -import_typing_as.py:5:13: FA100 Missing `from __future__ import annotations`, but imports: `typing.List` +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] = [] diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 586f9f2359e45e..a248c08cf613bf 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/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index a16dd2ea838c38..313ff0a8fd2511 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -198,21 +198,6 @@ pub const PEP_585_BUILTINS_ELIGIBLE: &[&[&str]] = &[ &["typing_extensions", "Type"], ]; -// See: https://peps.python.org/pep-0585/ -pub const PEP_585_SUBSCRIPT_ELIGIBLE: &[&[&str]] = &[ - &["typing", "DefaultDict"], - &["typing", "Deque"], - &["typing", "Dict"], - &["typing", "FrozenSet"], - &["typing", "List"], - &["typing", "Optional"], - &["typing", "Set"], - &["typing", "Tuple"], - &["typing", "Type"], - &["typing", "Union"], - &["typing_extensions", "Type"], -]; - // See: https://github.com/JelleZijlstra/autotyping/blob/0adba5ba0eee33c1de4ad9d0c79acfd737321dd9/autotyping/autotyping.py#L69-L91 pub static SIMPLE_MAGIC_RETURN_TYPES: Lazy> = Lazy::new(|| {