Fix phpstan/phpstan#12665: should return array{a: string, b: int, c: int} but returns non-empty-array<'a'|'b'|'c', int|string>#5193
Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Conversation
- Modified ConstantArrayTypeBuilder::setOffsetValueType() to add unmatched scalar keys as optional entries instead of degrading to a general array, when none of the union members match existing keys and the array is non-empty - New regression test in tests/PHPStan/Analyser/nsrt/bug-12665.php - New test for non-loop union offset setting in nsrt/set-constant-union-offset-on-constant-array.php - Updated array-fill-keys test expectations to reflect improved type precision Closes phpstan/phpstan#12665
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 building an array in a foreach loop by assigning values at constant string keys from a union type, PHPStan would degrade the entire array type to a general
non-empty-array<key, value>instead of preserving the constant array shape. This fix preserves the shape by adding unmatched keys as optional entries.Before:
non-empty-array<'a'|'b'|'c', int|string>After:
array{a: string, b?: int, c?: int}Changes
src/Type/Constant/ConstantArrayTypeBuilder.php: WhensetOffsetValueTypereceives a union of constant scalars where none match existing keys and the builder already has keys, the unmatched scalars are added as optional entries instead of degrading to a general array typetests/PHPStan/Analyser/nsrt/bug-12665.php: Regression test reproducing the reported issuetests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php: Test for non-loop union offset settingtests/PHPStan/Analyser/nsrt/array-fill-keys.php: Two assertions updated to reflect improved type precision from the fixRoot cause
In
ConstantArrayTypeBuilder::setOffsetValueType(), when the offset is a union of constant scalars (e.g.,'b'|'c'), the code extracts the individual scalar types and tries to match each one against existing keys. If ALL scalars match, it updates the value types. But if ANY scalar doesn't match, the entire matching was discarded and the code fell through to a degradation path that setdegradeToGeneralArray = true, losing the constant array shape entirely.The fix adds a new code path: when no scalars match existing keys (completely new keys being added) and the builder already has existing keys, the unmatched scalars are added as new optional entries. This preserves the constant array shape while correctly marking the new keys as optional (since only one of the union members is set in any given execution).
The condition
count($this->keyTypes) > 0ensures we only apply this behavior when extending an existing array, not when constructing a new one from scratch (which avoids regressions in array function return type extensions).Test
The regression test
bug-12665.phpreproduces the exact scenario from the issue:Without the fix, PHPStan infers
non-empty-array<'a'|'b'|'c', int|string>. With the fix, it correctly infersarray{a: string, b?: int, c?: int}, preserving the constant array shape with the new keys marked as optional.Fixes phpstan/phpstan#12665