Repository navigation
Re-process a stored scope-changing expression at a foreign ask position instead of re-anchoring it - #6832
Open
phpstan-bot wants to merge 1 commit into
Open
Re-process a stored scope-changing expression at a foreign ask position instead of re-anchoring it#6832phpstan-bot wants to merge 1 commit into
phpstan-bot wants to merge 1 commit into
Conversation
…on instead of re-anchoring it - NodeScopeResolver::processExprNode() answered an on-demand walk from a node's stored ExpressionResult re-anchored with atAskPosition(), which sets both the before and the after scope to the asking scope. For a node that changed the scope (assignment, assign-op, ++/--, list destructuring, ...) this dropped its effect: when ArrayMapFunctionReturnTypeExtension priced the synthetic `$callback(...)` call, the closure body's `$id = f()` was answered from storage without defining `$id`, so only the `if ($id === null)` branch survived and the callback seemed to return `null` only. - Such a node (stored after-scope !== before-scope) is now re-processed on the asking scope; its pure subnodes still answer from storage. Closures and arrow functions keep the existing answer (their stored result is their converged walk). The consume mode is unchanged. - Mirrored in turbo-ext/src/NodeScopeResolver.cpp (processExprNodeBody()). - Probed `+=`, `??=`, `++`, by-ref out parameter (preg_match), list destructuring, arrow function with an assignment and an array_filter() callback: `+=`, `++` and list destructuring had the same bug; the others already worked and are kept as coverage of the same path.
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
Since 2.3.0,
array_filter(array_map(function (string $a): ?string { $id = f($a); if ($id === null) { ... } return $id; }, explode('|', (string) ($data['x'] ?? ''))))was inferred as filtering anon-empty-list<null>, which reported a falsearrayFilter.alwaysEmpty. The callback's return type was computed wrong. The fix re-processes stored expressions that changed the scope when an on-demand walk asks for them at a different position. Before, they were re-anchored, and that dropped their effect.Changes
src/Analyser/NodeScopeResolver.php(processExprNode()): in the stored-result path (not consume mode), when the stored result's after-scope is not its before-scope and the node is not a closure or arrow function, callprocessExprNodeInternal()on the asking scope. Before, the code returnedatAskPosition($scope).turbo-ext/src/NodeScopeResolver.cpp(processExprNodeBody()): the same change in the native mirror.$i += ...,$i++,[$id] = [...]??=, by-ref out parameter (preg_match(..., $m)), an arrow function whose body assigns, anarray_filter()callbackRoot cause
ArrayMapFunctionReturnTypeExtensionprices the element type with a synthetic call,$callback(TypeExpr(...)). It is processed on demand withreturnStoredExpressionResultsenabled, so the real nodes in the closure body come from storage. A stored result is reused at a foreign position when the asking scope agrees on the variables the node reads.ExpressionResult::atAskPosition()then rebuilds the result withscope = beforeScope = asking scope. That is fine for pure nodes, but it loses every write a scope-changing node makes. Here$id = idFromAlias($alias)read$alias, which matched, but$idwas never defined. After theif ($id === null) {}merge,$idwas only "maybenull", so the callback appeared to returnnull. The??/ ternary in the other argument and the outer call (array_filter) are only what made the body's stored results visible to this on-demand walk. Re-processing nodes whose stored walk changed the scope restores their effects. Their side-effect-free subnodes (idFromAlias($alias)) still answer from storage.Test
tests/PHPStan/Analyser/nsrt/bug-15491.php: the issue's reproducer (assertTypeon the filtered result), plus the analogous cases above (+=,??=,++, by-ref out parameter, list destructuring, arrow function,array_filtercallback). Without the fix, the reproducer,+=,++and list destructuring fail.tests/PHPStan/Rules/Functions/ArrayFilterRuleTest::testBug15491withdata/bug-15491.php: the playground sample verbatim. It expects no error; without the fix it reportsarrayFilter.alwaysEmptyon line 16.Fixes phpstan/phpstan#15491
🤖 Generated with Claude Code