Visit PEP 764 inline TypedDicts' keys as non-type-expressions (#15073)

## Summary

Resolves #10812.

## Test Plan

`cargo nextest run` and `cargo insta test`.
This commit is contained in:
InSync 2024-12-30 16:34:55 +07:00 committed by GitHub
parent 8a98d88847
commit d4ee6abf4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 122 additions and 0 deletions

View file

@ -0,0 +1,28 @@
# Regression tests for:
# https://github.com/astral-sh/ruff/issues/10812
from typing import Annotated, Literal, TypedDict
# No errors
single: TypedDict[{"foo": int}]
# Error at `qux`
multiple: TypedDict[{
"bar": str,
"baz": list["qux"],
}]
# Error at `dolor`
nested: TypedDict[
"lorem": TypedDict[{
"ipsum": "dolor"
}],
"sit": Literal["amet"]
]
# Error at `adipiscing`, `eiusmod`, `tempor`
unpack: TypedDict[{
"consectetur": Annotated["adipiscing", "elit"]
**{"sed do": str, int: "eiusmod", **tempor}
}]

View file

@ -1505,6 +1505,20 @@ impl<'a> Visitor<'a> for Checker<'a> {
debug!("Found non-Expr::Tuple argument to PEP 593 Annotation.");
}
}
Some(typing::SubscriptKind::TypedDict) => {
if let Expr::Dict(ast::ExprDict { items, range: _ }) = slice.as_ref() {
for item in items {
if let Some(key) = &item.key {
self.visit_non_type_definition(key);
self.visit_type_definition(&item.value);
} else {
self.visit_non_type_definition(&item.value);
}
}
} else {
self.visit_non_type_definition(slice);
}
}
None => {
self.visit_expr(slice);
self.visit_expr_context(ctx);

View file

@ -159,6 +159,7 @@ mod tests {
#[test_case(Rule::UndefinedName, Path::new("F821_26.pyi"))]
#[test_case(Rule::UndefinedName, Path::new("F821_27.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_28.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_30.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
@ -325,6 +326,7 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::UnusedImport, Path::new("F401_31.py"))]
fn f401_allowed_unused_imports_option(rule_code: Rule, path: &Path) -> Result<()> {
let diagnostics = test_path(

View file

@ -0,0 +1,50 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text
---
F821_30.py:13:18: F821 Undefined name `qux`
|
11 | multiple: TypedDict[{
12 | "bar": str,
13 | "baz": list["qux"],
| ^^^ F821
14 | }]
|
F821_30.py:19:19: F821 Undefined name `dolor`
|
17 | nested: TypedDict[
18 | "lorem": TypedDict[{
19 | "ipsum": "dolor"
| ^^^^^ F821
20 | }],
21 | "sit": Literal["amet"]
|
F821_30.py:26:31: F821 Undefined name `adipiscing`
|
24 | # Error at `adipiscing`, `eiusmod`, `tempor`
25 | unpack: TypedDict[{
26 | "consectetur": Annotated["adipiscing", "elit"]
| ^^^^^^^^^^ F821
27 | **{"sed do": str, int: "eiusmod", **tempor}
28 | }]
|
F821_30.py:27:29: F821 Undefined name `eiusmod`
|
25 | unpack: TypedDict[{
26 | "consectetur": Annotated["adipiscing", "elit"]
27 | **{"sed do": str, int: "eiusmod", **tempor}
| ^^^^^^^ F821
28 | }]
|
F821_30.py:27:41: F821 Undefined name `tempor`
|
25 | unpack: TypedDict[{
26 | "consectetur": Annotated["adipiscing", "elit"]
27 | **{"sed do": str, int: "eiusmod", **tempor}
| ^^^^^^ F821
28 | }]
|