Repository: python/mypy

Issue:
False negative with ParamSpec
On mypy master, this does not have any errors:
```
from typing import Callable, ParamSpec

P = ParamSpec('P')

def twice(f: Callable[P, int], *args: P.args) -> int:
  return f(*args) + f(*args)
```
With 0.982 you get `main.py:5: error: Name "P.args" is not defined`. Not the best error message, but at least you get an error for invalid use.

This bisects to https://github.com/python/mypy/pull/13472/files cc @sobolevn 


Discussion:
Fixed! Sending a PR.

Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -69,6 +69,8 @@
 from mypy.nodes import (
     ARG_NAMED,
     ARG_POS,
+    ARG_STAR,
+    ARG_STAR2,
     CONTRAVARIANT,
     COVARIANT,
     GDEF,
@@ -843,6 +845,7 @@ def analyze_func_def(self, defn: FuncDef) -> None:
                 defn.type = result
                 self.add_type_alias_deps(analyzer.aliases_used)
                 self.check_function_signature(defn)
+                self.check_paramspec_definition(defn)
                 if isinstance(defn, FuncDef):
                     assert isinstance(defn.type, CallableType)
                     defn.type = set_callable_name(defn.type, defn)
@@ -1282,6 +1285,64 @@ def check_function_signature(self, fdef: FuncItem) -> None:
         elif len(sig.arg_types) > len(fdef.arguments):
             self.fail("Type signature has too many arguments", fdef, blocker=True)
 
+    def check_paramspec_definition(self, defn: FuncDef) -> None:
+        func = defn.type
+        assert isinstance(func, CallableType)
+
+        if not any(isinstance(var, ParamSpecType) for var in func.variables):
+            return  # Function does not have param spec variables
+
+        args = func.var_arg()
+        kwargs = func.kw_arg()
+        if args is None and kwargs is None:
+            return  # Looks like this function does not have starred args
+
+        args_defn_type = None
+        kwargs_defn_type = None
+        for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds):
+            if arg_kind == ARG_STAR:
+                args_defn_type = arg_def.type_annotation
+            elif arg_kind == ARG_STAR2:
+                kwargs_defn_type = arg_def.type_annotation
+
+        # This may happen on invalid `ParamSpec` args / kwargs definition,
+        # type analyzer sets types of arguments to `Any`, but keeps
+        # definition types as `UnboundType` for now.
+        if not (
+            (isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"))
+            or (
+                isinstance(kwargs_defn_type, UnboundType)
+                and kwargs_defn_type.name.endswith(".kwargs")
+            )
+        ):
+            # Looks like both `*args` and `**kwargs` are not `ParamSpec`
+            # It might be something else, skipping.
+            return
+
+        args_type = args.typ if args is not None else None
+        kwargs_type = kwargs.typ if kwargs is not None else None
+
+        if (
+            not isinstance(args_type, ParamSpecType)
+            or not isinstance(kwargs_type, ParamSpecType)
+            or args_type.name != kwargs_type.name
+        ):
+            if isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"):
+                param_name = args_defn_type.name.split(".")[0]
+            elif isinstance(kwargs_defn_type, UnboundType) and kwargs_defn_type.name.endswith(
+                ".kwargs"
+            ):
+                param_name = kwargs_defn_type.name.split(".")[0]
+            else:
+                # Fallback for cases that probably should not ever happen:
+                param_name = "P"
+
+            self.fail(
+                f'ParamSpec must have "*args" typed as "{param_name}.args" and "**kwargs" typed as "{param_name}.kwargs"',
+                func,
+                code=codes.VALID_TYPE,
+            )
+
     def visit_decorator(self, dec: Decorator) -> None:
         self.statement = dec
         # TODO: better don't modify them at all.


Test patch:
diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test
--- a/test-data/unit/check-parameter-specification.test
+++ b/test-data/unit/check-parameter-specification.test
@@ -1166,3 +1166,116 @@ def func3(callback: Callable[P1, str]) -> Callable[P1, str]:
         return "foo"
     return inner
 [builtins fixtures/paramspec.pyi]
+
+
+[case testInvalidParamSpecDefinitionsWithArgsKwargs]
+from typing import Callable, ParamSpec
+
+P = ParamSpec('P')
+
+def c1(f: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: ...
+def c2(f: Callable[P, int]) -> int: ...
+def c3(f: Callable[P, int], *args, **kwargs) -> int: ...
+
+# It is ok to define,
+def c4(f: Callable[P, int], *args: int, **kwargs: str) -> int:
+    # but not ok to call:
+    f(*args, **kwargs)  # E: Argument 1 has incompatible type "*Tuple[int, ...]"; expected "P.args" \
+                        # E: Argument 2 has incompatible type "**Dict[str, str]"; expected "P.kwargs"
+    return 1
+
+def f1(f: Callable[P, int], *args, **kwargs: P.kwargs) -> int: ...  # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
+def f2(f: Callable[P, int], *args: P.args, **kwargs) -> int: ...  # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
+def f3(f: Callable[P, int], *args: P.args) -> int: ...  # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
+def f4(f: Callable[P, int], **kwargs: P.kwargs) -> int: ...  # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
+
+# Error message test:
+P1 = ParamSpec('P1')
+
+def m1(f: Callable[P1, int], *a, **k: P1.kwargs) -> int: ...  # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
+[builtins fixtures/paramspec.pyi]
+
+
+[case testInvalidParamSpecAndConcatenateDefinitionsWithArgsKwargs]
+from typing import Callable, ParamSpec
+from typing_extensions import Concatenate
+
+P = ParamSpec('P')
+
+def c1(f: Callable[Concatenate[int, P], int], *args: P.args, **kwargs: P.kwargs) -> int: ...
+def c2(f: Callable[Concatenate[int, P], int]) -> int: ...
+def c3(f: Callable[Concatenate[int, P], int], *args, **kwargs) -> int: ...
+
+# It is ok to define,
+def c4(f: Callable[Concatenate[int, P], int], *args: int, **kwargs: str) -> int:
+    # but not ok to call:
+    f(1, *args, **kwargs)  # E: Argument 2 has incompatible type "*Tuple[int, ...]"; expected "P.args" \
+                           # E: Argument 3 has incompatible type "**Dict[str, str]"; expected "P.kwargs"
+    return 1
+
+def f1(f: Callable[Concatenate[int, P], int], *args, **kwargs: P.kwargs) -> int: ...  # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
+def f2(f: Callable[Concatenate[int, P], int], *args: P.args, **kwargs) -> int: ...  # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
+def f3(f: Callable[Concatenate[int, P], int], *args: P.args) -> int: ...  # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
+def f4(f: Callable[Concatenate[int, P], int], **kwargs: P.kwargs) -> int: ...  # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
+[builtins fixtures/paramspec.pyi]
+
+
+[case testValidParamSpecInsideGenericWithoutArgsAndKwargs]
+from typing import Callable, ParamSpec, Generic
+from typing_extensions import Concatenate
+
+P = ParamSpec('P')
+
+class Some(Generic[P]): ...
+
+def create(s: Some[P], *args: int): ...
+def update(s: Some[P], **kwargs: int): ...
+def delete(s: Some[P]): ...
+
+def from_callable1(c: Callable[P, int], *args: int, **kwargs: int) -> Some[P]: ...
+def from_callable2(c: Callable[P, int], **kwargs: int) -> Some[P]: ...
+def from_callable3(c: Callable[P, int], *args: int) -> Some[P]: ...
+
+def from_extra1(c: Callable[Concatenate[int, P], int], *args: int, **kwargs: int) -> Some[P]: ...
+def from_extra2(c: Callable[Concatenate[int, P], int], **kwargs: int) -> Some[P]: ...
+def from_extra3(c: Callable[Concatenate[int, P], int], *args: int) -> Some[P]: ...
+[builtins fixtures/paramspec.pyi]
+
+
+[case testUnboundParamSpec]
+from typing import Callable, ParamSpec
+
+P1 = ParamSpec('P1')
+P2 = ParamSpec('P2')
+
+def f0(f: Callable[P1, int], *args: P1.args, **kwargs: P2.kwargs): ...  # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
+
+def f1(*args: P1.args): ...  # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
+def f2(**kwargs: P1.kwargs): ...  # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
+def f3(*args: P1.args, **kwargs: int): ...  # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
+def f4(*args: int, **kwargs: P1.kwargs): ...  # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
+
+# Error message is based on the `args` definition:
+def f5(*args: P2.args, **kwargs: P1.kwargs): ...  # E: ParamSpec must have "*args" typed as "P2.args" and "**kwargs" typed as "P2.kwargs"
+def f6(*args: P1.args, **kwargs: P2.kwargs): ...  # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
+
+# Multiple `ParamSpec` variables can be found, they should not affect error message:
+P3 = ParamSpec('P3')
+
+def f7(first: Callable[P3, int], *args: P1.args, **kwargs: P2.kwargs): ...  # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs"
+def f8(first: Callable[P3, int], *args: P2.args, **kwargs: P1.kwargs): ...  # E: ParamSpec must have "*args" typed as "P2.args" and "**kwargs" typed as "P2.kwargs"
+[builtins fixtures/paramspec.pyi]
+
+
+[case testArgsKwargsWithoutParamSpecVar]
+from typing import Generic, Callable, ParamSpec
+
+P = ParamSpec('P')
+
+# This must be allowed:
+class Some(Generic[P]):
+    def call(self, *args: P.args, **kwargs: P.kwargs): ...
+
+# TODO: this probably should be reported.
+def call(*args: P.args, **kwargs: P.kwargs): ...
+[builtins fixtures/paramspec.pyi]