Infer ParamSpec constraint from arguments (#15896)

Fixes https://github.com/python/mypy/issues/12278
Fixes https://github.com/python/mypy/issues/13191 (more tricky nested
use cases with optional/keyword args still don't work, but they are
quite tricky to fix and may selectively fixed later)

This unfortunately requires some special-casing, here is its summary:
* If actual argument for `Callable[P, T]` is non-generic and non-lambda,
do not put it into inference second pass.
* If we are able to infer constraints for `P` without using arguments
mapped to `*args: P.args` etc., do not add the constraint for `P` vs
those arguments (this applies to both top-level callable constraints,
and for nested callable constraints against callables that are known to
have imprecise argument kinds).

(Btw TODO I added is not related to this PR, I just noticed something
obviously wrong)
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index 6de317f..4430d07 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -1987,7 +1987,7 @@
                 )
 
             arg_pass_nums = self.get_arg_infer_passes(
-                callee_type.arg_types, formal_to_actual, len(args)
+                callee_type, args, arg_types, formal_to_actual, len(args)
             )
 
             pass1_args: list[Type | None] = []
@@ -2001,6 +2001,7 @@
                 callee_type,
                 pass1_args,
                 arg_kinds,
+                arg_names,
                 formal_to_actual,
                 context=self.argument_infer_context(),
                 strict=self.chk.in_checked_function(),
@@ -2061,6 +2062,7 @@
                     callee_type,
                     arg_types,
                     arg_kinds,
+                    arg_names,
                     formal_to_actual,
                     context=self.argument_infer_context(),
                     strict=self.chk.in_checked_function(),
@@ -2140,6 +2142,7 @@
             callee_type,
             arg_types,
             arg_kinds,
+            arg_names,
             formal_to_actual,
             context=self.argument_infer_context(),
         )
@@ -2152,7 +2155,12 @@
         )
 
     def get_arg_infer_passes(
-        self, arg_types: list[Type], formal_to_actual: list[list[int]], num_actuals: int
+        self,
+        callee: CallableType,
+        args: list[Expression],
+        arg_types: list[Type],
+        formal_to_actual: list[list[int]],
+        num_actuals: int,
     ) -> list[int]:
         """Return pass numbers for args for two-pass argument type inference.
 
@@ -2163,8 +2171,28 @@
         lambdas more effectively.
         """
         res = [1] * num_actuals
-        for i, arg in enumerate(arg_types):
-            if arg.accept(ArgInferSecondPassQuery()):
+        for i, arg in enumerate(callee.arg_types):
+            skip_param_spec = False
+            p_formal = get_proper_type(callee.arg_types[i])
+            if isinstance(p_formal, CallableType) and p_formal.param_spec():
+                for j in formal_to_actual[i]:
+                    p_actual = get_proper_type(arg_types[j])
+                    # This is an exception from the usual logic where we put generic Callable
+                    # arguments in the second pass. If we have a non-generic actual, it is
+                    # likely to infer good constraints, for example if we have:
+                    #   def run(Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ...
+                    #   def test(x: int, y: int) -> int: ...
+                    #   run(test, 1, 2)
+                    # we will use `test` for inference, since it will allow to infer also
+                    # argument *names* for P <: [x: int, y: int].
+                    if (
+                        isinstance(p_actual, CallableType)
+                        and not p_actual.variables
+                        and not isinstance(args[j], LambdaExpr)
+                    ):
+                        skip_param_spec = True
+                        break
+            if not skip_param_spec and arg.accept(ArgInferSecondPassQuery()):
                 for j in formal_to_actual[i]:
                     res[j] = 2
         return res
@@ -4903,7 +4931,9 @@
             self.chk.fail(message_registry.CANNOT_INFER_LAMBDA_TYPE, e)
             return None, None
 
-        return callable_ctx, callable_ctx
+        # Type of lambda must have correct argument names, to prevent false
+        # negatives when lambdas appear in `ParamSpec` context.
+        return callable_ctx.copy_modified(arg_names=e.arg_names), callable_ctx
 
     def visit_super_expr(self, e: SuperExpr) -> Type:
         """Type check a super expression (non-lvalue)."""
@@ -5921,6 +5951,7 @@
         super().__init__(types.ANY_STRATEGY)
 
     def visit_callable_type(self, t: CallableType) -> bool:
+        # TODO: we need to check only for type variables of original callable.
         return self.query_types(t.arg_types) or t.accept(HasTypeVarQuery())
 
 
diff --git a/mypy/constraints.py b/mypy/constraints.py
index edce11e..0e59b54 100644
--- a/mypy/constraints.py
+++ b/mypy/constraints.py
@@ -108,6 +108,7 @@
     callee: CallableType,
     arg_types: Sequence[Type | None],
     arg_kinds: list[ArgKind],
+    arg_names: Sequence[str | None] | None,
     formal_to_actual: list[list[int]],
     context: ArgumentInferContext,
 ) -> list[Constraint]:
@@ -118,6 +119,20 @@
     constraints: list[Constraint] = []
     mapper = ArgTypeExpander(context)
 
+    param_spec = callee.param_spec()
+    param_spec_arg_types = []
+    param_spec_arg_names = []
+    param_spec_arg_kinds = []
+
+    incomplete_star_mapping = False
+    for i, actuals in enumerate(formal_to_actual):
+        for actual in actuals:
+            if actual is None and callee.arg_kinds[i] in (ARG_STAR, ARG_STAR2):
+                # We can't use arguments to infer ParamSpec constraint, if only some
+                # are present in the current inference pass.
+                incomplete_star_mapping = True
+                break
+
     for i, actuals in enumerate(formal_to_actual):
         if isinstance(callee.arg_types[i], UnpackType):
             unpack_type = callee.arg_types[i]
@@ -194,11 +209,47 @@
                 actual_type = mapper.expand_actual_type(
                     actual_arg_type, arg_kinds[actual], callee.arg_names[i], callee.arg_kinds[i]
                 )
-                # TODO: if callee has ParamSpec, we need to collect all actuals that map to star
-                # args and create single constraint between P and resulting Parameters instead.
-                c = infer_constraints(callee.arg_types[i], actual_type, SUPERTYPE_OF)
-                constraints.extend(c)
-
+                if (
+                    param_spec
+                    and callee.arg_kinds[i] in (ARG_STAR, ARG_STAR2)
+                    and not incomplete_star_mapping
+                ):
+                    # If actual arguments are mapped to ParamSpec type, we can't infer individual
+                    # constraints, instead store them and infer single constraint at the end.
+                    # It is impossible to map actual kind to formal kind, so use some heuristic.
+                    # This inference is used as a fallback, so relying on heuristic should be OK.
+                    param_spec_arg_types.append(
+                        mapper.expand_actual_type(
+                            actual_arg_type, arg_kinds[actual], None, arg_kinds[actual]
+                        )
+                    )
+                    actual_kind = arg_kinds[actual]
+                    param_spec_arg_kinds.append(
+                        ARG_POS if actual_kind not in (ARG_STAR, ARG_STAR2) else actual_kind
+                    )
+                    param_spec_arg_names.append(arg_names[actual] if arg_names else None)
+                else:
+                    c = infer_constraints(callee.arg_types[i], actual_type, SUPERTYPE_OF)
+                    constraints.extend(c)
+    if (
+        param_spec
+        and not any(c.type_var == param_spec.id for c in constraints)
+        and not incomplete_star_mapping
+    ):
+        # Use ParamSpec constraint from arguments only if there are no other constraints,
+        # since as explained above it is quite ad-hoc.
+        constraints.append(
+            Constraint(
+                param_spec,
+                SUPERTYPE_OF,
+                Parameters(
+                    arg_types=param_spec_arg_types,
+                    arg_kinds=param_spec_arg_kinds,
+                    arg_names=param_spec_arg_names,
+                    imprecise_arg_kinds=True,
+                ),
+            )
+        )
     return constraints
 
 
@@ -949,6 +1000,14 @@
             res: list[Constraint] = []
             cactual = self.actual.with_unpacked_kwargs()
             param_spec = template.param_spec()
+
+            template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type
+            if template.type_guard is not None:
+                template_ret_type = template.type_guard
+            if cactual.type_guard is not None:
+                cactual_ret_type = cactual.type_guard
+            res.extend(infer_constraints(template_ret_type, cactual_ret_type, self.direction))
+
             if param_spec is None:
                 # TODO: Erase template variables if it is generic?
                 if (
@@ -1008,34 +1067,6 @@
                     )
                     extra_tvars = True
 
-                if not cactual_ps:
-                    max_prefix_len = len([k for k in cactual.arg_kinds if k in (ARG_POS, ARG_OPT)])
-                    prefix_len = min(prefix_len, max_prefix_len)
-                    res.append(
-                        Constraint(
-                            param_spec,
-                            neg_op(self.direction),
-                            Parameters(
-                                arg_types=cactual.arg_types[prefix_len:],
-                                arg_kinds=cactual.arg_kinds[prefix_len:],
-                                arg_names=cactual.arg_names[prefix_len:],
-                                variables=cactual.variables
-                                if not type_state.infer_polymorphic
-                                else [],
-                            ),
-                        )
-                    )
-                else:
-                    if len(param_spec.prefix.arg_types) <= len(cactual_ps.prefix.arg_types):
-                        cactual_ps = cactual_ps.copy_modified(
-                            prefix=Parameters(
-                                arg_types=cactual_ps.prefix.arg_types[prefix_len:],
-                                arg_kinds=cactual_ps.prefix.arg_kinds[prefix_len:],
-                                arg_names=cactual_ps.prefix.arg_names[prefix_len:],
-                            )
-                        )
-                        res.append(Constraint(param_spec, neg_op(self.direction), cactual_ps))
-
                 # Compare prefixes as well
                 cactual_prefix = cactual.copy_modified(
                     arg_types=cactual.arg_types[:prefix_len],
@@ -1046,13 +1077,40 @@
                     infer_callable_arguments_constraints(prefix, cactual_prefix, self.direction)
                 )
 
-            template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type
-            if template.type_guard is not None:
-                template_ret_type = template.type_guard
-            if cactual.type_guard is not None:
-                cactual_ret_type = cactual.type_guard
-
-            res.extend(infer_constraints(template_ret_type, cactual_ret_type, self.direction))
+                param_spec_target: Type | None = None
+                skip_imprecise = (
+                    any(c.type_var == param_spec.id for c in res) and cactual.imprecise_arg_kinds
+                )
+                if not cactual_ps:
+                    max_prefix_len = len([k for k in cactual.arg_kinds if k in (ARG_POS, ARG_OPT)])
+                    prefix_len = min(prefix_len, max_prefix_len)
+                    # This logic matches top-level callable constraint exception, if we managed
+                    # to get other constraints for ParamSpec, don't infer one with imprecise kinds
+                    if not skip_imprecise:
+                        param_spec_target = Parameters(
+                            arg_types=cactual.arg_types[prefix_len:],
+                            arg_kinds=cactual.arg_kinds[prefix_len:],
+                            arg_names=cactual.arg_names[prefix_len:],
+                            variables=cactual.variables
+                            if not type_state.infer_polymorphic
+                            else [],
+                            imprecise_arg_kinds=cactual.imprecise_arg_kinds,
+                        )
+                else:
+                    if (
+                        len(param_spec.prefix.arg_types) <= len(cactual_ps.prefix.arg_types)
+                        and not skip_imprecise
+                    ):
+                        param_spec_target = cactual_ps.copy_modified(
+                            prefix=Parameters(
+                                arg_types=cactual_ps.prefix.arg_types[prefix_len:],
+                                arg_kinds=cactual_ps.prefix.arg_kinds[prefix_len:],
+                                arg_names=cactual_ps.prefix.arg_names[prefix_len:],
+                                imprecise_arg_kinds=cactual_ps.prefix.imprecise_arg_kinds,
+                            )
+                        )
+                if param_spec_target is not None:
+                    res.append(Constraint(param_spec, neg_op(self.direction), param_spec_target))
             if extra_tvars:
                 for c in res:
                     c.extra_tvars += cactual.variables
diff --git a/mypy/expandtype.py b/mypy/expandtype.py
index dc3dae6..7168d7c 100644
--- a/mypy/expandtype.py
+++ b/mypy/expandtype.py
@@ -336,6 +336,7 @@
                     arg_types=self.expand_types(t.arg_types),
                     ret_type=t.ret_type.accept(self),
                     type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None),
+                    imprecise_arg_kinds=(t.imprecise_arg_kinds or repl.imprecise_arg_kinds),
                 )
             elif isinstance(repl, ParamSpecType):
                 # We're substituting one ParamSpec for another; this can mean that the prefix
@@ -352,6 +353,7 @@
                     arg_names=t.arg_names[:-2] + prefix.arg_names + t.arg_names[-2:],
                     ret_type=t.ret_type.accept(self),
                     from_concatenate=t.from_concatenate or bool(repl.prefix.arg_types),
+                    imprecise_arg_kinds=(t.imprecise_arg_kinds or prefix.imprecise_arg_kinds),
                 )
 
         var_arg = t.var_arg()
diff --git a/mypy/infer.py b/mypy/infer.py
index f340879..ba4a1d2 100644
--- a/mypy/infer.py
+++ b/mypy/infer.py
@@ -33,6 +33,7 @@
     callee_type: CallableType,
     arg_types: Sequence[Type | None],
     arg_kinds: list[ArgKind],
+    arg_names: Sequence[str | None] | None,
     formal_to_actual: list[list[int]],
     context: ArgumentInferContext,
     strict: bool = True,
@@ -53,7 +54,7 @@
     """
     # Infer constraints.
     constraints = infer_constraints_for_callable(
-        callee_type, arg_types, arg_kinds, formal_to_actual, context
+        callee_type, arg_types, arg_kinds, arg_names, formal_to_actual, context
     )
 
     # Solve constraints.
diff --git a/mypy/types.py b/mypy/types.py
index 214978e..cf2c343 100644
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -1560,6 +1560,7 @@
         # TODO: variables don't really belong here, but they are used to allow hacky support
         # for forall . Foo[[x: T], T] by capturing generic callable with ParamSpec, see #15909
         "variables",
+        "imprecise_arg_kinds",
     )
 
     def __init__(
@@ -1570,6 +1571,7 @@
         *,
         variables: Sequence[TypeVarLikeType] | None = None,
         is_ellipsis_args: bool = False,
+        imprecise_arg_kinds: bool = False,
         line: int = -1,
         column: int = -1,
     ) -> None:
@@ -1582,6 +1584,7 @@
         self.min_args = arg_kinds.count(ARG_POS)
         self.is_ellipsis_args = is_ellipsis_args
         self.variables = variables or []
+        self.imprecise_arg_kinds = imprecise_arg_kinds
 
     def copy_modified(
         self,
@@ -1591,6 +1594,7 @@
         *,
         variables: Bogus[Sequence[TypeVarLikeType]] = _dummy,
         is_ellipsis_args: Bogus[bool] = _dummy,
+        imprecise_arg_kinds: Bogus[bool] = _dummy,
     ) -> Parameters:
         return Parameters(
             arg_types=arg_types if arg_types is not _dummy else self.arg_types,
@@ -1600,6 +1604,11 @@
                 is_ellipsis_args if is_ellipsis_args is not _dummy else self.is_ellipsis_args
             ),
             variables=variables if variables is not _dummy else self.variables,
+            imprecise_arg_kinds=(
+                imprecise_arg_kinds
+                if imprecise_arg_kinds is not _dummy
+                else self.imprecise_arg_kinds
+            ),
         )
 
     # TODO: here is a lot of code duplication with Callable type, fix this.
@@ -1696,6 +1705,7 @@
             "arg_kinds": [int(x.value) for x in self.arg_kinds],
             "arg_names": self.arg_names,
             "variables": [tv.serialize() for tv in self.variables],
+            "imprecise_arg_kinds": self.imprecise_arg_kinds,
         }
 
     @classmethod
@@ -1706,6 +1716,7 @@
             [ArgKind(x) for x in data["arg_kinds"]],
             data["arg_names"],
             variables=[cast(TypeVarLikeType, deserialize_type(v)) for v in data["variables"]],
+            imprecise_arg_kinds=data["imprecise_arg_kinds"],
         )
 
     def __hash__(self) -> int:
@@ -1762,6 +1773,7 @@
         "type_guard",  # T, if -> TypeGuard[T] (ret_type is bool in this case).
         "from_concatenate",  # whether this callable is from a concatenate object
         # (this is used for error messages)
+        "imprecise_arg_kinds",
         "unpack_kwargs",  # Was an Unpack[...] with **kwargs used to define this callable?
     )
 
@@ -1786,6 +1798,7 @@
         def_extras: dict[str, Any] | None = None,
         type_guard: Type | None = None,
         from_concatenate: bool = False,
+        imprecise_arg_kinds: bool = False,
         unpack_kwargs: bool = False,
     ) -> None:
         super().__init__(line, column)
@@ -1812,6 +1825,7 @@
         self.special_sig = special_sig
         self.from_type_type = from_type_type
         self.from_concatenate = from_concatenate
+        self.imprecise_arg_kinds = imprecise_arg_kinds
         if not bound_args:
             bound_args = ()
         self.bound_args = bound_args
@@ -1854,6 +1868,7 @@
         def_extras: Bogus[dict[str, Any]] = _dummy,
         type_guard: Bogus[Type | None] = _dummy,
         from_concatenate: Bogus[bool] = _dummy,
+        imprecise_arg_kinds: Bogus[bool] = _dummy,
         unpack_kwargs: Bogus[bool] = _dummy,
     ) -> CT:
         modified = CallableType(
@@ -1879,6 +1894,11 @@
             from_concatenate=(
                 from_concatenate if from_concatenate is not _dummy else self.from_concatenate
             ),
+            imprecise_arg_kinds=(
+                imprecise_arg_kinds
+                if imprecise_arg_kinds is not _dummy
+                else self.imprecise_arg_kinds
+            ),
             unpack_kwargs=unpack_kwargs if unpack_kwargs is not _dummy else self.unpack_kwargs,
         )
         # Optimization: Only NewTypes are supported as subtypes since
@@ -2191,6 +2211,7 @@
             "def_extras": dict(self.def_extras),
             "type_guard": self.type_guard.serialize() if self.type_guard is not None else None,
             "from_concatenate": self.from_concatenate,
+            "imprecise_arg_kinds": self.imprecise_arg_kinds,
             "unpack_kwargs": self.unpack_kwargs,
         }
 
@@ -2214,6 +2235,7 @@
                 deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None
             ),
             from_concatenate=data["from_concatenate"],
+            imprecise_arg_kinds=data["imprecise_arg_kinds"],
             unpack_kwargs=data["unpack_kwargs"],
         )
 
diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test
index 257fb92..ed1d59b 100644
--- a/test-data/unit/check-parameter-specification.test
+++ b/test-data/unit/check-parameter-specification.test
@@ -239,7 +239,6 @@
 f(g, 'x', y='x')  # E: Argument 2 to "f" has incompatible type "str"; expected "int"
 f(g, 1, y=1)  # E: Argument "y" to "f" has incompatible type "int"; expected "str"
 f(g)  # E: Missing positional arguments "x", "y" in call to "f"
-
 [builtins fixtures/dict.pyi]
 
 [case testParamSpecSpecialCase]
@@ -415,14 +414,19 @@
 T = TypeVar('T')
 
 # Similar to atexit.register
-def register(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> Callable[P, T]: ...  # N: "register" defined here
+def register(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> Callable[P, T]: ...
 
 def f(x: int) -> None: pass
+def g(x: int, y: str) -> None: pass
 
 reveal_type(register(lambda: f(1)))  # N: Revealed type is "def ()"
-reveal_type(register(lambda x: f(x), x=1))  # N: Revealed type is "def (x: Any)"
-register(lambda x: f(x))  # E: Missing positional argument "x" in call to "register"
-register(lambda x: f(x), y=1)  # E: Unexpected keyword argument "y" for "register"
+reveal_type(register(lambda x: f(x), x=1))  # N: Revealed type is "def (x: Literal[1]?)"
+register(lambda x: f(x))  # E: Cannot infer type of lambda \
+                          # E: Argument 1 to "register" has incompatible type "Callable[[Any], None]"; expected "Callable[[], None]"
+register(lambda x: f(x), y=1)  # E: Argument 1 to "register" has incompatible type "Callable[[Arg(int, 'x')], None]"; expected "Callable[[Arg(int, 'y')], None]"
+reveal_type(register(lambda x: f(x), 1))  # N: Revealed type is "def (Literal[1]?)"
+reveal_type(register(lambda x, y: g(x, y), 1, "a"))  # N: Revealed type is "def (Literal[1]?, Literal['a']?)"
+reveal_type(register(lambda x, y: g(x, y), 1, y="a"))  # N: Revealed type is "def (Literal[1]?, y: Literal['a']?)"
 [builtins fixtures/dict.pyi]
 
 [case testParamSpecInvalidCalls]
@@ -909,8 +913,7 @@
 
 reveal_type(A().func(f, 42))  # N: Revealed type is "builtins.int"
 
-# TODO: this should reveal `int`
-reveal_type(A().func(lambda x: x + x, 42))  # N: Revealed type is "Any"
+reveal_type(A().func(lambda x: x + x, 42))  # N: Revealed type is "builtins.int"
 [builtins fixtures/paramspec.pyi]
 
 [case testParamSpecConstraintOnOtherParamSpec]
@@ -1355,7 +1358,6 @@
 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]
 
@@ -1631,7 +1633,41 @@
 dec(test_with_bound)(A())  # OK
 [builtins fixtures/paramspec.pyi]
 
+[case testParamSpecArgumentParamInferenceRegular]
+from typing import TypeVar, Generic
+from typing_extensions import ParamSpec
+
+P = ParamSpec("P")
+class Foo(Generic[P]):
+    def call(self, *args: P.args, **kwargs: P.kwargs) -> None: ...
+def test(*args: P.args, **kwargs: P.kwargs) -> Foo[P]: ...
+
+reveal_type(test(1, 2))  # N: Revealed type is "__main__.Foo[[Literal[1]?, Literal[2]?]]"
+reveal_type(test(x=1, y=2))  # N: Revealed type is "__main__.Foo[[x: Literal[1]?, y: Literal[2]?]]"
+ints = [1, 2, 3]
+reveal_type(test(*ints))  # N: Revealed type is "__main__.Foo[[*builtins.int]]"
+[builtins fixtures/paramspec.pyi]
+
+[case testParamSpecArgumentParamInferenceGeneric]
+# flags: --new-type-inference
+from typing import Callable, TypeVar
+from typing_extensions import ParamSpec
+
+P = ParamSpec("P")
+R = TypeVar("R")
+def call(f: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
+    return f(*args, **kwargs)
+
+T = TypeVar("T")
+def identity(x: T) -> T:
+    return x
+
+reveal_type(call(identity, 2))  # N: Revealed type is "builtins.int"
+y: int = call(identity, 2)
+[builtins fixtures/paramspec.pyi]
+
 [case testParamSpecNestedApplyNoCrash]
+# flags: --new-type-inference
 from typing import Callable, TypeVar
 from typing_extensions import ParamSpec
 
@@ -1639,9 +1675,33 @@
 T = TypeVar("T")
 
 def apply(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: ...
-def test() -> None: ...
-# TODO: avoid this error, although it may be non-trivial.
-apply(apply, test)  # E: Argument 2 to "apply" has incompatible type "Callable[[], None]"; expected "Callable[P, T]"
+def test() -> int: ...
+reveal_type(apply(apply, test))  # N: Revealed type is "builtins.int"
+[builtins fixtures/paramspec.pyi]
+
+[case testParamSpecNestedApplyPosVsNamed]
+from typing import Callable, TypeVar
+from typing_extensions import ParamSpec
+
+P = ParamSpec("P")
+T = TypeVar("T")
+
+def apply(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> None: ...
+def test(x: int) -> int: ...
+apply(apply, test, x=42)  # OK
+apply(apply, test, 42)  # Also OK (but requires some special casing)
+[builtins fixtures/paramspec.pyi]
+
+[case testParamSpecApplyPosVsNamedOptional]
+from typing import Callable, TypeVar
+from typing_extensions import ParamSpec
+
+P = ParamSpec("P")
+T = TypeVar("T")
+
+def apply(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> None: ...
+def test(x: str = ..., y: int = ...) -> int: ...
+apply(test, y=42)  # OK
 [builtins fixtures/paramspec.pyi]
 
 [case testParamSpecPrefixSubtypingGenericInvalid]
diff --git a/test-data/unit/fixtures/paramspec.pyi b/test-data/unit/fixtures/paramspec.pyi
index 5e4b856..9b0089f 100644
--- a/test-data/unit/fixtures/paramspec.pyi
+++ b/test-data/unit/fixtures/paramspec.pyi
@@ -30,7 +30,8 @@
     def __iter__(self) -> Iterator[T]: ...
 
 class int:
-    def __neg__(self) -> 'int': ...
+    def __neg__(self) -> int: ...
+    def __add__(self, other: int) -> int: ...
 
 class bool(int): ...
 class float: ...
diff --git a/test-data/unit/typexport-basic.test b/test-data/unit/typexport-basic.test
index cd2afe2..c4c3a1d 100644
--- a/test-data/unit/typexport-basic.test
+++ b/test-data/unit/typexport-basic.test
@@ -727,7 +727,7 @@
 class B:
   a = None # type: A
 [out]
-LambdaExpr(2) : def (B) -> A
+LambdaExpr(2) : def (x: B) -> A
 MemberExpr(2) : A
 NameExpr(2) : B
 
@@ -756,7 +756,7 @@
   a = None # type: A
 [builtins fixtures/list.pyi]
 [out]
-LambdaExpr(2) : def (B) -> builtins.list[A]
+LambdaExpr(2) : def (x: B) -> builtins.list[A]
 ListExpr(2) : builtins.list[A]
 
 [case testLambdaAndHigherOrderFunction]
@@ -775,7 +775,7 @@
 CallExpr(9) : builtins.list[B]
 NameExpr(9) : def (f: def (A) -> B, a: builtins.list[A]) -> builtins.list[B]
 CallExpr(10) : B
-LambdaExpr(10) : def (A) -> B
+LambdaExpr(10) : def (x: A) -> B
 NameExpr(10) : def (a: A) -> B
 NameExpr(10) : builtins.list[A]
 NameExpr(10) : A
@@ -795,7 +795,7 @@
 [builtins fixtures/list.pyi]
 [out]
 NameExpr(10) : def (f: def (A) -> builtins.list[B], a: builtins.list[A]) -> builtins.list[B]
-LambdaExpr(11) : def (A) -> builtins.list[B]
+LambdaExpr(11) : def (x: A) -> builtins.list[B]
 ListExpr(11) : builtins.list[B]
 NameExpr(11) : def (a: A) -> B
 NameExpr(11) : builtins.list[A]
@@ -817,7 +817,7 @@
 --      context. Perhaps just fail instead?
 CallExpr(7) : builtins.list[Any]
 NameExpr(7) : def (f: builtins.list[def (A) -> Any], a: builtins.list[A]) -> builtins.list[Any]
-LambdaExpr(8) : def (A) -> A
+LambdaExpr(8) : def (x: A) -> A
 ListExpr(8) : builtins.list[def (A) -> Any]
 NameExpr(8) : A
 NameExpr(9) : builtins.list[A]
@@ -838,7 +838,7 @@
 [out]
 CallExpr(9) : builtins.list[B]
 NameExpr(9) : def (f: def (A) -> B, a: builtins.list[A]) -> builtins.list[B]
-LambdaExpr(10) : def (A) -> B
+LambdaExpr(10) : def (x: A) -> B
 MemberExpr(10) : B
 NameExpr(10) : A
 NameExpr(11) : builtins.list[A]
@@ -860,7 +860,7 @@
 CallExpr(9) : builtins.list[B]
 NameExpr(9) : def (f: def (A) -> B, a: builtins.list[A]) -> builtins.list[B]
 NameExpr(10) : builtins.list[A]
-LambdaExpr(11) : def (A) -> B
+LambdaExpr(11) : def (x: A) -> B
 MemberExpr(11) : B
 NameExpr(11) : A
 
@@ -1212,7 +1212,7 @@
 [builtins fixtures/list.pyi]
 [out]
 NameExpr(8) : Overload(def (x: builtins.int, f: def (builtins.int) -> builtins.int), def (x: builtins.str, f: def (builtins.str) -> builtins.str))
-LambdaExpr(9) : def (builtins.int) -> builtins.int
+LambdaExpr(9) : def (x: builtins.int) -> builtins.int
 NameExpr(9) : builtins.int
 
 [case testExportOverloadArgTypeNested]
@@ -1231,10 +1231,10 @@
     lambda x: x)
 [builtins fixtures/list.pyi]
 [out]
-LambdaExpr(9) : def (builtins.int) -> builtins.int
-LambdaExpr(10) : def (builtins.int) -> builtins.int
-LambdaExpr(12) : def (builtins.str) -> builtins.str
-LambdaExpr(13) : def (builtins.str) -> builtins.str
+LambdaExpr(9) : def (y: builtins.int) -> builtins.int
+LambdaExpr(10) : def (x: builtins.int) -> builtins.int
+LambdaExpr(12) : def (y: builtins.str) -> builtins.str
+LambdaExpr(13) : def (x: builtins.str) -> builtins.str
 
 -- TODO
 --