Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 68 additions & 14 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions test-data/unit/check-columns.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there are multiple positional arguments missing, it seems reasonable to not try to align them, so this looks fine, but it would be clearer if the order of the errors would be different (see my other comments).

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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you report this error after the 'Argument N to ...' errors, so that we'd report the errors in the same order as the arguments are in the call.

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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case it's not clear what the idea error message would be, since the caller accepts *args. One option would be to report missing positional argument x, but maybe it could result in confusing messages in other contexts, so these messages are reasonable. However, I think it would better to show the 'Missing positional argument ...' after the other messages, so that the messages could be shown in a logical order.

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"
Loading