mirror of https://github.com/astral-sh/ruff
[`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:
parent
7838d8c8af
commit
e0d5c7564f
|
|
@ -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]
|
||||
|
|
@ -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]
|
||||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
||||
|
|
@ -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
|
||||
|
|
||||
|
||||
|
||||
|
|
@ -2395,6 +2395,7 @@
|
|||
"PYI046",
|
||||
"PYI047",
|
||||
"PYI048",
|
||||
"PYI049",
|
||||
"PYI05",
|
||||
"PYI050",
|
||||
"PYI052",
|
||||
|
|
|
|||
Loading…
Reference in New Issue