From 22177e6915ffe5636f3dfdf15a7dd8d9d741b3ee Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 20 Jun 2025 08:36:49 +0530 Subject: [PATCH] [ty] Surface matched overload diagnostic directly (#18452) ## Summary This PR resolves the way diagnostics are reported for an invalid call to an overloaded function. If any of the steps in the overload call evaluation algorithm yields a matching overload but it's type checking that failed, the `no-matching-overload` diagnostic is incorrect because there is a matching overload, it's the arguments passed that are invalid as per the signature. So, this PR improves that by surfacing the diagnostics on the matching overload directly. It also provides additional context, specifically the matching overload where this error occurred and other non-matching overloads. Consider the following example: ```py from typing import overload @overload def f() -> None: ... @overload def f(x: int) -> int: ... @overload def f(x: int, y: int) -> int: ... def f(x: int | None = None, y: int | None = None) -> int | None: return None f("a") ``` We get: Screenshot 2025-06-18 at 11 07 10 ## Test Plan Update test cases, resolve existing todos and validate the updated snapshots. --- .../resources/mdtest/call/builtins.md | 20 +- .../resources/mdtest/call/methods.md | 2 +- .../resources/mdtest/call/overloads.md | 47 +++- .../resources/mdtest/descriptor_protocol.md | 2 +- .../diagnostics/single_matching_overload.md | 168 ++++++++++++ .../mdtest/diagnostics/union_call.md | 22 +- .../resources/mdtest/narrow/type.md | 2 +- ..._Call_to_function_wit…_(8fdf5a06afc7d4fe).snap | 226 ++++++++++++++++ ..._Limited_number_of_ov…_(93e9a157fdca3ab2).snap | 59 +++++ ..._Cover_non-keyword_re…_(707b284610419a54).snap | 248 +++++++++++------- .../ty_python_semantic/src/types/call/bind.rs | 195 ++++++++++++-- .../ty_python_semantic/src/types/function.rs | 2 +- 12 files changed, 856 insertions(+), 137 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/diagnostics/single_matching_overload.md create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over…_-_Single_matching_over…_-_Call_to_function_wit…_(8fdf5a06afc7d4fe).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over…_-_Single_matching_over…_-_Limited_number_of_ov…_(93e9a157fdca3ab2).snap 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,