Skip to content

Commit 90fa4fd

Browse files
authored
[ty] Add snapshot tests for advanced invalid-assignment scenarios (#23581)
## Summary Add some basic scenarios for `invalid-assignment` diagnostics that we plan to improve in the near future. The snapshots don't really need to be reviewed (I skimmed them). They only document the status quo.
1 parent 13b983c commit 90fa4fd

17 files changed

Lines changed: 914 additions & 10 deletions

File tree

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# Invalid assignment diagnostics
2+
3+
<!-- snapshot-diagnostics -->
4+
5+
```toml
6+
[environment]
7+
python-version = "3.12"
8+
```
9+
10+
This file contains various scenarios of `invalid-assignment` (and related) diagnostics where we
11+
(attempt to) do better than just report "type X is not assignable to type Y".
12+
13+
## Basic
14+
15+
Mainly for comparison: this is the most basic kind of `invalid-assignment` diagnostic:
16+
17+
```py
18+
def _(source: str):
19+
target: bytes = source # error: [invalid-assignment]
20+
```
21+
22+
## Unions
23+
24+
Assigning a union to a non-union:
25+
26+
```py
27+
def _(source: str | None):
28+
target: str = source # error: [invalid-assignment]
29+
```
30+
31+
Assigning a non-union to a union:
32+
33+
```py
34+
def _(source: int):
35+
target: str | None = source # error: [invalid-assignment]
36+
```
37+
38+
Assigning a union to a union:
39+
40+
```py
41+
def _(source: str | None):
42+
target: bytes | None = source # error: [invalid-assignment]
43+
```
44+
45+
## Tuples
46+
47+
Wrong element types:
48+
49+
```py
50+
def _(source: tuple[int, str, bool]):
51+
target: tuple[int, bytes, bool] = source # error: [invalid-assignment]
52+
```
53+
54+
Wrong number of elements:
55+
56+
```py
57+
def _(source: tuple[int, str]):
58+
target: tuple[int, str, bool] = source # error: [invalid-assignment]
59+
```
60+
61+
## `Callable`
62+
63+
Assigning a function to a `Callable`
64+
65+
```py
66+
from typing import Any, Callable
67+
68+
def source(x: int, y: str) -> None:
69+
raise NotImplementedError
70+
71+
target: Callable[[int, bytes], bool] = source # error: [invalid-assignment]
72+
```
73+
74+
Assigning a `Callable` to a `Callable` with wrong parameter type:
75+
76+
```py
77+
def _(source: Callable[[int, str], bool]):
78+
target: Callable[[int, bytes], bool] = source # error: [invalid-assignment]
79+
```
80+
81+
Assigning a `Callable` to a `Callable` with wrong return type:
82+
83+
```py
84+
def _(source: Callable[[int, bytes], None]):
85+
target: Callable[[int, bytes], bool] = source # error: [invalid-assignment]
86+
```
87+
88+
Assigning a `Callable` to a `Callable` with wrong number of parameters:
89+
90+
```py
91+
def _(source: Callable[[int, str], bool]):
92+
target: Callable[[int], bool] = source # error: [invalid-assignment]
93+
```
94+
95+
Assigning a class to a `Callable`
96+
97+
```py
98+
class Number:
99+
def __init__(self, value: int): ...
100+
101+
target: Callable[[str], Any] = Number # error: [invalid-assignment]
102+
```
103+
104+
## Function assignability and overrides
105+
106+
Liskov checks use function-to-function assignability.
107+
108+
Wrong parameter type:
109+
110+
```py
111+
class Parent:
112+
def method(self, x: str) -> bool:
113+
raise NotImplementedError
114+
115+
class Child1(Parent):
116+
# error: [invalid-method-override]
117+
def method(self, x: bytes) -> bool:
118+
raise NotImplementedError
119+
```
120+
121+
Wrong return type:
122+
123+
```py
124+
class Child2(Parent):
125+
# error: [invalid-method-override]
126+
def method(self, x: str) -> None:
127+
raise NotImplementedError
128+
```
129+
130+
Wrong non-positional-only parameter name:
131+
132+
```py
133+
class Child3(Parent):
134+
# error: [invalid-method-override]
135+
def method(self, y: str):
136+
raise NotImplementedError
137+
```
138+
139+
## `TypedDict`
140+
141+
Incompatible field types:
142+
143+
```py
144+
from typing import Any, TypedDict
145+
146+
class Person(TypedDict):
147+
name: str
148+
149+
class Other(TypedDict):
150+
name: bytes
151+
152+
def _(source: Person):
153+
target: Other = source # error: [invalid-assignment]
154+
```
155+
156+
Missing required fields:
157+
158+
```py
159+
class PersonWithAge(TypedDict):
160+
name: str
161+
age: int
162+
163+
def _(source: Person):
164+
target: PersonWithAge = source # error: [invalid-assignment]
165+
```
166+
167+
Assigning a `TypedDict` to a `dict`
168+
169+
```py
170+
class Person(TypedDict):
171+
name: str
172+
173+
def _(source: Person):
174+
target: dict[str, Any] = source # error: [invalid-assignment]
175+
```
176+
177+
## Protocols
178+
179+
Missing protocol members:
180+
181+
```py
182+
from typing import Protocol
183+
184+
class SupportsCheck(Protocol):
185+
def check(self, x: int, y: str) -> bool: ...
186+
187+
class DoesNotHaveCheck: ...
188+
189+
def _(source: DoesNotHaveCheck):
190+
target: SupportsCheck = source # error: [invalid-assignment]
191+
```
192+
193+
Incompatible types for protocol members:
194+
195+
```py
196+
class CheckWithWrongSignature:
197+
def check(self, x: int, y: bytes) -> bool:
198+
return False
199+
200+
def _(source: CheckWithWrongSignature):
201+
target: SupportsCheck = source # error: [invalid-assignment]
202+
```
203+
204+
## Type aliases
205+
206+
Type aliases should be expanded in diagnostics to understand the underlying incompatibilities:
207+
208+
```py
209+
from typing import Protocol
210+
211+
class SupportsName(Protocol):
212+
def name(self) -> str: ...
213+
214+
class HasName:
215+
def name(self) -> bytes:
216+
return b""
217+
218+
type StringOrName = str | SupportsName
219+
220+
def _(source: HasName):
221+
target: SupportsName = source # error: [invalid-assignment]
222+
```
223+
224+
## Deeply nested incompatibilities
225+
226+
```py
227+
from typing import Callable
228+
229+
def source(x: tuple[int, str]) -> bool:
230+
return False
231+
232+
target: Callable[[tuple[int, bytes]], bool] = source # error: [invalid-assignment]
233+
```
234+
235+
## Multiple nested incompatibilities
236+
237+
```py
238+
from typing import Protocol
239+
240+
class SupportsCheck(Protocol):
241+
def check1(self, x: str): ...
242+
def check2(self, x: int) -> bool: ...
243+
244+
class Incompatible:
245+
def check1(self, x: bytes): ...
246+
def check2(self, x: int) -> None: ...
247+
248+
def _(source: Incompatible):
249+
target: SupportsCheck = source # error: [invalid-assignment]
250+
```

crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md renamed to crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md

File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
6+
---
7+
mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Basic
8+
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md
9+
---
10+
11+
# Python source files
12+
13+
## mdtest_snippet.py
14+
15+
```
16+
1 | def _(source: str):
17+
2 | target: bytes = source # error: [invalid-assignment]
18+
```
19+
20+
# Diagnostics
21+
22+
```
23+
error[invalid-assignment]: Object of type `str` is not assignable to `bytes`
24+
--> src/mdtest_snippet.py:2:13
25+
|
26+
1 | def _(source: str):
27+
2 | target: bytes = source # error: [invalid-assignment]
28+
| ----- ^^^^^^ Incompatible value of type `str`
29+
| |
30+
| Declared type
31+
|
32+
info: rule `invalid-assignment` is enabled by default
33+
34+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
6+
---
7+
mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Deeply nested incompatibilities
8+
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md
9+
---
10+
11+
# Python source files
12+
13+
## mdtest_snippet.py
14+
15+
```
16+
1 | from typing import Callable
17+
2 |
18+
3 | def source(x: tuple[int, str]) -> bool:
19+
4 | return False
20+
5 |
21+
6 | target: Callable[[tuple[int, bytes]], bool] = source # error: [invalid-assignment]
22+
```
23+
24+
# Diagnostics
25+
26+
```
27+
error[invalid-assignment]: Object of type `def source(x: tuple[int, str]) -> bool` is not assignable to `(tuple[int, bytes], /) -> bool`
28+
--> src/mdtest_snippet.py:6:9
29+
|
30+
4 | return False
31+
5 |
32+
6 | target: Callable[[tuple[int, bytes]], bool] = source # error: [invalid-assignment]
33+
| ----------------------------------- ^^^^^^ Incompatible value of type `def source(x: tuple[int, str]) -> bool`
34+
| |
35+
| Declared type
36+
|
37+
info: rule `invalid-assignment` is enabled by default
38+
39+
```

0 commit comments

Comments
 (0)