From 7838d8c8af56cc28883257eeb18fff1e5e62deaa Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Fri, 28 Jul 2023 21:21:29 -0300 Subject: [PATCH] Implement PYI047 (#6134) ## Summary Checks for the presence of unused private `typing.TypeAlias` definitions. ref #848 ## Test Plan Snapshots and manual runs of flake8 --- .../test/fixtures/flake8_pyi/PYI047.py | 22 +++++ .../test/fixtures/flake8_pyi/PYI047.pyi | 22 +++++ .../checkers/ast/analyze/deferred_scopes.rs | 4 + crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/flake8_pyi/mod.rs | 2 + .../rules/unused_private_type_definition.rs | 84 +++++++++++++++++++ ...__flake8_pyi__tests__PYI047_PYI047.py.snap | 4 + ..._flake8_pyi__tests__PYI047_PYI047.pyi.snap | 20 +++++ ruff.schema.json | 1 + 9 files changed, 160 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI047.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI047.pyi create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI047_PYI047.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI047_PYI047.pyi.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI047.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI047.py new file mode 100644 index 0000000000000..91bc2cd413c1b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI047.py @@ -0,0 +1,22 @@ +import typing +import sys +from typing import TypeAlias + + +_UnusedPrivateTypeAlias: TypeAlias = int | None +_T: typing.TypeAlias = str + +# OK +_UsedPrivateTypeAlias: TypeAlias = int | None + +def func(arg: _UsedPrivateTypeAlias) -> _UsedPrivateTypeAlias: + ... + + +if sys.version_info > (3, 9): + _PrivateTypeAlias: TypeAlias = str | None +else: + _PrivateTypeAlias: TypeAlias = float | None + + +def func2(arg: _PrivateTypeAlias) -> None: ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI047.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI047.pyi new file mode 100644 index 0000000000000..91bc2cd413c1b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI047.pyi @@ -0,0 +1,22 @@ +import typing +import sys +from typing import TypeAlias + + +_UnusedPrivateTypeAlias: TypeAlias = int | None +_T: typing.TypeAlias = str + +# OK +_UsedPrivateTypeAlias: TypeAlias = int | None + +def func(arg: _UsedPrivateTypeAlias) -> _UsedPrivateTypeAlias: + ... + + +if sys.version_info > (3, 9): + _PrivateTypeAlias: TypeAlias = str | None +else: + _PrivateTypeAlias: TypeAlias = float | None + + +def func2(arg: _PrivateTypeAlias) -> None: ... diff --git a/crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs b/crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs index 8fb6363c19bcd..ce1308e186217 100644 --- a/crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs +++ b/crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs @@ -25,6 +25,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) { Rule::UnusedLambdaArgument, Rule::UnusedMethodArgument, Rule::UnusedPrivateProtocol, + Rule::UnusedPrivateTypeAlias, Rule::UnusedPrivateTypeVar, Rule::UnusedStaticMethodArgument, Rule::UnusedVariable, @@ -223,6 +224,9 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) { if checker.enabled(Rule::UnusedPrivateProtocol) { flake8_pyi::rules::unused_private_protocol(checker, scope, &mut diagnostics); } + if checker.enabled(Rule::UnusedPrivateTypeAlias) { + flake8_pyi::rules::unused_private_type_alias(checker, scope, &mut diagnostics); + } } if matches!( diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index c00f576a38e88..47a02947be97b 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -654,6 +654,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "044") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::FutureAnnotationsInStub), (Flake8Pyi, "045") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::IterMethodReturnIterable), (Flake8Pyi, "046") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnusedPrivateProtocol), + (Flake8Pyi, "047") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnusedPrivateTypeAlias), (Flake8Pyi, "048") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StubBodyMultipleStatements), (Flake8Pyi, "050") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NoReturnArgumentAnnotationInStub), (Flake8Pyi, "052") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnannotatedAssignmentInStub), diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 2cbb90c3e0d81..b3f601d7be900 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -97,6 +97,8 @@ mod tests { #[test_case(Rule::UnusedPrivateTypeVar, Path::new("PYI018.pyi"))] #[test_case(Rule::UnusedPrivateProtocol, Path::new("PYI046.py"))] #[test_case(Rule::UnusedPrivateProtocol, Path::new("PYI046.pyi"))] + #[test_case(Rule::UnusedPrivateTypeAlias, Path::new("PYI047.py"))] + #[test_case(Rule::UnusedPrivateTypeAlias, Path::new("PYI047.pyi"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unused_private_type_definition.rs b/crates/ruff/src/rules/flake8_pyi/rules/unused_private_type_definition.rs index 2d5e0d3082b37..70b6c83127f99 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unused_private_type_definition.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unused_private_type_definition.rs @@ -73,6 +73,44 @@ impl Violation for UnusedPrivateProtocol { } } +/// ## What it does +/// Checks for the presence of unused private `typing.TypeAlias` definitions. +/// +/// ## Why is this bad? +/// A private `typing.TypeAlias` that is defined but not used is likely a +/// mistake, and should either be used, made public, or removed to avoid +/// confusion. +/// +/// ## Example +/// ```python +/// import typing +/// +/// _UnusedTypeAlias: typing.TypeAlias = int +/// ``` +/// +/// Use instead: +/// ```python +/// import typing +/// +/// _UsedTypeAlias: typing.TypeAlias = int +/// +/// +/// def func(arg: _UsedTypeAlias) -> _UsedTypeAlias: +/// ... +/// ``` +#[violation] +pub struct UnusedPrivateTypeAlias { + name: String, +} + +impl Violation for UnusedPrivateTypeAlias { + #[derive_message_formats] + fn message(&self) -> String { + let UnusedPrivateTypeAlias { name } = self; + format!("Private TypeAlias `{name}` is never used") + } +} + /// PYI018 pub(crate) fn unused_private_type_var( checker: &Checker, @@ -157,3 +195,49 @@ pub(crate) fn unused_private_protocol( )); } } + +/// PYI047 +pub(crate) fn unused_private_type_alias( + checker: &Checker, + scope: &Scope, + diagnostics: &mut Vec, +) { + for binding in scope + .binding_ids() + .map(|binding_id| checker.semantic().binding(binding_id)) + { + if !(binding.kind.is_assignment() && binding.is_private_declaration()) { + continue; + } + if binding.is_used() { + continue; + } + + let Some(source) = binding.source else { + continue; + }; + let Stmt::AnnAssign(ast::StmtAnnAssign { + target, annotation, .. + }) = checker.semantic().stmts[source] + else { + continue; + }; + let Some(ast::ExprName { id, .. }) = target.as_name_expr() else { + continue; + }; + + if !checker + .semantic() + .match_typing_expr(annotation, "TypeAlias") + { + continue; + } + + diagnostics.push(Diagnostic::new( + UnusedPrivateTypeAlias { + name: id.to_string(), + }, + binding.range, + )); + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI047_PYI047.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI047_PYI047.py.snap new file mode 100644 index 0000000000000..d1aa2e9116558 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI047_PYI047.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__PYI047_PYI047.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI047_PYI047.pyi.snap new file mode 100644 index 0000000000000..2d1f0275d7a5e --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI047_PYI047.pyi.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI047.pyi:6:1: PYI047 Private TypeAlias `_UnusedPrivateTypeAlias` is never used + | +6 | _UnusedPrivateTypeAlias: TypeAlias = int | None + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI047 +7 | _T: typing.TypeAlias = str + | + +PYI047.pyi:7:1: PYI047 Private TypeAlias `_T` is never used + | +6 | _UnusedPrivateTypeAlias: TypeAlias = int | None +7 | _T: typing.TypeAlias = str + | ^^ PYI047 +8 | +9 | # OK + | + + diff --git a/ruff.schema.json b/ruff.schema.json index f56257eb82373..225ac06c6d663 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2393,6 +2393,7 @@ "PYI044", "PYI045", "PYI046", + "PYI047", "PYI048", "PYI05", "PYI050",