Skip to content

Fix phpstan/phpstan#12790: false positive “Match expression does not handle remaining value: int<...>” for match (count(...)) { ...}#5228

Merged
ondrejmirtes merged 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-t7sy050
Mar 16, 2026
Merged

Fix phpstan/phpstan#12790: false positive “Match expression does not handle remaining value: int<...>” for match (count(...)) { ...}#5228
ondrejmirtes merged 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-t7sy050

Conversation

@phpstan-bot
Copy link
Collaborator

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

  • Modified src/Analyser/TypeSpecifier.php in the resolveNormalizedIdentical method: when specifyTypesForCountFuncCall returns successfully and the count expression is wrapped in AlwaysRememberedExpr (as happens in match expressions), also narrow the count expression type alongside the array type
  • Added regression test tests/PHPStan/Rules/Comparison/data/bug-12790.php with corresponding test method in MatchExpressionRuleTest.php

Root cause

In match expressions, the subject expression (e.g., count($r)) is wrapped in AlwaysRememberedExpr to preserve its type across match arms. When the TypeSpecifier processes count($r) === 1 in the falsy branch (to narrow types for subsequent arms), specifyTypesForCountFuncCall correctly narrows the array $r to exclude the matched count, but it does not narrow the AlwaysRememberedExpr(count($r)) type itself. Since AlwaysRememberedExpr stores its type in expression holders and doesn't re-evaluate, the count type remains as the original int<1, 2> even after the array is narrowed. This causes subsequent match arms to not properly reduce the remaining type to never.

The fix adds count expression narrowing (for both the AlwaysRememberedExpr wrapper and the unwrapped expression) when specifyTypesForCountFuncCall succeeds and the expression is wrapped in AlwaysRememberedExpr.

Test

Added tests/PHPStan/Rules/Comparison/data/bug-12790.php which creates an array with count int<1, 2> and uses a match expression with arms covering both values (1 and 2). The test expects no match.unhandled error.

Fixes phpstan/phpstan#12790

- 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
Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@phpstan-bot
Copy link
Collaborator Author

Done. Here's a summary of the changes:

Approach: Instead of special-casing AlwaysRememberedExpr at the specifyTypesForCountFuncCall result site (which the reviewer disliked), the fix uses $leftExpr (potentially wrapped) instead of $unwrappedLeftExpr when calling $this->create() for function expression type narrowing. Since create() already handles AlwaysRememberedExpr unwrapping internally, this is a more general solution.

Changes in TypeSpecifier.php:

  • count() isZero case (line 2571): $unwrappedLeftExpr$leftExpr
  • specifyTypesForCountFuncCall case (line 2589): Replaced instanceof AlwaysRememberedExpr check with $leftExpr !== $unwrappedLeftExpr and uses $this->create($leftExpr, ...) (which handles both wrapped and unwrapped via create())
  • count() truthy case (line 2594): $unwrappedLeftExpr$leftExpr
  • strlen() isZero case (line 2620): $unwrappedLeftExpr$leftExpr
  • strlen() truthy case (line 2629): $unwrappedLeftExpr$leftExpr

Test additions: Added match (strlen($s)) test case to bug-12790.php to cover the strlen scenario.

@ondrejmirtes ondrejmirtes merged commit 2ee73af into phpstan:2.1.x Mar 16, 2026
650 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-t7sy050 branch March 16, 2026 16:26
@phpstan-bot
Copy link
Collaborator Author

I processed this review but have nothing to report.

1 similar comment
@phpstan-bot
Copy link
Collaborator Author

I processed this review but have nothing to report.

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.

2 participants