Fix phpstan/phpstan#12790: false positive “Match expression does not handle remaining value: int<...>” for match (count(...)) { ...}#5228
Conversation
- When match subject is count() wrapped in AlwaysRememberedExpr, the specifyTypesForCountFuncCall path narrowed the array but not the count expression type itself, so subsequent arms saw the original unnarrowed int range - Also narrow the AlwaysRememberedExpr and unwrapped count expression when specifyTypesForCountFuncCall succeeds - New regression test in tests/PHPStan/Rules/Comparison/data/bug-12790.php Closes phpstan/phpstan#12790
ondrejmirtes
left a comment
There was a problem hiding this comment.
Don't like the fix, AlwaysRememberedExpr should be unwrapped elsewhere.
Try to come up with similar code examples that are not covered by this fix, and then fix it in a general way. There are already a few places in TypeSpecifier that unwrap AlwaysRememberedExpr like that.
…n count/strlen handling Instead of special-casing AlwaysRememberedExpr at the specifyTypesForCountFuncCall result site, pass $leftExpr (which may be wrapped) to $this->create() which already handles AlwaysRememberedExpr unwrapping internally. This generalizes the fix to also cover count() isZero, count() truthy, strlen() isZero, and strlen() truthy cases. For specifyTypesForCountFuncCall, use $leftExpr !== $unwrappedLeftExpr check to only add func type narrowing when the expression is wrapped, avoiding a regression in unconditional count expression narrowing. Also adds a strlen() match expression test case to bug-12790.php. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's a summary of the changes: Approach: Instead of special-casing Changes in
Test additions: Added |
|
I processed this review but have nothing to report. |
1 similar comment
|
I processed this review but have nothing to report. |
Summary
False positive "Match expression does not handle remaining value" when using
match (count($array))where the array has a known finite count range (e.g.,int<1, 2>) and all values are covered by match arms.Changes
src/Analyser/TypeSpecifier.phpin theresolveNormalizedIdenticalmethod: whenspecifyTypesForCountFuncCallreturns successfully and the count expression is wrapped inAlwaysRememberedExpr(as happens in match expressions), also narrow the count expression type alongside the array typetests/PHPStan/Rules/Comparison/data/bug-12790.phpwith corresponding test method inMatchExpressionRuleTest.phpRoot cause
In match expressions, the subject expression (e.g.,
count($r)) is wrapped inAlwaysRememberedExprto preserve its type across match arms. When the TypeSpecifier processescount($r) === 1in the falsy branch (to narrow types for subsequent arms),specifyTypesForCountFuncCallcorrectly narrows the array$rto exclude the matched count, but it does not narrow theAlwaysRememberedExpr(count($r))type itself. SinceAlwaysRememberedExprstores its type in expression holders and doesn't re-evaluate, the count type remains as the originalint<1, 2>even after the array is narrowed. This causes subsequent match arms to not properly reduce the remaining type tonever.The fix adds count expression narrowing (for both the
AlwaysRememberedExprwrapper and the unwrapped expression) whenspecifyTypesForCountFuncCallsucceeds and the expression is wrapped inAlwaysRememberedExpr.Test
Added
tests/PHPStan/Rules/Comparison/data/bug-12790.phpwhich creates an array with countint<1, 2>and uses a match expression with arms covering both values (1 and 2). The test expects nomatch.unhandlederror.Fixes phpstan/phpstan#12790