Pre-compute count-specific conditional expressions to narrow list types when count() is stored in a variable#5461
Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Conversation
…es when `count()` is stored in a variable
- When `$count = count($list)` is assigned, pre-compute conditional
expressions for count values 1-8 so that `$count === N` narrows
`$list` to the exact array shape (e.g. `array{T, T, T}` for N=3)
- Previously, only inline `count($list) === 3` narrowed correctly;
storing the count in a variable only gave `non-empty-list<T>`
- The fix extends AssignHandler to call specifyTypesInCondition with
synthetic `count($expr) === N` comparisons for small N values,
storing the results as ConditionalExpressionHolders
- Works for count() and sizeof() with a single argument on list and
constant array types
- Analogous cases verified: sizeof() alias, explode() results,
non-empty-list types, switch statements, PHPDoc list types
- strlen() variable narrowing is a separate pre-existing issue with
a different mechanism (no TypeSpecifyingExtension) — not addressed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When
count()is used inline in a condition (e.g.if (count($list) === 3)), PHPStan correctly narrows alist<T>toarray{T, T, T}. However, when the count result is stored in a variable first ($n = count($list); if ($n === 3)), the narrowing only producednon-empty-list<T>— losing the exact array shape and causing false "Offset might not exist" warnings.Changes
src/Analyser/ExprHandler/AssignHandler.php$var = count($arrayExpr)(orsizeof()) is assigned with a single argument, and the array is a list or constant array type, pre-compute conditional expressions for count values 1 through 8$varvalue (e.g.int(3)) to the precise narrowed array type (e.g.array{T, T, T})specifyTypesInCondition+processSureTypesForConditionalExpressionsAfterAssignmechanismCOUNT_CONDITIONAL_LIMIT = 8constant to bound the pre-computation!$type instanceof ConstantIntegerTypeto skip when count is already knownRoot cause
The conditional expression mechanism in
AssignHandlerpre-computes type narrowings at variable assignment time. For$count = count($list), it previously only computed:$countis non-zero →$listisnon-empty-list(viaCountFunctionTypeSpecifyingExtension)When
$count === 3was later checked, the truthy conditional fired via supertype match (int<1, max>⊇int(3)), giving onlynon-empty-list. The more precisearray{T, T, T}narrowing fromspecifyTypesForCountFuncCallwas never invoked because the variable comparison didn't reach the count-specific code path inresolveNormalizedIdentical(which requires the expression to be aFuncCall, not aVariable).The fix pre-computes the count-specific narrowing for small integer values (1-8) at assignment time, storing them as conditional expressions that fire on exact match when the count variable is compared to a specific integer.
Analogous cases probed
sizeof()alias: works (checked for in the same condition) ✓explode()results: works (produceslist<string>which is narrowed) ✓non-empty-listtypes: works ✓switchstatement: works (each case narrows independently) ✓COUNT_NORMALmode (2 args): excluded from pre-computation, falls back tonon-empty-list— acceptable since the mode could affect semanticsnon-empty-listvia truthy conditional — acceptable tradeoffstrlen()variable narrowing: separate pre-existing issue —strlen()has noFunctionTypeSpecifyingExtension, so neither truthy nor exact-value conditional expressions are created for string narrowingcount()doesn't narrowarray{a: string, b?: int}by count valueTest
tests/PHPStan/Analyser/nsrt/bug-14464.php— regression test for the reported issue: variable count with==and===onpreg_splitresult and PHPDoc list typestests/PHPStan/Analyser/nsrt/bug-14464-analogous.php— tests for analogous cases:sizeof(),explode(),non-empty-list, range comparisons, values beyond limit, count with mode, andswitchstatementsFixes phpstan/phpstan#14464