diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 9990caaeb7a1..1ac66aa57654 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2813,7 +2813,9 @@ def check_overload_call( code = None else: code = codes.OPERATOR - self.msg.no_variant_matches_arguments(callee, arg_types, context, code=code) + self.msg.no_variant_matches_arguments( + callee, arg_types, context, arg_names=arg_names, arg_kinds=arg_kinds, code=code + ) result = self.check_call( target, diff --git a/mypy/messages.py b/mypy/messages.py index 1e589e1bdf04..be2840580fcd 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -44,6 +44,7 @@ ArgKind, CallExpr, Context, + Decorator, Expression, FuncDef, IndexExpr, @@ -1085,14 +1086,89 @@ def no_variant_matches_arguments( arg_types: list[Type], context: Context, *, + arg_names: Sequence[str | None] | None, + arg_kinds: list[ArgKind] | None = None, code: ErrorCode | None = None, ) -> None: code = code or codes.CALL_OVERLOAD name = callable_name(overload) if name: name_str = f" of {name}" + for_func = f" for overloaded function {name}" else: name_str = "" + for_func = "" + + # For keyword argument errors + unexpected_kwargs: list[tuple[str, Type]] = [] + if arg_names is not None and arg_kinds is not None: + all_valid_kwargs: set[str] = set() + for item in overload.items: + for i, arg_name in enumerate(item.arg_names): + if arg_name is not None and item.arg_kinds[i] != ARG_STAR: + all_valid_kwargs.add(arg_name) + if item.is_kw_arg: + all_valid_kwargs.clear() + break + + if all_valid_kwargs: + for i, (arg_name, arg_kind) in enumerate(zip(arg_names, arg_kinds)): + if arg_kind == ARG_NAMED and arg_name is not None: + if arg_name not in all_valid_kwargs: + unexpected_kwargs.append((arg_name, arg_types[i])) + + if unexpected_kwargs: + for kwarg_name, kwarg_type in unexpected_kwargs: + matching_type_args: list[str] = [] + not_matching_type_args: list[str] = [] + matching_variant: CallableType | None = None + + for item in overload.items: + has_type_match = False + for i, formal_type in enumerate(item.arg_types): + formal_name = item.arg_names[i] + if formal_name is not None and item.arg_kinds[i] != ARG_STAR: + if is_subtype(kwarg_type, formal_type): + if formal_name not in matching_type_args: + matching_type_args.append(formal_name) + has_type_match = True + else: + if formal_name not in not_matching_type_args: + not_matching_type_args.append(formal_name) + if has_type_match and matching_variant is None: + matching_variant = item + + matches = best_matches(kwarg_name, matching_type_args, n=3) + if not matches: + matches = best_matches(kwarg_name, not_matching_type_args, n=3) + + msg = f'Unexpected keyword argument "{kwarg_name}"' + for_func + + if matching_variant is not None and matching_variant.definition is not None: + defn = matching_variant.definition + if isinstance(defn, Decorator): + func_line = defn.func.line + else: + func_line = defn.line + msg += f" defined on line {func_line}" + + if matches: + msg += f"; did you mean {pretty_seq(matches, 'or')}?" + self.fail(msg, context, code=code) + + if matching_variant is None: + self.note( + f"Possible overload variant{plural_s(len(overload.items))}:", + context, + code=code, + ) + for item in overload.items: + self.note( + pretty_callable(item, self.options), context, offset=4, code=code + ) + + return + arg_types_str = ", ".join(format_type(arg, self.options) for arg in arg_types) num_args = len(arg_types) if num_args == 0: diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 30b1f1a68e15..4c4cf2982b5c 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2561,3 +2561,21 @@ def last_known_value() -> None: x, y, z = xy # E: Unpacking a string is disallowed reveal_type(z) # N: Revealed type is "builtins.str" [builtins fixtures/primitives.pyi] + +[case testInvalidArgumentInOverloadError] +from typing import overload, Union + +@overload +def f(foobar: int) -> None: ... + +@overload +def f(foobar: str) -> None: ... + +def f(foobar: Union[int, str]) -> None: + pass + +f(fobar=1) # E: Unexpected keyword argument "fobar" for overloaded function "f" defined on line 4; did you mean "foobar"? +f(random=[1,2,3]) # E: Unexpected keyword argument "random" for overloaded function "f" \ + # N: Possible overload variants: \ + # N: def f(foobar: int) -> None \ + # N: def f(foobar: str) -> None