Skip to content

Fix phpstan/phpstan#13857: Incorrect narrowing of nested array after assignment#5162

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-wzuv34l
Closed

Fix phpstan/phpstan#13857: Incorrect narrowing of nested array after assignment#5162
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-wzuv34l

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Fixes phpstan/phpstan#13857

When assigning to a nested array with a non-constant key ($array[$id]['state'] = 'foo'), PHPStan incorrectly narrowed the type of ALL elements in the outer array, not just the element being modified. This caused array<int, array{state: string}> to become non-empty-array<int, array{state: 'foo'}>.

Root cause

In AssignHandler::produceArrayDimFetchAssignValueToWrite(), the reverse loop that processes nested array dim fetch assignments from innermost to outermost passed $unionValues = ($i === 0) to setOffsetValueType(). For outer levels ($i > 0) with non-constant keys, this caused ArrayType::setOffsetValueType() to replace the entire item type with the new value type, discarding the original item type.

Fix

The fix changes the $unionValues logic so that non-constant keys (keys with no constant scalar decomposition) always use union semantics, preserving the original item type alongside the new value type:

$unionValues = $i === 0
    || ($offsetType !== null && count($offsetType->getConstantScalarTypes()) === 0);

This correctly reflects that when modifying $array[$id]['state'] where $id is non-constant, we only know that ONE element was modified — the other elements retain their original types.

Test expectation changes

Several existing test expectations were updated to reflect the more correct (but less precise) types:

  • assign-nested-arrays.php: {bar: 1, baz: 2}{bar: 1, baz?: 2} (second assignment to same non-constant key can't guarantee both keys exist on all elements)
  • pr-4390.php: Inner array key range lost (non-constant loop variable key)
  • bug-10438.php: list<string>list<string>|string (union with existing item type)
  • bug-11679.php: array{foo: true}array{foo?: bool} (only one element modified)
  • bug-12927.php: Lost hasOffsetValue precision for non-constant key assignment
  • slevomat-foreach-array-key-exists-bug.php: More general item type after non-constant key modification
  • AutowiredAttributeServicesExtension.php: PHPDoc changed non-empty-list to list (append in loop with non-constant key)

All updated types are type-theoretically correct — the previous types were over-precise, incorrectly claiming knowledge about all array elements when only one was modified.

…stant key

When assigning to a nested array with a non-constant key like
`$array[$id]['state'] = 'foo'`, PHPStan incorrectly inferred that ALL
elements of the outer array had the narrowed type, not just the one
being modified. For example, `array<int, array{state: string}>` became
`non-empty-array<int, array{state: 'foo'}>` instead of the correct
`non-empty-array<int, array{state: string}>`.

The fix ensures that when the outer array key is non-constant (no
constant scalar decomposition), the new value type is unioned with
the existing item type rather than replacing it entirely, since we
cannot know which specific element was modified.

Fixes phpstan/phpstan#13857
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-wzuv34l branch March 26, 2026 08:34
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.

3 participants