Build an (unsealed) array shape in ArrayType::intersectKeyArray() when the other operand has known sealedness#5792
Merged
ondrejmirtes merged 1 commit intoJun 2, 2026
Conversation
…n the other operand has known sealedness
- When `array_intersect_key()`'s first argument is a general `ArrayType` and the
other operand is one or more constant array shapes with a definite sealedness
(`isUnsealed()` is `yes` or `no`, i.e. bleeding edge), `ArrayType::intersectKeyArray()`
now produces a `ConstantArrayType` with each shape key as an optional offset and the
first array's value type, instead of degrading to `array<keys, value>`.
- Unsealed extras of the other shape are carried over via `makeUnsealed()`, with the
unsealed key narrowed to the first array's key type.
- Keys that cannot intersect the first array's key type are dropped.
- Non-bleeding-edge shapes (`isUnsealed()` is `maybe`) keep the previous general-array
behavior, so the non-bleeding-edge path is unaffected.
- Updated array-intersect-key.php expectations to the more precise shapes
(e.g. `array{foo?: string}`, `array{17: string}`, `list{0?: string, 1?: string}`).
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
array_intersect_key($options, ['a' => null, 'b' => null])where$optionsis ageneral
array<mixed>was inferred asarray<'a'|'b', mixed>, so passing the resultto a parameter typed
array{a?: mixed, b?: mixed}reported a false positive — eventhough the two types describe the same set of values. The result is now inferred as
the array shape
array{a?: mixed, b?: mixed}.Changes
src/Type/ArrayType.php:intersectKeyArray()now inspects$otherArraysType->getConstantArrays(). When all of those shapes have a definitesealedness (
isUnsealed()is notmaybe), it builds aConstantArrayTypeviaConstantArrayTypeBuilder— each shape key becomes an optional offset (the generalfirst array may or may not contain it) carrying the first array's value type. Keys
that cannot intersect the first array's key type are skipped, and an unsealed extra
of the other shape is reproduced with
makeUnsealed()(its key narrowed to the firstarray's key type). The shape-building is performed in a new private helper
intersectConstantArrayShape().tests/PHPStan/Analyser/nsrt/array-intersect-key.php: updated expectations to thenow-more-precise shapes (
array{foo?: string},array{17?: string},array{''?: string},array{17: string},array{17: 'foo'},list{0?: string, 1?: string}).tests/PHPStan/Analyser/nsrt/bug-14747.php: new regression test.Root cause
ArrayType::intersectKeyArray()only had two precise outcomes (empty array when keysare disjoint,
$thiswhen the other key type covers ours) and otherwise fell back to ageneral
array<otherKeys, ourValue>. It never reconstructed an array shape from theother operand's constant keys, so the optional-key information available on the right
side of
array_intersect_key()was lost. The fix reconstructs that shape whenever theother operand exposes constant array shapes with a known sealedness.
Probed analogous cases
intersectKeyArray()implementations on accessory array types(
NonEmptyArrayType,HasOffsetType,HasOffsetValueType,OversizedArrayType,AccessoryArrayListType) return$this/MixedTypeand are combined byIntersectionType::intersectKeyArray(). With the newArrayTypeshape they nowcompose correctly and even more precisely — e.g.
non-empty-array<17, string>&hasOffset(17)intersected with
[17 => 'bar']is nowarray{17: string}(a required key) instead ofthe previous accessory-intersection form.
array_intersect_keyis the only built-in routed throughintersectKeyArray;array_diff_key/array_intersect_assoc/array_diff_assochave no dedicated extensionand cannot narrow to a bounded shape (their results keep unbounded keys), so they are
not affected.
isUnsealed()=maybe, so the general-arraybehavior is preserved off bleeding edge.
Test
tests/PHPStan/Analyser/nsrt/bug-14747.phpreproduces the reported case(
array{a?: mixed, b?: mixed}) plus literal-shape, out-of-range-key, andunion-of-shapes variants. It fails before the fix and passes after.
array-intersect-key.phpassertions updated to the corrected, more preciseresults.
Fixes phpstan/phpstan#14747