Repository navigation
Use the enclosing body's send-decided template argument resolutions inside nested closure bodies - #6819
Open
phpstan-bot wants to merge 1 commit into
Open
Use the enclosing body's send-decided template argument resolutions inside nested closure bodies#6819phpstan-bot wants to merge 1 commit into
phpstan-bot wants to merge 1 commit into
Conversation
…nside nested closure bodies
- TemplateArgumentFrame: record which resolutions a send decided (sentResolutionKeys,
filled by TemplateArgumentResolver::collectSentResolutionKeys()) and expose them via
resolveSentFromEnclosingBody(), which walks up the parent frames
- ResolvedFunctionVariantWithOriginal::unresolvedOrResolvedTemplateArgument(): while a
closure body observes, reuse an enclosing body's send-decided resolution of the call
site instead of creating a fresh marker that the closure's own pass resolves to the
literal initial type (method, function and static calls)
- NewHandler (unresolvedArgumentList(), rekeyParentTemplateArgument()) and
GenericTypeTemplateTraverser: same for `new` sites - constructor-inferred arguments,
classes without a constructor, and arguments inherited from a parent constructor
- ClosureTypeResolver::getClosureType(): walk a closure body in a single pass while the
enclosing body is observing, so its markers reach the enclosing body (the generic call
taking the closure, e.g. run(function () { ... })) instead of being resolved by the
closure's own pass before the enclosing body could see the send that decides them
- Only send-decided resolutions are reused: an argument the enclosing body merely
inferred (e.g. from a closure parameter, closureCreatesGenericObject) stays with the
closure's own walk, which infers it per invocation
- Ported to the turbo extension: TemplateArgumentFrame.cpp, TemplateArgumentResolver.cpp,
ResolvedFunctionVariantWithOriginal.cpp, NewHandler.cpp, ClosureTypeResolver.cpp,
support.h, regenerated TemplateArgumentFrame.h / TemplateArgumentResolver.h
- Probed and already correct: arrow functions (part of the enclosing body), nested
closures inside arrow functions, closures stored in a variable inside a closure
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
With bleeding edge (
unresolvedTemplateArguments), a generic call inside a closure that has a statement body kept its literal template arguments. The declared return type of the enclosing method did not widen them, while the same code in an arrow function worked. For example,keyBy()inferred'negative'|'positive'whereColl<string, int>was expected. When the closure was called through a variable, the internalunresolved(...)marker even showed up in the error message.The enclosing body already resolves those sites correctly, because its observation pass sees the send to
@return Coll<string, int>. The nested closure body ignored that resolution and solved the site again with only its own information. This PR makes the nested body reuse a resolution from an enclosing body when a send decided it.Changes
src/Analyser/Generics/TemplateArgumentFrame.php: newsentResolutionKeysandresolveSentFromEnclosingBody(). It returns the resolution of the nearest frame (walking up the parents) where a send decided the argument.src/Analyser/Generics/TemplateArgumentResolver.php:collectSentResolutionKeys()passes the keys of non-closure-signature observations with sends to both frames thatresolve()builds.withObservedClosures()andwithStaticVariableTypes()carry them along.src/Reflection/ResolvedFunctionVariantWithOriginal.php(unresolvedOrResolvedTemplateArgument()): while observing, uses the enclosing send-decided resolution before creating a marker. This covers method, function and static calls.src/Analyser/Traverser/GenericTypeTemplateTraverser.php,src/Analyser/ExprHandler/NewHandler.php(unresolvedArgumentList(),rekeyParentTemplateArgument()): the same fornewsites. That includes constructor-inferred arguments, classes without a constructor, and arguments inherited from a parent constructor.src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php(getClosureType()): while the enclosing frame is observing, the closure body is walked in a single pass. Its markers then reach the enclosing body, for example through the return type ofrun(function () { ... }). Before, the closure's own two-pass resolution consumed them first.turbo-ext/src/TemplateArgumentFrame.cppturbo-ext/src/TemplateArgumentResolver.cppturbo-ext/src/ResolvedFunctionVariantWithOriginal.cppturbo-ext/src/NewHandler.cppturbo-ext/src/ClosureTypeResolver.cppturbo-ext/src/support.hturbo-ext/src/generated/TemplateArgumentFrame.handTemplateArgumentResolver.hAnalogous cases fixed, each covered by the rule test:
static functionclosuresnewwith constructor-inferred argumentscollect([...]))Coll::make([...]))newof a class without a constructornewof a child class that inherits a generic parent constructor (block closure and closure variable)Probed and already correct (no change needed): arrow functions, which are part of the enclosing body, and closures stored in variables inside other closures.
Root cause
Template argument resolution runs once per function-like body (
StatementsHandler::processBodyStmtNodesTwoPass()). A closure body has its own frame, while an arrow function is part of the enclosing body. Inside a closure's observation pass, every generic call ornewcreated a newUnresolvedTemplateArgumentTypemarker. With only a raw native return type (: Coll) and no send inside the closure, it resolved to the literal initial type. Two paths were affected:string) was ignored, so the closure created a new marker.ClosureTypeResolver::getClosureType()walked the body with template argument resolution enabled even during the enclosing observation pass. The closure's return type reachedrun()'sTalready resolved to the literal, so the enclosing body never saw a marker it could resolve.A first version that reused any enclosing resolution regressed
closure-signature-from-usages-generics.php. There the enclosing body infersnew Box($v)only from the closure parameter (no send), and the closure's own per-invocation walk knows better (Box<1>). So only resolutions that a send decided are reused.Test
tests/PHPStan/Rules/Methods/data/bug-15415.phpwithReturnTypeRuleTest::testBug15415()covers:blockClosure,closureInVariable), plusarrowFunctionanddirectas controlsmapWithKeys()-style callback with a bodyBefore the fix it reported 11 false
return.typeerrors (andargument.typeerrors in the nested cases), includingColl<unresolved('negative'|'positive'), int>.tests/PHPStan/Analyser/nsrt/bug-15415.phpassertsColl<string, int>inside the closure body, for the result ofrun(), and for$f(). All three assertions failed before.Full test suite passes with and without the turbo extension.
make phpstanpasses. Turbo smoke and signature-parity tests pass. Analysis output is identical with the extension on and off for the reproducers.Fixes phpstan/phpstan#15415
🤖 Generated with Claude Code