diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 9990caaeb7a17..cc5e6763bf675 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -33,7 +33,7 @@ from mypy.maptype import map_instance_to_supertype from mypy.meet import is_overlapping_types, narrow_declared_type from mypy.message_registry import ErrorMessage -from mypy.messages import MessageBuilder, format_type +from mypy.messages import MessageBuilder, callable_name, format_type from mypy.nodes import ( ARG_NAMED, ARG_POS, @@ -1798,20 +1798,27 @@ def check_callable_call( arg_types = self.infer_arg_types_in_context(callee, args, arg_kinds, formal_to_actual) - self.check_argument_count( - callee, - arg_types, - arg_kinds, - arg_names, - formal_to_actual, - context, - object_type, - callable_name, - ) + if not self._detect_missing_positional_arg(callee, arg_types, arg_kinds, args, context): + self.check_argument_count( + callee, + arg_types, + arg_kinds, + arg_names, + formal_to_actual, + context, + object_type, + callable_name, + ) - self.check_argument_types( - arg_types, arg_kinds, args, callee, formal_to_actual, context, object_type=object_type - ) + self.check_argument_types( + arg_types, + arg_kinds, + args, + callee, + formal_to_actual, + context, + object_type=object_type, + ) if ( callee.is_type_obj() @@ -2340,6 +2347,53 @@ def apply_inferred_arguments( # arguments. return self.apply_generic_arguments(callee_type, inferred_args, context) + def _detect_missing_positional_arg( + self, + callee: CallableType, + arg_types: list[Type], + arg_kinds: list[ArgKind], + args: list[Expression], + context: Context, + ) -> bool: + """Try to identify a single missing positional argument using type alignment. + + If the caller and callee are just positional arguments and exactly one arg is missing, + we scan left to right to find which argument skipped. If there is an error, report it + and return True, or return False to fall back to normal checking. + """ + if not all(k == ARG_POS for k in callee.arg_kinds): + return False + if not all(k == ARG_POS for k in arg_kinds): + return False + if len(arg_kinds) != len(callee.arg_kinds) - 1: + return False + + skip_idx: int | None = None + j = 0 + for i in range(len(callee.arg_types)): + if j >= len(arg_types): + skip_idx = i + break + if is_subtype(arg_types[j], callee.arg_types[i], options=self.chk.options): + j += 1 + elif skip_idx is None: + skip_idx = i + else: + return False + + if skip_idx is None or j != len(arg_types): + return False + + param_name = callee.arg_names[skip_idx] + callee_name = callable_name(callee) + if param_name is None or callee_name is None: + return False + + msg = f'Missing positional argument "{param_name}" in call to {callee_name}' + ctx = args[skip_idx] if skip_idx < len(args) else context + self.msg.fail(msg, ctx, code=codes.CALL_ARG) + return True + def check_argument_count( self, callee: CallableType, diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index 8458f0ac27dcd..13d4ab125e7c7 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -409,6 +409,14 @@ main:2:10:2:17: error: Incompatible types in assignment (expression has type "st main:6:3:7:1: error: Argument 1 to "f" has incompatible type "int"; expected "str" main:8:1:8:4: error: Value of type "int" is not indexable +[case testColumnsMissingPositionalArgShiftDetected] +def f(x: int, y: str, z: bytes, aa: int) -> None: ... +f(1, b'x', 1) # E:6: Missing positional argument "y" in call to "f" +def g(x: int, y: str, z: bytes) -> None: ... +g("hello", b'x') # E:3: Missing positional argument "x" in call to "g" +g(1, "hello") # E:1: Missing positional argument "z" in call to "g" +[builtins fixtures/primitives.pyi] + [case testEndColumnsWithTooManyTypeVars] # flags: --pretty import typing diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index b54dffe836b8e..444f908c244e1 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3703,3 +3703,74 @@ foo(*args) # E: Argument 1 to "foo" has incompatible type "*list[object]"; expe kwargs: dict[str, object] foo(**kwargs) # E: Argument 1 to "foo" has incompatible type "**dict[str, object]"; expected "P" [builtins fixtures/dict.pyi] + +[case testMissingPositionalArgShiftDetectedMiddle] +def f(x: int, y: str, z: bytes, aa: int) -> None: ... + +f(1, b'x', 1) +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Missing positional argument "y" in call to "f" + +[case testMissingPositionalArgShiftDetectedFirst] +def f(x: int, y: str, z: bytes) -> None: ... + +f("hello", b'x') +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Missing positional argument "x" in call to "f" + +[case testMissingPositionalArgShiftDetectedManyArgs] +def f(a: int, b: str, c: float, d: list[int], e: tuple[str, ...]) -> None: ... + +f(1, 1.5, [1, 2, 3], ("a", "b")) +[builtins fixtures/list.pyi] +[out] +main:3: error: Missing positional argument "b" in call to "f" + +[case testMissingPositionalArgShiftDetectedLast] +def f(x: int, y: str, z: bytes) -> None: ... + +f(1, "hello") +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Missing positional argument "z" in call to "f" + +[case testMissingPositionalArgNoShiftPattern] +def f(x: int, y: str, z: bytes) -> None: ... + +f("wrong", 123) +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Missing positional argument "z" in call to "f" +main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" +main:3: error: Argument 2 to "f" has incompatible type "int"; expected "str" + +[case testMissingPositionalArgMultipleMissing] +def f(a: int, b: str, c: float, d: list[int]) -> None: ... + +f(1.5, [1, 2, 3]) +[builtins fixtures/list.pyi] +[out] +main:3: error: Missing positional arguments "c", "d" in call to "f" +main:3: error: Argument 1 to "f" has incompatible type "float"; expected "int" +main:3: error: Argument 2 to "f" has incompatible type "list[int]"; expected "str" + +[case testMissingPositionalArgWithDefaults] +def f(x: int, y: str, z: bytes = b'default') -> None: ... + +f("hello") +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Missing positional argument "y" in call to "f" +main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" + +[case testMissingPositionalArgWithStarArgs] +def f(x: int, y: str, z: bytes, *args: int) -> None: ... + +f("hello", b'x') +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Missing positional argument "z" in call to "f" +main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" +main:3: error: Argument 2 to "f" has incompatible type "bytes"; expected "str"