ty_python_semantic: improve failed overloaded function call

The diagnostic now includes a pointer to the implementation definition
along with each possible overload.

This doesn't include information about *why* each overload failed. But
given the emphasis on concise output (since there can be *many*
unmatched overloads), it's not totally clear how to include that
additional information.

Fixes #274
This commit is contained in:
Andrew Gallant 2025-05-13 12:37:14 -04:00 committed by Andrew Gallant
parent 451c5db7a3
commit faf54c0181
8 changed files with 921 additions and 79 deletions

View file

@ -16,3 +16,265 @@ def f(x: int | str) -> int | str:
f(b"foo") # error: [no-matching-overload]
```
## Call to function with many unmatched overloads
Note that it would be fine to use `pow` here as an example of a routine with many overloads, but at
time of writing (2025-05-14), ty doesn't support some of the type signatures of those overloads.
Which in turn makes snapshotting a bit annoying, since the output can depend on how ty is compiled
(because of how `Todo` types are dealt with when `debug_assertions` is enabled versus disabled).
```py
from typing import overload
class Foo: ...
@overload
def foo(a: int, b: int, c: int): ...
@overload
def foo(a: str, b: int, c: int): ...
@overload
def foo(a: int, b: str, c: int): ...
@overload
def foo(a: int, b: int, c: str): ...
@overload
def foo(a: str, b: str, c: int): ...
@overload
def foo(a: int, b: str, c: str): ...
@overload
def foo(a: str, b: str, c: str): ...
@overload
def foo(a: int, b: int, c: int): ...
@overload
def foo(a: float, b: int, c: int): ...
@overload
def foo(a: int, b: float, c: int): ...
@overload
def foo(a: int, b: int, c: float): ...
@overload
def foo(a: float, b: float, c: int): ...
@overload
def foo(a: int, b: float, c: float): ...
@overload
def foo(a: float, b: float, c: float): ...
@overload
def foo(a: str, b: str, c: str): ...
@overload
def foo(a: float, b: str, c: str): ...
@overload
def foo(a: str, b: float, c: str): ...
@overload
def foo(a: str, b: str, c: float): ...
@overload
def foo(a: float, b: float, c: str): ...
@overload
def foo(a: str, b: float, c: float): ...
@overload
def foo(a: float, b: float, c: float): ...
def foo(a, b, c): ...
foo(Foo(), Foo()) # error: [no-matching-overload]
```
## Call to function with too many unmatched overloads
This is like the above example, but has an excessive number of overloads to the point that ty will
cut off the list in the diagnostic and emit a message stating the number of omitted overloads.
```py
from typing import overload
class Foo: ...
@overload
def foo(a: int, b: int, c: int): ...
@overload
def foo(a: str, b: int, c: int): ...
@overload
def foo(a: int, b: str, c: int): ...
@overload
def foo(a: int, b: int, c: str): ...
@overload
def foo(a: str, b: str, c: int): ...
@overload
def foo(a: int, b: str, c: str): ...
@overload
def foo(a: str, b: str, c: str): ...
@overload
def foo(a: int, b: int, c: int): ...
@overload
def foo(a: float, b: int, c: int): ...
@overload
def foo(a: int, b: float, c: int): ...
@overload
def foo(a: int, b: int, c: float): ...
@overload
def foo(a: float, b: float, c: int): ...
@overload
def foo(a: int, b: float, c: float): ...
@overload
def foo(a: float, b: float, c: float): ...
@overload
def foo(a: str, b: str, c: str): ...
@overload
def foo(a: float, b: str, c: str): ...
@overload
def foo(a: str, b: float, c: str): ...
@overload
def foo(a: str, b: str, c: float): ...
@overload
def foo(a: float, b: float, c: str): ...
@overload
def foo(a: str, b: float, c: float): ...
@overload
def foo(a: float, b: float, c: float): ...
@overload
def foo(a: list[int], b: list[int], c: list[int]): ...
@overload
def foo(a: list[str], b: list[int], c: list[int]): ...
@overload
def foo(a: list[int], b: list[str], c: list[int]): ...
@overload
def foo(a: list[int], b: list[int], c: list[str]): ...
@overload
def foo(a: list[str], b: list[str], c: list[int]): ...
@overload
def foo(a: list[int], b: list[str], c: list[str]): ...
@overload
def foo(a: list[str], b: list[str], c: list[str]): ...
@overload
def foo(a: list[int], b: list[int], c: list[int]): ...
@overload
def foo(a: list[float], b: list[int], c: list[int]): ...
@overload
def foo(a: list[int], b: list[float], c: list[int]): ...
@overload
def foo(a: list[int], b: list[int], c: list[float]): ...
@overload
def foo(a: list[float], b: list[float], c: list[int]): ...
@overload
def foo(a: list[int], b: list[float], c: list[float]): ...
@overload
def foo(a: list[float], b: list[float], c: list[float]): ...
@overload
def foo(a: list[str], b: list[str], c: list[str]): ...
@overload
def foo(a: list[float], b: list[str], c: list[str]): ...
@overload
def foo(a: list[str], b: list[float], c: list[str]): ...
@overload
def foo(a: list[str], b: list[str], c: list[float]): ...
@overload
def foo(a: list[float], b: list[float], c: list[str]): ...
@overload
def foo(a: list[str], b: list[float], c: list[float]): ...
@overload
def foo(a: list[float], b: list[float], c: list[float]): ...
@overload
def foo(a: bool, b: bool, c: bool): ...
@overload
def foo(a: str, b: bool, c: bool): ...
@overload
def foo(a: bool, b: str, c: bool): ...
@overload
def foo(a: bool, b: bool, c: str): ...
@overload
def foo(a: str, b: str, c: bool): ...
@overload
def foo(a: bool, b: str, c: str): ...
@overload
def foo(a: str, b: str, c: str): ...
@overload
def foo(a: int, b: int, c: int): ...
@overload
def foo(a: bool, b: int, c: int): ...
@overload
def foo(a: int, b: bool, c: int): ...
@overload
def foo(a: int, b: int, c: bool): ...
@overload
def foo(a: bool, b: bool, c: int): ...
@overload
def foo(a: int, b: bool, c: bool): ...
@overload
def foo(a: str, b: str, c: str): ...
@overload
def foo(a: float, b: bool, c: bool): ...
@overload
def foo(a: bool, b: float, c: bool): ...
@overload
def foo(a: bool, b: bool, c: float): ...
@overload
def foo(a: float, b: float, c: bool): ...
@overload
def foo(a: bool, b: float, c: float): ...
def foo(a, b, c): ...
foo(Foo(), Foo()) # error: [no-matching-overload]
```
## Calls to overloaded functions with lots of parameters
```py
from typing import overload
@overload
def f(
lion: int,
turtle: int,
tortoise: int,
goat: int,
capybara: int,
chicken: int,
ostrich: int,
gorilla: int,
giraffe: int,
condor: int,
kangaroo: int,
anaconda: int,
tarantula: int,
millipede: int,
leopard: int,
hyena: int,
) -> int: ...
@overload
def f(
lion: str,
turtle: str,
tortoise: str,
goat: str,
capybara: str,
chicken: str,
ostrich: str,
gorilla: str,
giraffe: str,
condor: str,
kangaroo: str,
anaconda: str,
tarantula: str,
millipede: str,
leopard: str,
hyena: str,
) -> str: ...
def f(
lion: int | str,
turtle: int | str,
tortoise: int | str,
goat: int | str,
capybara: int | str,
chicken: int | str,
ostrict: int | str,
gorilla: int | str,
giraffe: int | str,
condor: int | str,
kangaroo: int | str,
anaconda: int | str,
tarantula: int | str,
millipede: int | str,
leopard: int | str,
hyena: int | str,
) -> int | str:
return 0
f(b"foo") # error: [no-matching-overload]
```

View file

@ -0,0 +1,120 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: no_matching_overload.md - No matching overload diagnostics - Call to function with many unmatched overloads
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import overload
2 |
3 | class Foo: ...
4 |
5 | @overload
6 | def foo(a: int, b: int, c: int): ...
7 | @overload
8 | def foo(a: str, b: int, c: int): ...
9 | @overload
10 | def foo(a: int, b: str, c: int): ...
11 | @overload
12 | def foo(a: int, b: int, c: str): ...
13 | @overload
14 | def foo(a: str, b: str, c: int): ...
15 | @overload
16 | def foo(a: int, b: str, c: str): ...
17 | @overload
18 | def foo(a: str, b: str, c: str): ...
19 | @overload
20 | def foo(a: int, b: int, c: int): ...
21 | @overload
22 | def foo(a: float, b: int, c: int): ...
23 | @overload
24 | def foo(a: int, b: float, c: int): ...
25 | @overload
26 | def foo(a: int, b: int, c: float): ...
27 | @overload
28 | def foo(a: float, b: float, c: int): ...
29 | @overload
30 | def foo(a: int, b: float, c: float): ...
31 | @overload
32 | def foo(a: float, b: float, c: float): ...
33 | @overload
34 | def foo(a: str, b: str, c: str): ...
35 | @overload
36 | def foo(a: float, b: str, c: str): ...
37 | @overload
38 | def foo(a: str, b: float, c: str): ...
39 | @overload
40 | def foo(a: str, b: str, c: float): ...
41 | @overload
42 | def foo(a: float, b: float, c: str): ...
43 | @overload
44 | def foo(a: str, b: float, c: float): ...
45 | @overload
46 | def foo(a: float, b: float, c: float): ...
47 | def foo(a, b, c): ...
48 |
49 | foo(Foo(), Foo()) # error: [no-matching-overload]
```
# Diagnostics
```
error[no-matching-overload]: No overload of function `foo` matches arguments
--> src/mdtest_snippet.py:49:1
|
47 | def foo(a, b, c): ...
48 |
49 | foo(Foo(), Foo()) # error: [no-matching-overload]
| ^^^^^^^^^^^^^^^^^
|
info: First overload defined here
--> src/mdtest_snippet.py:6:5
|
5 | @overload
6 | def foo(a: int, b: int, c: int): ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 | @overload
8 | def foo(a: str, b: int, c: int): ...
|
info: Possible overloads for function `foo`:
info: (a: int, b: int, c: int) -> Unknown
info: (a: str, b: int, c: int) -> Unknown
info: (a: int, b: str, c: int) -> Unknown
info: (a: int, b: int, c: str) -> Unknown
info: (a: str, b: str, c: int) -> Unknown
info: (a: int, b: str, c: str) -> Unknown
info: (a: str, b: str, c: str) -> Unknown
info: (a: int, b: int, c: int) -> Unknown
info: (a: int | float, b: int, c: int) -> Unknown
info: (a: int, b: int | float, c: int) -> Unknown
info: (a: int, b: int, c: int | float) -> Unknown
info: (a: int | float, b: int | float, c: int) -> Unknown
info: (a: int, b: int | float, c: int | float) -> Unknown
info: (a: int | float, b: int | float, c: int | float) -> Unknown
info: (a: str, b: str, c: str) -> Unknown
info: (a: int | float, b: str, c: str) -> Unknown
info: (a: str, b: int | float, c: str) -> Unknown
info: (a: str, b: str, c: int | float) -> Unknown
info: (a: int | float, b: int | float, c: str) -> Unknown
info: (a: str, b: int | float, c: int | float) -> Unknown
info: (a: int | float, b: int | float, c: int | float) -> Unknown
info: Overload implementation defined here
--> src/mdtest_snippet.py:47:5
|
45 | @overload
46 | def foo(a: float, b: float, c: float): ...
47 | def foo(a, b, c): ...
| ^^^^^^^^^^^^
48 |
49 | foo(Foo(), Foo()) # error: [no-matching-overload]
|
info: rule `no-matching-overload` is enabled by default
```

View file

@ -0,0 +1,230 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: no_matching_overload.md - No matching overload diagnostics - Call to function with too many unmatched overloads
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import overload
2 |
3 | class Foo: ...
4 |
5 | @overload
6 | def foo(a: int, b: int, c: int): ...
7 | @overload
8 | def foo(a: str, b: int, c: int): ...
9 | @overload
10 | def foo(a: int, b: str, c: int): ...
11 | @overload
12 | def foo(a: int, b: int, c: str): ...
13 | @overload
14 | def foo(a: str, b: str, c: int): ...
15 | @overload
16 | def foo(a: int, b: str, c: str): ...
17 | @overload
18 | def foo(a: str, b: str, c: str): ...
19 | @overload
20 | def foo(a: int, b: int, c: int): ...
21 | @overload
22 | def foo(a: float, b: int, c: int): ...
23 | @overload
24 | def foo(a: int, b: float, c: int): ...
25 | @overload
26 | def foo(a: int, b: int, c: float): ...
27 | @overload
28 | def foo(a: float, b: float, c: int): ...
29 | @overload
30 | def foo(a: int, b: float, c: float): ...
31 | @overload
32 | def foo(a: float, b: float, c: float): ...
33 | @overload
34 | def foo(a: str, b: str, c: str): ...
35 | @overload
36 | def foo(a: float, b: str, c: str): ...
37 | @overload
38 | def foo(a: str, b: float, c: str): ...
39 | @overload
40 | def foo(a: str, b: str, c: float): ...
41 | @overload
42 | def foo(a: float, b: float, c: str): ...
43 | @overload
44 | def foo(a: str, b: float, c: float): ...
45 | @overload
46 | def foo(a: float, b: float, c: float): ...
47 | @overload
48 | def foo(a: list[int], b: list[int], c: list[int]): ...
49 | @overload
50 | def foo(a: list[str], b: list[int], c: list[int]): ...
51 | @overload
52 | def foo(a: list[int], b: list[str], c: list[int]): ...
53 | @overload
54 | def foo(a: list[int], b: list[int], c: list[str]): ...
55 | @overload
56 | def foo(a: list[str], b: list[str], c: list[int]): ...
57 | @overload
58 | def foo(a: list[int], b: list[str], c: list[str]): ...
59 | @overload
60 | def foo(a: list[str], b: list[str], c: list[str]): ...
61 | @overload
62 | def foo(a: list[int], b: list[int], c: list[int]): ...
63 | @overload
64 | def foo(a: list[float], b: list[int], c: list[int]): ...
65 | @overload
66 | def foo(a: list[int], b: list[float], c: list[int]): ...
67 | @overload
68 | def foo(a: list[int], b: list[int], c: list[float]): ...
69 | @overload
70 | def foo(a: list[float], b: list[float], c: list[int]): ...
71 | @overload
72 | def foo(a: list[int], b: list[float], c: list[float]): ...
73 | @overload
74 | def foo(a: list[float], b: list[float], c: list[float]): ...
75 | @overload
76 | def foo(a: list[str], b: list[str], c: list[str]): ...
77 | @overload
78 | def foo(a: list[float], b: list[str], c: list[str]): ...
79 | @overload
80 | def foo(a: list[str], b: list[float], c: list[str]): ...
81 | @overload
82 | def foo(a: list[str], b: list[str], c: list[float]): ...
83 | @overload
84 | def foo(a: list[float], b: list[float], c: list[str]): ...
85 | @overload
86 | def foo(a: list[str], b: list[float], c: list[float]): ...
87 | @overload
88 | def foo(a: list[float], b: list[float], c: list[float]): ...
89 | @overload
90 | def foo(a: bool, b: bool, c: bool): ...
91 | @overload
92 | def foo(a: str, b: bool, c: bool): ...
93 | @overload
94 | def foo(a: bool, b: str, c: bool): ...
95 | @overload
96 | def foo(a: bool, b: bool, c: str): ...
97 | @overload
98 | def foo(a: str, b: str, c: bool): ...
99 | @overload
100 | def foo(a: bool, b: str, c: str): ...
101 | @overload
102 | def foo(a: str, b: str, c: str): ...
103 | @overload
104 | def foo(a: int, b: int, c: int): ...
105 | @overload
106 | def foo(a: bool, b: int, c: int): ...
107 | @overload
108 | def foo(a: int, b: bool, c: int): ...
109 | @overload
110 | def foo(a: int, b: int, c: bool): ...
111 | @overload
112 | def foo(a: bool, b: bool, c: int): ...
113 | @overload
114 | def foo(a: int, b: bool, c: bool): ...
115 | @overload
116 | def foo(a: str, b: str, c: str): ...
117 | @overload
118 | def foo(a: float, b: bool, c: bool): ...
119 | @overload
120 | def foo(a: bool, b: float, c: bool): ...
121 | @overload
122 | def foo(a: bool, b: bool, c: float): ...
123 | @overload
124 | def foo(a: float, b: float, c: bool): ...
125 | @overload
126 | def foo(a: bool, b: float, c: float): ...
127 | def foo(a, b, c): ...
128 |
129 | foo(Foo(), Foo()) # error: [no-matching-overload]
```
# Diagnostics
```
error[no-matching-overload]: No overload of function `foo` matches arguments
--> src/mdtest_snippet.py:129:1
|
127 | def foo(a, b, c): ...
128 |
129 | foo(Foo(), Foo()) # error: [no-matching-overload]
| ^^^^^^^^^^^^^^^^^
|
info: First overload defined here
--> src/mdtest_snippet.py:6:5
|
5 | @overload
6 | def foo(a: int, b: int, c: int): ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 | @overload
8 | def foo(a: str, b: int, c: int): ...
|
info: Possible overloads for function `foo`:
info: (a: int, b: int, c: int) -> Unknown
info: (a: str, b: int, c: int) -> Unknown
info: (a: int, b: str, c: int) -> Unknown
info: (a: int, b: int, c: str) -> Unknown
info: (a: str, b: str, c: int) -> Unknown
info: (a: int, b: str, c: str) -> Unknown
info: (a: str, b: str, c: str) -> Unknown
info: (a: int, b: int, c: int) -> Unknown
info: (a: int | float, b: int, c: int) -> Unknown
info: (a: int, b: int | float, c: int) -> Unknown
info: (a: int, b: int, c: int | float) -> Unknown
info: (a: int | float, b: int | float, c: int) -> Unknown
info: (a: int, b: int | float, c: int | float) -> Unknown
info: (a: int | float, b: int | float, c: int | float) -> Unknown
info: (a: str, b: str, c: str) -> Unknown
info: (a: int | float, b: str, c: str) -> Unknown
info: (a: str, b: int | float, c: str) -> Unknown
info: (a: str, b: str, c: int | float) -> Unknown
info: (a: int | float, b: int | float, c: str) -> Unknown
info: (a: str, b: int | float, c: int | float) -> Unknown
info: (a: int | float, b: int | float, c: int | float) -> Unknown
info: (a: list[int], b: list[int], c: list[int]) -> Unknown
info: (a: list[str], b: list[int], c: list[int]) -> Unknown
info: (a: list[int], b: list[str], c: list[int]) -> Unknown
info: (a: list[int], b: list[int], c: list[str]) -> Unknown
info: (a: list[str], b: list[str], c: list[int]) -> Unknown
info: (a: list[int], b: list[str], c: list[str]) -> Unknown
info: (a: list[str], b: list[str], c: list[str]) -> Unknown
info: (a: list[int], b: list[int], c: list[int]) -> Unknown
info: (a: list[int | float], b: list[int], c: list[int]) -> Unknown
info: (a: list[int], b: list[int | float], c: list[int]) -> Unknown
info: (a: list[int], b: list[int], c: list[int | float]) -> Unknown
info: (a: list[int | float], b: list[int | float], c: list[int]) -> Unknown
info: (a: list[int], b: list[int | float], c: list[int | float]) -> Unknown
info: (a: list[int | float], b: list[int | float], c: list[int | float]) -> Unknown
info: (a: list[str], b: list[str], c: list[str]) -> Unknown
info: (a: list[int | float], b: list[str], c: list[str]) -> Unknown
info: (a: list[str], b: list[int | float], c: list[str]) -> Unknown
info: (a: list[str], b: list[str], c: list[int | float]) -> Unknown
info: (a: list[int | float], b: list[int | float], c: list[str]) -> Unknown
info: (a: list[str], b: list[int | float], c: list[int | float]) -> Unknown
info: (a: list[int | float], b: list[int | float], c: list[int | float]) -> Unknown
info: (a: bool, b: bool, c: bool) -> Unknown
info: (a: str, b: bool, c: bool) -> Unknown
info: (a: bool, b: str, c: bool) -> Unknown
info: (a: bool, b: bool, c: str) -> Unknown
info: (a: str, b: str, c: bool) -> Unknown
info: (a: bool, b: str, c: str) -> Unknown
info: (a: str, b: str, c: str) -> Unknown
info: (a: int, b: int, c: int) -> Unknown
info: ... omitted 11 overloads
info: Overload implementation defined here
--> src/mdtest_snippet.py:127:5
|
125 | @overload
126 | def foo(a: bool, b: float, c: float): ...
127 | def foo(a, b, c): ...
| ^^^^^^^^^^^^
128 |
129 | foo(Foo(), Foo()) # error: [no-matching-overload]
|
info: rule `no-matching-overload` is enabled by default
```

View file

@ -16,27 +16,46 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_
2 |
3 | @overload
4 | def f(x: int) -> int: ...
5 |
6 | @overload
7 | def f(x: str) -> str: ...
8 |
9 | def f(x: int | str) -> int | str:
10 | return x
11 |
12 | f(b"foo") # error: [no-matching-overload]
5 | @overload
6 | def f(x: str) -> str: ...
7 | def f(x: int | str) -> int | str:
8 | return x
9 |
10 | f(b"foo") # error: [no-matching-overload]
```
# Diagnostics
```
error[no-matching-overload]: No overload of function `f` matches arguments
--> src/mdtest_snippet.py:12:1
--> src/mdtest_snippet.py:10:1
|
10 | return x
11 |
12 | f(b"foo") # error: [no-matching-overload]
8 | return x
9 |
10 | f(b"foo") # error: [no-matching-overload]
| ^^^^^^^^^
|
info: First overload defined here
--> src/mdtest_snippet.py:4:5
|
3 | @overload
4 | def f(x: int) -> int: ...
| ^^^^^^^^^^^^^^^^
5 | @overload
6 | def f(x: str) -> str: ...
|
info: Possible overloads for function `f`:
info: (x: int) -> int
info: (x: str) -> str
info: Overload implementation defined here
--> src/mdtest_snippet.py:7:5
|
5 | @overload
6 | def f(x: str) -> str: ...
7 | def f(x: int | str) -> int | str:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | return x
|
info: rule `no-matching-overload` is enabled by default
```

View file

@ -0,0 +1,148 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: no_matching_overload.md - No matching overload diagnostics - Calls to overloaded functions with lots of parameters
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import overload
2 |
3 | @overload
4 | def f(
5 | lion: int,
6 | turtle: int,
7 | tortoise: int,
8 | goat: int,
9 | capybara: int,
10 | chicken: int,
11 | ostrich: int,
12 | gorilla: int,
13 | giraffe: int,
14 | condor: int,
15 | kangaroo: int,
16 | anaconda: int,
17 | tarantula: int,
18 | millipede: int,
19 | leopard: int,
20 | hyena: int,
21 | ) -> int: ...
22 | @overload
23 | def f(
24 | lion: str,
25 | turtle: str,
26 | tortoise: str,
27 | goat: str,
28 | capybara: str,
29 | chicken: str,
30 | ostrich: str,
31 | gorilla: str,
32 | giraffe: str,
33 | condor: str,
34 | kangaroo: str,
35 | anaconda: str,
36 | tarantula: str,
37 | millipede: str,
38 | leopard: str,
39 | hyena: str,
40 | ) -> str: ...
41 | def f(
42 | lion: int | str,
43 | turtle: int | str,
44 | tortoise: int | str,
45 | goat: int | str,
46 | capybara: int | str,
47 | chicken: int | str,
48 | ostrict: int | str,
49 | gorilla: int | str,
50 | giraffe: int | str,
51 | condor: int | str,
52 | kangaroo: int | str,
53 | anaconda: int | str,
54 | tarantula: int | str,
55 | millipede: int | str,
56 | leopard: int | str,
57 | hyena: int | str,
58 | ) -> int | str:
59 | return 0
60 |
61 | f(b"foo") # error: [no-matching-overload]
```
# Diagnostics
```
error[no-matching-overload]: No overload of function `f` matches arguments
--> src/mdtest_snippet.py:61:1
|
59 | return 0
60 |
61 | f(b"foo") # error: [no-matching-overload]
| ^^^^^^^^^
|
info: First overload defined here
--> src/mdtest_snippet.py:4:5
|
3 | @overload
4 | def f(
| _____^
5 | | lion: int,
6 | | turtle: int,
7 | | tortoise: int,
8 | | goat: int,
9 | | capybara: int,
10 | | chicken: int,
11 | | ostrich: int,
12 | | gorilla: int,
13 | | giraffe: int,
14 | | condor: int,
15 | | kangaroo: int,
16 | | anaconda: int,
17 | | tarantula: int,
18 | | millipede: int,
19 | | leopard: int,
20 | | hyena: int,
21 | | ) -> int: ...
| |________^
22 | @overload
23 | def f(
|
info: Possible overloads for function `f`:
info: (lion: int, turtle: int, tortoise: int, goat: int, capybara: int, chicken: int, ostrich: int, gorilla: int, giraffe: int, condor: int, kangaroo: int, anaconda: int, tarantula: int, millipede: int, leopard: int, hyena: int) -> int
info: (lion: str, turtle: str, tortoise: str, goat: str, capybara: str, chicken: str, ostrich: str, gorilla: str, giraffe: str, condor: str, kangaroo: str, anaconda: str, tarantula: str, millipede: str, leopard: str, hyena: str) -> str
info: Overload implementation defined here
--> src/mdtest_snippet.py:41:5
|
39 | hyena: str,
40 | ) -> str: ...
41 | def f(
| _____^
42 | | lion: int | str,
43 | | turtle: int | str,
44 | | tortoise: int | str,
45 | | goat: int | str,
46 | | capybara: int | str,
47 | | chicken: int | str,
48 | | ostrict: int | str,
49 | | gorilla: int | str,
50 | | giraffe: int | str,
51 | | condor: int | str,
52 | | kangaroo: int | str,
53 | | anaconda: int | str,
54 | | tarantula: int | str,
55 | | millipede: int | str,
56 | | leopard: int | str,
57 | | hyena: int | str,
58 | | ) -> int | str:
| |______________^
59 | return 0
|
info: rule `no-matching-overload` is enabled by default
```

View file

@ -5350,36 +5350,6 @@ impl<'db> Type<'db> {
}
}
/// Returns a tuple of two spans. The first is
/// the span for the identifier of the function
/// definition for `self`. The second is
/// the span for the return type in the function
/// definition for `self`.
///
/// If there are no meaningful spans, then this
/// returns `None`. For example, when this type
/// isn't callable or if the function has no
/// declared return type.
///
/// # Performance
///
/// Note that this may introduce cross-module
/// dependencies. This can have an impact on
/// the effectiveness of incremental caching
/// and should therefore be used judiciously.
///
/// An example of a good use case is to improve
/// a diagnostic.
fn return_type_span(&self, db: &'db dyn Db) -> Option<(Span, Span)> {
match *self {
Type::FunctionLiteral(function) => function.return_type_span(db),
Type::BoundMethod(bound_method) => {
Type::FunctionLiteral(bound_method.function(db)).return_type_span(db)
}
_ => None,
}
}
/// Returns a tuple of two spans. The first is
/// the span for the identifier of the function
/// definition for `self`. The second is
@ -5410,9 +5380,34 @@ impl<'db> Type<'db> {
) -> Option<(Span, Span)> {
match *self {
Type::FunctionLiteral(function) => function.parameter_span(db, parameter_index),
Type::BoundMethod(bound_method) => {
Type::FunctionLiteral(bound_method.function(db)).parameter_span(db, parameter_index)
}
Type::BoundMethod(bound_method) => bound_method
.function(db)
.parameter_span(db, parameter_index),
_ => None,
}
}
/// Returns a collection of useful spans for a
/// function signature. These are useful for
/// creating annotations on diagnostics.
///
/// If there are no meaningful spans, then this
/// returns `None`. For example, when this type
/// isn't callable.
///
/// # Performance
///
/// Note that this may introduce cross-module
/// dependencies. This can have an impact on
/// the effectiveness of incremental caching
/// and should therefore be used judiciously.
///
/// An example of a good use case is to improve
/// a diagnostic.
fn function_spans(&self, db: &'db dyn Db) -> Option<FunctionSpans> {
match *self {
Type::FunctionLiteral(function) => function.spans(db),
Type::BoundMethod(bound_method) => bound_method.function(db).spans(db),
_ => None,
}
}
@ -6297,7 +6292,8 @@ impl<'db> BoolError<'db> {
.member(context.db(), "__bool__")
.into_lookup_result()
.ok()
.and_then(|quals| quals.inner_type().return_type_span(context.db()))
.and_then(|quals| quals.inner_type().function_spans(context.db()))
.and_then(|spans| Some((spans.name, spans.return_type?)))
{
sub.annotate(
Annotation::primary(return_type_span).message("Incorrect return type"),
@ -6894,37 +6890,6 @@ impl<'db> FunctionType<'db> {
}
}
/// Returns a tuple of two spans. The first is
/// the span for the identifier of the function
/// definition for `self`. The second is
/// the span for the return type in the function
/// definition for `self`.
///
/// If there are no meaningful spans, then this
/// returns `None`. For example, when this type
/// isn't callable or if the function has no
/// declared return type.
///
/// # Performance
///
/// Note that this may introduce cross-module
/// dependencies. This can have an impact on
/// the effectiveness of incremental caching
/// and should therefore be used judiciously.
///
/// An example of a good use case is to improve
/// a diagnostic.
fn return_type_span(&self, db: &'db dyn Db) -> Option<(Span, Span)> {
let function_scope = self.body_scope(db);
let span = Span::from(function_scope.file(db));
let node = function_scope.node(db);
let func_def = node.as_function()?;
let return_type_range = func_def.returns.as_ref()?.range();
let name_span = span.clone().with_range(func_def.name.range);
let return_type_span = span.with_range(return_type_range);
Some((name_span, return_type_span))
}
/// Returns a tuple of two spans. The first is
/// the span for the identifier of the function
/// definition for `self`. The second is
@ -6949,7 +6914,7 @@ impl<'db> FunctionType<'db> {
/// An example of a good use case is to improve
/// a diagnostic.
fn parameter_span(
&self,
self,
db: &'db dyn Db,
parameter_index: Option<usize>,
) -> Option<(Span, Span)> {
@ -6970,6 +6935,55 @@ impl<'db> FunctionType<'db> {
let parameter_span = span.with_range(range);
Some((name_span, parameter_span))
}
/// Returns a collection of useful spans for a
/// function signature. These are useful for
/// creating annotations on diagnostics.
///
/// # Performance
///
/// Note that this may introduce cross-module
/// dependencies. This can have an impact on
/// the effectiveness of incremental caching
/// and should therefore be used judiciously.
///
/// An example of a good use case is to improve
/// a diagnostic.
fn spans(self, db: &'db dyn Db) -> Option<FunctionSpans> {
let function_scope = self.body_scope(db);
let span = Span::from(function_scope.file(db));
let node = function_scope.node(db);
let func_def = node.as_function()?;
let return_type_range = func_def.returns.as_ref().map(|returns| returns.range());
let mut signature = func_def.name.range.cover(func_def.parameters.range);
if let Some(return_type_range) = return_type_range {
signature = signature.cover(return_type_range);
}
Some(FunctionSpans {
signature: span.clone().with_range(signature),
name: span.clone().with_range(func_def.name.range),
parameters: span.clone().with_range(func_def.parameters.range),
return_type: return_type_range.map(|range| span.clone().with_range(range)),
})
}
}
/// A collection of useful spans for annotating functions.
///
/// This can be retrieved via `FunctionType::spans` or
/// `Type::function_spans`.
struct FunctionSpans {
/// The span of the entire function "signature." This includes
/// the name, parameter list and return type (if present).
signature: Span,
/// The span of the function name. i.e., `foo` in `def foo(): ...`.
name: Span,
/// The span of the parameter list, including the opening and
/// closing parentheses.
#[expect(dead_code)]
parameters: Span,
/// The span of the annotated return type, if present.
return_type: Option<Span>,
}
fn signature_cycle_recover<'db>(

View file

@ -1100,6 +1100,13 @@ impl<'db> CallableBinding<'db> {
);
}
_overloads => {
// When the number of unmatched overloads exceeds this number, we stop
// printing them to avoid excessive output.
//
// An example of a routine with many many overloads:
// https://github.com/henribru/google-api-python-client-stubs/blob/master/googleapiclient-stubs/discovery.pyi
const MAXIMUM_OVERLOADS: usize = 50;
let Some(builder) = context.report_lint(&NO_MATCHING_OVERLOAD, node) else {
return;
};
@ -1113,6 +1120,48 @@ impl<'db> CallableBinding<'db> {
String::new()
}
));
if let Some(function) = self.signature_type.into_function_literal() {
if let Some(overloaded_function) = function.to_overloaded(context.db()) {
if let Some(spans) = overloaded_function
.overloads
.first()
.and_then(|overload| overload.spans(context.db()))
{
let mut sub =
SubDiagnostic::new(Severity::Info, "First overload defined here");
sub.annotate(Annotation::primary(spans.signature));
diag.sub(sub);
}
diag.info(format_args!(
"Possible overloads for function `{}`:",
function.name(context.db())
));
let overloads = &function.signature(context.db()).overloads.overloads;
for overload in overloads.iter().take(MAXIMUM_OVERLOADS) {
diag.info(format_args!(" {}", overload.display(context.db())));
}
if overloads.len() > MAXIMUM_OVERLOADS {
diag.info(format_args!(
"... omitted {remaining} overloads",
remaining = overloads.len() - MAXIMUM_OVERLOADS
));
}
if let Some(spans) = overloaded_function
.implementation
.and_then(|function| function.spans(context.db()))
{
let mut sub = SubDiagnostic::new(
Severity::Info,
"Overload implementation defined here",
);
sub.annotate(Annotation::primary(spans.signature));
diag.sub(sub);
}
}
}
if let Some(union_diag) = union_diag {
union_diag.add_union_context(context.db(), &mut diag);
}

View file

@ -123,7 +123,7 @@ pub(crate) struct CallableSignature<'db> {
///
/// By using `SmallVec`, we avoid an extra heap allocation for the common case of a
/// non-overloaded callable.
overloads: SmallVec<[Signature<'db>; 1]>,
pub(crate) overloads: SmallVec<[Signature<'db>; 1]>,
}
impl<'db> CallableSignature<'db> {