Skip to content

Commit 7a30820

Browse files
ondrejmirtesclaude
andcommitted
Write a shape's optional keys as optional into a general array's item shape
ArrayType::setExistingOffsetValueType() merged a written constant array into the item shape all-or-nothing: once with every key required and once with every optional key unset. A written shape whose optional keys can be present independently (a nested `$a[$i][$j][$k]['x'] = ...` write in a loop) then came out as `array{}|array{k1: T, ..., kN: T}`, which is unsound for a value holding only some of the keys. Optional keys are now written as optional: an existing key keeps its certainty and unions the value, a missing one is added as optional. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfQHLTyyoMfjfa4sXJCGa5
1 parent f530893 commit 7a30820

1 file changed

Lines changed: 15 additions & 0 deletions

File tree

‎src/Type/ArrayType.php‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,22 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T
428428
if ($this->itemType->isConstantArray()->yes() && $valueType->isConstantArray()->yes()) {
429429
$newItemTypes = [];
430430

431+
$itemConstantArrays = $this->itemType->getConstantArrays();
431432
foreach ($valueType->getConstantArrays() as $constArray) {
433+
if ($constArray->getOptionalKeys() !== [] && count($itemConstantArrays) === 1) {
434+
// A written shape with optional keys is not all-or-nothing: each
435+
// optional key may or may not be present on its own, so it is
436+
// written as optional (present keys keep their certainty, the
437+
// value unions with what the key held) instead of once with every
438+
// key required and once with every optional key unset.
439+
$builder = ConstantArrayTypeBuilder::createFromConstantArray($itemConstantArrays[0]);
440+
foreach ($constArray->getKeyTypes() as $i => $keyType) {
441+
$builder->setOffsetValueType($keyType, $constArray->getOffsetValueType($keyType), $constArray->isOptionalKey($i));
442+
}
443+
$newItemTypes[] = TypeCombinator::intersect($builder->getArray(), ...TypeUtils::getAccessoryTypes($this->itemType));
444+
continue;
445+
}
446+
432447
$newItemType = $this->itemType;
433448
$optionalKeyTypes = [];
434449
foreach ($constArray->getKeyTypes() as $i => $keyType) {

0 commit comments

Comments
 (0)