From 4887bdf20581f815c2bfa3edc2d93c4e92e6d03b Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 6 Aug 2025 09:36:33 +0200 Subject: [PATCH] [ty] Infer types for key-based access on TypedDicts (#19763) ## Summary This PR adds type inference for key-based access on `TypedDict`s and a new diagnostic for invalid subscript accesses: ```py class Person(TypedDict): name: str age: int | None alice = Person(name="Alice", age=25) reveal_type(alice["name"]) # revealed: str reveal_type(alice["age"]) # revealed: int | None alice["naem"] # Unknown key "naem" - did you mean "name"? ``` ## Test Plan Updated Markdown tests --- Cargo.lock | 1 + crates/ty/docs/rules.md | 147 +++++++++------ crates/ty_python_semantic/Cargo.toml | 1 + .../resources/mdtest/narrow/assignment.md | 3 +- ...ict`_-_Diagnostics_(e5289abf5c570c29).snap | 77 ++++++++ .../resources/mdtest/typed_dict.md | 67 +++++++ crates/ty_python_semantic/src/lib.rs | 1 + crates/ty_python_semantic/src/types.rs | 36 +++- crates/ty_python_semantic/src/types/class.rs | 61 +++--- .../src/types/diagnostic.rs | 52 ++++++ crates/ty_python_semantic/src/types/infer.rs | 175 ++++++++++++------ .../ty_python_semantic/src/types/instance.rs | 16 +- ty.schema.json | 10 + 13 files changed, 489 insertions(+), 158 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap 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)",