Skip to content

Project call-target narrowings through stored booleans#5528

Merged
ondrejmirtes merged 2 commits into2.1.xfrom
method-call-narrow
Apr 24, 2026
Merged

Project call-target narrowings through stored booleans#5528
ondrejmirtes merged 2 commits into2.1.xfrom
method-call-narrow

Conversation

@ondrejmirtes
Copy link
Copy Markdown
Member

@ondrejmirtes ondrejmirtes commented Apr 24, 2026

Summary

  • Extends the conditional-expression-holder machinery used when assigning a comparison to a variable (e.g. $ok = $x->foo() !== null) so the narrowing of $x->foo() survives through a later if ($ok) { … }. Previously only Variable, PropertyFetch, ArrayDimFetch and FuncCall sure-type targets were projected, so MethodCall, NullsafeMethodCall and StaticCall were silently dropped.
  • For call-typed sure-type targets (FuncCall, MethodCall, NullsafeMethodCall, StaticCall) the projection only fires when the sure-type expression is a sub-expression of the assigned RHS — not the RHS itself. That keeps the narrowing of sub-call $x->foo() in $ok = $x->foo() !== null, while dropping the falsey-scalar loop's $this->nullable() === null-style projections that would otherwise wrongly re-narrow fresh calls after subsequent reassignments (the regression in this playground snippet where line 18 flipped from int|null to null).
  • PHPStan virtual nodes (e.g. NativeTypeExpr), scalars and const-fetches are rejected up front so internal nodes don't leak into the conditional-expression map and numeric-string exprStrings don't collide with PHP's array-key autocast.

Test plan

  • New NSRT conditional-expr-narrowing-through-variable.php covers the pure-method-call + stored-boolean case and the preg_match(..., $matches) by-ref case (variable narrowing must survive even when the RHS has impure points).
  • New NSRT try-catch-reassign-method-narrowing.php locks in the playground snippet: inside a catch block, \$device = \$this->nullable(); must be int|null, not null — the original conditional holders from the first \$device = \$this->nullable() must not leak past the reassignment.
  • Full tests/PHPStan/Analyser/NodeScopeResolverTest.php: 1532/1532.
  • tests/PHPStan/Analyser/ + tests/PHPStan/Rules/: 5646 tests, 59 pre-existing skips, 0 failures.

🤖 Generated with Claude Code

closes phpstan/phpstan#9455
closes phpstan/phpstan#5207

ondrejmirtes and others added 2 commits April 24, 2026 18:39
Extends the conditional-expression machinery used for assignments like
`$ok = $x->foo() !== null` so the narrowing survives through a later
`if ($ok) { … }`. Previously only Variable/PropertyFetch/ArrayDimFetch/FuncCall
sure-type targets were projected, so method calls (and static/nullsafe calls)
were dropped entirely.

For call-typed sure-type targets (FuncCall, MethodCall, NullsafeMethodCall,
StaticCall) we only project when the sure-type expression is a sub-expression
of the assigned RHS — not the RHS itself. That keeps the narrowing of
`$x->foo()` in `$ok = $x->foo() !== null` while dropping the falsey-scalar
loop's `$this->nullable() === null` projections that would wrongly survive
across subsequent reassignments.

PHPStan virtual nodes (e.g. NativeTypeExpr), scalars and const-fetches are
rejected to avoid numeric-string array-key autocast and internal nodes
leaking into the conditional-expression map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ondrejmirtes ondrejmirtes merged commit d31559c into 2.1.x Apr 24, 2026
129 of 130 checks passed
@ondrejmirtes ondrejmirtes deleted the method-call-narrow branch April 24, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant