Skip to content

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
phpstan-bot:create-pull-request/patch-qaxqf38
Open

Fix phpstan/phpstan#12665: should return array{a: string, b: int, c: int} but returns non-empty-array<'a'|'b'|'c', int|string>#5193
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-qaxqf38

Conversation

@phpstan-bot
Copy link
Collaborator

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

  • Modified src/Type/Constant/ConstantArrayTypeBuilder.php: When setOffsetValueType receives 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 type
  • Added tests/PHPStan/Analyser/nsrt/bug-12665.php: Regression test reproducing the reported issue
  • Added tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php: Test for non-loop union offset setting
  • Updated tests/PHPStan/Analyser/nsrt/array-fill-keys.php: Two assertions updated to reflect improved type precision from the fix

Root 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 set degradeToGeneralArray = 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) > 0 ensures 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.php reproduces the exact scenario from the issue:

$array = ['a' => $s];
foreach (['b', 'c'] as $letter) {
    $array[$letter] = $i;
}
assertType('array{a: string, b?: int, c?: int}', $array);

Without the fix, PHPStan infers non-empty-array<'a'|'b'|'c', int|string>. With the fix, it correctly infers array{a: string, b?: int, c?: int}, preserving the constant array shape with the new keys marked as optional.

Fixes phpstan/phpstan#12665

- 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
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.

1 participant