Skip to content

Commit dab3d4e

Browse files
authored
[ty] Improve invalid-argument-type diagnostics where a union type was provided (#21044)
1 parent 0169551 commit dab3d4e

File tree

3 files changed

+189
-0
lines changed

3 files changed

+189
-0
lines changed

crates/ty_python_semantic/resources/mdtest/call/function.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,30 @@ def f(x: int = 1, y: str = "foo") -> int:
820820
reveal_type(f(y=2, x="bar")) # revealed: int
821821
```
822822

823+
### Diagnostics for union types where the union is not assignable
824+
825+
<!-- snapshot-diagnostics -->
826+
827+
```py
828+
from typing import Sized
829+
830+
class Foo: ...
831+
class Bar: ...
832+
class Baz: ...
833+
834+
def f(x: Sized): ...
835+
def g(
836+
a: str | Foo,
837+
b: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo,
838+
c: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar,
839+
d: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar | Baz,
840+
):
841+
f(a) # error: [invalid-argument-type]
842+
f(b) # error: [invalid-argument-type]
843+
f(c) # error: [invalid-argument-type]
844+
f(d) # error: [invalid-argument-type]
845+
```
846+
823847
## Too many positional arguments
824848

825849
### One too many
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
---
6+
mdtest name: function.md - Call expression - Wrong argument type - Diagnostics for union types where the union is not assignable
7+
mdtest path: crates/ty_python_semantic/resources/mdtest/call/function.md
8+
---
9+
10+
# Python source files
11+
12+
## mdtest_snippet.py
13+
14+
```
15+
1 | from typing import Sized
16+
2 |
17+
3 | class Foo: ...
18+
4 | class Bar: ...
19+
5 | class Baz: ...
20+
6 |
21+
7 | def f(x: Sized): ...
22+
8 | def g(
23+
9 | a: str | Foo,
24+
10 | b: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo,
25+
11 | c: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar,
26+
12 | d: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar | Baz,
27+
13 | ):
28+
14 | f(a) # error: [invalid-argument-type]
29+
15 | f(b) # error: [invalid-argument-type]
30+
16 | f(c) # error: [invalid-argument-type]
31+
17 | f(d) # error: [invalid-argument-type]
32+
```
33+
34+
# Diagnostics
35+
36+
```
37+
error[invalid-argument-type]: Argument to function `f` is incorrect
38+
--> src/mdtest_snippet.py:14:7
39+
|
40+
12 | d: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar | Baz,
41+
13 | ):
42+
14 | f(a) # error: [invalid-argument-type]
43+
| ^ Expected `Sized`, found `str | Foo`
44+
15 | f(b) # error: [invalid-argument-type]
45+
16 | f(c) # error: [invalid-argument-type]
46+
|
47+
info: Element `Foo` of this union is not assignable to `Sized`
48+
info: Function defined here
49+
--> src/mdtest_snippet.py:7:5
50+
|
51+
5 | class Baz: ...
52+
6 |
53+
7 | def f(x: Sized): ...
54+
| ^ -------- Parameter declared here
55+
8 | def g(
56+
9 | a: str | Foo,
57+
|
58+
info: rule `invalid-argument-type` is enabled by default
59+
60+
```
61+
62+
```
63+
error[invalid-argument-type]: Argument to function `f` is incorrect
64+
--> src/mdtest_snippet.py:15:7
65+
|
66+
13 | ):
67+
14 | f(a) # error: [invalid-argument-type]
68+
15 | f(b) # error: [invalid-argument-type]
69+
| ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 5 union elements`
70+
16 | f(c) # error: [invalid-argument-type]
71+
17 | f(d) # error: [invalid-argument-type]
72+
|
73+
info: Element `Foo` of this union is not assignable to `Sized`
74+
info: Function defined here
75+
--> src/mdtest_snippet.py:7:5
76+
|
77+
5 | class Baz: ...
78+
6 |
79+
7 | def f(x: Sized): ...
80+
| ^ -------- Parameter declared here
81+
8 | def g(
82+
9 | a: str | Foo,
83+
|
84+
info: rule `invalid-argument-type` is enabled by default
85+
86+
```
87+
88+
```
89+
error[invalid-argument-type]: Argument to function `f` is incorrect
90+
--> src/mdtest_snippet.py:16:7
91+
|
92+
14 | f(a) # error: [invalid-argument-type]
93+
15 | f(b) # error: [invalid-argument-type]
94+
16 | f(c) # error: [invalid-argument-type]
95+
| ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 6 union elements`
96+
17 | f(d) # error: [invalid-argument-type]
97+
|
98+
info: Union elements `Foo` and `Bar` are not assignable to `Sized`
99+
info: Function defined here
100+
--> src/mdtest_snippet.py:7:5
101+
|
102+
5 | class Baz: ...
103+
6 |
104+
7 | def f(x: Sized): ...
105+
| ^ -------- Parameter declared here
106+
8 | def g(
107+
9 | a: str | Foo,
108+
|
109+
info: rule `invalid-argument-type` is enabled by default
110+
111+
```
112+
113+
```
114+
error[invalid-argument-type]: Argument to function `f` is incorrect
115+
--> src/mdtest_snippet.py:17:7
116+
|
117+
15 | f(b) # error: [invalid-argument-type]
118+
16 | f(c) # error: [invalid-argument-type]
119+
17 | f(d) # error: [invalid-argument-type]
120+
| ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 7 union elements`
121+
|
122+
info: Union element `Foo`, and 2 more union elements, are not assignable to `Sized`
123+
info: Function defined here
124+
--> src/mdtest_snippet.py:7:5
125+
|
126+
5 | class Baz: ...
127+
6 |
128+
7 | def f(x: Sized): ...
129+
| ^ -------- Parameter declared here
130+
8 | def g(
131+
9 | a: str | Foo,
132+
|
133+
info: rule `invalid-argument-type` is enabled by default
134+
135+
```

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3519,6 +3519,36 @@ impl<'db> BindingError<'db> {
35193519
"Expected `{expected_ty_display}`, found `{provided_ty_display}`"
35203520
));
35213521

3522+
if let Type::Union(union) = provided_ty {
3523+
let union_elements = union.elements(context.db());
3524+
let invalid_elements: Vec<Type<'db>> = union
3525+
.elements(context.db())
3526+
.iter()
3527+
.filter(|element| !element.is_assignable_to(context.db(), *expected_ty))
3528+
.copied()
3529+
.collect();
3530+
let first_invalid_element = invalid_elements[0].display(context.db());
3531+
if invalid_elements.len() < union_elements.len() {
3532+
match &invalid_elements[1..] {
3533+
[] => diag.info(format_args!(
3534+
"Element `{first_invalid_element}` of this union \
3535+
is not assignable to `{expected_ty_display}`",
3536+
)),
3537+
[single] => diag.info(format_args!(
3538+
"Union elements `{first_invalid_element}` and `{}` \
3539+
are not assignable to `{expected_ty_display}`",
3540+
single.display(context.db()),
3541+
)),
3542+
rest => diag.info(format_args!(
3543+
"Union element `{first_invalid_element}`, \
3544+
and {} more union elements, \
3545+
are not assignable to `{expected_ty_display}`",
3546+
rest.len(),
3547+
)),
3548+
}
3549+
}
3550+
}
3551+
35223552
if let Some(matching_overload) = matching_overload {
35233553
if let Some((name_span, parameter_span)) =
35243554
matching_overload.get(context.db()).and_then(|overload| {

0 commit comments

Comments
 (0)