Replace instead of union when writing to optional keys via union offset in ConstantArrayTypeBuilder#5566
Merged
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom Apr 29, 2026
Conversation
…et in `ConstantArrayTypeBuilder`
- In `ConstantArrayTypeBuilder::setOffsetValueType`, when handling a union
offset type (e.g. `'a'|'b'`) where all scalar types match existing keys,
the builder always unioned the new value with the old value at each
matching key. For optional keys, this is incorrect: the key's presence
implies it was the target of the write, so the old value is stale and
should be replaced, not unioned.
- The fix: when `$optional` is false and the matching key is in
`$this->optionalKeys`, replace the value instead of unioning. Required
keys still union correctly since their old values represent valid
alternative states.
- Updated bug-11846.php assertions: the old test was asserting the buggy
overapproximate behavior (`array{}|array{array{}}` instead of the
correct `array{array{}}`).
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 assigning to nested array keys using a union offset type (e.g.
$result[$k]['x'] = 1; $result[$k]['y'] = 2;where$kis'a'|'b'), PHPStan incorrectly marked subsequent inner keys as optional. The second write's value was unioned with the stale value from before that write, causingyto appear asy?: 2instead ofy: 2.Changes
src/Type/Constant/ConstantArrayTypeBuilder.php: In the union-key matching path ofsetOffsetValueType, when$optionalisfalseand the matching key is in$this->optionalKeys, replace the value instead of unioning. For optional keys, the key's presence implies it was the target of the union-key write, so the old value is irrelevant (the key either doesn't exist or was the write target — in both cases, the old per-key value shouldn't be unioned with the new value).tests/PHPStan/Analyser/nsrt/bug-11846.php: Updated assertions that were testing the buggy overapproximate behavior. The old assertions includedarray{}|array{array{}}as the value type, but the correct type isarray{array{}}since both sequential writes always execute together.tests/PHPStan/Analyser/nsrt/bug-14551.php: New regression test covering:non-empty-list<'a'|'b'>with two nested key writes0|1)'a'|'b'|'c')Root cause
ConstantArrayTypeBuilder::setOffsetValueTypehandles union offset types (like'a'|'b') by extracting the constant scalar types and, when all match existing keys, updating each key's value withTypeCombinator::union(old_value, new_value). This union is correct for required keys (the key exists regardless of which union member was the write target, so the value is either the new value or the unchanged old value). But for optional keys, the union is incorrect: an optional key's presence implies it was the write target (since it only exists because a union-key write created it), so the old value is stale and the new value should replace it outright.The
$optionalparameter (which controls union-vs-replace behavior for single-key writes) was not being consulted in the union-key code path — the fix aligns the behavior.Test
tests/PHPStan/Analyser/nsrt/bug-14551.php: Seven test functions covering the reported bug and structurally analogous cases (integer keys, three-way unions, nested vs non-nested writes, required-key preservation).tests/PHPStan/Analyser/nsrt/bug-11846.php: Updated existing assertions to match the now-correct (more precise) behavior.Fixes phpstan/phpstan#14551