[ty] Support __setitem__ and improve __getitem__ related diagnostics (#19578)

## Summary

Adds validation to subscript assignment expressions.

```py
class Foo: ...

class Bar:
    __setattr__ = None

class Baz:
    def __setitem__(self, index: str, value: int) -> None:
        pass

# We now emit a diagnostic on these statements
Foo()[1] = 2
Bar()[1] = 2
Baz()[1] = 2

```

Also improves error messages on invalid `__getitem__` expressions

## Test Plan

Update mdtests and add more to `subscript/instance.md`

---------

Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
Matthew Mckee 2025-08-01 08:23:27 +01:00 committed by GitHub
parent 5c5d50d57a
commit b30d97e5e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 196 additions and 17 deletions

View file

@ -181,7 +181,7 @@ def f(l: list[int]):
# but if it was greater than that, it will not be an error.
reveal_type(l[0]) # revealed: int
# error: [call-non-callable]
# error: [invalid-argument-type]
del l["string"]
l[0] = 1

View file

@ -253,6 +253,7 @@ does["not"]["exist"] = 0
reveal_type(does["not"]["exist"]) # revealed: Unknown
non_subscriptable = 1
# error: [invalid-assignment]
non_subscriptable[0] = 0
# error: [non-subscriptable]
reveal_type(non_subscriptable[0]) # revealed: Unknown
@ -317,7 +318,7 @@ def f(c: C, s: str):
reveal_type(c.x) # revealed: int | None
s = c.x # error: [invalid-assignment]
# TODO: This assignment is invalid and should result in an error.
# error: [invalid-assignment] "Method `__setitem__` of type `Overload[(key: SupportsIndex, value: int, /) -> None, (key: slice[Any, Any, Any], value: Iterable[int], /) -> None]` cannot be called with a key of type `Literal[0]` and a value of type `str` on object of type `list[int]`"
c.l[0] = s
reveal_type(c.l[0]) # revealed: int
```

View file

@ -14,7 +14,7 @@ a = NotSubscriptable()[0] # error: "Cannot subscript object of type `NotSubscri
class NotSubscriptable:
__getitem__ = None
# error: "Method `__getitem__` of type `Unknown | None` is not callable on object of type `NotSubscriptable`"
# error: "Method `__getitem__` of type `Unknown | None` is possibly not callable on object of type `NotSubscriptable`"
a = NotSubscriptable()[0]
```
@ -43,6 +43,18 @@ def _(flag: bool):
reveal_type(Identity()[0]) # revealed: int | str
```
## `__getitem__` with invalid index argument
```py
class Identity:
def __getitem__(self, index: int) -> int:
return index
a = Identity()
# error: [invalid-argument-type] "Method `__getitem__` of type `bound method Identity.__getitem__(index: int) -> int` cannot be called with key of type `Literal["a"]` on object of type `Identity`"
a["a"]
```
## `__setitem__` with no `__getitem__`
```py
@ -53,3 +65,45 @@ class NoGetitem:
a = NoGetitem()
a[0] = 0
```
## Subscript store with no `__setitem__`
```py
class NoSetitem: ...
a = NoSetitem()
a[0] = 0 # error: "Cannot assign to object of type `NoSetitem` with no `__setitem__` method"
```
## `__setitem__` not callable
```py
class NoSetitem:
__setitem__ = None
a = NoSetitem()
a[0] = 0 # error: "Method `__setitem__` of type `Unknown | None` is possibly not callable on object of type `NoSetitem`"
```
## Valid `__setitem__` method
```py
class Identity:
def __setitem__(self, index: int, value: int) -> None:
pass
a = Identity()
a[0] = 0
```
## `__setitem__` with invalid index argument
```py
class Identity:
def __setitem__(self, index: int, value: int) -> None:
pass
a = Identity()
# error: [invalid-assignment] "Method `__setitem__` of type `bound method Identity.__setitem__(index: int, value: int) -> None` cannot be called with a key of type `Literal["a"]` and a value of type `Literal[0]` on object of type `Identity`"
a["a"] = 0
```

View file

@ -17,7 +17,7 @@ reveal_type(x[0]) # revealed: Unknown
# TODO reveal list[int]
reveal_type(x[0:1]) # revealed: list[Unknown]
# error: [call-non-callable]
# error: [invalid-argument-type]
reveal_type(x["a"]) # revealed: Unknown
```
@ -29,11 +29,9 @@ In assignment, we might also have a named assignment. This should also get type
x = [1, 2, 3]
x[0 if (y := 2) else 1] = 5
# TODO: better error than "method `__getitem__` not callable on type `list`"
# error: [call-non-callable]
# error: [invalid-assignment]
x["a" if (y := 2) else 1] = 6
# TODO: better error than "method `__getitem__` not callable on type `list`"
# error: [call-non-callable]
# error: [invalid-assignment]
x["a" if (y := 2) else "b"] = 6
```