From f1e380d5bfe632cde08c5cad8f2613c26e7d0e8b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 27 Dec 2024 00:59:29 +0100 Subject: [PATCH] Add NoMatch for invalid signatures in overloads --- mypy/checkexpr.py | 10 ++++++++++ mypy/typeanal.py | 18 +++++++++++++++++- mypy/types.py | 4 ++++ mypy/typeshed/stdlib/typing_extensions.pyi | 2 ++ test-data/unit/check-overloading.test | 18 ++++++++++++++++++ test-data/unit/lib-stub/typing_extensions.pyi | 2 ++ 6 files changed, 53 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 964149fa8df4..52f60d124393 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1485,6 +1485,16 @@ def check_call_expr_with_callee_type( proper_callee = get_proper_type(callee_type) if isinstance(e.callee, (NameExpr, MemberExpr)): self.chk.warn_deprecated_overload_item(e.callee.node, e, target=callee_type) + if ( + isinstance((p_type := get_proper_type(ret_type)), AnyType) + and p_type.type_of_any == TypeOfAny.no_match + and isinstance(proper_callee, CallableType) + ): + self.chk.fail( + f'No matching overload found for "{proper_callee.name}"', + context=e, + code=codes.CALL_OVERLOAD, + ) if isinstance(e.callee, RefExpr) and isinstance(proper_callee, CallableType): # Cache it for find_isinstance_check() if proper_callee.type_guard is not None: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 751ed85ea6f3..a4ae46722d7c 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -65,6 +65,7 @@ FINAL_TYPE_NAMES, LITERAL_TYPE_NAMES, NEVER_NAMES, + NO_MATCH_NAMES, TYPE_ALIAS_NAMES, AnyType, BoolTypeQuery, @@ -227,6 +228,7 @@ def __init__( allow_typed_dict_special_forms: bool = False, allow_param_spec_literals: bool = False, allow_unpack: bool = False, + allow_no_match: bool = False, report_invalid_types: bool = True, prohibit_self_type: str | None = None, prohibit_special_class_field_types: str | None = None, @@ -282,6 +284,7 @@ def __init__( self.allow_type_any = allow_type_any self.allow_type_var_tuple = False self.allow_unpack = allow_unpack + self.allow_no_match = allow_no_match def lookup_qualified( self, name: str, ctx: Context, suppress_errors: bool = False @@ -694,6 +697,15 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ return self.anal_type(t.args[0]) elif fullname in NEVER_NAMES: return UninhabitedType() + elif fullname in NO_MATCH_NAMES: + if not self.allow_no_match: + self.fail( + "NoMatch can only be used as an overload return annotation", + t, + code=codes.VALID_TYPE, + ) + return AnyType(TypeOfAny.from_error) + return AnyType(TypeOfAny.no_match) elif fullname in LITERAL_TYPE_NAMES: return self.analyze_literal_type(t) elif fullname in ANNOTATED_TYPE_NAMES: @@ -1158,7 +1170,7 @@ def visit_callable_type( arg_types=arg_types, arg_kinds=arg_kinds, arg_names=arg_names, - ret_type=self.anal_type(t.ret_type, nested=nested), + ret_type=self.anal_type(t.ret_type, nested=nested, allow_no_match=True), # If the fallback isn't filled in yet, # its type will be the falsey FakeInfo fallback=(t.fallback if t.fallback.type else self.named_type("builtins.function")), @@ -1877,6 +1889,7 @@ def anal_type( allow_unpack: bool = False, allow_ellipsis: bool = False, allow_typed_dict_special_forms: bool = False, + allow_no_match: bool = False, ) -> Type: if nested: self.nesting_level += 1 @@ -1886,6 +1899,8 @@ def anal_type( self.allow_ellipsis = allow_ellipsis old_allow_unpack = self.allow_unpack self.allow_unpack = allow_unpack + old_no_match = self.allow_no_match + self.allow_no_match = allow_no_match try: analyzed = t.accept(self) finally: @@ -1894,6 +1909,7 @@ def anal_type( self.allow_typed_dict_special_forms = old_allow_typed_dict_special_forms self.allow_ellipsis = old_allow_ellipsis self.allow_unpack = old_allow_unpack + self.allow_no_match = old_no_match if ( not allow_param_spec and isinstance(analyzed, ParamSpecType) diff --git a/mypy/types.py b/mypy/types.py index e92ab0889991..76a1b1a40d7f 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -173,6 +173,8 @@ # Supported @override decorator names. OVERRIDE_DECORATOR_NAMES: Final = ("typing.override", "typing_extensions.override") +NO_MATCH_NAMES: Final = ("typing.NoMatch", "typing_extensions.NoMatch") + # A placeholder used for Bogus[...] parameters _dummy: Final[Any] = object() @@ -209,6 +211,8 @@ class TypeOfAny: # used to ignore Anys inserted by the suggestion engine when # generating constraints. suggestion_engine: Final = 9 + # Does this Any come from NoMatch overload return annotation? + no_match: Final = 10 def deserialize_type(data: JsonDict | str) -> Type: diff --git a/mypy/typeshed/stdlib/typing_extensions.pyi b/mypy/typeshed/stdlib/typing_extensions.pyi index a6b606e6b670..d6199a809c91 100644 --- a/mypy/typeshed/stdlib/typing_extensions.pyi +++ b/mypy/typeshed/stdlib/typing_extensions.pyi @@ -529,6 +529,8 @@ else: ReadOnly: _SpecialForm TypeIs: _SpecialForm +NoMatch: _SpecialForm + class Doc: documentation: str def __init__(self, documentation: str, /) -> None: ... diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index 9d01ce6bd480..b7d4a92342e4 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -6768,3 +6768,21 @@ class D(Generic[T]): a: D[str] # E: Type argument "str" of "D" must be a subtype of "C" reveal_type(a.f(1)) # N: Revealed type is "builtins.int" reveal_type(a.f("x")) # N: Revealed type is "builtins.str" + +[case testOverloadNoMatch] +from typing import Sequence, overload +from typing_extensions import NoMatch + +@overload +def f1(args: str) -> NoMatch: ... +@overload +def f1(arg: Sequence[str]) -> int: ... +def f1(arg): ... + +reveal_type(f1(["Hello", "World"])) # N: Revealed type is "builtins.int" +reveal_type(f1("Hello World")) # E: No matching overload found for "f1" \ + # N: Revealed type is "Any" + +y: NoMatch # E: NoMatch can only be used as an overload return annotation +def f2(arg: NoMatch) -> int: ... # E: NoMatch can only be used as an overload return annotation +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index cb054b0e6b4f..998233f1e63b 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -45,6 +45,8 @@ ReadOnly: _SpecialForm Self: _SpecialForm +NoMatch: _SpecialForm + @final class TypeAliasType: def __init__(