Skip to content

Commit ef7e8a6

Browse files
authored
Improve error messages when positional argument is missing (#20591)
This PR improves error messages when positional argument is missing from a function call. Previously when a user forgets a positional argument, mypy would previously emit multiple type errors because the subsequent arguments would be "shifted" and mismatched with their expected types. Instead of showing multiple type errors, it emits a single consolidated message that suggests which argument might be missing.
1 parent 620af70 commit ef7e8a6

File tree

3 files changed

+199
-13
lines changed

3 files changed

+199
-13
lines changed

mypy/checkexpr.py

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from mypy.maptype import map_instance_to_supertype
3535
from mypy.meet import is_overlapping_types, narrow_declared_type
3636
from mypy.message_registry import ErrorMessage
37-
from mypy.messages import MessageBuilder, format_type
37+
from mypy.messages import MessageBuilder, callable_name, format_type
3838
from mypy.nodes import (
3939
ARG_NAMED,
4040
ARG_POS,
@@ -1794,20 +1794,60 @@ def check_callable_call(
17941794

17951795
arg_types = self.infer_arg_types_in_context(callee, args, arg_kinds, formal_to_actual)
17961796

1797-
self.check_argument_count(
1798-
callee,
1799-
arg_types,
1800-
arg_kinds,
1801-
arg_names,
1802-
formal_to_actual,
1803-
context,
1804-
object_type,
1805-
callable_name,
1797+
might_have_shifted_args = (
1798+
not self.msg.prefer_simple_messages()
1799+
and all(k == ARG_POS for k in callee.arg_kinds)
1800+
and all(k == ARG_POS for k in arg_kinds)
1801+
and len(arg_kinds) == len(callee.arg_kinds) - 1
18061802
)
18071803

1808-
self.check_argument_types(
1809-
arg_types, arg_kinds, args, callee, formal_to_actual, context, object_type=object_type
1810-
)
1804+
if might_have_shifted_args:
1805+
with self.msg.filter_errors(save_filtered_errors=True) as w:
1806+
ok = self.check_argument_count(
1807+
callee,
1808+
arg_types,
1809+
arg_kinds,
1810+
arg_names,
1811+
formal_to_actual,
1812+
context,
1813+
object_type,
1814+
callable_name,
1815+
)
1816+
if not ok and self._detect_missing_positional_arg(
1817+
callee, arg_types, arg_kinds, args, context
1818+
):
1819+
pass
1820+
else:
1821+
self.msg.add_errors(w.filtered_errors())
1822+
self.check_argument_types(
1823+
arg_types,
1824+
arg_kinds,
1825+
args,
1826+
callee,
1827+
formal_to_actual,
1828+
context,
1829+
object_type=object_type,
1830+
)
1831+
else:
1832+
self.check_argument_count(
1833+
callee,
1834+
arg_types,
1835+
arg_kinds,
1836+
arg_names,
1837+
formal_to_actual,
1838+
context,
1839+
object_type,
1840+
callable_name,
1841+
)
1842+
self.check_argument_types(
1843+
arg_types,
1844+
arg_kinds,
1845+
args,
1846+
callee,
1847+
formal_to_actual,
1848+
context,
1849+
object_type=object_type,
1850+
)
18111851

18121852
if (
18131853
callee.is_type_obj()
@@ -2337,6 +2377,57 @@ def apply_inferred_arguments(
23372377
# arguments.
23382378
return self.apply_generic_arguments(callee_type, inferred_args, context)
23392379

2380+
def _detect_missing_positional_arg(
2381+
self,
2382+
callee: CallableType,
2383+
arg_types: list[Type],
2384+
arg_kinds: list[ArgKind],
2385+
args: list[Expression],
2386+
context: Context,
2387+
) -> bool:
2388+
"""Try to identify a single missing positional argument using type alignment.
2389+
2390+
If the caller and callee are just positional arguments and exactly one arg is missing,
2391+
we scan left to right to find which argument skipped. If only the last argument is missing,
2392+
we return False since it's already handled in a desired manner. If there is an error,
2393+
report it and return True, or return False to fall back to normal checking.
2394+
"""
2395+
if not all(k == ARG_POS for k in callee.arg_kinds):
2396+
return False
2397+
if not all(k == ARG_POS for k in arg_kinds):
2398+
return False
2399+
if len(arg_kinds) != len(callee.arg_kinds) - 1:
2400+
return False
2401+
2402+
skip_idx: int | None = None
2403+
j = 0
2404+
for i in range(len(callee.arg_types)):
2405+
if j >= len(arg_types):
2406+
skip_idx = i
2407+
break
2408+
if is_subtype(arg_types[j], callee.arg_types[i], options=self.chk.options):
2409+
j += 1
2410+
elif skip_idx is None:
2411+
skip_idx = i
2412+
else:
2413+
return False
2414+
2415+
if skip_idx is None or j != len(arg_types):
2416+
return False
2417+
2418+
if skip_idx == len(callee.arg_types) - 1:
2419+
return False
2420+
2421+
param_name = callee.arg_names[skip_idx]
2422+
callee_name = callable_name(callee)
2423+
if param_name is None or callee_name is None:
2424+
return False
2425+
2426+
msg = f'Missing positional argument "{param_name}" in call to {callee_name}'
2427+
ctx = args[skip_idx] if skip_idx < len(args) else context
2428+
self.msg.fail(msg, ctx, code=codes.CALL_ARG)
2429+
return True
2430+
23402431
def check_argument_count(
23412432
self,
23422433
callee: CallableType,

test-data/unit/check-columns.test

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,14 @@ main:2:10:2:17: error: Incompatible types in assignment (expression has type "st
408408
main:6:3:7:1: error: Argument 1 to "f" has incompatible type "int"; expected "str"
409409
main:8:1:8:4: error: Value of type "int" is not indexable
410410

411+
[case testColumnsMissingPositionalArgShiftDetected]
412+
def f(x: int, y: str, z: bytes, aa: int) -> None: ...
413+
f(1, b'x', 1) # E:6: Missing positional argument "y" in call to "f"
414+
def g(x: int, y: str, z: bytes) -> None: ...
415+
g("hello", b'x') # E:3: Missing positional argument "x" in call to "g"
416+
g(1, "hello") # E:1: Missing positional argument "z" in call to "g"
417+
[builtins fixtures/primitives.pyi]
418+
411419
[case testEndColumnsWithTooManyTypeVars]
412420
# flags: --pretty
413421
import typing

test-data/unit/check-functions.test

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3768,3 +3768,90 @@ class C:
37683768

37693769
def defer() -> int:
37703770
return 1
3771+
3772+
[case testMissingPositionalArgShiftDetectedMiddle]
3773+
def f(x: int, y: str, z: bytes, aa: int) -> None: ...
3774+
3775+
f(1, b'x', 1)
3776+
[builtins fixtures/primitives.pyi]
3777+
[out]
3778+
main:3: error: Missing positional argument "y" in call to "f"
3779+
3780+
[case testMissingPositionalArgShiftDetectedFirst]
3781+
def f(x: int, y: str, z: bytes) -> None: ...
3782+
3783+
f("hello", b'x')
3784+
[builtins fixtures/primitives.pyi]
3785+
[out]
3786+
main:3: error: Missing positional argument "x" in call to "f"
3787+
3788+
[case testMissingPositionalArgShiftDetectedManyArgs]
3789+
def f(a: int, b: str, c: float, d: list[int], e: tuple[str, ...]) -> None: ...
3790+
3791+
f(1, 1.5, [1, 2, 3], ("a", "b"))
3792+
[builtins fixtures/list.pyi]
3793+
[out]
3794+
main:3: error: Missing positional argument "b" in call to "f"
3795+
3796+
[case testMissingPositionalArgShiftDetectedLast]
3797+
def f(x: int, y: str, z: bytes) -> None: ...
3798+
3799+
f(1, "hello")
3800+
[builtins fixtures/primitives.pyi]
3801+
[out]
3802+
main:3: error: Missing positional argument "z" in call to "f"
3803+
3804+
[case testMissingPositionalArgNoShiftPattern]
3805+
def f(x: int, y: str, z: bytes) -> None: ...
3806+
3807+
f("wrong", 123)
3808+
[builtins fixtures/primitives.pyi]
3809+
[out]
3810+
main:3: error: Missing positional argument "z" in call to "f"
3811+
main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int"
3812+
main:3: error: Argument 2 to "f" has incompatible type "int"; expected "str"
3813+
3814+
[case testMissingPositionalArgNoShiftPatternLast]
3815+
def f(x: int, y: str, z: bytes) -> None: ...
3816+
3817+
f(123, "wrong")
3818+
[builtins fixtures/primitives.pyi]
3819+
[out]
3820+
main:3: error: Missing positional argument "z" in call to "f"
3821+
3822+
[case testMissingPositionalArgNoShiftPatternNone]
3823+
def f(x: int) -> None: ...
3824+
3825+
f()
3826+
[builtins fixtures/primitives.pyi]
3827+
[out]
3828+
main:3: error: Missing positional argument "x" in call to "f"
3829+
3830+
[case testMissingPositionalArgMultipleMissing]
3831+
def f(a: int, b: str, c: float, d: list[int]) -> None: ...
3832+
3833+
f(1.5, [1, 2, 3])
3834+
[builtins fixtures/list.pyi]
3835+
[out]
3836+
main:3: error: Missing positional arguments "c", "d" in call to "f"
3837+
main:3: error: Argument 1 to "f" has incompatible type "float"; expected "int"
3838+
main:3: error: Argument 2 to "f" has incompatible type "list[int]"; expected "str"
3839+
3840+
[case testMissingPositionalArgWithDefaults]
3841+
def f(x: int, y: str, z: bytes = b'default') -> None: ...
3842+
3843+
f("hello")
3844+
[builtins fixtures/primitives.pyi]
3845+
[out]
3846+
main:3: error: Missing positional argument "y" in call to "f"
3847+
main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int"
3848+
3849+
[case testMissingPositionalArgWithStarArgs]
3850+
def f(x: int, y: str, z: bytes, *args: int) -> None: ...
3851+
3852+
f("hello", b'x')
3853+
[builtins fixtures/primitives.pyi]
3854+
[out]
3855+
main:3: error: Missing positional argument "z" in call to "f"
3856+
main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int"
3857+
main:3: error: Argument 2 to "f" has incompatible type "bytes"; expected "str"

0 commit comments

Comments
 (0)