Skip to content

Commit

Permalink
[flake8-pyi] Implement PYI049 (#6136)
Browse files Browse the repository at this point in the history
## Summary

Checks for the presence of unused private `typing.TypedDict`
definitions.

ref #848 

## Test Plan

Snapshots and manual runs of flake8
  • Loading branch information
LaBatata101 committed Jul 29, 2023
1 parent 7838d8c commit e0d5c75
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 0 deletions.
18 changes: 18 additions & 0 deletions crates/ruff/resources/test/fixtures/flake8_pyi/PYI049.py
Original file line number Diff line number Diff line change
@@ -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]
32 changes: 32 additions & 0 deletions crates/ruff/resources/test/fixtures/flake8_pyi/PYI049.pyi
Original file line number Diff line number Diff line change
@@ -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]
4 changes: 4 additions & 0 deletions crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UnusedPrivateProtocol,
Rule::UnusedPrivateTypeAlias,
Rule::UnusedPrivateTypeVar,
Rule::UnusedPrivateTypedDict,
Rule::UnusedStaticMethodArgument,
Rule::UnusedVariable,
]) {
Expand Down Expand Up @@ -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!(
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff/src/rules/flake8_pyi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---

Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit e0d5c75

Please sign in to comment.