[`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
This commit is contained in:
Victor Hugo Gomes 2023-07-28 21:34:36 -03:00 committed by GitHub
parent 7838d8c8af
commit e0d5c7564f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 164 additions and 0 deletions

View File

@ -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]

View File

@ -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]

View File

@ -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!(

View File

@ -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),

View File

@ -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(

View File

@ -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<Diagnostic>,
) {
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,
));
}
}

View File

@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---

View File

@ -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
|

1
ruff.schema.json generated
View File

@ -2395,6 +2395,7 @@
"PYI046",
"PYI047",
"PYI048",
"PYI049",
"PYI05",
"PYI050",
"PYI052",