Skip to content

Conversation

@BurntSushi
Copy link
Member

@BurntSushi BurntSushi commented May 13, 2025

This PR takes a crack at improving diagnostics for invalid overloaded
function calls as a result of there being no matching overloads.

I think this is a strict improvement on the status quo, but it's
definitely not as good as we can do. In particular, this only
adds sub-diagnostics that points to each unmatched overload, but
doesn't say why each overload does not match. I didn't do this
because 1) this seemed like an improvement we could merge as-is and
2) I wasn't sure how to match up the overloaded FunctionTypes
returned by FunctionType::to_overloaded with the overloads in a
CallableBinding. It seems like they are in correspondence with
one another, but I'm not sure if this is an API guarantee.

I guess ideally (from my uninformed perspective), it would be nice to
have the overloaded FunctionType on the Binding. But I wasn't
certain how best to go about that, or if that's the right thing to do.

Fixes astral-sh/ty#274

@MichaReiser MichaReiser changed the title [ty] ty_python_semantic: improve diagnostics for failure to call overloaded function [ty] improve diagnostics for failure to call overloaded function May 13, 2025
@MichaReiser MichaReiser added ty Multi-file analysis & type inference diagnostics Related to reporting of diagnostics. labels May 13, 2025
@BurntSushi BurntSushi requested review from dhruvmanila and removed request for dcreager and sharkdp May 13, 2025 17:19
@BurntSushi
Copy link
Member Author

Here are screenshots showing before/after for this Python file:

from typing import overload

@overload
def f(x: int) -> int: ...

@overload
def f(x: str) -> str: ...

def f(x: int | str) -> int | str:
    return x

f(b"foo")

Before:

before

After:

after

@github-actions
Copy link
Contributor

github-actions bot commented May 13, 2025

mypy_primer results

No ecosystem changes detected ✅

@AlexWaygood
Copy link
Member

I'm not sure having a separate subdiagnostic for each overload definition scales well for functions with many overloads. Here's a single diagnostic I get for a bad pow() call with your branch:

error[no-matching-overload]: No overload of function `pow` matches arguments
 --> foo.py:3:1
  |
1 | class Foo: ...
2 |
3 | pow(Foo(), Foo())
  | ^^^^^^^^^^^^^^^^^
  |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1700:5
     |
1698 | # but adding a `NoReturn` overload isn't a good solution for expressing that (see #8566).
1699 | @overload
1700 | def pow(base: int, exp: int, mod: int) -> int: ...
     |     ^^^
1701 | @overload
1702 | def pow(base: int, exp: Literal[0], mod: None = None) -> Literal[1]: ...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1702:5
     |
1700 | def pow(base: int, exp: int, mod: int) -> int: ...
1701 | @overload
1702 | def pow(base: int, exp: Literal[0], mod: None = None) -> Literal[1]: ...
     |     ^^^
1703 | @overload
1704 | def pow(base: int, exp: _PositiveInteger, mod: None = None) -> int: ...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1704:5
     |
1702 | def pow(base: int, exp: Literal[0], mod: None = None) -> Literal[1]: ...
1703 | @overload
1704 | def pow(base: int, exp: _PositiveInteger, mod: None = None) -> int: ...
     |     ^^^
1705 | @overload
1706 | def pow(base: int, exp: _NegativeInteger, mod: None = None) -> float: ...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1706:5
     |
1704 | def pow(base: int, exp: _PositiveInteger, mod: None = None) -> int: ...
1705 | @overload
1706 | def pow(base: int, exp: _NegativeInteger, mod: None = None) -> float: ...
     |     ^^^
1707 |
1708 | # int base & positive-int exp -> int; int base & negative-int exp -> float
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1711:5
     |
1709 | # return type must be Any as `int | float` causes too many false-positive errors
1710 | @overload
1711 | def pow(base: int, exp: int, mod: None = None) -> Any: ...
     |     ^^^
1712 | @overload
1713 | def pow(base: _PositiveInteger, exp: float, mod: None = None) -> float: ...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1713:5
     |
1711 | def pow(base: int, exp: int, mod: None = None) -> Any: ...
1712 | @overload
1713 | def pow(base: _PositiveInteger, exp: float, mod: None = None) -> float: ...
     |     ^^^
1714 | @overload
1715 | def pow(base: _NegativeInteger, exp: float, mod: None = None) -> complex: ...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1715:5
     |
1713 | def pow(base: _PositiveInteger, exp: float, mod: None = None) -> float: ...
1714 | @overload
1715 | def pow(base: _NegativeInteger, exp: float, mod: None = None) -> complex: ...
     |     ^^^
1716 | @overload
1717 | def pow(base: float, exp: int, mod: None = None) -> float: ...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1717:5
     |
1715 | def pow(base: _NegativeInteger, exp: float, mod: None = None) -> complex: ...
1716 | @overload
1717 | def pow(base: float, exp: int, mod: None = None) -> float: ...
     |     ^^^
1718 |
1719 | # float base & float exp could return float or complex
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1723:5
     |
1721 | # as `float | complex` causes too many false-positive errors
1722 | @overload
1723 | def pow(base: float, exp: complex | _SupportsSomeKindOfPow, mod: None = None) -> Any: ...
     |     ^^^
1724 | @overload
1725 | def pow(base: complex, exp: complex | _SupportsSomeKindOfPow, mod: None = None) -> complex: ...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1725:5
     |
1723 | def pow(base: float, exp: complex | _SupportsSomeKindOfPow, mod: None = None) -> Any: ...
1724 | @overload
1725 | def pow(base: complex, exp: complex | _SupportsSomeKindOfPow, mod: None = None) -> complex: ...
     |     ^^^
1726 | @overload
1727 | def pow(base: _SupportsPow2[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co: ...  # type: ignore[overload-overlap]
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1727:5
     |
1725 | def pow(base: complex, exp: complex | _SupportsSomeKindOfPow, mod: None = None) -> complex: ...
1726 | @overload
1727 | def pow(base: _SupportsPow2[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co: ...  # type: ignore[overload-overlap]
     |     ^^^
1728 | @overload
1729 | def pow(base: _SupportsPow3NoneOnly[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co: ...  # type: ignore[overload-ov...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1729:5
     |
1727 | def pow(base: _SupportsPow2[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co: ...  # type: ignore[overload-overlap]
1728 | @overload
1729 | def pow(base: _SupportsPow3NoneOnly[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co: ...  # type: ignore[overload-ov...
     |     ^^^
1730 | @overload
1731 | def pow(base: _SupportsPow3[_E_contra, _M_contra, _T_co], exp: _E_contra, mod: _M_contra) -> _T_co: ...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1731:5
     |
1729 | def pow(base: _SupportsPow3NoneOnly[_E_contra, _T_co], exp: _E_contra, mod: None = None) -> _T_co: ...  # type: ignore[overload-ov...
1730 | @overload
1731 | def pow(base: _SupportsPow3[_E_contra, _M_contra, _T_co], exp: _E_contra, mod: _M_contra) -> _T_co: ...
     |     ^^^
1732 | @overload
1733 | def pow(base: _SupportsSomeKindOfPow, exp: float, mod: None = None) -> Any: ...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1733:5
     |
1731 | def pow(base: _SupportsPow3[_E_contra, _M_contra, _T_co], exp: _E_contra, mod: _M_contra) -> _T_co: ...
1732 | @overload
1733 | def pow(base: _SupportsSomeKindOfPow, exp: float, mod: None = None) -> Any: ...
     |     ^^^
1734 | @overload
1735 | def pow(base: _SupportsSomeKindOfPow, exp: complex, mod: None = None) -> complex: ...
     |
info: Unmatched overload defined here
    --> stdlib/builtins.pyi:1735:5
     |
1733 | def pow(base: _SupportsSomeKindOfPow, exp: float, mod: None = None) -> Any: ...
1734 | @overload
1735 | def pow(base: _SupportsSomeKindOfPow, exp: complex, mod: None = None) -> complex: ...
     |     ^^^
1736 |
1737 | quit: _sitebuiltins.Quitter
     |
info: rule `no-matching-overload` is enabled by default

Found 1 diagnostic

Does pow() have a lot of overloads? Yeah, it does. I've seen real-world functions out there in the wild with hundreds of overloads, though. (Usually generated code! But still code that we need to be able to handle!)

@AlexWaygood
Copy link
Member

By comparison, here's mypy's diagnostic for the same bad pow() call. It's much more concise, but still tells me everything I need to know:

main.py:3: error: No overload variant of "pow" matches argument types "Foo", "Foo"  [call-overload]
main.py:3: note: Possible overload variants:
main.py:3: note:     def pow(base: int, exp: int, mod: int) -> int
main.py:3: note:     def pow(base: int, exp: Literal[0], mod: None = ...) -> Literal[1]
main.py:3: note:     def pow(base: int, exp: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25], mod: None = ...) -> int
main.py:3: note:     def pow(base: int, exp: Literal[-1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -19, -20], mod: None = ...) -> float
main.py:3: note:     def pow(base: int, exp: int, mod: None = ...) -> Any
main.py:3: note:     def pow(base: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25], exp: float, mod: None = ...) -> float
main.py:3: note:     def pow(base: Literal[-1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -19, -20], exp: float, mod: None = ...) -> complex
main.py:3: note:     def pow(base: float, exp: int, mod: None = ...) -> float
main.py:3: note:     def pow(base: float, exp: complex | _SupportsPow2[Any, Any] | _SupportsPow3NoneOnly[Any, Any] | _SupportsPow3[Any, Any, Any], mod: None = ...) -> Any
main.py:3: note:     def pow(base: complex, exp: complex | _SupportsPow2[Any, Any] | _SupportsPow3NoneOnly[Any, Any] | _SupportsPow3[Any, Any, Any], mod: None = ...) -> complex
main.py:3: note:     def [_E, _T_co] pow(base: _SupportsPow2[_E, _T_co], exp: _E, mod: None = ...) -> _T_co
main.py:3: note:     def [_E, _T_co] pow(base: _SupportsPow3NoneOnly[_E, _T_co], exp: _E, mod: None = ...) -> _T_co
main.py:3: note:     def [_E, _M, _T_co] pow(base: _SupportsPow3[_E, _M, _T_co], exp: _E, mod: _M) -> _T_co
main.py:3: note:     def pow(base: _SupportsPow2[Any, Any] | _SupportsPow3NoneOnly[Any, Any] | _SupportsPow3[Any, Any, Any], exp: float, mod: None = ...) -> Any
main.py:3: note:     def pow(base: _SupportsPow2[Any, Any] | _SupportsPow3NoneOnly[Any, Any] | _SupportsPow3[Any, Any, Any], exp: complex, mod: None = ...) -> complex
Found 1 error in 1 file (checked 1 source file)

@MichaReiser
Copy link
Member

What does rustc do if there's multiple possible methods to call (I think I saw this in the past). Does it use notes or sub diagnostics?

@BurntSushi
Copy link
Member Author

Notes are sub-diagnostics.

@AlexWaygood Those don't include the surrounding context or line numbers of the function definitions though? I think that might be swinging too far in the concise direction?

In any case, we don't have a way, today, of rendering concise single-line code snippets. One thing we could do, I think relatively easily, is add a way of specifying the context window size for code snippets. It's hard-coded to 2 lines (above and below), but we could shrink that down to 0. It won't be as concise as mypy, but it will be more concise than what we have today.

I think this PR is an improvement over the status quo. I definitely do not claim it is the best we can do though.

@AlexWaygood
Copy link
Member

AlexWaygood commented May 13, 2025

Those don't include the surrounding context or line numbers of the function definitions though? I think that might be swinging too far in the concise direction?

Why are those useful in this situation? I find them distracting, personally; I think all I care about in this situation is the types, if I'm a user. I think you can get a pretty-printed version of the signature of each overload by calling .signature(db).display(db) on each overload, and then that could be easily incorporated into a note; we don't necessarily need an annotated snippet of the original source code here.

@sharkdp
Copy link
Contributor

sharkdp commented May 13, 2025

we don't necessarily need an annotated snippet of the original source code here.

Two counter-arguments (even though I also partially agree with you @AlexWaygood):

  • Not all overloads come from typeshed. The mistake might not be related to the call-site. The overload could be wrong as well. In this case, I might want to jump to the overload definition.
  • An overload could theoretically include a (doc) comment with more information. In this case, showing the context might be useful.

@AlexWaygood
Copy link
Member

I agree that having one link back to the original source code is useful, for sure. But a separate snippet for each overload feels quite excessive to me!

@sharkdp
Copy link
Contributor

sharkdp commented May 13, 2025

But a separate snippet for each overload feels quite excessive to me!

Yes, maybe. But that pow example also shouldn't be our target to calibrate this, I think? I actually have the opposite problem in an example I just tried. This includes too little context. I can't even see the whole function signature, because only the function name seems to belong to the snippet range.

error[no-matching-overload]: No overload of function `dataclass` matches arguments
  --> /home/shark/pydantic_test/main.py:12:2
   |
12 | @dataclasses.dataclass(init="no")
   |  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
13 | class Config:
14 |     name: str
   |
info: Unmatched overload defined here
  --> /home/shark/pydantic_test/.venv/lib/python3.13/site-packages/pydantic/dataclasses.py:32:9
   |
30 |     @dataclass_transform(field_specifiers=(dataclasses.field, Field, PrivateAttr))
31 |     @overload
32 |     def dataclass(
   |         ^^^^^^^^^
33 |         *,
34 |         init: Literal[False] = False,
   |
info: Unmatched overload defined here
  --> /home/shark/pydantic_test/.venv/lib/python3.13/site-packages/pydantic/dataclasses.py:49:9
   |
47 |     @dataclass_transform(field_specifiers=(dataclasses.field, Field, PrivateAttr))
48 |     @overload
49 |     def dataclass(
   |         ^^^^^^^^^
50 |         _cls: type[_T],  # type: ignore
51 |         *,
   |
info: Overload implementation defined here
   --> /home/shark/pydantic_test/.venv/lib/python3.13/site-packages/pydantic/dataclasses.py:98:5
    |
 97 | @dataclass_transform(field_specifiers=(dataclasses.field, Field, PrivateAttr))
 98 | def dataclass(
    |     ^^^^^^^^^
 99 |     _cls: type[_T] | None = None,
100 |     *,
    |
info: rule `no-matching-overload` is enabled by default

@AlexWaygood
Copy link
Member

AlexWaygood commented May 13, 2025

This includes too little context. I can't even see the whole function signature, because only the function name seems to belong to the snippet range.

But this would also be solved by my proposal of printing the signature of each overload as a note rather than attempting to go back to the original source code of each overload as an annotation, I think?

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a huge improvement to what we have today. The only thing I would change (in this iteration) is to highlight the function up to all arguments so that the entire signature is visible

I do agree with Alex that it would be nice to have a more compact representation. But I don't think this should be blocking for this improvement (and getting there is probably also easier because of what's implemented in this PR)

Copy link
Member

@dhruvmanila dhruvmanila left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Currently, there are only a few reasons for why an overload might not match - arity mismatch for the arguments passed in to the required parameters or a non-assignable type. This excludes the reasons due to Todo type because ty doesn't support certain features like unpacking arguments.

I'm thinking to either tackle that as part of astral-sh/ty#104 or follow-up to that i.e., store the reason for why a certain overload is not matched. But, if you're planning to do this as a follow-up to this PR, I'm happy to just extend that when the full algorithm is implemented.

@AlexWaygood
Copy link
Member

AlexWaygood commented May 13, 2025

Here's a diff relative to this PR branch that would implement my suggestion:

diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs
index 33fab5cc6a..7930b3c896 100644
--- a/crates/ty_python_semantic/src/types/call/bind.rs
+++ b/crates/ty_python_semantic/src/types/call/bind.rs
@@ -1113,34 +1113,37 @@ impl<'db> CallableBinding<'db> {
                         String::new()
                     }
                 ));
-                if let Some(overloaded_types) = self
-                    .signature_type
-                    .into_function_literal()
-                    .and_then(|funty| funty.to_overloaded(context.db()))
-                {
-                    for overload_type in &overloaded_types.overloads {
-                        if let Some((name_span, _)) =
-                            overload_type.parameter_span(context.db(), None)
+                if let Some(function) = self.signature_type.into_function_literal() {
+                    if let Some(overloaded_function) = function.to_overloaded(context.db()) {
+                        if let Some((first_overload_span, _)) =
+                            overloaded_function.overloads[0].parameter_span(context.db(), None)
+                        {
+                            let mut sub =
+                                SubDiagnostic::new(Severity::Info, "First overload defined here");
+                            sub.annotate(Annotation::primary(first_overload_span));
+                            diag.sub(sub);
+                        }
+
+                        diag.info(format_args!(
+                            "Possible overloads for function `{}`:",
+                            function.name(context.db())
+                        ));
+                        for overload in &function.signature(context.db()).overloads.overloads {
+                            diag.info(format_args!("  {}", overload.display(context.db())));
+                        }
+
+                        if let Some((name_span, _)) = overloaded_function
+                            .implementation
+                            .and_then(|funty| funty.parameter_span(context.db(), None))
                         {
                             let mut sub = SubDiagnostic::new(
                                 Severity::Info,
-                                "Unmatched overload defined here",
+                                "Overload implementation defined here",
                             );
                             sub.annotate(Annotation::primary(name_span));
                             diag.sub(sub);
                         }
                     }
-                    if let Some((name_span, _)) = overloaded_types
-                        .implementation
-                        .and_then(|funty| funty.parameter_span(context.db(), None))
-                    {
-                        let mut sub = SubDiagnostic::new(
-                            Severity::Info,
-                            "Overload implementation defined here",
-                        );
-                        sub.annotate(Annotation::primary(name_span));
-                        diag.sub(sub);
-                    }
                 }
                 if let Some(union_diag) = union_diag {
                     union_diag.add_union_context(context.db(), &mut diag);
diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs
index 25169d3f57..182cce3eb4 100644
--- a/crates/ty_python_semantic/src/types/signatures.rs
+++ b/crates/ty_python_semantic/src/types/signatures.rs
@@ -123,7 +123,7 @@ pub(crate) struct CallableSignature<'db> {
     ///
     /// By using `SmallVec`, we avoid an extra heap allocation for the common case of a
     /// non-overloaded callable.
-    overloads: SmallVec<[Signature<'db>; 1]>,
+    pub(crate) overloads: SmallVec<[Signature<'db>; 1]>,
 }

Here's what the diagnostic would look like for my example with that change applied:

image

And here's what the diagnostic would look like on @sharkdp's example (in release mode, without the Todo-type messages):

image

@AlexWaygood
Copy link
Member

I've seen real-world functions out there in the wild with hundreds of overloads, though. (Usually generated code! But still code that we need to be able to handle!)

Here's the project I was thinking of. Around 11,000 lines of overloads for a single function: https://github.com/henribru/google-api-python-client-stubs/blob/master/googleapiclient-stubs/discovery.pyi

@BurntSushi
Copy link
Member Author

@dhruvmanila

store the reason for why a certain overload is not matched

Does this already happen as part of errors on Binding? (In my first example above, it has BindingError::InvalidArgumentType.)

It looks like support for `@overload` has been added since this test was
created, so we remove the TODO and add a snippet (from #274).
I found the previous code somewhat harder to read. Namely, a `for`
loop was being used to encode "execute zero or one times, but not
more." Which is sometimes okay, but it seemed clearer to me to use
more explicit case analysis here.

This should have no behavioral changes.
These are, after all, specific to function types. The methods on `Type`
are more like conveniences that return something when the type *happens*
to be a function. But defining them on `FunctionType` itself makes it
easy to call them when you have a `FunctionType` instead of a `Type`.
@BurntSushi
Copy link
Member Author

@AlexWaygood Thank you! I ended up taking your patch. I'm still a little worried about printing reformatted code, but given that it seems like "tons of overloads" is not altogether uncommon, maybe it does make sense to prioritize concision here.

I did also tweak the spans (on the implementation and the first unmatched overload) to cover the entire parameter list, which should at least give good context to address @sharkdp's and @MichaReiser's concerns.

I've also added snapshot tests to cover these cases (pow and something resembling @sharkdp's use case, i.e., a function with a lot of parameters).

As for @dhruvmanila:

store the reason for why a certain overload is not matched

I think there are two challenges with surfacing this information at present. The first is information display. If there are a lot of unmatched overloads, then how do we preserve conciseness while also showing the reason why each overload didn't match? That seems tricky. The other challenge here, I think, is how to match up the overload bindings with the overload types returned by FunctionType::to_overloaded. (But you might know how to solve that challenge. It just isn't obvious to me.)

@BurntSushi BurntSushi force-pushed the ag/diag-overload branch 2 times, most recently from 5620ce0 to 22854da Compare May 14, 2025 13:58
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks you -- this looks awesome!

@BurntSushi
Copy link
Member Author

I switched the snapshot test from using pow to something more custom, since it seems like some of the pow signatures get Todo types in them depending on whether debug_assertions are enabled. (Which makes snapshotting annoying.)

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

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

Fixes #274
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work, thank you!!

@BurntSushi BurntSushi merged commit faf54c0 into main May 14, 2025
35 checks passed
@BurntSushi BurntSushi deleted the ag/diag-overload branch May 14, 2025 15:13
@dhruvmanila
Copy link
Member

I think there are two challenges with surfacing this information at present. The first is information display. If there are a lot of unmatched overloads, then how do we preserve conciseness while also showing the reason why each overload didn't match? That seems tricky.

Yeah, this is tricky. I haven't thought about how much information should be displayed for this case. One way would be to group overloads based on the reason. For example, if there are multiple overloads which are not matched because of arity mismatch, they can be grouped under a single sub-diagnostic. We can then have sub-diagnostics for each reason and each of those sub-diagnostics would indicate the overloads that have been excluded for that specific reason. Does that make sense?

There are still open questions for the above approach like should we group multiple overloads which have invalid argument type at different positions and if so, how do we highlight the arguments in each of those overloads.

If this isn't a feasible solution, we could move back to highlighting each of the overloads like in the first version of this PR and limit the number of overloads that are displayed. The current limit is 50 but that seems a lot as well if we display the code frame as well.

The other challenge here, I think, is how to match up the overload bindings with the overload types returned by FunctionType::to_overloaded. (But you might know how to solve that challenge. It just isn't obvious to me.)

I'm not exactly sure what do you mean here. Can you say more?

@AlexWaygood
Copy link
Member

Fixes #274

@BurntSushi -- we need to include the full link to the GitHub issue now that the issues are in a different repo ("Fixes astral-sh/ty#274" rather than "Fixes #274")

@BurntSushi
Copy link
Member Author

BurntSushi commented May 14, 2025

The other challenge here, I think, is how to match up the overload bindings with the overload types returned by FunctionType::to_overloaded. (But you might know how to solve that challenge. It just isn't obvious to me.)

I'm not exactly sure what do you mean here. Can you say more?

There is a sequence of overload bindings and also a sequence of overload types. They might be in correspondence, but it's unclear to me if that's true. (And maybe there is another way to match them up. This could very well be a problem of my own ignorance here!)

@dhruvmanila
Copy link
Member

There is a sequence of overload bindings and also a sequence of overload types. They might be in correspondence, but it's unclear to me if that's true.

Thank you for the reference. Yes, I think those should be in correspondence. So, every overload should create a corresponding Binding while the Binding::errors field would signal whether the binding succeeded or not. (cc @dcreager who has more context on the call semantics.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

diagnostics Related to reporting of diagnostics. ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve diagnostics for failed call to overloaded function

6 participants