diff --git a/Cargo.lock b/Cargo.lock
index 4925f1a866..da883be1d4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4310,6 +4310,7 @@ dependencies = [
"serde",
"smallvec",
"static_assertions",
+ "strsim",
"strum",
"strum_macros",
"tempfile",
diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 563ed1aee3..7512093005 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -36,7 +36,7 @@ def test(): -> "int":
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L99)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L100)
**What it does**
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L143)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L144)
**What it does**
@@ -88,7 +88,7 @@ f(int) # error
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L169)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L170)
**What it does**
@@ -117,7 +117,7 @@ a = 1
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L194)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L195)
**What it does**
@@ -147,7 +147,7 @@ class C(A, B): ...
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L220)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L221)
**What it does**
@@ -177,7 +177,7 @@ class B(A): ...
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286)
**What it does**
@@ -202,7 +202,7 @@ class B(A, A): ...
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L306)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L307)
**What it does**
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L448)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L449)
**What it does**
@@ -334,7 +334,7 @@ class C(A, B): ...
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L472)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L473)
**What it does**
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L338)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L339)
**What it does**
@@ -445,7 +445,7 @@ an atypical memory layout.
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L492)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L518)
**What it does**
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L532)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L558)
**What it does**
@@ -496,7 +496,7 @@ a: int = ''
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1536)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1562)
**What it does**
@@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L554)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L580)
**What it does**
@@ -550,7 +550,7 @@ class A(42): ... # error: [invalid-base]
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L605)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L631)
**What it does**
@@ -575,7 +575,7 @@ with 1:
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L626)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L652)
**What it does**
@@ -602,7 +602,7 @@ a: str
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L649)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L675)
**What it does**
@@ -644,7 +644,7 @@ except ZeroDivisionError:
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L685)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L711)
**What it does**
@@ -670,12 +670,41 @@ class C[U](Generic[T]): ...
- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)
+## `invalid-key`
+
+
+Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
+[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) ·
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L493)
+
+
+**What it does**
+
+Checks for subscript accesses with invalid keys.
+
+**Why is this bad?**
+
+Using an invalid key will raise a `KeyError` at runtime.
+
+**Examples**
+
+```python
+from typing import TypedDict
+
+class Person(TypedDict):
+ name: str
+ age: int
+
+alice = Person(name="Alice", age=30)
+alice["height"] # KeyError: 'height'
+```
+
## `invalid-legacy-type-variable`
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L711)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L737)
**What it does**
@@ -708,7 +737,7 @@ def f(t: TypeVar("U")): ...
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L760)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L786)
**What it does**
@@ -740,7 +769,7 @@ class B(metaclass=f): ...
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L787)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L813)
**What it does**
@@ -788,7 +817,7 @@ def foo(x: int) -> int: ...
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L830)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L856)
**What it does**
@@ -812,7 +841,7 @@ def f(a: int = ''): ...
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L420)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L421)
**What it does**
@@ -844,7 +873,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L850)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L876)
Checks for `raise` statements that raise non-exceptions or use invalid
@@ -891,7 +920,7 @@ def g():
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L513)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L539)
**What it does**
@@ -914,7 +943,7 @@ def func() -> int:
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L893)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L919)
**What it does**
@@ -968,7 +997,7 @@ TODO #14889
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L765)
**What it does**
@@ -993,7 +1022,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L932)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L958)
**What it does**
@@ -1021,7 +1050,7 @@ TYPE_CHECKING = ''
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L982)
**What it does**
@@ -1049,7 +1078,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1008)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1034)
**What it does**
@@ -1081,7 +1110,7 @@ f(10) # Error
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L980)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1006)
**What it does**
@@ -1113,7 +1142,7 @@ class C:
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1036)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062)
**What it does**
@@ -1146,7 +1175,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1065)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1091)
**What it does**
@@ -1169,7 +1198,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1084)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1110)
**What it does**
@@ -1196,7 +1225,7 @@ func("string") # error: [no-matching-overload]
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1107)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1133)
**What it does**
@@ -1218,7 +1247,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1125)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1151)
**What it does**
@@ -1242,7 +1271,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1202)
**What it does**
@@ -1296,7 +1325,7 @@ def test(): -> "int":
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1512)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1538)
**What it does**
@@ -1324,7 +1353,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1267)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1293)
**What it does**
@@ -1351,7 +1380,7 @@ class B(A): ... # Error raised here
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1312)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1338)
**What it does**
@@ -1376,7 +1405,7 @@ f("foo") # Error raised here
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1316)
**What it does**
@@ -1402,7 +1431,7 @@ def _(x: int):
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1333)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1359)
**What it does**
@@ -1446,7 +1475,7 @@ class A:
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1390)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1416)
**What it does**
@@ -1471,7 +1500,7 @@ f(x=1, y=2) # Error raised here
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1437)
**What it does**
@@ -1497,7 +1526,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1433)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1459)
**What it does**
@@ -1520,7 +1549,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1452)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1478)
**What it does**
@@ -1543,7 +1572,7 @@ print(x) # NameError: name 'x' is not defined
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1145)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1171)
**What it does**
@@ -1578,7 +1607,7 @@ b1 < b2 < b1 # exception raised here
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1497)
**What it does**
@@ -1604,7 +1633,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1493)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1519)
**What it does**
@@ -1627,7 +1656,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L264)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L265)
**What it does**
@@ -1680,7 +1709,7 @@ a = 20 / 0 # type: ignore
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1197)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1223)
**What it does**
@@ -1706,7 +1735,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L117)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L118)
**What it does**
@@ -1736,7 +1765,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1219)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1245)
**What it does**
@@ -1766,7 +1795,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1590)
**What it does**
@@ -1791,7 +1820,7 @@ cast(int, f()) # Redundant
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1372)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1398)
**What it does**
@@ -1842,7 +1871,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1585)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1611)
**What it does**
@@ -1896,7 +1925,7 @@ def g():
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L572)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L598)
**What it does**
@@ -1933,7 +1962,7 @@ class D(C): ... # error: [unsupported-base]
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L246)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L247)
**What it does**
@@ -1955,7 +1984,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
-[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1245)
+[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1271)
**What it does**
diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml
index 370b0a4154..50ab542a29 100644
--- a/crates/ty_python_semantic/Cargo.toml
+++ b/crates/ty_python_semantic/Cargo.toml
@@ -48,6 +48,7 @@ test-case = { workspace = true }
memchr = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
+strsim = "0.11.1"
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing", "os"] }
diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md
index 146342d694..f8eb97bec5 100644
--- a/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md
+++ b/crates/ty_python_semantic/resources/mdtest/narrow/assignment.md
@@ -244,8 +244,7 @@ class D(TypedDict):
td = D(x=1, label="a")
td["x"] = 0
-# TODO: should be Literal[0]
-reveal_type(td["x"]) # revealed: @Todo(Support for `TypedDict`)
+reveal_type(td["x"]) # revealed: Literal[0]
# error: [unresolved-reference]
does["not"]["exist"] = 0
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap
new file mode 100644
index 0000000000..15dcf719c2
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap
@@ -0,0 +1,77 @@
+---
+source: crates/ty_test/src/lib.rs
+expression: snapshot
+---
+---
+mdtest name: typed_dict.md - `TypedDict` - Diagnostics
+mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
+---
+
+# Python source files
+
+## mdtest_snippet.py
+
+```
+ 1 | from typing import TypedDict, Final
+ 2 |
+ 3 | class Person(TypedDict):
+ 4 | name: str
+ 5 | age: int | None
+ 6 |
+ 7 | def access_invalid_literal_string_key(person: Person):
+ 8 | person["naem"] # error: [invalid-key]
+ 9 |
+10 | NAME_KEY: Final = "naem"
+11 |
+12 | def access_invalid_key(person: Person):
+13 | person[NAME_KEY] # error: [invalid-key]
+14 |
+15 | def access_with_str_key(person: Person, str_key: str):
+16 | person[str_key] # error: [invalid-key]
+```
+
+# Diagnostics
+
+```
+error[invalid-key]: Invalid key access on TypedDict `Person`
+ --> src/mdtest_snippet.py:8:5
+ |
+ 7 | def access_invalid_literal_string_key(person: Person):
+ 8 | person["naem"] # error: [invalid-key]
+ | ------ ^^^^^^ Unknown key "naem" - did you mean "name"?
+ | |
+ | TypedDict `Person`
+ 9 |
+10 | NAME_KEY: Final = "naem"
+ |
+info: rule `invalid-key` is enabled by default
+
+```
+
+```
+error[invalid-key]: Invalid key access on TypedDict `Person`
+ --> src/mdtest_snippet.py:13:5
+ |
+12 | def access_invalid_key(person: Person):
+13 | person[NAME_KEY] # error: [invalid-key]
+ | ------ ^^^^^^^^ Unknown key "naem" - did you mean "name"?
+ | |
+ | TypedDict `Person`
+14 |
+15 | def access_with_str_key(person: Person, str_key: str):
+ |
+info: rule `invalid-key` is enabled by default
+
+```
+
+```
+error[invalid-key]: TypedDict `Person` cannot be indexed with a key of type `str`
+ --> src/mdtest_snippet.py:16:12
+ |
+15 | def access_with_str_key(person: Person, str_key: str):
+16 | person[str_key] # error: [invalid-key]
+ | ^^^^^^^
+ |
+info: rule `invalid-key` is enabled by default
+
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md
index 2882397404..c44d11897e 100644
--- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md
+++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md
@@ -25,12 +25,21 @@ alice: Person = {"name": "Alice", "age": 30}
reveal_type(alice["name"]) # revealed: Unknown
# TODO: this should be `int | None`
reveal_type(alice["age"]) # revealed: Unknown
+
+# TODO: this should reveal `Unknown`, and it should emit an error
+reveal_type(alice["non_existing"]) # revealed: Unknown
```
Inhabitants can also be created through a constructor call:
```py
bob = Person(name="Bob", age=25)
+
+reveal_type(bob["name"]) # revealed: str
+reveal_type(bob["age"]) # revealed: int | None
+
+# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
+reveal_type(bob["non_existing"]) # revealed: Unknown
```
Methods that are available on `dict`s are also available on `TypedDict`s:
@@ -127,6 +136,39 @@ dangerous(alice)
reveal_type(alice["name"]) # revealed: Unknown
```
+## Key-based access
+
+```py
+from typing import TypedDict, Final, Literal, Any
+
+class Person(TypedDict):
+ name: str
+ age: int | None
+
+NAME_FINAL: Final = "name"
+AGE_FINAL: Final[Literal["age"]] = "age"
+
+def _(person: Person, literal_key: Literal["age"], union_of_keys: Literal["age", "name"], str_key: str, unknown_key: Any) -> None:
+ reveal_type(person["name"]) # revealed: str
+ reveal_type(person["age"]) # revealed: int | None
+
+ reveal_type(person[NAME_FINAL]) # revealed: str
+ reveal_type(person[AGE_FINAL]) # revealed: int | None
+
+ reveal_type(person[literal_key]) # revealed: int | None
+
+ reveal_type(person[union_of_keys]) # revealed: int | None | str
+
+ # error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
+ reveal_type(person["non_existing"]) # revealed: Unknown
+
+ # error: [invalid-key] "TypedDict `Person` cannot be indexed with a key of type `str`"
+ reveal_type(person[str_key]) # revealed: Unknown
+
+ # No error here:
+ reveal_type(person[unknown_key]) # revealed: Unknown
+```
+
## Methods on `TypedDict`
```py
@@ -333,4 +375,29 @@ reveal_type(Message.__required_keys__) # revealed: @Todo(Support for `TypedDict
msg.content
```
+## Diagnostics
+
+
+
+Snapshot tests for diagnostic messages including suggestions:
+
+```py
+from typing import TypedDict, Final
+
+class Person(TypedDict):
+ name: str
+ age: int | None
+
+def access_invalid_literal_string_key(person: Person):
+ person["naem"] # error: [invalid-key]
+
+NAME_KEY: Final = "naem"
+
+def access_invalid_key(person: Person):
+ person[NAME_KEY] # error: [invalid-key]
+
+def access_with_str_key(person: Person, str_key: str):
+ person[str_key] # error: [invalid-key]
+```
+
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs
index c185942247..5cd424c82a 100644
--- a/crates/ty_python_semantic/src/lib.rs
+++ b/crates/ty_python_semantic/src/lib.rs
@@ -46,6 +46,7 @@ mod util;
#[cfg(feature = "testing")]
pub mod pull_types;
+type FxOrderMap = ordermap::map::OrderMap>;
type FxOrderSet = ordermap::set::OrderSet>;
type FxIndexMap = indexmap::IndexMap>;
type FxIndexSet = indexmap::IndexSet>;
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index 70f8cf9cb8..eaeca3ee5b 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -38,6 +38,7 @@ use crate::semantic_index::scope::ScopeId;
use crate::semantic_index::{imported_modules, place_table, semantic_index};
use crate::suppression::check_suppressions;
use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding};
+use crate::types::class::{CodeGeneratorKind, Field};
pub(crate) use crate::types::class_base::ClassBase;
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
@@ -61,7 +62,7 @@ use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signat
use crate::types::tuple::{TupleSpec, TupleType};
use crate::unpack::EvaluationMode;
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
-use crate::{Db, FxOrderSet, Module, Program};
+use crate::{Db, FxOrderMap, FxOrderSet, Module, Program};
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
use instance::Protocol;
pub use instance::{NominalInstanceType, ProtocolInstanceType};
@@ -669,10 +670,6 @@ impl<'db> Type<'db> {
matches!(self, Type::Dynamic(_))
}
- pub(crate) const fn is_typed_dict(&self) -> bool {
- matches!(self, Type::TypedDict(..))
- }
-
/// Returns the top materialization (or upper bound materialization) of this type, which is the
/// most general form of the type that is fully static.
#[must_use]
@@ -834,6 +831,17 @@ impl<'db> Type<'db> {
.expect("Expected a Type::EnumLiteral variant")
}
+ pub(crate) const fn is_typed_dict(&self) -> bool {
+ matches!(self, Type::TypedDict(..))
+ }
+
+ pub(crate) fn into_typed_dict(self) -> Option> {
+ match self {
+ Type::TypedDict(typed_dict) => Some(typed_dict),
+ _ => None,
+ }
+ }
+
/// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`.
/// Since a `ClassType` must be specialized, apply the default specialization to any
/// unspecialized generic class literal.
@@ -5289,15 +5297,15 @@ impl<'db> Type<'db> {
],
),
_ if class.is_typed_dict(db) => {
- Type::TypedDict(TypedDictType::new(db, ClassType::NonGeneric(*class)))
+ TypedDictType::from(db, ClassType::NonGeneric(*class))
}
_ => Type::instance(db, class.default_specialization(db)),
};
Ok(ty)
}
- Type::GenericAlias(alias) if alias.is_typed_dict(db) => Ok(Type::TypedDict(
- TypedDictType::new(db, ClassType::from(*alias)),
- )),
+ Type::GenericAlias(alias) if alias.is_typed_dict(db) => {
+ Ok(TypedDictType::from(db, ClassType::from(*alias)))
+ }
Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))),
Type::SubclassOf(_)
@@ -5644,6 +5652,7 @@ impl<'db> Type<'db> {
return KnownClass::Dict
.to_specialized_class_type(db, [KnownClass::Str.to_instance(db), Type::object(db)])
.map(Type::from)
+ // Guard against user-customized typesheds with a broken `dict` class
.unwrap_or_else(Type::unknown);
}
@@ -9000,6 +9009,15 @@ pub struct TypedDictType<'db> {
impl get_size2::GetSize for TypedDictType<'_> {}
impl<'db> TypedDictType<'db> {
+ pub(crate) fn from(db: &'db dyn Db, defining_class: ClassType<'db>) -> Type<'db> {
+ Type::TypedDict(Self::new(db, defining_class))
+ }
+
+ pub(crate) fn items(self, db: &'db dyn Db) -> FxOrderMap> {
+ let (class_literal, specialization) = self.defining_class(db).class_literal(db);
+ class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict)
+ }
+
pub(crate) fn apply_type_mapping<'a>(
self,
db: &'db dyn Db,
diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
index 64464d0a1b..f4e5372ed4 100644
--- a/crates/ty_python_semantic/src/types/class.rs
+++ b/crates/ty_python_semantic/src/types/class.rs
@@ -1,4 +1,3 @@
-use std::hash::BuildHasherDefault;
use std::sync::{LazyLock, Mutex};
use super::TypeVarVariance;
@@ -9,6 +8,7 @@ use super::{
function::{FunctionDecorators, FunctionType},
infer_expression_type, infer_unpack_types,
};
+use crate::FxOrderMap;
use crate::module_resolver::KnownModule;
use crate::semantic_index::definition::{Definition, DefinitionState};
use crate::semantic_index::scope::NodeWithScopeKind;
@@ -23,9 +23,9 @@ use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signatu
use crate::types::tuple::TupleSpec;
use crate::types::{
BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams,
- DeprecatedInstance, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation,
- TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type,
- infer_definition_types, todo_type,
+ DeprecatedInstance, KnownInstanceType, StringLiteralType, TypeAliasType, TypeMapping,
+ TypeRelation, TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
+ declaration_type, infer_definition_types, todo_type,
};
use crate::{
Db, FxIndexMap, FxOrderSet, Program,
@@ -54,9 +54,7 @@ use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast, PythonVersion};
use ruff_text_size::{Ranged, TextRange};
-use rustc_hash::{FxHashSet, FxHasher};
-
-type FxOrderMap = ordermap::map::OrderMap>;
+use rustc_hash::FxHashSet;
fn explicit_bases_cycle_recover<'db>(
_db: &'db dyn Db,
@@ -1097,11 +1095,11 @@ impl MethodDecorator {
}
}
-/// Metadata regarding a dataclass field/attribute.
+/// Metadata regarding a dataclass field/attribute or a `TypedDict` "item" / key-value pair.
#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) struct DataclassField<'db> {
+pub(crate) struct Field<'db> {
/// The declared type of the field
- pub(crate) field_ty: Type<'db>,
+ pub(crate) declared_ty: Type<'db>,
/// The type of the default value for this field
pub(crate) default_ty: Option>,
@@ -1858,8 +1856,8 @@ impl<'db> ClassLiteral<'db> {
let mut kw_only_field_seen = false;
for (
field_name,
- DataclassField {
- mut field_ty,
+ Field {
+ declared_ty: mut field_ty,
mut default_ty,
init_only: _,
init,
@@ -2038,17 +2036,28 @@ impl<'db> ClassLiteral<'db> {
Some(CallableType::function_like(db, signature))
}
(CodeGeneratorKind::TypedDict, "__getitem__") => {
- // TODO: synthesize a set of overloads with precise types
- let signature = Signature::new(
- Parameters::new([
- Parameter::positional_only(Some(Name::new_static("self")))
- .with_annotated_type(instance_ty),
- Parameter::positional_only(Some(Name::new_static("key"))),
- ]),
- Some(todo_type!("Support for `TypedDict`")),
- );
+ let fields = self.fields(db, specialization, field_policy);
- Some(CallableType::function_like(db, signature))
+ // Add (key -> value type) overloads for all TypedDict items ("fields"):
+ let overloads = fields.iter().map(|(name, field)| {
+ let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
+
+ Signature::new(
+ Parameters::new([
+ Parameter::positional_only(Some(Name::new_static("self")))
+ .with_annotated_type(instance_ty),
+ Parameter::positional_only(Some(Name::new_static("key")))
+ .with_annotated_type(key_type),
+ ]),
+ Some(field.declared_ty),
+ )
+ });
+
+ Some(Type::Callable(CallableType::new(
+ db,
+ CallableSignature::from_overloads(overloads),
+ true,
+ )))
}
(CodeGeneratorKind::TypedDict, "get") => {
// TODO: synthesize a set of overloads with precise types
@@ -2143,7 +2152,7 @@ impl<'db> ClassLiteral<'db> {
db: &'db dyn Db,
specialization: Option>,
field_policy: CodeGeneratorKind,
- ) -> FxOrderMap> {
+ ) -> FxOrderMap> {
if field_policy == CodeGeneratorKind::NamedTuple {
// NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the
// fields of this class only.
@@ -2190,7 +2199,7 @@ impl<'db> ClassLiteral<'db> {
self,
db: &'db dyn Db,
specialization: Option>,
- ) -> FxOrderMap> {
+ ) -> FxOrderMap> {
let mut attributes = FxOrderMap::default();
let class_body_scope = self.body_scope(db);
@@ -2242,8 +2251,8 @@ impl<'db> ClassLiteral<'db> {
attributes.insert(
symbol.name().clone(),
- DataclassField {
- field_ty: attr_ty.apply_optional_specialization(db, specialization),
+ Field {
+ declared_ty: attr_ty.apply_optional_specialization(db, specialization),
default_ty,
init_only: attr.is_init_var(),
init,
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 32af3ba73b..409888fb0b 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -40,6 +40,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INSTANCE_LAYOUT_CONFLICT);
registry.register_lint(&INCONSISTENT_MRO);
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
+ registry.register_lint(&INVALID_KEY);
registry.register_lint(&INVALID_ARGUMENT_TYPE);
registry.register_lint(&INVALID_RETURN_TYPE);
registry.register_lint(&INVALID_ASSIGNMENT);
@@ -489,6 +490,31 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Checks for subscript accesses with invalid keys.
+ ///
+ /// ## Why is this bad?
+ /// Using an invalid key will raise a `KeyError` at runtime.
+ ///
+ /// ## Examples
+ /// ```python
+ /// from typing import TypedDict
+ ///
+ /// class Person(TypedDict):
+ /// name: str
+ /// age: int
+ ///
+ /// alice = Person(name="Alice", age=30)
+ /// alice["height"] # KeyError: 'height'
+ /// ```
+ pub(crate) static INVALID_KEY = {
+ summary: "detects invalid subscript accesses",
+ status: LintStatus::preview("1.0.0"),
+ default_level: Level::Error,
+ }
+}
+
declare_lint! {
/// ## What it does
/// Detects call arguments whose type is not assignable to the corresponding typed parameter.
@@ -2591,3 +2617,29 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, "resolving modules");
}
+
+/// Suggest a name from `existing_names` that is similar to `wrong_name`.
+pub(super) fn did_you_mean, T: AsRef>(
+ existing_names: impl Iterator- ,
+ wrong_name: T,
+) -> Option {
+ if wrong_name.as_ref().len() < 3 {
+ return None;
+ }
+
+ existing_names
+ .filter(|ref id| id.as_ref().len() >= 2)
+ .map(|ref id| {
+ (
+ id.as_ref().to_string(),
+ strsim::damerau_levenshtein(
+ &id.as_ref().to_lowercase(),
+ &wrong_name.as_ref().to_lowercase(),
+ ),
+ )
+ })
+ .min_by_key(|(_, dist)| *dist)
+ // Heuristic to filter out bad matches
+ .filter(|(_, dist)| *dist <= 3)
+ .map(|(id, _)| id)
+}
diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs
index e29056c01b..0f76eb2ddb 100644
--- a/crates/ty_python_semantic/src/types/infer.rs
+++ b/crates/ty_python_semantic/src/types/infer.rs
@@ -90,21 +90,21 @@ use crate::semantic_index::{
ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, semantic_index,
};
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
-use crate::types::class::{CodeGeneratorKind, DataclassField, MetaclassErrorKind, SliceLiteral};
+use crate::types::class::{CodeGeneratorKind, Field, MetaclassErrorKind, SliceLiteral};
use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
- INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
- INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases,
- POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics,
- UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
- UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
- report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
- report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
- report_invalid_assignment, report_invalid_attribute_assignment,
- report_invalid_generator_function_return_type, report_invalid_return_type,
- report_possibly_unbound_attribute,
+ INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_PARAMETER_DEFAULT,
+ INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS,
+ IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT,
+ TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
+ UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, did_you_mean,
+ report_implicit_return_type, report_instance_layout_conflict,
+ report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
+ report_invalid_arguments_to_callable, report_invalid_assignment,
+ report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
+ report_invalid_return_type, report_possibly_unbound_attribute,
};
use crate::types::enums::is_enum_class;
use crate::types::function::{
@@ -1352,8 +1352,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let specialization = None;
let mut kw_only_field_names = vec![];
- for (name, DataclassField { field_ty, .. }) in
- class.fields(self.db(), specialization, field_policy)
+ for (
+ name,
+ Field {
+ declared_ty: field_ty,
+ ..
+ },
+ ) in class.fields(self.db(), specialization, field_policy)
{
let Some(instance) = field_ty.into_nominal_instance() else {
continue;
@@ -1909,17 +1914,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// TODO: also consider qualifiers on the attribute
return (ty, is_modifiable);
}
- } else if let AnyNodeRef::ExprSubscript(ast::ExprSubscript {
- value,
- slice,
- ctx,
- ..
- }) = node
+ } else if let AnyNodeRef::ExprSubscript(
+ subscript @ ast::ExprSubscript {
+ value, slice, ctx, ..
+ },
+ ) = node
{
let value_ty = self.infer_expression(value);
let slice_ty = self.infer_expression(slice);
- let result_ty = self
- .infer_subscript_expression_types(value, value_ty, slice_ty, *ctx);
+ let result_ty = self.infer_subscript_expression_types(
+ subscript, value_ty, slice_ty, *ctx,
+ );
return (result_ty, is_modifiable);
}
}
@@ -2030,24 +2035,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// pyright. TODO: Other standard library classes may also be considered safe. Also,
// subclasses of these safe classes that do not override `__getitem__/__setitem__`
// may be considered safe.
- let safe_mutable_classes = [
- KnownClass::List.to_instance(db),
- KnownClass::Dict.to_instance(db),
- KnownClass::Bytearray.to_instance(db),
- KnownClass::DefaultDict.to_instance(db),
- SpecialFormType::ChainMap.instance_fallback(db),
- SpecialFormType::Counter.instance_fallback(db),
- SpecialFormType::Deque.instance_fallback(db),
- SpecialFormType::OrderedDict.instance_fallback(db),
- SpecialFormType::TypedDict.instance_fallback(db),
- ];
- if safe_mutable_classes.iter().all(|safe_mutable_class| {
- !value_ty.is_equivalent_to(db, *safe_mutable_class)
- && value_ty
- .generic_origin(db)
- .zip(safe_mutable_class.generic_origin(db))
- .is_none_or(|(l, r)| l != r)
- }) {
+ let is_safe_mutable_class = || {
+ let safe_mutable_classes = [
+ KnownClass::List.to_instance(db),
+ KnownClass::Dict.to_instance(db),
+ KnownClass::Bytearray.to_instance(db),
+ KnownClass::DefaultDict.to_instance(db),
+ SpecialFormType::ChainMap.instance_fallback(db),
+ SpecialFormType::Counter.instance_fallback(db),
+ SpecialFormType::Deque.instance_fallback(db),
+ SpecialFormType::OrderedDict.instance_fallback(db),
+ SpecialFormType::TypedDict.instance_fallback(db),
+ ];
+
+ safe_mutable_classes.iter().any(|safe_mutable_class| {
+ value_ty.is_equivalent_to(db, *safe_mutable_class)
+ || value_ty
+ .generic_origin(db)
+ .zip(safe_mutable_class.generic_origin(db))
+ .is_some_and(|(l, r)| l == r)
+ })
+ };
+
+ if !value_ty.is_typed_dict() && !is_safe_mutable_class() {
bound_ty = declared_ty;
}
}
@@ -8454,7 +8464,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ExprContext::Store => {
let value_ty = self.infer_expression(value);
let slice_ty = self.infer_expression(slice);
- self.infer_subscript_expression_types(value, value_ty, slice_ty, *ctx);
+ self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx);
Type::Never
}
ExprContext::Del => {
@@ -8464,7 +8474,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ExprContext::Invalid => {
let value_ty = self.infer_expression(value);
let slice_ty = self.infer_expression(slice);
- self.infer_subscript_expression_types(value, value_ty, slice_ty, *ctx);
+ self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx);
Type::unknown()
}
}
@@ -8493,7 +8503,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// Even if we can obtain the subscript type based on the assignments, we still perform default type inference
// (to store the expression type and to report errors).
let slice_ty = self.infer_expression(slice);
- self.infer_subscript_expression_types(value, value_ty, slice_ty, *ctx);
+ self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx);
return ty;
}
}
@@ -8532,7 +8542,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
let slice_ty = self.infer_expression(slice);
- let result_ty = self.infer_subscript_expression_types(value, value_ty, slice_ty, *ctx);
+ let result_ty = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx);
self.narrow_expr_with_applicable_constraints(subscript, result_ty, &constraint_keys)
}
@@ -8583,7 +8593,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_subscript_expression_types(
&self,
- value_node: &'ast ast::Expr,
+ subscript: &ast::ExprSubscript,
value_ty: Type<'db>,
slice_ty: Type<'db>,
expr_context: ExprContext,
@@ -8591,12 +8601,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let db = self.db();
let context = &self.context;
+ let value_node = subscript.value.as_ref();
+
let inferred = match (value_ty, slice_ty) {
(Type::NominalInstance(instance), _)
if instance.class.is_known(db, KnownClass::VersionInfo) =>
{
Some(self.infer_subscript_expression_types(
- value_node,
+ subscript,
Type::version_info_tuple(db),
slice_ty,
expr_context,
@@ -8604,7 +8616,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
(Type::Union(union), _) => Some(union.map(db, |element| {
- self.infer_subscript_expression_types(value_node, *element, slice_ty, expr_context)
+ self.infer_subscript_expression_types(subscript, *element, slice_ty, expr_context)
})),
// TODO: we can map over the intersection and fold the results back into an intersection,
@@ -8735,7 +8747,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::Tuple(_) | Type::StringLiteral(_) | Type::BytesLiteral(_),
Type::BooleanLiteral(bool),
) => Some(self.infer_subscript_expression_types(
- value_node,
+ subscript,
value_ty,
Type::IntLiteral(i64::from(bool)),
expr_context,
@@ -8830,7 +8842,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
//
// See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem
match value_ty.try_call_dunder(db, "__getitem__", CallArguments::positional([slice_ty])) {
- Ok(outcome) => return outcome.return_type(db),
+ Ok(outcome) => {
+ return outcome.return_type(db);
+ }
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
if let Some(builder) =
context.report_lint(&POSSIBLY_UNBOUND_IMPLICIT_CALL, value_node)
@@ -8856,16 +8870,61 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
CallErrorKind::BindingError => {
- if let Some(builder) =
- context.report_lint(&INVALID_ARGUMENT_TYPE, value_node)
- {
- builder.into_diagnostic(format_args!(
- "Method `__getitem__` of type `{}` cannot be called with key of \
- type `{}` on object of type `{}`",
- bindings.callable_type().display(db),
- slice_ty.display(db),
- value_ty.display(db),
- ));
+ if let Some(typed_dict) = value_ty.into_typed_dict() {
+ let slice_node = subscript.slice.as_ref();
+
+ if let Some(builder) = context.report_lint(&INVALID_KEY, slice_node) {
+ match slice_ty {
+ Type::StringLiteral(key) => {
+ let key = key.value(db);
+ let typed_dict_name = value_ty.display(db);
+
+ let mut diagnostic = builder.into_diagnostic(format_args!(
+ "Invalid key access on TypedDict `{typed_dict_name}`",
+ ));
+
+ diagnostic.annotate(
+ self.context.secondary(value_node).message(
+ format_args!("TypedDict `{typed_dict_name}`"),
+ ),
+ );
+
+ let items = typed_dict.items(db);
+ let existing_keys =
+ items.iter().map(|(name, _)| name.as_str());
+
+ diagnostic.set_primary_message(format!(
+ "Unknown key \"{key}\"{hint}",
+ hint = if let Some(suggestion) =
+ did_you_mean(existing_keys, key)
+ {
+ format!(" - did you mean \"{suggestion}\"?")
+ } else {
+ String::new()
+ }
+ ));
+
+ diagnostic
+ }
+ _ => builder.into_diagnostic(format_args!(
+ "TypedDict `{}` cannot be indexed with a key of type `{}`",
+ value_ty.display(db),
+ slice_ty.display(db),
+ )),
+ };
+ }
+ } else {
+ if let Some(builder) =
+ context.report_lint(&INVALID_ARGUMENT_TYPE, value_node)
+ {
+ builder.into_diagnostic(format_args!(
+ "Method `__getitem__` of type `{}` cannot be called with key of \
+ type `{}` on object of type `{}`",
+ bindings.callable_type().display(db),
+ slice_ty.display(db),
+ value_ty.display(db),
+ ));
+ }
}
}
CallErrorKind::PossiblyNotCallable => {
diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs
index 2df9bafc9e..736e6ca4a8 100644
--- a/crates/ty_python_semantic/src/types/instance.rs
+++ b/crates/ty_python_semantic/src/types/instance.rs
@@ -9,7 +9,9 @@ use crate::types::cyclic::PairVisitor;
use crate::types::enums::is_single_member_enum;
use crate::types::protocol_class::walk_protocol_interface;
use crate::types::tuple::TupleType;
-use crate::types::{DynamicType, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance};
+use crate::types::{
+ DynamicType, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, TypedDictType,
+};
use crate::{Db, FxOrderSet};
pub(super) use synthesized_protocol::SynthesizedProtocolType;
@@ -24,10 +26,16 @@ impl<'db> Type<'db> {
(ClassType::Generic(alias), Some(KnownClass::Tuple)) => {
Self::tuple(TupleType::new(db, alias.specialization(db).tuple(db)))
}
- _ if class.class_literal(db).0.is_protocol(db) => {
- Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
+ _ => {
+ let class_literal = class.class_literal(db).0;
+ if class_literal.is_protocol(db) {
+ Self::ProtocolInstance(ProtocolInstanceType::from_class(class))
+ } else if class_literal.is_typed_dict(db) {
+ TypedDictType::from(db, class)
+ } else {
+ Self::NominalInstance(NominalInstanceType::from_class(class))
+ }
}
- _ => Self::NominalInstance(NominalInstanceType::from_class(class)),
}
}
diff --git a/ty.schema.json b/ty.schema.json
index d91025b7f6..777a4d8afd 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -521,6 +521,16 @@
}
]
},
+ "invalid-key": {
+ "title": "detects invalid subscript accesses",
+ "description": "## What it does\nChecks for subscript accesses with invalid keys.\n\n## Why is this bad?\nUsing an invalid key will raise a `KeyError` at runtime.\n\n## Examples\n```python\nfrom typing import TypedDict\n\nclass Person(TypedDict):\n name: str\n age: int\n\nalice = Person(name=\"Alice\", age=30)\nalice[\"height\"] # KeyError: 'height'\n```",
+ "default": "error",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"invalid-legacy-type-variable": {
"title": "detects invalid legacy type variables",
"description": "## What it does\nChecks for the creation of invalid legacy `TypeVar`s\n\n## Why is this bad?\nThere are several requirements that you must follow when creating a legacy `TypeVar`.\n\n## Examples\n```python\nfrom typing import TypeVar\n\nT = TypeVar(\"T\") # okay\nQ = TypeVar(\"S\") # error: TypeVar name must match the variable it's assigned to\nT = TypeVar(\"T\") # error: TypeVars should not be redefined\n\n# error: TypeVar must be immediately assigned to a variable\ndef f(t: TypeVar(\"U\")): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)",