From 69393b2e6ee1eec4fc69906f8f6514994218e3b3 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Thu, 15 May 2025 11:39:14 -0400 Subject: [PATCH] [ty] Improve invalid method calls for unmatched overloads (#18122) This makes an easy tweak to allow our diagnostics for unmatched overloads to apply to method calls. Previously, they only worked for function calls. There is at least one other case worth addressing too, namely, class literals. e.g., `type()`. We had a diagnostic snapshot test case to track it. Closes astral-sh/ty#274 --- .../diagnostics/no_matching_overload.md | 26 ++++++++ ...-_A_class_constructor_…_(dd9f8a8f736a329).snap | 29 +++++++++ ..._A_method_call_with_u…_(31cb5f881221158e).snap | 63 +++++++++++++++++++ .../ty_python_semantic/src/types/call/bind.rs | 16 ++++- 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_class_constructor_…_(dd9f8a8f736a329).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_method_call_with_u…_(31cb5f881221158e).snap diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md index c71e608e6e..19627f8351 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md @@ -278,3 +278,29 @@ def f( f(b"foo") # error: [no-matching-overload] ``` + +## A method call with unmatched overloads + +```py +from typing import overload + +class Foo: + @overload + def bar(self, x: int) -> int: ... + @overload + def bar(self, x: str) -> str: ... + def bar(self, x: int | str) -> int | str: + return x + +foo = Foo() +foo.bar(b"wat") # error: [no-matching-overload] +``` + +## A class constructor with unmatched overloads + +TODO: At time of writing (2025-05-15), this has non-ideal diagnostics that doesn't show the +unmatched overloads. + +```py +type() # error: [no-matching-overload] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_class_constructor_…_(dd9f8a8f736a329).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_class_constructor_…_(dd9f8a8f736a329).snap new file mode 100644 index 0000000000..9a446afe71 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_class_constructor_…_(dd9f8a8f736a329).snap @@ -0,0 +1,29 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: no_matching_overload.md - No matching overload diagnostics - A class constructor with unmatched overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | type() # error: [no-matching-overload] +``` + +# Diagnostics + +``` +error[no-matching-overload]: No overload of class `type` matches arguments + --> src/mdtest_snippet.py:1:1 + | +1 | type() # error: [no-matching-overload] + | ^^^^^^ + | +info: rule `no-matching-overload` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_method_call_with_u…_(31cb5f881221158e).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_method_call_with_u…_(31cb5f881221158e).snap new file mode 100644 index 0000000000..66eccf602a --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_method_call_with_u…_(31cb5f881221158e).snap @@ -0,0 +1,63 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: no_matching_overload.md - No matching overload diagnostics - A method call with unmatched overloads +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import overload + 2 | + 3 | class Foo: + 4 | @overload + 5 | def bar(self, x: int) -> int: ... + 6 | @overload + 7 | def bar(self, x: str) -> str: ... + 8 | def bar(self, x: int | str) -> int | str: + 9 | return x +10 | +11 | foo = Foo() +12 | foo.bar(b"wat") # error: [no-matching-overload] +``` + +# Diagnostics + +``` +error[no-matching-overload]: No overload of bound method `bar` matches arguments + --> src/mdtest_snippet.py:12:1 + | +11 | foo = Foo() +12 | foo.bar(b"wat") # error: [no-matching-overload] + | ^^^^^^^^^^^^^^^ + | +info: First overload defined here + --> src/mdtest_snippet.py:5:9 + | +3 | class Foo: +4 | @overload +5 | def bar(self, x: int) -> int: ... + | ^^^^^^^^^^^^^^^^^^^^^^^^ +6 | @overload +7 | def bar(self, x: str) -> str: ... + | +info: Possible overloads for bound method `bar`: +info: (self, x: int) -> int +info: (self, x: str) -> str +info: Overload implementation defined here + --> src/mdtest_snippet.py:8:9 + | +6 | @overload +7 | def bar(self, x: str) -> str: ... +8 | def bar(self, x: int | str) -> int | str: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +9 | return x + | +info: rule `no-matching-overload` 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 52189b9602..7f0500815b 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1120,7 +1120,19 @@ impl<'db> CallableBinding<'db> { String::new() } )); - if let Some(function) = self.signature_type.into_function_literal() { + // 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 { if let Some(overloaded_function) = function.to_overloaded(context.db()) { if let Some(spans) = overloaded_function .overloads @@ -1134,7 +1146,7 @@ impl<'db> CallableBinding<'db> { } diag.info(format_args!( - "Possible overloads for function `{}`:", + "Possible overloads for {kind} `{}`:", function.name(context.db()) ));