From e0d5c7564fe4ecab90d6cd4ac3ebbb3b7e9e245b Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Fri, 28 Jul 2023 21:34:36 -0300 Subject: [PATCH] [`flake8-pyi`] Implement PYI049 (#6136) ## Summary Checks for the presence of unused private `typing.TypedDict` definitions. ref #848 ## Test Plan Snapshots and manual runs of flake8 --- .../test/fixtures/flake8_pyi/PYI049.py | 18 ++++ .../test/fixtures/flake8_pyi/PYI049.pyi | 32 +++++++ .../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__PYI049_PYI049.py.snap | 4 + ..._flake8_pyi__tests__PYI049_PYI049.pyi.snap | 18 ++++ ruff.schema.json | 1 + 9 files changed, 164 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI049.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI049.pyi create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI049_PYI049.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI049_PYI049.pyi.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI049.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI049.py new file mode 100644 index 0000000000..5c4738d2e0 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI049.py @@ -0,0 +1,18 @@ +import typing +from typing import TypedDict + + +class _UnusedTypedDict(TypedDict): + foo: str + + +class _UnusedTypedDict2(typing.TypedDict): + bar: int + + +class _UsedTypedDict(TypedDict): + foo: bytes + + +class _CustomClass(_UsedTypedDict): + bar: list[int] diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI049.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI049.pyi new file mode 100644 index 0000000000..2e8c6ee256 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI049.pyi @@ -0,0 +1,32 @@ +import sys +import typing +from typing import TypedDict + + +class _UnusedTypedDict(TypedDict): + foo: str + + +class _UnusedTypedDict2(typing.TypedDict): + bar: int + + +# OK +class _UsedTypedDict(TypedDict): + foo: bytes + + +class _CustomClass(_UsedTypedDict): + bar: list[int] + + +if sys.version_info >= (3, 10): + class _UsedTypedDict2(TypedDict): + foo: int +else: + class _UsedTypedDict2(TypedDict): + foo: float + + +class _CustomClass2(_UsedTypedDict2): + bar: list[int] diff --git a/crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs b/crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs index ce1308e186..f020fcadbb 100644 --- a/crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs +++ b/crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs @@ -27,6 +27,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) { Rule::UnusedPrivateProtocol, Rule::UnusedPrivateTypeAlias, Rule::UnusedPrivateTypeVar, + Rule::UnusedPrivateTypedDict, Rule::UnusedStaticMethodArgument, Rule::UnusedVariable, ]) { @@ -227,6 +228,9 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) { if checker.enabled(Rule::UnusedPrivateTypeAlias) { flake8_pyi::rules::unused_private_type_alias(checker, scope, &mut diagnostics); } + if checker.enabled(Rule::UnusedPrivateTypedDict) { + flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics); + } } if matches!( diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 47a02947be..9018dcc946 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -656,6 +656,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (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, "049") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnusedPrivateTypedDict), (Flake8Pyi, "050") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NoReturnArgumentAnnotationInStub), (Flake8Pyi, "052") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnannotatedAssignmentInStub), (Flake8Pyi, "054") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NumericLiteralTooLong), diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index b3f601d7be..6c363fdc6b 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -99,6 +99,8 @@ mod tests { #[test_case(Rule::UnusedPrivateProtocol, Path::new("PYI046.pyi"))] #[test_case(Rule::UnusedPrivateTypeAlias, Path::new("PYI047.py"))] #[test_case(Rule::UnusedPrivateTypeAlias, Path::new("PYI047.pyi"))] + #[test_case(Rule::UnusedPrivateTypedDict, Path::new("PYI049.py"))] + #[test_case(Rule::UnusedPrivateTypedDict, Path::new("PYI049.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 70b6c83127..2a80276d72 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 @@ -111,6 +111,48 @@ impl Violation for UnusedPrivateTypeAlias { } } +/// ## What it does +/// Checks for the presence of unused private `typing.TypedDict` definitions. +/// +/// ## Why is this bad? +/// A private `typing.TypedDict` 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 +/// +/// +/// class _UnusedPrivateTypedDict(typing.TypedDict): +/// foo: list[int] +/// ``` +/// +/// Use instead: +/// ```python +/// import typing +/// +/// +/// class _UsedPrivateTypedDict(typing.TypedDict): +/// foo: set[str] +/// +/// +/// def func(arg: _UsedPrivateTypedDict) -> _UsedPrivateTypedDict: +/// ... +/// ``` +#[violation] +pub struct UnusedPrivateTypedDict { + name: String, +} + +impl Violation for UnusedPrivateTypedDict { + #[derive_message_formats] + fn message(&self) -> String { + let UnusedPrivateTypedDict { name } = self; + format!("Private TypedDict `{name}` is never used") + } +} + /// PYI018 pub(crate) fn unused_private_type_var( checker: &Checker, @@ -241,3 +283,45 @@ pub(crate) fn unused_private_type_alias( )); } } + +/// PYI049 +pub(crate) fn unused_private_typed_dict( + 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_class_definition() && binding.is_private_declaration()) { + continue; + } + if binding.is_used() { + continue; + } + + let Some(source) = binding.source else { + continue; + }; + let Stmt::ClassDef(ast::StmtClassDef { name, bases, .. }) = + checker.semantic().stmts[source] + else { + continue; + }; + + if !bases + .iter() + .any(|base| checker.semantic().match_typing_expr(base, "TypedDict")) + { + continue; + } + + diagnostics.push(Diagnostic::new( + UnusedPrivateTypedDict { + name: name.to_string(), + }, + binding.range, + )); + } +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI049_PYI049.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI049_PYI049.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI049_PYI049.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__PYI049_PYI049.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI049_PYI049.pyi.snap new file mode 100644 index 0000000000..f07884832c --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI049_PYI049.pyi.snap @@ -0,0 +1,18 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI049.pyi:6:7: PYI049 Private TypedDict `_UnusedTypedDict` is never used + | +6 | class _UnusedTypedDict(TypedDict): + | ^^^^^^^^^^^^^^^^ PYI049 +7 | foo: str + | + +PYI049.pyi:10:7: PYI049 Private TypedDict `_UnusedTypedDict2` is never used + | +10 | class _UnusedTypedDict2(typing.TypedDict): + | ^^^^^^^^^^^^^^^^^ PYI049 +11 | bar: int + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 225ac06c6d..d238900a59 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2395,6 +2395,7 @@ "PYI046", "PYI047", "PYI048", + "PYI049", "PYI05", "PYI050", "PYI052",