Skip to content

Commit 288f15d

Browse files
ondrejmirtesclaude
andcommitted
Carry unsealed slots through array_combine() all-constant fast path
`ArrayCombineHelper` paired only the explicit `getValueTypes()` of the keys and values arrays, dropping unsealed extras — `array_combine(array{'a', 'b', ...<int, string>}, array{1, 2, ...<int, int>})` inferred a sealed `array{a: 1, b: 2}` even though the matching extras pair up into further entries. When both inputs are unsealed (their extra counts are both unbounded and must match for the call to succeed), attach the result's unsealed slot: the keys' unsealed value (as an array key) mapped to the values' unsealed value. If only one side is unsealed the sealed side caps the size, so no extras survive and the result stays sealed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e0165b4 commit 288f15d

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

src/Type/Php/ArrayCombineHelper.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,22 @@ public function getReturnAndThrowType(Expr $firstArg, Expr $secondArg, Scope $sc
6666
$builder->setOffsetValueType($keyType, $valueType);
6767
}
6868

69+
// When both inputs carry unsealed extras (of matching,
70+
// unbounded count) the extra positions pair up: the keys'
71+
// unsealed value becomes a key, the values' unsealed value
72+
// becomes its value. If only one side is unsealed, the
73+
// sealed side caps the size, so no extras can survive.
74+
$keysUnsealed = $constantKeysArray->getUnsealedTypes();
75+
$valuesUnsealed = $constantValueArrays->getUnsealedTypes();
76+
if (
77+
$constantKeysArray->isUnsealed()->yes()
78+
&& $constantValueArrays->isUnsealed()->yes()
79+
&& $keysUnsealed !== null
80+
&& $valuesUnsealed !== null
81+
) {
82+
$builder->makeUnsealed($keysUnsealed[1]->toArrayKey(), $valuesUnsealed[1]);
83+
}
84+
6985
$results[] = $builder->getArray();
7086
}
7187

tests/PHPStan/Analyser/nsrt/array-combine-php8.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,27 @@ function withUnionAsKey(int|bool $oneOrBool)
162162

163163
assertType("array{1: 'bar'}", array_combine($keys, ['bar']));
164164
}
165+
166+
/**
167+
* @param array{'a', 'b', ...<int, string>} $keys
168+
* @param array{1, 2, ...<int, int>} $values
169+
*/
170+
function bothUnsealed(array $keys, array $values)
171+
{
172+
// Both arrays carry unsealed extras of matching (unbounded) count;
173+
// the extra key/value pairs become the result's unsealed slot:
174+
// `<string, int>` (the keys' unsealed value, as a key, mapped to the
175+
// values' unsealed value).
176+
assertType('array{a: 1, b: 2, ...<string, int>}', array_combine($keys, $values));
177+
}
178+
179+
/**
180+
* @param array{'a', 'b', ...<int, string>} $keys
181+
*/
182+
function onlyKeysUnsealed(array $keys)
183+
{
184+
// The values array is sealed (exactly 2), so array_combine only
185+
// succeeds when the keys array also has exactly 2 — the extras can't
186+
// exist. Result stays sealed.
187+
assertType('array{a: 1, b: 2}', array_combine($keys, [1, 2]));
188+
}

0 commit comments

Comments
 (0)