From 81617572292a2c0a1a60ee5135d2f9cfe6b6f336 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Mon, 12 Jun 2023 13:20:16 +0200 Subject: [PATCH] [flake8-pyi] Implement PYI044 (#5021) ## Summary This implements PYI044. This rule checks if `from __future__ import annotations` is used in stub files as it has no effect in stub files, since type checkers automatically treat stubs as having those semantics. Updates https://github.com/astral-sh/ruff/issues/848 ## Test Plan Added a test case and snapshots. --- .../test/fixtures/flake8_pyi/PYI044.py | 7 ++++ .../test/fixtures/flake8_pyi/PYI044.pyi | 7 ++++ crates/ruff/src/checkers/ast/mod.rs | 3 ++ crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/flake8_pyi/mod.rs | 2 ++ .../rules/future_annotations_in_stub.rs | 33 +++++++++++++++++++ crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 2 ++ ...__flake8_pyi__tests__PYI044_PYI044.py.snap | 4 +++ ..._flake8_pyi__tests__PYI044_PYI044.pyi.snap | 13 ++++++++ ruff.schema.json | 1 + 10 files changed, 73 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi create mode 100644 crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py new file mode 100644 index 0000000000..6439df7f6d --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.py @@ -0,0 +1,7 @@ +# Bad import. +from __future__ import annotations # Not PYI044 (not a stubfile). + +# Good imports. +from __future__ import Something +import sys +from socket import AF_INET diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi new file mode 100644 index 0000000000..18018deee6 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI044.pyi @@ -0,0 +1,7 @@ +# Bad import. +from __future__ import annotations # PYI044. + +# Good imports. +from __future__ import Something +import sys +from socket import AF_INET diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 6334ca31ef..57c497456d 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1121,6 +1121,9 @@ where if self.enabled(Rule::UnaliasedCollectionsAbcSetImport) { flake8_pyi::rules::unaliased_collections_abc_set_import(self, import_from); } + if self.enabled(Rule::FutureAnnotationsInStub) { + flake8_pyi::rules::from_future_import(self, import_from); + } } for alias in names { if let Some("__future__") = module { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 911fe85ebc..962190fbbd 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -616,6 +616,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "035") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnassignedSpecialVariableInStub), (Flake8Pyi, "042") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::SnakeCaseTypeAlias), (Flake8Pyi, "043") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TSuffixedTypeAlias), + (Flake8Pyi, "044") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::FutureAnnotationsInStub), (Flake8Pyi, "045") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::IterMethodReturnIterable), (Flake8Pyi, "048") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StubBodyMultipleStatements), (Flake8Pyi, "050") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NoReturnArgumentAnnotationInStub), diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 783018fba8..b790c3c77e 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -54,6 +54,8 @@ mod tests { #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.pyi"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.pyi"))] + #[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.py"))] + #[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))] #[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs new file mode 100644 index 0000000000..2968980034 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/future_annotations_in_stub.rs @@ -0,0 +1,33 @@ +use rustpython_parser::ast::StmtImportFrom; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +#[violation] +pub struct FutureAnnotationsInStub; + +impl Violation for FutureAnnotationsInStub { + #[derive_message_formats] + fn message(&self) -> String { + format!("`from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics") + } +} + +/// PYI044 +pub(crate) fn from_future_import(checker: &mut Checker, target: &StmtImportFrom) { + if let StmtImportFrom { + range, + module: Some(name), + names, + .. + } = target + { + if name == "__future__" && names.iter().any(|alias| &*alias.name == "annotations") { + checker + .diagnostics + .push(Diagnostic::new(FutureAnnotationsInStub, *range)); + } + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index ea3a26a1ad..6286ebdca3 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -4,6 +4,7 @@ pub(crate) use collections_named_tuple::*; pub(crate) use docstring_in_stubs::*; pub(crate) use duplicate_union_member::*; pub(crate) use ellipsis_in_non_empty_class_body::*; +pub(crate) use future_annotations_in_stub::*; pub(crate) use iter_method_return_iterable::*; pub(crate) use no_return_argument_annotation::*; pub(crate) use non_empty_stub_body::*; @@ -28,6 +29,7 @@ mod collections_named_tuple; mod docstring_in_stubs; mod duplicate_union_member; mod ellipsis_in_non_empty_class_body; +mod future_annotations_in_stub; mod iter_method_return_iterable; mod no_return_argument_annotation; mod non_empty_stub_body; diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap new file mode 100644 index 0000000000..5e52d03969 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI044_PYI044.pyi.snap @@ -0,0 +1,13 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics + | +1 | # Bad import. +2 | from __future__ import annotations # PYI044. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044 +3 | +4 | # Good imports. + | + + diff --git a/ruff.schema.json b/ruff.schema.json index e23fad53f2..f2190aecf4 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2304,6 +2304,7 @@ "PYI04", "PYI042", "PYI043", + "PYI044", "PYI045", "PYI048", "PYI05",