Narrow the match subject when its assigned result is later narrowed#5808
Conversation
- Extract `MatchHandler::getArmScopesAndTypes()` from `resolveType()`, returning each reachable arm's body type together with the scope in which the match subject is narrowed to that arm's condition (preserving the enum fast path).
- In `AssignHandler`, for a `$var = match (...) { … }` assignment, assign each arm body type to `$var` inside the arm's subject-narrowed scope and merge the per-arm scopes with `MutatingScope::mergeWith()` exactly like an equivalent `if`/`elseif`/`else` chain. This reuses the existing conditional-expression machinery so the holders linking `$var` and the subject are identical to the `if` form.
- A later narrowing of the assigned result (e.g. `if ($class === null) return;`) now narrows the subject too, matching the long-standing behaviour of the `if` equivalent. Works for `mixed`, scalar, `int`, enum subjects, `match (true)` with comparison arms, and non-null defaults.
ondrejmirtes
left a comment
There was a problem hiding this comment.
Add regression test for phpstan/phpstan#10843
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Done. I addressed the review by adding the requested regression test for issue #10843. What I didVerified #10843 is actually fixed by this PR. Issue #10843 is the reverse direction of the PR's main case: instead of narrowing the result from the subject, here narrowing the subject (
So this is a genuine regression test — it fails before the fix and passes after. Added Verified: Committed as a separate logical change ("Add regression test for #10843") and pushed to the PR branch. |
Summary
A narrowing applied to a variable assigned from an
if/elseif/elsechain is projected back onto the condition subject (e.g. after$class = ...; if ($class === null) return;, the subject$xis narrowed to the values that produced a non-null$class). The same did not happen when the value was produced by amatchexpression — the subject stayedmixed. This change makes$var = match (...)build the same subject↔result relationship as theifform.Changes
src/Analyser/ExprHandler/MatchHandler.php: extractedgetArmScopesAndTypes()fromresolveType(). It returns, per reachable arm,[scope, bodyType]wherescopehas the match subject narrowed to that arm's condition.resolveType()now unions the body types from this method. The enum fast path is preserved, so huge-enum matches keep their performance.src/Analyser/ExprHandler/AssignHandler.php: when the assigned expression of a$var = ...is aMatch_, callprocessMatchForConditionalExpressionsAfterAssign(). It assigns each arm's body type to$varinside that arm's subject-narrowed scope, then folds the per-arm scopes together withMutatingScope::mergeWith($scope, true)in the same order anif/elseif/elsechain would. The conditional-expression holders that fall out of the merge are lifted onto the real scope. Added a smallmergeConditionalExpressions()helper.MatchHandleris now injected intoAssignHandler.Root cause
PHPStan records "conditional expression holders" that capture relationships like "when
$classis'some_class',$xis'aa'". Forif/elseif/else, these are produced automatically byMutatingScope::mergeWith()/createConditionalExpressions()because the assignment$class = …happens inside each branch scope where the subject is already narrowed. Amatchexpression resolves to a single union before the assignment, so the assignment to$classhappens outside the per-arm narrowed scopes and the relationship was lost. The fix reconstructs the per-arm narrowed scopes, performs the assignment inside them, and reuses the exact same merge machinery — guaranteeingmatchandifproduce identical holders.Test
tests/PHPStan/Analyser/nsrt/bug-14772.phpcovers, withassertType():mixedsubject, narrowing the result to non-null narrows the subject to'aa'|'bb'|'cc');'some_class'→'aa');match (true)with===comparison arms;intsubject;defaultarm (narrowing to the default value yieldsmixed~('aa'|'bb'));1|2);Suit::Hearts).Each assertion fails before the fix (the subject stays
mixed/int) and passes after, matching theifequivalent.Fixes phpstan/phpstan#14772
Fixes phpstan/phpstan#10843