Repository navigation
Treat match (true) as exhaustive when the arms' falsey narrowing leaves an expression typed never - #6956
Conversation
VincentLanglet
left a comment
There was a problem hiding this comment.
Compare the strategy with the one from #6947
Complexify / Performance / Other case handled
|
I compared this PR with #6947 by running #6947's test files against this branch. The two take different approaches. Strategy
Complexity
Performance
Cases each one handles
So this PR is smaller and cheaper, and covers more Change pushed: the PR description said
Without the fix, the exhaustive Verification: |
There was a problem hiding this comment.
php implementation looks good. it seems to miss someting in the CPP version though, see https://github.com/phpstan/phpstan-src/actions/runs/41916761866/job/126878382368?pr=6956
✗ turbo-ext/src/generated/MatchHandler.h is stale — run php turbo-ext/bin/generate-declarations.php
|
I fixed the stale-header CI failure by regenerating What was wrong: the new private What the commit contains: only Checks:
|
staabm
left a comment
There was a problem hiding this comment.
windows builds are failling
|
Still waiting on the full test suite and static analysis. I fixed the Windows build failure and pushed it with one other fix as two commits. The Windows failure wasn't caused by this PR: the 2.3.x base branch fails the same way. The Windows turbo build ( That code passes the template function The mutation testing failure ( Checks:
Still failing, also failing on 2.3.x:
|
staabm
left a comment
There was a problem hiding this comment.
move the windows build failure test in a separate pull request with its own description to keep it separate from this PR
c01146d to
b5c8771
Compare
|
I moved the Windows build fix out of this PR, but I couldn't open the separate PR. The token I have isn't allowed to create pull requests on This PR (#6956):
The separate fix:
Checks on this branch:
|
33220bd to
8705e21
Compare
staabm
left a comment
There was a problem hiding this comment.
I think this looks good. lets wait a bit with merging. we need a green 2.3.x before
We risk conflict on cpp... And the bot is not fixing them. I had to close/regenerate the PR everytime. |
…aves an expression typed `never` - MatchHandler: for a constant-boolean subject (`match (true)` / `match (false)`) with no default arm and no always-true condition, check the scope left after every arm's falsey narrowing. If an expression that still had a possible value before the arms is now certainly `never` (a variable, a property fetch, ...), no arm failing to match is impossible, so the match is exhaustive. Before this, exhaustiveness relied on the last arm's condition being typed constant `true`. That works for `is_float()` (its stub has a conditional return type) but not for functions/methods that only declare `@phpstan-assert-if-true`. - Mirrored the new hasExpressionNarrowedToNever() in turbo-ext/src/MatchHandler.cpp. - Analogous cases covered by the same fix and tested: instance methods, static methods and functions with `@phpstan-assert-if-true`, mixing them with `is_*()`, property fetch subjects, array offset subjects, multi-condition arms and `match (false)`. Non-exhaustive cases (`object` with `instanceof` arms, a union with a member no arm covers) are still reported.
…p` in bug-11374 test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oNever() Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Narrowing always writes a certain (Yes) holder, so a maybe-certain never-typed expression cannot reach this check - the condition was dead and left an escaped mutant. scopeHasNeverVariable() also ignores certainty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8705e21 to
cdf764a
Compare
|
Thank you! |
Summary
match (true)arms that call a function or method with@phpstan-assert-if-truenarrowed the subject expression correctly. Even so, PHPStan reportedMatch expression does not handle remaining value: truewhen every case was covered. Built-inis_float()& co. worked only because their stubs use a conditional return type. That types the last arm's condition as constanttrue. A user-defined assert-if-true function keeps its declaredboolreturn type, so the last arm never became "always true".With this change, a
matchon a constant boolean is also exhaustive when, after all arms fail, an expression narrowed by the arms has no possible value left (it isnever).Changes
src/Analyser/ExprHandler/MatchHandler.php: new privatehasExpressionNarrowedToNever(). It walks the tracked expression types of the "no arm matched" scope and looks for an expression that is certainlyneverand was notneverbefore the arms. It is checked next to the existingisScopeConditionallyImpossible()for constant-boolean subjects.turbo-ext/src/MatchHandler.cpp: native mirror of the same check, with the same short-circuit order.Analogous cases, all fixed by the same check and covered in the test:
@phpstan-assert-if-trueon instance methods, static methods and functionsis_*()/ assert-method arms (only the last-arm-is-assert case was broken before)$this->prop) and array offset subject ($a['x'])match (false)Probed and already correct, kept as negative or guard cases:
instanceofon anA|Bunion was already exhaustive.objectwithinstanceof A/instanceof Barms is still correctly reported.int|float|stringsubject with only int and float arms is still correctly reported.Root cause
The falsey narrowing of each arm was applied correctly: after
isInt($v)andisFloat($v)both fail,$visnever. Butmatch (true)exhaustiveness looked only at the subject itself (true) and at whether some arm condition was typed constanttrue. A contradiction in the narrowed scope was only detected for boolean variables with conditional expressions (isScopeConditionallyImpossible()). An ordinary expression narrowed toneverwas not.Test
tests/PHPStan/Rules/Comparison/data/bug-11374.phpwithMatchExpressionRuleTest::testBug11374():Without the fix the test reports 7 false
match.unhandlederrors; with the fix only the two genuinely non-exhaustive matches are reported. The full suite andmake phpstanpass, and the suite also passes with the turbo extension loaded. The turbo smoke test, method-parity check and walk-trace differential over the test data are identical.Fixes phpstan/phpstan#11374
🤖 Generated with Claude Code