Skip to content

Commit 97829f9

Browse files
ondrejmirtesclaude
andcommitted
Include unsealed extras in ConstantArrayTypeBuilder degraded-array form
`ConstantArrayTypeBuilder::getArray()` in the degrade-to-general-array branch builds `new ArrayType(union($keyTypes), union($valueTypes))` from the explicit slots only — the unsealed extras' key/value contribution was silently dropped. Union the unsealed key/value into the degraded `ArrayType`'s key and value unions when the builder carries real extras. Fixes shuffleArray (and any other call site that round-trips through `degradeToGeneralArray()`) on unsealed sources, where the unsealed value type now reaches the final list/array form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 600b998 commit 97829f9

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

src/Type/Constant/ConstantArrayTypeBuilder.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,8 +473,21 @@ public function getArray(): Type
473473
$itemTypes = $this->valueTypes;
474474
}
475475

476+
$keyTypesForArray = $this->keyTypes;
477+
// Real unsealed extras describe additional key/value pairs that
478+
// belong in the degraded `ArrayType`'s key/value unions too —
479+
// otherwise the degraded type silently drops them.
480+
if ($this->unsealed !== null) {
481+
[$unsealedKey, $unsealedValue] = $this->unsealed;
482+
$isExplicitNever = $unsealedKey instanceof NeverType && $unsealedKey->isExplicit();
483+
if (!$isExplicitNever) {
484+
$keyTypesForArray[] = $unsealedKey;
485+
$itemTypes[] = $unsealedValue;
486+
}
487+
}
488+
476489
$array = new ArrayType(
477-
TypeCombinator::union(...$this->keyTypes),
490+
TypeCombinator::union(...$keyTypesForArray),
478491
TypeCombinator::union(...$itemTypes),
479492
);
480493

tests/PHPStan/Analyser/nsrt/unsealed-derivations.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,24 @@ public function chunkPreservingKeys(array $arr): void
496496

497497
}
498498

499+
class ShuffleArray
500+
{
501+
502+
/**
503+
* @param array{a: 1, b: 2, ...<int, string>} $arr
504+
*/
505+
public function shufflePreservesUnsealedValues(array $arr): void
506+
{
507+
// `shuffle` reorders + reindexes. Through `getValuesArray()`
508+
// the source's unsealed value type contributes to the result's
509+
// value union — the final degraded list type includes `string`
510+
// alongside the explicit values `1` and `2`.
511+
shuffle($arr);
512+
assertType('non-empty-list<1|2|string>', $arr);
513+
}
514+
515+
}
516+
499517
class CountNarrowing
500518
{
501519

0 commit comments

Comments
 (0)