You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Declared return type does not widen literal template arguments inside a closure with a body (unresolvedTemplateArguments) #15415
With bleedingEdge on 2.3.0, a generic call inside a closure with a body keeps its literal template arguments. The declared return type does not widen them. In an arrow function, it does.
In the snippet, keyBy() infers TNewKey as 'negative'|'positive'. The method declares @return Coll<string, int>, and TKey is invariant:
direct() returns the call: no error.
arrowFunction() returns it through run(fn (): Coll => ...): no error.
blockClosure() returns it through run(function () use ($c): Coll { return ...; }): return.type, because the result is Coll<'negative'|'positive', int>.
There is no error without bleedingEdge, with featureToggles.unresolvedTemplateArguments: false, or on 2.2.17. It still happens on 2.3.x at f9a915969a9710d6e7f4c1ae3e9249b015115753.
Laravel's Collection has an invariant TKey too. I found this with keyBy() inside once(function () { ... }), and with a mapWithKeys() callback with a body that returns an inner collection.
I think this comes from where template arguments are resolved. StatementsHandler::processBodyStmtNodesTwoPass() gives an Expr\Closure body its own two-pass resolution, but an arrow function is part of the enclosing body. So the marker that keyBy() creates inside the closure is resolved by the closure's own pass. That pass only sees the return to the raw Coll, not the outer @return Coll<string, int>, so it falls back to the literal keys.
A related case shows the marker itself in an error, when the closure is called through a variable:
Activity
SanderMuller commentedon Oct 6, 2026
I think this comes from where template arguments are resolved.
StatementsHandler::processBodyStmtNodesTwoPass()gives anExpr\Closurebody its own two-pass resolution, but an arrow function is part of the enclosing body. So the marker thatkeyBy()creates inside the closure is resolved by the closure's own pass. That pass only sees thereturnto the rawColl, not the outer@return Coll<string, int>, so it falls back to the literal keys.A related case shows the marker itself in an error, when the closure is called through a variable:
https://phpstan.org/r/a53ff970-0411-4103-be2f-ad7590a155a8
This is bleedingEdge only, on 2.3.0 and on 2.3.x at f9a915969. The class doc of
UnresolvedTemplateArgumentTypesays rules never see it.