Fix phpstan/phpstan#14297: Regression: type narrowing on non-empty-list array shape#5227
Conversation
- Handle falsey context in specifyTypesForCountFuncCall for constant arrays by computing the remaining size range and recursing with negated context - Update test expectations in list-count.php for improved narrowing - Add array_values() to UnionType/IntersectionType constructor calls that lost list type due to more precise count narrowing - New regression test in tests/PHPStan/Analyser/nsrt/bug-14297.php Closes phpstan/phpstan#14297
ondrejmirtes
left a comment
There was a problem hiding this comment.
In src/Reflection/InitializerExprTypeResolver.php and src/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.php you added array_values calls.
I suspect it's because make phpstan started showing errors.
But in these cases it's obvious the array is already a list. Fix the analyser so that these calls are not needed. And remove them so that the diff doesn't touch any unrelated code to the actual type inference and narrowing.
… optional keys When ConstantArrayTypeBuilder builds arrays with optional keys at sequential positions, its nextAutoIndexes tracking degrades isList to "maybe" even though the result is definitively a list. Fix this by calling makeList() on the built array when we know the original type was a list. This removes the workaround array_values() calls in InitializerExprTypeResolver and ArrayChangeKeyCaseFunctionReturnTypeExtension that were needed because the analyser incorrectly inferred those arrays might not be lists. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's what I did: Root cause: When Fix (in Removed workarounds: Reverted the |
|
I processed this review but have nothing to report. |
|
Please see phpstan/phpstan#11488 (comment), this change does not seem to be right. |
|
@mvorisek Please open a new bug report and show what this has broken. You have a few hours until release 😊 |
Summary
When a constant array type with optional keys (e.g.,
non-empty-list{0?: 'a'|'b', 1?: 'b'}) was narrowed using acount()check likecount($a) === 2, the true branch correctly narrowed to the specific shape, but the else branch kept the original unnarrowed type instead of narrowing to the complementary shape.Changes
specifyTypesForCountFuncCall()insrc/Analyser/TypeSpecifier.php— for constant array types, computes the remaining possible size by removing the checked size from the array's size range, then recursively calls with the negated (truthy) context to build the narrowed typetests/PHPStan/Analyser/nsrt/list-count.phpfor the improved else-branch narrowing behaviorarray_values()insrc/Reflection/InitializerExprTypeResolver.phpandsrc/Type/Php/ArrayChangeKeyCaseFunctionReturnTypeExtension.phpto preserve list types for constructor parameters affected by the more precise narrowingRoot cause
The
specifyTypesForCountFuncCall()method had acontinuethat skipped array types in the falsey context whenisSizeSuperTypeOfArraySizewasmaybe. This meant constant arrays with optional keys — where the size is a range rather than exact — got no narrowing in the else branch. The fix computes what count values remain possible after excluding the checked value, and then reuses the existing truthy narrowing logic to build the correct narrowed type.Test
Added
tests/PHPStan/Analyser/nsrt/bug-14297.phpwhich reproduces the original issue: after filtering and counting a list with optional keys, verifying thatcount($a) === 2in the else branch correctly narrows to the single-element shape.Fixes phpstan/phpstan#14297