diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index a2bee55560..c9bb662177 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -48,13 +48,13 @@ The following calls are also invalid, due to incorrect argument types: ```py class Base: ... -# error: [no-matching-overload] "No overload of class `type` matches arguments" +# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`" type(b"Foo", (), {}) -# error: [no-matching-overload] "No overload of class `type` matches arguments" +# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found ``" type("Foo", Base, {}) -# error: [no-matching-overload] "No overload of class `type` matches arguments" +# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`" type("Foo", (1, 2), {}) # TODO: this should be an error @@ -90,12 +90,18 @@ str(errors="replace") ### Invalid calls ```py -str(1, 2) # error: [no-matching-overload] -str(o=1) # error: [no-matching-overload] +# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `bytes | bytearray`, found `Literal[1]`" +# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[2]`" +str(1, 2) + +# error: [no-matching-overload] +str(o=1) # First argument is not a bytes-like object: -str("Müsli", "utf-8") # error: [no-matching-overload] +# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `bytes | bytearray`, found `Literal["Müsli"]`" +str("Müsli", "utf-8") # Second argument is not a valid encoding: -str(b"M\xc3\xbcsli", b"utf-8") # error: [no-matching-overload] +# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[b"utf-8"]`" +str(b"M\xc3\xbcsli", b"utf-8") ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index e6fde2e261..da7237e85b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -235,7 +235,7 @@ method_wrapper(C(), None) method_wrapper(None, C) # Passing `None` without an `owner` argument is an -# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" +# error: [invalid-argument-type] "Argument to method wrapper `__get__` of function `f` is incorrect: Expected `~None`, found `None`" method_wrapper(None) # Passing something that is not assignable to `type` as the `owner` argument is an diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index d49ee12639..a66e28ad7e 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -74,6 +74,8 @@ from typing import overload def f() -> None: ... @overload def f(x: int) -> int: ... +@overload +def f(x: int, y: int) -> int: ... ``` If the arity check only matches a single overload, it should be evaluated as a regular @@ -81,15 +83,19 @@ If the arity check only matches a single overload, it should be evaluated as a r call should be reported directly and not as a `no-matching-overload` error. ```py +from typing_extensions import reveal_type + from overloaded import f reveal_type(f()) # revealed: None -# TODO: This should be `invalid-argument-type` instead -# error: [no-matching-overload] +# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`" reveal_type(f("a")) # revealed: Unknown ``` +More examples of this diagnostic can be found in the +[single_matching_overload.md](../diagnostics/single_matching_overload.md) document. + ### Multiple matches `overloaded.pyi`: @@ -400,6 +406,43 @@ def _(x: SomeEnum): reveal_type(f(x)) # revealed: A ``` +### No matching overloads + +> If argument expansion has been applied to all arguments and one or more of the expanded argument +> lists cannot be evaluated successfully, generate an error and stop. + +`overloaded.pyi`: + +```pyi +from typing import overload + +class A: ... +class B: ... +class C: ... +class D: ... + +@overload +def f(x: A) -> A: ... +@overload +def f(x: B) -> B: ... +``` + +```py +from overloaded import A, B, C, D, f + +def _(ab: A | B, ac: A | C, cd: C | D): + reveal_type(f(ab)) # revealed: A | B + + # The `[A | C]` argument list is expanded to `[A], [C]` where the first list matches the first + # overload while the second list doesn't match any of the overloads, so we generate an + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(ac)) # revealed: Unknown + + # None of the expanded argument lists (`[C], [D]`) match any of the overloads, so we generate an + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(cd)) # revealed: Unknown +``` + ## Filtering overloads with variadic arguments and parameters TODO diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 86b16e4202..0993c4d4d2 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -607,7 +607,7 @@ wrapper_descriptor() wrapper_descriptor(f) # Calling it without the `owner` argument if `instance` is not `None` is an -# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" +# error: [invalid-argument-type] "Argument to wrapper descriptor `FunctionType.__get__` is incorrect: Expected `~None`, found `None`" wrapper_descriptor(f, None) # But calling it with an instance is fine (in this case, the `owner` argument is optional): diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md new file mode 100644 index 0000000000..63b762b320 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md @@ -0,0 +1,168 @@ +# Single matching overload + + + +## Limited number of overloads + +`overloaded.pyi`: + +```pyi +from typing import overload + +@overload +def f() -> None: ... +@overload +def f(x: int) -> int: ... +@overload +def f(x: int, y: int) -> int: ... +``` + +```py +from overloaded import f + +f("a") # error: [invalid-argument-type] +``` + +## Call to function with too many unmatched overloads + +This 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. + +`overloaded.pyi`: + +```pyi +from typing import overload + +@overload +def foo(a: int): ... +@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): ... +``` + +```py +from typing import overload + +from overloaded import foo + +foo("foo") # error: [invalid-argument-type] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md index e009c137a3..4f406c165f 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md @@ -59,6 +59,7 @@ just ensuring that we get test coverage for each of the possible diagnostic mess ```py from inspect import getattr_static +from typing import overload def f1() -> int: return 0 @@ -72,11 +73,19 @@ def f3(a: int, b: int) -> int: def f4[T: str](x: T) -> int: return 0 -class OverloadExample: - def f(self, x: str) -> int: - return 0 +@overload +def f5() -> None: ... +@overload +def f5(x: str) -> str: ... +def f5(x: str | None = None) -> str | None: + return x -f5 = getattr_static(OverloadExample, "f").__get__ +@overload +def f6() -> None: ... +@overload +def f6(x: str, y: str) -> str: ... +def f6(x: str | None = None, y: str | None = None) -> str | None: + return x + y if x and y else None def _(n: int): class PossiblyNotCallable: @@ -96,14 +105,17 @@ def _(n: int): f = 5 elif n == 5: f = f5 + elif n == 6: + f = f6 else: f = PossiblyNotCallable() # error: [too-many-positional-arguments] # error: [invalid-argument-type] "Argument to function `f2` is incorrect: Expected `str`, found `Literal[3]`" # error: [missing-argument] # error: [invalid-argument-type] "Argument to function `f4` is incorrect: Argument type `Literal[3]` does not satisfy upper bound of type variable `T`" + # error: [invalid-argument-type] "Argument to function `f5` is incorrect: Expected `str`, found `Literal[3]`" + # error: [no-matching-overload] "No overload of function `f6` matches arguments" # error: [call-non-callable] "Object of type `Literal[5]` is not callable" - # error: [no-matching-overload] # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" x = f(3) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type.md b/crates/ty_python_semantic/resources/mdtest/narrow/type.md index 5487a1af41..004d53be3f 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type.md @@ -78,7 +78,7 @@ No narrowing should occur if `type` is used to dynamically create a class: def _(x: str | int): # The following diagnostic is valid, since the three-argument form of `type` # can only be called with `str` as the first argument. - # error: [no-matching-overload] "No overload of class `type` matches arguments" + # error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`" if type(x, (), {}) is str: reveal_type(x) # revealed: str | int else: diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over…_-_Single_matching_over…_-_Call_to_function_wit…_(8fdf5a06afc7d4fe).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over…_-_Single_matching_over…_-_Call_to_function_wit…_(8fdf5a06afc7d4fe).snap new file mode 100644 index 0000000000..3761148d09 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over…_-_Single_matching_over…_-_Call_to_function_wit…_(8fdf5a06afc7d4fe).snap @@ -0,0 +1,226 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: single_matching_overload.md - Single matching overload - Call to function with too many unmatched overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md +--- + +# Python source files + +## overloaded.pyi + +``` + 1 | from typing import overload + 2 | + 3 | @overload + 4 | def foo(a: int): ... + 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): ... +``` + +## mdtest_snippet.py + +``` +1 | from typing import overload +2 | +3 | from overloaded import foo +4 | +5 | foo("foo") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `foo` is incorrect + --> src/mdtest_snippet.py:5:5 + | +3 | from overloaded import foo +4 | +5 | foo("foo") # error: [invalid-argument-type] + | ^^^^^ Expected `int`, found `Literal["foo"]` + | +info: Matching overload defined here + --> src/overloaded.pyi:4:5 + | +3 | @overload +4 | def foo(a: int): ... + | ^^^ ------ Parameter declared here +5 | @overload +6 | def foo(a: int, b: int, c: int): ... + | +info: Non-matching 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: ... omitted 12 overloads +info: rule `invalid-argument-type` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over…_-_Single_matching_over…_-_Limited_number_of_ov…_(93e9a157fdca3ab2).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over…_-_Single_matching_over…_-_Limited_number_of_ov…_(93e9a157fdca3ab2).snap new file mode 100644 index 0000000000..198de896b9 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over…_-_Single_matching_over…_-_Limited_number_of_ov…_(93e9a157fdca3ab2).snap @@ -0,0 +1,59 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: single_matching_overload.md - Single matching overload - Limited number of overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md +--- + +# Python source files + +## overloaded.pyi + +``` +1 | from typing import overload +2 | +3 | @overload +4 | def f() -> None: ... +5 | @overload +6 | def f(x: int) -> int: ... +7 | @overload +8 | def f(x: int, y: int) -> int: ... +``` + +## mdtest_snippet.py + +``` +1 | from overloaded import f +2 | +3 | f("a") # error: [invalid-argument-type] +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `f` is incorrect + --> src/mdtest_snippet.py:3:3 + | +1 | from overloaded import f +2 | +3 | f("a") # error: [invalid-argument-type] + | ^^^ Expected `int`, found `Literal["a"]` + | +info: Matching overload defined here + --> src/overloaded.pyi:6:5 + | +4 | def f() -> None: ... +5 | @overload +6 | def f(x: int) -> int: ... + | ^ ------ Parameter declared here +7 | @overload +8 | def f(x: int, y: int) -> int: ... + | +info: Non-matching overloads for function `f`: +info: () -> None +info: (x: int, y: int) -> int +info: rule `invalid-argument-type` is enabled by default + +``` diff --git a/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 b/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 index b8924b5134..b3244bc8cb 100644 --- a/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 +++ b/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 @@ -13,176 +13,236 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.m ``` 1 | from inspect import getattr_static - 2 | - 3 | def f1() -> int: - 4 | return 0 - 5 | - 6 | def f2(name: str) -> int: - 7 | return 0 - 8 | - 9 | def f3(a: int, b: int) -> int: -10 | return 0 -11 | -12 | def f4[T: str](x: T) -> int: -13 | return 0 -14 | -15 | class OverloadExample: -16 | def f(self, x: str) -> int: -17 | return 0 -18 | -19 | f5 = getattr_static(OverloadExample, "f").__get__ -20 | -21 | def _(n: int): -22 | class PossiblyNotCallable: -23 | if n == 0: -24 | def __call__(self) -> int: -25 | return 0 -26 | -27 | if n == 0: -28 | f = f1 -29 | elif n == 1: -30 | f = f2 -31 | elif n == 2: -32 | f = f3 -33 | elif n == 3: -34 | f = f4 -35 | elif n == 4: -36 | f = 5 -37 | elif n == 5: -38 | f = f5 -39 | else: -40 | f = PossiblyNotCallable() -41 | # error: [too-many-positional-arguments] -42 | # error: [invalid-argument-type] "Argument to function `f2` is incorrect: Expected `str`, found `Literal[3]`" -43 | # error: [missing-argument] -44 | # error: [invalid-argument-type] "Argument to function `f4` is incorrect: Argument type `Literal[3]` does not satisfy upper bound of type variable `T`" -45 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" -46 | # error: [no-matching-overload] -47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" -48 | x = f(3) + 2 | from typing import overload + 3 | + 4 | def f1() -> int: + 5 | return 0 + 6 | + 7 | def f2(name: str) -> int: + 8 | return 0 + 9 | +10 | def f3(a: int, b: int) -> int: +11 | return 0 +12 | +13 | def f4[T: str](x: T) -> int: +14 | return 0 +15 | +16 | @overload +17 | def f5() -> None: ... +18 | @overload +19 | def f5(x: str) -> str: ... +20 | def f5(x: str | None = None) -> str | None: +21 | return x +22 | +23 | @overload +24 | def f6() -> None: ... +25 | @overload +26 | def f6(x: str, y: str) -> str: ... +27 | def f6(x: str | None = None, y: str | None = None) -> str | None: +28 | return x + y if x and y else None +29 | +30 | def _(n: int): +31 | class PossiblyNotCallable: +32 | if n == 0: +33 | def __call__(self) -> int: +34 | return 0 +35 | +36 | if n == 0: +37 | f = f1 +38 | elif n == 1: +39 | f = f2 +40 | elif n == 2: +41 | f = f3 +42 | elif n == 3: +43 | f = f4 +44 | elif n == 4: +45 | f = 5 +46 | elif n == 5: +47 | f = f5 +48 | elif n == 6: +49 | f = f6 +50 | else: +51 | f = PossiblyNotCallable() +52 | # error: [too-many-positional-arguments] +53 | # error: [invalid-argument-type] "Argument to function `f2` is incorrect: Expected `str`, found `Literal[3]`" +54 | # error: [missing-argument] +55 | # error: [invalid-argument-type] "Argument to function `f4` is incorrect: Argument type `Literal[3]` does not satisfy upper bound of type variable `T`" +56 | # error: [invalid-argument-type] "Argument to function `f5` is incorrect: Expected `str`, found `Literal[3]`" +57 | # error: [no-matching-overload] "No overload of function `f6` matches arguments" +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) ``` # Diagnostics ``` error[call-non-callable]: Object of type `Literal[5]` is not callable - --> src/mdtest_snippet.py:48:9 + --> src/mdtest_snippet.py:60:9 | -46 | # error: [no-matching-overload] -47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" -48 | x = f(3) +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) | ^^^^ | info: Union variant `Literal[5]` is incompatible with this call site -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 | () | PossiblyNotCallable` +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] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `call-non-callable` is enabled by default ``` ``` error[call-non-callable]: Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method) - --> src/mdtest_snippet.py:48:9 + --> src/mdtest_snippet.py:60:9 | -46 | # error: [no-matching-overload] -47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" -48 | x = f(3) +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) | ^^^^ | info: Union variant `PossiblyNotCallable` is incompatible with this call site -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 | () | PossiblyNotCallable` +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] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `call-non-callable` is enabled by default ``` ``` error[missing-argument]: No argument provided for required parameter `b` of function `f3` - --> src/mdtest_snippet.py:48:9 + --> src/mdtest_snippet.py:60:9 | -46 | # error: [no-matching-overload] -47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" -48 | x = f(3) +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) | ^^^^ | info: Union variant `def f3(a: int, b: int) -> int` is incompatible with this call site -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 | () | PossiblyNotCallable` +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] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `missing-argument` is enabled by default ``` ``` -error[no-matching-overload]: No overload of method wrapper `__get__` of function `f` matches arguments - --> src/mdtest_snippet.py:48:9 +error[no-matching-overload]: No overload of function `f6` matches arguments + --> src/mdtest_snippet.py:60:9 | -46 | # error: [no-matching-overload] -47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" -48 | x = f(3) +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) | ^^^^ | -info: Union variant `` is incompatible with this call site -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 | () | PossiblyNotCallable` +info: First overload defined here + --> src/mdtest_snippet.py:24:5 + | +23 | @overload +24 | def f6() -> None: ... + | ^^^^^^^^^^^^ +25 | @overload +26 | def f6(x: str, y: str) -> str: ... + | +info: Possible overloads for function `f6`: +info: () -> None +info: (x: str, y: str) -> str +info: Overload implementation defined here + --> src/mdtest_snippet.py:27:5 + | +25 | @overload +26 | def f6(x: str, y: str) -> str: ... +27 | def f6(x: str | None = None, y: str | None = None) -> str | None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +28 | return x + y if x and y else None + | +info: Union variant `Overload[() -> None, (x: str, y: str) -> str]` is incompatible with this call site +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] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `no-matching-overload` is enabled by default ``` ``` error[invalid-argument-type]: Argument to function `f2` is incorrect - --> src/mdtest_snippet.py:48:11 + --> src/mdtest_snippet.py:60:11 | -46 | # error: [no-matching-overload] -47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" -48 | x = f(3) +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) | ^ Expected `str`, found `Literal[3]` | info: Function defined here - --> src/mdtest_snippet.py:6:5 + --> src/mdtest_snippet.py:7:5 | -4 | return 0 -5 | -6 | def f2(name: str) -> int: +5 | return 0 +6 | +7 | def f2(name: str) -> int: | ^^ --------- Parameter declared here -7 | return 0 +8 | return 0 | info: Union variant `def f2(name: str) -> int` is incompatible with this call site -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 | () | PossiblyNotCallable` +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] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `invalid-argument-type` is enabled by default ``` ``` error[invalid-argument-type]: Argument to function `f4` is incorrect - --> src/mdtest_snippet.py:48:11 + --> src/mdtest_snippet.py:60:11 | -46 | # error: [no-matching-overload] -47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" -48 | x = f(3) +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) | ^ Argument type `Literal[3]` does not satisfy upper bound of type variable `T` | info: Type variable defined here - --> src/mdtest_snippet.py:12:8 + --> src/mdtest_snippet.py:13:8 | -10 | return 0 -11 | -12 | def f4[T: str](x: T) -> int: +11 | return 0 +12 | +13 | def f4[T: str](x: T) -> int: | ^^^^^^ -13 | return 0 +14 | return 0 | info: Union variant `def f4(x: T) -> int` is incompatible with this call site -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 | () | PossiblyNotCallable` +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] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to function `f5` is incorrect + --> src/mdtest_snippet.py:60:11 + | +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) + | ^ Expected `str`, found `Literal[3]` + | +info: Matching overload defined here + --> src/mdtest_snippet.py:19:5 + | +17 | def f5() -> None: ... +18 | @overload +19 | def f5(x: str) -> str: ... + | ^^ ------ Parameter declared here +20 | def f5(x: str | None = None) -> str | None: +21 | return x + | +info: Non-matching overloads for function `f5`: +info: () -> None +info: Union variant `Overload[() -> None, (x: str) -> str]` is incompatible with this call site +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] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `invalid-argument-type` is enabled by default ``` ``` error[too-many-positional-arguments]: Too many positional arguments to function `f1`: expected 0, got 1 - --> src/mdtest_snippet.py:48:11 + --> src/mdtest_snippet.py:60:11 | -46 | # error: [no-matching-overload] -47 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" -48 | x = f(3) +58 | # error: [call-non-callable] "Object of type `Literal[5]` is not callable" +59 | # error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" +60 | x = f(3) | ^ | info: Union variant `def f1() -> int` is incompatible with this call site -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 | () | PossiblyNotCallable` +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] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable` info: rule `too-many-positional-arguments` is enabled by default ``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index fb41f559af..8d4f4b7600 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -4,6 +4,7 @@ //! union of types, each of which might contain multiple overloads. use std::collections::HashSet; +use std::fmt; use itertools::Itertools; use ruff_db::parsed::parsed_module; @@ -21,7 +22,9 @@ use crate::types::diagnostic::{ NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, }; -use crate::types::function::{DataclassTransformerParams, FunctionDecorators, KnownFunction}; +use crate::types::function::{ + DataclassTransformerParams, FunctionDecorators, FunctionType, KnownFunction, OverloadLiteral, +}; use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError}; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ @@ -257,7 +260,7 @@ impl<'db> Bindings<'db> { } }; - // Each special case listed here should have a corresponding clause in `Type::signatures`. + // Each special case listed here should have a corresponding clause in `Type::bindings`. for binding in &mut self.elements { let binding_type = binding.callable_type; for (overload_index, overload) in binding.matching_overloads_mut() { @@ -1032,6 +1035,7 @@ impl<'db> From> for Bindings<'db> { dunder_call_is_possibly_unbound: false, bound_type: None, overload_call_return_type: None, + matching_overload_index: None, overloads: smallvec![from], }; Bindings { @@ -1086,6 +1090,22 @@ pub(crate) struct CallableBinding<'db> { /// [`Unknown`]: crate::types::DynamicType::Unknown overload_call_return_type: Option>, + /// The index of the overload that matched for this overloaded callable. + /// + /// This is [`Some`] only for step 1 and 4 of the [overload call evaluation algorithm][1]. + /// + /// The main use of this field is to surface the diagnostics for a matching overload directly + /// instead of using the `no-matching-overload` diagnostic. This is mentioned in the spec: + /// + /// > If only one candidate overload remains, it is the winning match. Evaluate it as if it + /// > were a non-overloaded function call and stop. + /// + /// Other steps of the algorithm do not set this field because this use case isn't relevant for + /// them. + /// + /// [1]: https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation + matching_overload_index: Option, + /// The bindings of each overload of this callable. Will be empty if the type is not callable. /// /// By using `SmallVec`, we avoid an extra heap allocation for the common case of a @@ -1108,6 +1128,7 @@ impl<'db> CallableBinding<'db> { dunder_call_is_possibly_unbound: false, bound_type: None, overload_call_return_type: None, + matching_overload_index: None, overloads, } } @@ -1119,6 +1140,7 @@ impl<'db> CallableBinding<'db> { dunder_call_is_possibly_unbound: false, bound_type: None, overload_call_return_type: None, + matching_overload_index: None, overloads: smallvec![], } } @@ -1169,10 +1191,9 @@ impl<'db> CallableBinding<'db> { return; } MatchingOverloadIndex::Single(index) => { - // If only one candidate overload remains, it is the winning match. - // TODO: Evaluate it as a regular (non-overloaded) call. This means that any - // diagnostics reported in this check should be reported directly instead of - // reporting it as `no-matching-overload`. + // If only one candidate overload remains, it is the winning match. Evaluate it as + // a regular (non-overloaded) call. + self.matching_overload_index = Some(index); self.overloads[index].check_types( db, argument_types.as_ref(), @@ -1585,15 +1606,48 @@ impl<'db> CallableBinding<'db> { self.signature_type, callable_description.as_ref(), union_diag, + None, ); } _overloads => { - // When the number of unmatched overloads exceeds this number, we stop - // printing them to avoid excessive output. + // TODO: This should probably be adapted to handle more + // types of callables[1]. At present, it just handles + // standard function and method calls. // - // 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; + // [1]: https://github.com/astral-sh/ty/issues/274#issuecomment-2881856028 + let function_type_and_kind = match self.signature_type { + Type::FunctionLiteral(function) => Some((FunctionKind::Function, function)), + Type::BoundMethod(bound_method) => Some(( + FunctionKind::BoundMethod, + bound_method.function(context.db()), + )), + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { + Some((FunctionKind::MethodWrapper, function)) + } + _ => None, + }; + + // If there is a single matching overload, the diagnostics should be reported + // directly for that overload. + if let Some(matching_overload_index) = self.matching_overload_index { + let callable_description = + CallableDescription::new(context.db(), self.signature_type); + let matching_overload = + function_type_and_kind.map(|(kind, function)| MatchingOverloadLiteral { + index: matching_overload_index, + kind, + function, + }); + self.overloads[matching_overload_index].report_diagnostics( + context, + node, + self.signature_type, + callable_description.as_ref(), + union_diag, + matching_overload.as_ref(), + ); + return; + } let Some(builder) = context.report_lint(&NO_MATCHING_OVERLOAD, node) else { return; @@ -1608,18 +1662,6 @@ impl<'db> CallableBinding<'db> { String::new() } )); - // TODO: This should probably be adapted to handle more - // types of callables[1]. At present, it just handles - // standard function and method calls. - // - // [1]: https://github.com/astral-sh/ty/issues/274#issuecomment-2881856028 - let function_type_and_kind = match self.signature_type { - Type::FunctionLiteral(function) => Some(("function", function)), - Type::BoundMethod(bound_method) => { - Some(("bound method", bound_method.function(context.db()))) - } - _ => None, - }; if let Some((kind, function)) = function_type_and_kind { let (overloads, implementation) = function.overloads_and_implementation(context.db()); @@ -2033,9 +2075,17 @@ impl<'db> Binding<'db> { callable_ty: Type<'db>, callable_description: Option<&CallableDescription>, union_diag: Option<&UnionDiagnostic<'_, '_>>, + matching_overload: Option<&MatchingOverloadLiteral<'db>>, ) { for error in &self.errors { - error.report_diagnostic(context, node, callable_ty, callable_description, union_diag); + error.report_diagnostic( + context, + node, + callable_ty, + callable_description, + union_diag, + matching_overload, + ); } } @@ -2331,6 +2381,7 @@ impl<'db> BindingError<'db> { callable_ty: Type<'db>, callable_description: Option<&CallableDescription>, union_diag: Option<&UnionDiagnostic<'_, '_>>, + matching_overload: Option<&MatchingOverloadLiteral<'_>>, ) { match self { Self::InvalidArgumentType { @@ -2358,7 +2409,48 @@ impl<'db> BindingError<'db> { diag.set_primary_message(format_args!( "Expected `{expected_ty_display}`, found `{provided_ty_display}`" )); - if let Some((name_span, parameter_span)) = + + if let Some(matching_overload) = matching_overload { + if let Some((name_span, parameter_span)) = + matching_overload.get(context.db()).and_then(|overload| { + overload.parameter_span(context.db(), Some(parameter.index)) + }) + { + let mut sub = + SubDiagnostic::new(Severity::Info, "Matching overload defined here"); + sub.annotate(Annotation::primary(name_span)); + sub.annotate( + Annotation::secondary(parameter_span) + .message("Parameter declared here"), + ); + diag.sub(sub); + diag.info(format_args!( + "Non-matching overloads for {} `{}`:", + matching_overload.kind, + matching_overload.function.name(context.db()) + )); + let (overloads, _) = matching_overload + .function + .overloads_and_implementation(context.db()); + for (overload_index, overload) in + overloads.iter().enumerate().take(MAXIMUM_OVERLOADS) + { + if overload_index == matching_overload.index { + continue; + } + diag.info(format_args!( + " {}", + overload.signature(context.db(), None).display(context.db()) + )); + } + if overloads.len() > MAXIMUM_OVERLOADS { + diag.info(format_args!( + "... omitted {remaining} overloads", + remaining = overloads.len() - MAXIMUM_OVERLOADS + )); + } + } + } else if let Some((name_span, parameter_span)) = callable_ty.parameter_span(context.db(), Some(parameter.index)) { let mut sub = SubDiagnostic::new(Severity::Info, "Function defined here"); @@ -2368,6 +2460,7 @@ impl<'db> BindingError<'db> { ); diag.sub(sub); } + if let Some(union_diag) = union_diag { union_diag.add_union_context(context.db(), &mut diag); } @@ -2573,3 +2666,55 @@ impl UnionDiagnostic<'_, '_> { diag.sub(sub); } } + +/// Represents the matching overload of a function literal that was found via the overload call +/// evaluation algorithm. +struct MatchingOverloadLiteral<'db> { + /// The position of the matching overload in the list of overloads. + index: usize, + /// The kind of function this overload is for. + kind: FunctionKind, + /// The function literal that this overload belongs to. + /// + /// This is used to retrieve the overload at the given index. + function: FunctionType<'db>, +} + +impl<'db> MatchingOverloadLiteral<'db> { + /// Returns the [`OverloadLiteral`] representing this matching overload. + fn get(&self, db: &'db dyn Db) -> Option> { + let (overloads, _) = self.function.overloads_and_implementation(db); + + // TODO: This should actually be safe to index directly but isn't so as of this writing. + // The main reason is that we've custom overload signatures that are constructed manually + // and does not belong to any file. For example, the `__get__` method of a function literal + // has a custom overloaded signature. So, when we try to retrieve the actual overloads + // above, we get an empty list of overloads because the implementation of that method + // relies on it existing in the file. + overloads.get(self.index).copied() + } +} + +#[derive(Clone, Copy, Debug)] +enum FunctionKind { + Function, + BoundMethod, + MethodWrapper, +} + +impl fmt::Display for FunctionKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FunctionKind::Function => write!(f, "function"), + FunctionKind::BoundMethod => write!(f, "bound method"), + FunctionKind::MethodWrapper => write!(f, "method wrapper `__get__` of function"), + } + } +} + +// 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; diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index ae693bf731..3abc64830b 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -296,7 +296,7 @@ impl<'db> OverloadLiteral<'db> { ) } - fn parameter_span( + pub(crate) fn parameter_span( self, db: &'db dyn Db, parameter_index: Option,