Repository: python/mypy

Issue:
`ParamSpec`: mypy is happy for args to be annotated with `P.kwargs` and kwargs to be annotated with `P.args`
**Bug Report**

Mypy has no issues with this snippet of code. It probably should:

```python
from typing import Callable
from typing_extensions import ParamSpec

P = ParamSpec("P")

def func(callback: Callable[P, str]) -> Callable[P, str]:
    def inner(*args: P.kwargs, **kwargs: P.args) -> str:
        return 'baz'
    return inner
```

This bug was accidentally discovered in #13459.

**To Reproduce**

https://mypy-play.net/?mypy=latest&python=3.10&gist=87f5f5277409f0fd77675e8dcc6a11fc

**Expected Behavior**

Mypy should emit two errors for this snippet:

1. `*args` have been annotated with `P.kwargs` instead of `P.args`
2. `**kwargs` have been annotated with `P.args` instead of `P.kwargs`

**Actual Behavior**

No error is emitted.


Discussion:

Resolving patch:
diff --git a/mypy/typeanal.py b/mypy/typeanal.py
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -838,17 +838,21 @@ def anal_type_guard_arg(self, t: UnboundType, fullname: str) -> Type | None:
 
     def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type:
         """Analyze signature argument type for *args and **kwargs argument."""
-        # TODO: Check that suffix and kind match
         if isinstance(t, UnboundType) and t.name and "." in t.name and not t.args:
             components = t.name.split(".")
-            sym = self.lookup_qualified(".".join(components[:-1]), t)
+            tvar_name = ".".join(components[:-1])
+            sym = self.lookup_qualified(tvar_name, t)
             if sym is not None and isinstance(sym.node, ParamSpecExpr):
                 tvar_def = self.tvar_scope.get_binding(sym)
                 if isinstance(tvar_def, ParamSpecType):
                     if kind == ARG_STAR:
                         make_paramspec = paramspec_args
+                        if components[-1] != "args":
+                            self.fail(f'Use "{tvar_name}.args" for variadic "*" parameter', t)
                     elif kind == ARG_STAR2:
                         make_paramspec = paramspec_kwargs
+                        if components[-1] != "kwargs":
+                            self.fail(f'Use "{tvar_name}.kwargs" for variadic "**" parameter', t)
                     else:
                         assert False, kind
                     return make_paramspec(


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
@@ -1112,3 +1112,18 @@ def func(callback: Callable[P, str]) -> Callable[P, str]:
         return "foo"
     return inner
 [builtins fixtures/paramspec.pyi]
+
+[case testParamSpecArgsAndKwargsMissmatch]
+from typing import Callable
+from typing_extensions import ParamSpec
+
+P1 = ParamSpec("P1")
+
+def func(callback: Callable[P1, str]) -> Callable[P1, str]:
+    def inner(
+        *args: P1.kwargs,   # E: Use "P1.args" for variadic "*" parameter
+        **kwargs: P1.args,  # E: Use "P1.kwargs" for variadic "**" parameter
+    ) -> str:
+        return "foo"
+    return inner
+[builtins fixtures/paramspec.pyi]