mirror of https://github.com/astral-sh/ruff
[flake8-pyi] Fix PYI049 false negatives on call-based TypedDicts (#9567)
## Summary Fixes another of the bullet points from #8771 ## Test Plan `cargo test` / `cargo insta review`
This commit is contained in:
parent
7be706641d
commit
b3a6f0ce81
|
|
@ -16,3 +16,8 @@ class _UsedTypedDict(TypedDict):
|
|||
|
||||
class _CustomClass(_UsedTypedDict):
|
||||
bar: list[int]
|
||||
|
||||
_UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int})
|
||||
_UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
|
||||
|
||||
def uses_UsedTypedDict3(arg: _UsedTypedDict3) -> None: ...
|
||||
|
|
|
|||
|
|
@ -30,3 +30,8 @@ else:
|
|||
|
||||
class _CustomClass2(_UsedTypedDict2):
|
||||
bar: list[int]
|
||||
|
||||
_UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int})
|
||||
_UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
|
||||
|
||||
def uses_UsedTypedDict3(arg: _UsedTypedDict3) -> None: ...
|
||||
|
|
|
|||
|
|
@ -323,11 +323,16 @@ pub(crate) fn unused_private_typed_dict(
|
|||
scope: &Scope,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
for binding in scope
|
||||
.binding_ids()
|
||||
.map(|binding_id| checker.semantic().binding(binding_id))
|
||||
.map(|binding_id| semantic.binding(binding_id))
|
||||
{
|
||||
if !(binding.kind.is_class_definition() && binding.is_private_declaration()) {
|
||||
if !binding.is_private_declaration() {
|
||||
continue;
|
||||
}
|
||||
if !(binding.kind.is_class_definition() || binding.kind.is_assignment()) {
|
||||
continue;
|
||||
}
|
||||
if binding.is_used() {
|
||||
|
|
@ -337,23 +342,64 @@ pub(crate) fn unused_private_typed_dict(
|
|||
let Some(source) = binding.source else {
|
||||
continue;
|
||||
};
|
||||
let Stmt::ClassDef(class_def) = checker.semantic().statement(source) else {
|
||||
|
||||
let Some(class_name) = extract_typeddict_name(semantic.statement(source), semantic) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !class_def
|
||||
.bases()
|
||||
.iter()
|
||||
.any(|base| checker.semantic().match_typing_expr(base, "TypedDict"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
diagnostics.push(Diagnostic::new(
|
||||
UnusedPrivateTypedDict {
|
||||
name: class_def.name.to_string(),
|
||||
name: class_name.to_string(),
|
||||
},
|
||||
binding.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_typeddict_name<'a>(stmt: &'a Stmt, semantic: &SemanticModel) -> Option<&'a str> {
|
||||
let is_typeddict = |expr: &ast::Expr| semantic.match_typing_expr(expr, "TypedDict");
|
||||
match stmt {
|
||||
// E.g. return `Some("Foo")` for the first one of these classes,
|
||||
// and `Some("Bar")` for the second:
|
||||
//
|
||||
// ```python
|
||||
// import typing
|
||||
// from typing import TypedDict
|
||||
//
|
||||
// class Foo(TypedDict):
|
||||
// x: int
|
||||
//
|
||||
// T = typing.TypeVar("T")
|
||||
//
|
||||
// class Bar(typing.TypedDict, typing.Generic[T]):
|
||||
// y: T
|
||||
// ```
|
||||
Stmt::ClassDef(class_def @ ast::StmtClassDef { name, .. }) => {
|
||||
if class_def.bases().iter().any(is_typeddict) {
|
||||
Some(name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
// E.g. return `Some("Baz")` for this assignment,
|
||||
// which is an accepted alternative way of creating a TypedDict type:
|
||||
//
|
||||
// ```python
|
||||
// import typing
|
||||
// Baz = typing.TypedDict("Baz", {"z": bytes})
|
||||
// ```
|
||||
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
|
||||
let [target] = targets.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
let ast::ExprName { id, .. } = target.as_name_expr()?;
|
||||
let ast::ExprCall { func, .. } = value.as_call_expr()?;
|
||||
if is_typeddict(func) {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,4 +15,13 @@ PYI049.py:9:7: PYI049 Private TypedDict `_UnusedTypedDict2` is never used
|
|||
10 | bar: int
|
||||
|
|
||||
|
||||
PYI049.py:20:1: PYI049 Private TypedDict `_UnusedTypedDict3` is never used
|
||||
|
|
||||
18 | bar: list[int]
|
||||
19 |
|
||||
20 | _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int})
|
||||
| ^^^^^^^^^^^^^^^^^ PYI049
|
||||
21 | _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
|
||||
|
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,4 +15,13 @@ PYI049.pyi:10:7: PYI049 Private TypedDict `_UnusedTypedDict2` is never used
|
|||
11 | bar: int
|
||||
|
|
||||
|
||||
PYI049.pyi:34:1: PYI049 Private TypedDict `_UnusedTypedDict3` is never used
|
||||
|
|
||||
32 | bar: list[int]
|
||||
33 |
|
||||
34 | _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int})
|
||||
| ^^^^^^^^^^^^^^^^^ PYI049
|
||||
35 | _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes})
|
||||
|
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue