Skip to content

Commit

Permalink
Add NoMatch for invalid signatures in overloads
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed Dec 27, 2024
1 parent ec04f73 commit f1e380d
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 1 deletion.
10 changes: 10 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 17 additions & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
FINAL_TYPE_NAMES,
LITERAL_TYPE_NAMES,
NEVER_NAMES,
NO_MATCH_NAMES,
TYPE_ALIAS_NAMES,
AnyType,
BoolTypeQuery,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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")),
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions mypy/typeshed/stdlib/typing_extensions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,8 @@ else:
ReadOnly: _SpecialForm
TypeIs: _SpecialForm

NoMatch: _SpecialForm

class Doc:
documentation: str
def __init__(self, documentation: str, /) -> None: ...
Expand Down
18 changes: 18 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
2 changes: 2 additions & 0 deletions test-data/unit/lib-stub/typing_extensions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ ReadOnly: _SpecialForm

Self: _SpecialForm

NoMatch: _SpecialForm

@final
class TypeAliasType:
def __init__(
Expand Down

0 comments on commit f1e380d

Please sign in to comment.