[ty] Baseline for subscript assignment diagnostics (#21404)

## Summary

Add (snapshot) tests for subscript assignment diagnostics. This is
mainly intended to establish a baseline before I hope to improve some of
these messages.
This commit is contained in:
David Peter 2025-11-12 15:29:26 +01:00 committed by GitHub
parent e8e8180888
commit 84c3cecad6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 549 additions and 1 deletions

View File

@ -0,0 +1,31 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Invalid key type
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | config: dict[str, int] = {}
2 | config[0] = 3 # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Method `__setitem__` of type `bound method dict[str, int].__setitem__(key: str, value: int, /) -> None` cannot be called with a key of type `Literal[0]` and a value of type `Literal[3]` on object of type `dict[str, int]`
--> src/mdtest_snippet.py:2:1
|
1 | config: dict[str, int] = {}
2 | config[0] = 3 # error: [invalid-assignment]
| ^^^^^^
|
info: rule `invalid-assignment` is enabled by default
```

View File

@ -0,0 +1,36 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Invalid key type for `TypedDict`
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypedDict
2 |
3 | class Config(TypedDict):
4 | retries: int
5 |
6 | def _(config: Config) -> None:
7 | config[0] = 3 # error: [invalid-key]
```
# Diagnostics
```
error[invalid-key]: Cannot access `Config` with a key of type `Literal[0]`. Only string literals are allowed as keys on TypedDicts.
--> src/mdtest_snippet.py:7:12
|
6 | def _(config: Config) -> None:
7 | config[0] = 3 # error: [invalid-key]
| ^
|
info: rule `invalid-key` is enabled by default
```

View File

@ -0,0 +1,31 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Invalid value type
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | config: dict[str, int] = {}
2 | config["retries"] = "three" # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Method `__setitem__` of type `bound method dict[str, int].__setitem__(key: str, value: int, /) -> None` cannot be called with a key of type `Literal["retries"]` and a value of type `Literal["three"]` on object of type `dict[str, int]`
--> src/mdtest_snippet.py:2:1
|
1 | config: dict[str, int] = {}
2 | config["retries"] = "three" # error: [invalid-assignment]
| ^^^^^^
|
info: rule `invalid-assignment` is enabled by default
```

View File

@ -0,0 +1,48 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Invalid value type for `TypedDict`
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypedDict
2 |
3 | class Config(TypedDict):
4 | retries: int
5 |
6 | def _(config: Config) -> None:
7 | config["retries"] = "three" # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Invalid assignment to key "retries" with declared type `int` on TypedDict `Config`
--> src/mdtest_snippet.py:7:5
|
6 | def _(config: Config) -> None:
7 | config["retries"] = "three" # error: [invalid-assignment]
| ------ --------- ^^^^^^^ value of type `Literal["three"]`
| | |
| | key has declared type `int`
| TypedDict `Config`
|
info: Item declaration
--> src/mdtest_snippet.py:4:5
|
3 | class Config(TypedDict):
4 | retries: int
| ------------ Item declared here
5 |
6 | def _(config: Config) -> None:
|
info: rule `invalid-assignment` is enabled by default
```

View File

@ -0,0 +1,38 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Misspelled key for `TypedDict`
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypedDict
2 |
3 | class Config(TypedDict):
4 | retries: int
5 |
6 | def _(config: Config) -> None:
7 | config["Retries"] = 30.0 # error: [invalid-key]
```
# Diagnostics
```
error[invalid-key]: Invalid key for TypedDict `Config`
--> src/mdtest_snippet.py:7:5
|
6 | def _(config: Config) -> None:
7 | config["Retries"] = 30.0 # error: [invalid-key]
| ------ ^^^^^^^^^ Unknown key "Retries" - did you mean "retries"?
| |
| TypedDict `Config`
|
info: rule `invalid-key` is enabled by default
```

View File

@ -0,0 +1,35 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - No `__setitem__` method
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | class ReadOnlyDict:
2 | def __getitem__(self, key: str) -> int:
3 | return 42
4 |
5 | config = ReadOnlyDict()
6 | config["retries"] = 3 # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Cannot assign to object of type `ReadOnlyDict` with no `__setitem__` method
--> src/mdtest_snippet.py:6:1
|
5 | config = ReadOnlyDict()
6 | config["retries"] = 3 # error: [invalid-assignment]
| ^^^^^^
|
info: rule `invalid-assignment` is enabled by default
```

View File

@ -0,0 +1,31 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Possibly missing `__setitem__` method
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | def _(config: dict[str, int] | None) -> None:
2 | config["retries"] = 3 # error: [possibly-missing-implicit-call]
```
# Diagnostics
```
warning[possibly-missing-implicit-call]: Method `__setitem__` of type `dict[str, int] | None` may be missing
--> src/mdtest_snippet.py:2:5
|
1 | def _(config: dict[str, int] | None) -> None:
2 | config["retries"] = 3 # error: [possibly-missing-implicit-call]
| ^^^^^^
|
info: rule `possibly-missing-implicit-call` is enabled by default
```

View File

@ -0,0 +1,40 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Unknown key for all elemens of a union
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypedDict
2 |
3 | class Person(TypedDict):
4 | name: str
5 |
6 | class Animal(TypedDict):
7 | name: str
8 | legs: int
9 |
10 | def _(being: Person | Animal) -> None:
11 | being["surname"] = "unknown" # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Method `__setitem__` of type `(key: Literal["name"], value: str, /) -> None` cannot be called with a key of type `Literal["surname"]` and a value of type `Literal["unknown"]` on object of type `Person | Animal`
--> src/mdtest_snippet.py:11:5
|
10 | def _(being: Person | Animal) -> None:
11 | being["surname"] = "unknown" # error: [invalid-assignment]
| ^^^^^
|
info: rule `invalid-assignment` is enabled by default
```

View File

@ -0,0 +1,40 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Unknown key for one element of a union
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypedDict
2 |
3 | class Person(TypedDict):
4 | name: str
5 |
6 | class Animal(TypedDict):
7 | name: str
8 | legs: int
9 |
10 | def _(being: Person | Animal) -> None:
11 | being["legs"] = 4 # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Method `__setitem__` of type `(key: Literal["name"], value: str, /) -> None` cannot be called with a key of type `Literal["legs"]` and a value of type `Literal[4]` on object of type `Person | Animal`
--> src/mdtest_snippet.py:11:5
|
10 | def _(being: Person | Animal) -> None:
11 | being["legs"] = 4 # error: [invalid-assignment]
| ^^^^^
|
info: rule `invalid-assignment` is enabled by default
```

View File

@ -0,0 +1,31 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Wrong value type for one element of a union
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | def _(config: dict[str, int] | dict[str, str]) -> None:
2 | config["retries"] = 3 # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Method `__setitem__` of type `(bound method dict[str, int].__setitem__(key: str, value: int, /) -> None) | (bound method dict[str, str].__setitem__(key: str, value: str, /) -> None)` cannot be called with a key of type `Literal["retries"]` and a value of type `Literal[3]` on object of type `dict[str, int] | dict[str, str]`
--> src/mdtest_snippet.py:2:5
|
1 | def _(config: dict[str, int] | dict[str, str]) -> None:
2 | config["retries"] = 3 # error: [invalid-assignment]
| ^^^^^^
|
info: rule `invalid-assignment` is enabled by default
```

View File

@ -0,0 +1,31 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Wrong value type for all elements of a union
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
# Python source files
## mdtest_snippet.py
```
1 | def _(config: dict[str, int] | dict[str, str]) -> None:
2 | config["retries"] = 3.0 # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Method `__setitem__` of type `(bound method dict[str, int].__setitem__(key: str, value: int, /) -> None) | (bound method dict[str, str].__setitem__(key: str, value: str, /) -> None)` cannot be called with a key of type `Literal["retries"]` and a value of type `float` on object of type `dict[str, int] | dict[str, str]`
--> src/mdtest_snippet.py:2:5
|
1 | def _(config: dict[str, int] | dict[str, str]) -> None:
2 | config["retries"] = 3.0 # error: [invalid-assignment]
| ^^^^^^
|
info: rule `invalid-assignment` is enabled by default
```

View File

@ -0,0 +1,117 @@
# Subscript assignment diagnostics
<!-- snapshot-diagnostics -->
## Invalid value type
```py
config: dict[str, int] = {}
config["retries"] = "three" # error: [invalid-assignment]
```
## Invalid key type
```py
config: dict[str, int] = {}
config[0] = 3 # error: [invalid-assignment]
```
## Invalid value type for `TypedDict`
```py
from typing import TypedDict
class Config(TypedDict):
retries: int
def _(config: Config) -> None:
config["retries"] = "three" # error: [invalid-assignment]
```
## Invalid key type for `TypedDict`
```py
from typing import TypedDict
class Config(TypedDict):
retries: int
def _(config: Config) -> None:
config[0] = 3 # error: [invalid-key]
```
## Misspelled key for `TypedDict`
```py
from typing import TypedDict
class Config(TypedDict):
retries: int
def _(config: Config) -> None:
config["Retries"] = 30.0 # error: [invalid-key]
```
## No `__setitem__` method
```py
class ReadOnlyDict:
def __getitem__(self, key: str) -> int:
return 42
config = ReadOnlyDict()
config["retries"] = 3 # error: [invalid-assignment]
```
## Possibly missing `__setitem__` method
```py
def _(config: dict[str, int] | None) -> None:
config["retries"] = 3 # error: [possibly-missing-implicit-call]
```
## Unknown key for one element of a union
```py
from typing import TypedDict
class Person(TypedDict):
name: str
class Animal(TypedDict):
name: str
legs: int
def _(being: Person | Animal) -> None:
being["legs"] = 4 # error: [invalid-assignment]
```
## Unknown key for all elemens of a union
```py
from typing import TypedDict
class Person(TypedDict):
name: str
class Animal(TypedDict):
name: str
legs: int
def _(being: Person | Animal) -> None:
being["surname"] = "unknown" # error: [invalid-assignment]
```
## Wrong value type for one element of a union
```py
def _(config: dict[str, int] | dict[str, str]) -> None:
config["retries"] = 3 # error: [invalid-assignment]
```
## Wrong value type for all elements of a union
```py
def _(config: dict[str, int] | dict[str, str]) -> None:
config["retries"] = 3.0 # error: [invalid-assignment]
```

View File

@ -526,10 +526,20 @@ class Person(TypedDict):
name: str
age: int | None
class Animal(TypedDict):
name: str
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:
def _(
person: Person,
being: Person | Animal,
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
@ -548,18 +558,30 @@ def _(person: Person, literal_key: Literal["age"], union_of_keys: Literal["age",
# No error here:
reveal_type(person[unknown_key]) # revealed: Unknown
reveal_type(being["name"]) # revealed: str
# TODO: A type of `int | None | Unknown` might be better here. The `str` is mixed in
# because `Animal.__getitem__` can only return `str`.
# error: [invalid-key] "Invalid key for TypedDict `Animal`"
reveal_type(being["age"]) # revealed: int | None | str
```
### Writing
```py
from typing_extensions import TypedDict, Final, Literal, LiteralString, Any
from ty_extensions import Intersection
class Person(TypedDict):
name: str
surname: str
age: int | None
class Animal(TypedDict):
name: str
legs: int
NAME_FINAL: Final = "name"
AGE_FINAL: Final[Literal["age"]] = "age"
@ -583,6 +605,23 @@ def _(person: Person, union_of_keys: Literal["name", "surname"]):
# error: [invalid-assignment] "Cannot assign value of type `Literal[1]` to key of type `Literal["name", "surname"]` on TypedDict `Person`"
person[union_of_keys] = 1
def _(being: Person | Animal):
being["name"] = "Being"
# error: [invalid-assignment] "Method `__setitem__` of type `(Overload[(key: Literal["name"], value: str, /) -> None, (key: Literal["surname"], value: str, /) -> None, (key: Literal["age"], value: int | None, /) -> None]) | (Overload[(key: Literal["name"], value: str, /) -> None, (key: Literal["legs"], value: int, /) -> None])` cannot be called with a key of type `Literal["name"]` and a value of type `Literal[1]` on object of type `Person | Animal`"
being["name"] = 1
# error: [invalid-assignment] "Method `__setitem__` of type `(Overload[(key: Literal["name"], value: str, /) -> None, (key: Literal["surname"], value: str, /) -> None, (key: Literal["age"], value: int | None, /) -> None]) | (Overload[(key: Literal["name"], value: str, /) -> None, (key: Literal["legs"], value: int, /) -> None])` cannot be called with a key of type `Literal["surname"]` and a value of type `Literal["unknown"]` on object of type `Person | Animal`"
being["surname"] = "unknown"
def _(centaur: Intersection[Person, Animal]):
centaur["name"] = "Chiron"
centaur["age"] = 100
centaur["legs"] = 4
# TODO: This should be an `invalid-key` error
centaur["unknown"] = "value"
def _(person: Person, union_of_keys: Literal["name", "age"], unknown_value: Any):
person[union_of_keys] = unknown_value