Repository navigation
Do not infer by-ref use effects at invocations for a closure assigned to its own by-ref variable - #6818
Open
phpstan-bot wants to merge 1 commit into
Open
Do not infer by-ref use effects at invocations for a closure assigned to its own by-ref variable#6818phpstan-bot wants to merge 1 commit into
phpstan-bot wants to merge 1 commit into
Conversation
… to its own by-ref variable - ClosureSignatureInference::getByRefUseMarkers() returns no markers for a closure that is the right side of an assignment to one of its by-ref uses (`$f = function () use (&$f)`). The body can reach the closure itself through that variable in ways the invocation tracking does not follow, so those by-ref uses keep the creation-time fixpoint. Before this, a body that only reached itself indirectly (a nested closure or arrow function capturing `$f`, an alias, `call_user_func($f)`, `$f->__invoke()`, or just reading `$f`) was entered with `$f` as it was before the assignment: `null`. - getAssignedClosures() takes a `$nested` flag (cached under its own attribute) to also look inside nested function-likes. A closure created in another closure's body is observed through the enclosing function's frame. - The turbo-ext C++ mirror (ClosureSignatureInference.cpp and its generated header) is updated the same way. - Considered first: making the existing "body invokes its own by-ref variable" guard follow captures into nested closures and arrow functions. That fixes the reported nested case but misses aliasing, call_user_func, __invoke and plain reads. It also makes mutual recursion through a captured variable less precise, so I didn't use it.
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 (
closureSignaturesFromUsages), a self-referencing closure such as$fx = static function () use (&$fx) { … }saw$fxasnullin its body whenever it reached itself in a way other than a direct$fx()call. The reported case is a nested closure that captures$fxand calls it. That produced false positives like "Trying to invoke null but it's not a callable."The fix: a closure assigned to one of its own by-ref variables no longer gets by-ref markers. Its by-ref uses keep the creation-time fixpoint, which is the behaviour without the feature toggle.
Changes
src/Analyser/Generics/ClosureSignatureInference.phpgetByRefUseMarkers()returns[]when the closure is assigned to one of its by-ref variables.getAssignedClosures()takes a$nestedflag (cached under a newNESTED_ASSIGNED_CLOSURES_ATTRIBUTE). With it, the scan also covers closures assigned inside nested function-likes. A closure created in another closure's body is observed through the enclosing function's frame, so the top-level-only map would miss it.turbo-ext/src/ClosureSignatureInference.cppandturbo-ext/src/generated/ClosureSignatureInference.h: the same change in the native mirror (new attribute string, private constant,nestedparameter, self-assignment check).side-by-side.phppairs all methods,smoke.phppasses, and the regression test passes with the extension loaded, including with the PHP change stashed.Analogous cases, all fixed by the same check (each was
nullbefore):$fxby value or by reference$fx()call_user_func($fx)$fx->__invoke()$alias = $fx; $alias();)$fxwithout calling itRoot cause
A closure written where nothing types it gets a marker per by-ref use. Its body is then entered with the by-ref variable's type at creation, joined with its types at the invocations that were seen. For
$fx = function () use (&$fx)the creation state is before the assignment, so it isnull. The only guard against this was syntactic: the body directly calling$fx(). Any other way of reaching the closure through$fxslipped past it.I first tried extending that guard to follow captures into nested closures and arrow functions. It still missed aliasing,
call_user_func,__invokeand plain reads. It also made mutual recursion through a captured variable less precise ((Closure)|nullbecamenull), so I dropped it in favour of checking the self-assignment directly.Test
tests/PHPStan/Analyser/nsrt/bug-15442.php: the reproducer from the issue, plus the nested arrow function, nested by-ref capture,call_user_func,__invoke, alias, read-only, passed-as-callback and inside-a-closure variants. A control checks that another closure's by-ref scalar is still followed at its invocations.tests/PHPStan/Rules/Functions/CallCallablesRuleTest::testBug15442: no "Trying to invoke null" errors for the issue's code or the arrow-function variant.null) and the rule test reports 2 errors.Fixes phpstan/phpstan#15442