Skip to content

Commit ad34311

Browse files
committed
[ty] Surface matched overload diagnostics directly
1 parent 913f136 commit ad34311

File tree

8 files changed

+108
-59
lines changed

8 files changed

+108
-59
lines changed

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ The following calls are also invalid, due to incorrect argument types:
4848
```py
4949
class Base: ...
5050

51-
# error: [no-matching-overload] "No overload of class `type` matches arguments"
51+
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
5252
type(b"Foo", (), {})
5353

54-
# error: [no-matching-overload] "No overload of class `type` matches arguments"
54+
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `<class 'Base'>`"
5555
type("Foo", Base, {})
5656

57-
# error: [no-matching-overload] "No overload of class `type` matches arguments"
57+
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
5858
type("Foo", (1, 2), {})
5959

6060
# TODO: this should be an error
@@ -90,12 +90,18 @@ str(errors="replace")
9090
### Invalid calls
9191

9292
```py
93-
str(1, 2) # error: [no-matching-overload]
94-
str(o=1) # error: [no-matching-overload]
93+
# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `bytes | bytearray`, found `Literal[1]`"
94+
# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[2]`"
95+
str(1, 2)
96+
97+
# error: [no-matching-overload]
98+
str(o=1)
9599

96100
# First argument is not a bytes-like object:
97-
str("Müsli", "utf-8") # error: [no-matching-overload]
101+
# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `bytes | bytearray`, found `Literal["Müsli"]`"
102+
str("Müsli", "utf-8")
98103

99104
# Second argument is not a valid encoding:
100-
str(b"M\xc3\xbcsli", b"utf-8") # error: [no-matching-overload]
105+
# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[b"utf-8"]`"
106+
str(b"M\xc3\xbcsli", b"utf-8")
101107
```

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ method_wrapper(C(), None)
235235
method_wrapper(None, C)
236236

237237
# Passing `None` without an `owner` argument is an
238-
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
238+
# error: [invalid-argument-type] "Argument to method wrapper `__get__` of function `f` is incorrect: Expected `~None`, found `None`"
239239
method_wrapper(None)
240240

241241
# Passing something that is not assignable to `type` as the `owner` argument is an

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,7 @@ from overloaded import f
8585

8686
reveal_type(f()) # revealed: None
8787

88-
# TODO: This should be `invalid-argument-type` instead
89-
# error: [no-matching-overload]
88+
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`"
9089
reveal_type(f("a")) # revealed: Unknown
9190
```
9291

@@ -400,6 +399,43 @@ def _(x: SomeEnum):
400399
reveal_type(f(x)) # revealed: A
401400
```
402401

402+
### No matching overloads
403+
404+
> If argument expansion has been applied to all arguments and one or more of the expanded argument
405+
> lists cannot be evaluated successfully, generate an error and stop.
406+
407+
`overloaded.pyi`:
408+
409+
```pyi
410+
from typing import overload
411+
412+
class A: ...
413+
class B: ...
414+
class C: ...
415+
class D: ...
416+
417+
@overload
418+
def f(x: A) -> A: ...
419+
@overload
420+
def f(x: B) -> B: ...
421+
```
422+
423+
```py
424+
from overloaded import A, B, C, D, f
425+
426+
def _(ab: A | B, ac: A | C, cd: C | D):
427+
reveal_type(f(ab)) # revealed: A | B
428+
429+
# The `[A | C]` argument list is expanded to `[A], [C]` where the first list matches the first
430+
# overload while the second list doesn't match any of the overloads, so we generate an
431+
# error: [no-matching-overload] "No overload of function `f` matches arguments"
432+
reveal_type(f(ac)) # revealed: Unknown
433+
434+
# None of the expanded argument lists (`[C], [D]`) match any of the overloads, so we generate an
435+
# error: [no-matching-overload] "No overload of function `f` matches arguments"
436+
reveal_type(f(cd)) # revealed: Unknown
437+
```
438+
403439
## Filtering overloads with variadic arguments and parameters
404440

405441
TODO

crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ wrapper_descriptor()
607607
wrapper_descriptor(f)
608608

609609
# Calling it without the `owner` argument if `instance` is not `None` is an
610-
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
610+
# error: [invalid-argument-type] "Argument to wrapper descriptor `FunctionType.__get__` is incorrect: Expected `~None`, found `None`"
611611
wrapper_descriptor(f, None)
612612

613613
# But calling it with an instance is fine (in this case, the `owner` argument is optional):

crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ def _(n: int):
103103
# error: [missing-argument]
104104
# error: [invalid-argument-type] "Argument to function `f4` is incorrect: Argument type `Literal[3]` does not satisfy upper bound of type variable `T`"
105105
# error: [call-non-callable] "Object of type `Literal[5]` is not callable"
106-
# error: [no-matching-overload]
107106
# error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
108107
x = f(3)
109108
```

crates/ty_python_semantic/resources/mdtest/narrow/type.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ No narrowing should occur if `type` is used to dynamically create a class:
7878
def _(x: str | int):
7979
# The following diagnostic is valid, since the three-argument form of `type`
8080
# can only be called with `str` as the first argument.
81-
# error: [no-matching-overload] "No overload of class `type` matches arguments"
81+
# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`"
8282
if type(x, (), {}) is str:
8383
reveal_type(x) # revealed: str | int
8484
else:

crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Cover_non-keyword_re…_(707b284610419a54).snap

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,19 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.m
5757
43 | # error: [missing-argument]
5858
44 | # error: [invalid-argument-type] "Argument to function `f4` is incorrect: Argument type `Literal[3]` does not satisfy upper bound of type variable `T`"
5959
45 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable"
60-
46 | # error: [no-matching-overload]
61-
47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
62-
48 | x = f(3)
60+
46 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
61+
47 | x = f(3)
6362
```
6463

6564
# Diagnostics
6665

6766
```
6867
error[call-non-callable]: Object of type `Literal[5]` is not callable
69-
--> src/mdtest_snippet.py:48:9
68+
--> src/mdtest_snippet.py:47:9
7069
|
71-
46 | # error: [no-matching-overload]
72-
47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
73-
48 | x = f(3)
70+
45 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable"
71+
46 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
72+
47 | x = f(3)
7473
| ^^^^
7574
|
7675
info: Union variant `Literal[5]` is incompatible with this call site
@@ -81,11 +80,11 @@ info: rule `call-non-callable` is enabled by default
8180

8281
```
8382
error[call-non-callable]: Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)
84-
--> src/mdtest_snippet.py:48:9
83+
--> src/mdtest_snippet.py:47:9
8584
|
86-
46 | # error: [no-matching-overload]
87-
47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
88-
48 | x = f(3)
85+
45 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable"
86+
46 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
87+
47 | x = f(3)
8988
| ^^^^
9089
|
9190
info: Union variant `PossiblyNotCallable` is incompatible with this call site
@@ -96,11 +95,11 @@ info: rule `call-non-callable` is enabled by default
9695

9796
```
9897
error[missing-argument]: No argument provided for required parameter `b` of function `f3`
99-
--> src/mdtest_snippet.py:48:9
98+
--> src/mdtest_snippet.py:47:9
10099
|
101-
46 | # error: [no-matching-overload]
102-
47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
103-
48 | x = f(3)
100+
45 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable"
101+
46 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
102+
47 | x = f(3)
104103
| ^^^^
105104
|
106105
info: Union variant `def f3(a: int, b: int) -> int` is incompatible with this call site
@@ -109,28 +108,13 @@ info: rule `missing-argument` is enabled by default
109108

110109
```
111110

112-
```
113-
error[no-matching-overload]: No overload of method wrapper `__get__` of function `f` matches arguments
114-
--> src/mdtest_snippet.py:48:9
115-
|
116-
46 | # error: [no-matching-overload]
117-
47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
118-
48 | x = f(3)
119-
| ^^^^
120-
|
121-
info: Union variant `<method-wrapper `__get__` of `f`>` is incompatible with this call site
122-
info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4(x: T) -> int) | Literal[5] | Unknown | (<method-wrapper `__get__` of `f`>) | PossiblyNotCallable`
123-
info: rule `no-matching-overload` is enabled by default
124-
125-
```
126-
127111
```
128112
error[invalid-argument-type]: Argument to function `f2` is incorrect
129-
--> src/mdtest_snippet.py:48:11
113+
--> src/mdtest_snippet.py:47:11
130114
|
131-
46 | # error: [no-matching-overload]
132-
47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
133-
48 | x = f(3)
115+
45 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable"
116+
46 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
117+
47 | x = f(3)
134118
| ^ Expected `str`, found `Literal[3]`
135119
|
136120
info: Function defined here
@@ -150,11 +134,11 @@ info: rule `invalid-argument-type` is enabled by default
150134

151135
```
152136
error[invalid-argument-type]: Argument to function `f4` is incorrect
153-
--> src/mdtest_snippet.py:48:11
137+
--> src/mdtest_snippet.py:47:11
154138
|
155-
46 | # error: [no-matching-overload]
156-
47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
157-
48 | x = f(3)
139+
45 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable"
140+
46 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
141+
47 | x = f(3)
158142
| ^ Argument type `Literal[3]` does not satisfy upper bound of type variable `T`
159143
|
160144
info: Type variable defined here
@@ -174,11 +158,11 @@ info: rule `invalid-argument-type` is enabled by default
174158

175159
```
176160
error[too-many-positional-arguments]: Too many positional arguments to function `f1`: expected 0, got 1
177-
--> src/mdtest_snippet.py:48:11
161+
--> src/mdtest_snippet.py:47:11
178162
|
179-
46 | # error: [no-matching-overload]
180-
47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
181-
48 | x = f(3)
163+
45 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable"
164+
46 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
165+
47 | x = f(3)
182166
| ^
183167
|
184168
info: Union variant `def f1() -> int` is incompatible with this call site

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,7 @@ impl<'db> From<Binding<'db>> for Bindings<'db> {
10321032
dunder_call_is_possibly_unbound: false,
10331033
bound_type: None,
10341034
overload_call_return_type: None,
1035+
matched_overload_index: None,
10351036
overloads: smallvec![from],
10361037
};
10371038
Bindings {
@@ -1086,6 +1087,13 @@ pub(crate) struct CallableBinding<'db> {
10861087
/// [`Unknown`]: crate::types::DynamicType::Unknown
10871088
overload_call_return_type: Option<OverloadCallReturnType<'db>>,
10881089

1090+
/// The index of the overload that matched for this overloaded callable.
1091+
///
1092+
/// This is [`Some`] only if step 1 and 4 of the [overload call evaluation algorithm][1].
1093+
///
1094+
/// [1]: https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation
1095+
matched_overload_index: Option<usize>,
1096+
10891097
/// The bindings of each overload of this callable. Will be empty if the type is not callable.
10901098
///
10911099
/// By using `SmallVec`, we avoid an extra heap allocation for the common case of a
@@ -1108,6 +1116,7 @@ impl<'db> CallableBinding<'db> {
11081116
dunder_call_is_possibly_unbound: false,
11091117
bound_type: None,
11101118
overload_call_return_type: None,
1119+
matched_overload_index: None,
11111120
overloads,
11121121
}
11131122
}
@@ -1119,6 +1128,7 @@ impl<'db> CallableBinding<'db> {
11191128
dunder_call_is_possibly_unbound: false,
11201129
bound_type: None,
11211130
overload_call_return_type: None,
1131+
matched_overload_index: None,
11221132
overloads: smallvec![],
11231133
}
11241134
}
@@ -1169,10 +1179,9 @@ impl<'db> CallableBinding<'db> {
11691179
return;
11701180
}
11711181
MatchingOverloadIndex::Single(index) => {
1172-
// If only one candidate overload remains, it is the winning match.
1173-
// TODO: Evaluate it as a regular (non-overloaded) call. This means that any
1174-
// diagnostics reported in this check should be reported directly instead of
1175-
// reporting it as `no-matching-overload`.
1182+
// If only one candidate overload remains, it is the winning match. Evaluate it as
1183+
// a regular (non-overloaded) call.
1184+
self.matched_overload_index = Some(index);
11761185
self.overloads[index].check_types(
11771186
db,
11781187
argument_types.as_ref(),
@@ -1595,6 +1604,21 @@ impl<'db> CallableBinding<'db> {
15951604
// https://github.com/henribru/google-api-python-client-stubs/blob/master/googleapiclient-stubs/discovery.pyi
15961605
const MAXIMUM_OVERLOADS: usize = 50;
15971606

1607+
// If there is a single matching overload, the diagnostics should be reported
1608+
// directly for that overload.
1609+
if let Some(matched_overload_index) = self.matched_overload_index {
1610+
let callable_description =
1611+
CallableDescription::new(context.db(), self.signature_type);
1612+
self.overloads[matched_overload_index].report_diagnostics(
1613+
context,
1614+
node,
1615+
self.signature_type,
1616+
callable_description.as_ref(),
1617+
union_diag,
1618+
);
1619+
return;
1620+
}
1621+
15981622
let Some(builder) = context.report_lint(&NO_MATCHING_OVERLOAD, node) else {
15991623
return;
16001624
};

0 commit comments

Comments
 (0)