Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
}

$results = [];
$valueResults = [];
foreach ($this->keyTypes as $i => $keyType) {
$hasOffset = $type->hasOffsetValueType($keyType);
if ($bothDefinite && $hasOffset->no() && $typeUnsealedness->yes()) {
Expand All @@ -820,6 +821,18 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
$results[] = IsSuperTypeOfResult::createMaybe();
}

// The offset value at one of $type's explicit keys depends only on
// the value type stored there, so keys sharing their value types on
// both sides (keys written through a union offset) compare them once.
$otherIndex = $type->getKeyIndex($keyType);
$valuePairKey = $otherIndex !== null
? spl_object_id($this->valueTypes[$i]) . ':' . spl_object_id($type->valueTypes[$otherIndex])
: null;
if ($valuePairKey !== null && array_key_exists($valuePairKey, $valueResults)) {
$results[] = $valueResults[$valuePairKey];
continue;
}

$otherValueType = $type->getOffsetValueType($keyType);
if ($otherValueType instanceof ErrorType && $bothDefinite && $typeUnsealedness->yes()) {
[, $typeUnsealedValue] = $type->unsealed;
Expand All @@ -829,6 +842,9 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
if ($isValueSuperType->no()) {
return $isValueSuperType->decorateReasons(static fn (string $reason) => sprintf('Offset %s: %s', $keyType->describe(VerbosityLevel::value()), $reason));
}
if ($valuePairKey !== null) {
$valueResults[$valuePairKey] = $isValueSuperType;
}
$results[] = $isValueSuperType;
}

Expand Down Expand Up @@ -2559,6 +2575,18 @@ private function getKeysOrValuesArray(array $types, ?Type $unsealedSourceType):
$optionalKeys = [];
$maxIndex = 0;

// Position $i of the result can hold any source value from $i up to
// $maxIndex. Both ends of that window only move forward, so the
// window unions are kept as a two-stack queue: $suffixUnions[$j] is
// the union of $types[$j..$suffixEnd] and $tailUnion the union of
// the values added after $suffixEnd. Each value enters the suffix
// unions once, which keeps this linear in the number of keys instead
// of unioning every window from scratch.
$suffixUnions = [];
$suffixEnd = -1;
$tailUnion = null;
$tailEnd = -1;

foreach ($types as $i => $type) {
$keyTypes[] = new ConstantIntegerType($i);

Expand All @@ -2572,7 +2600,26 @@ private function getKeysOrValuesArray(array $types, ?Type $unsealedSourceType):
if ($i === $maxIndex) {
$valueTypes[] = $type;
} else {
$valueTypes[] = TypeCombinator::union(...array_slice($types, $i, $maxIndex - $i + 1));
$windowEnd = min($maxIndex, $count - 1);
if ($i === $windowEnd) {
$valueTypes[] = TypeCombinator::union($type);
} else {
if ($i > $suffixEnd) {
$suffixEnd = $windowEnd;
$suffixUnions = [$suffixEnd => $types[$suffixEnd]];
for ($j = $suffixEnd - 1; $j >= $i; $j--) {
$suffixUnions[$j] = TypeCombinator::union($types[$j], $suffixUnions[$j + 1]);
}
$tailUnion = null;
$tailEnd = $suffixEnd;
}
for ($j = $tailEnd + 1; $j <= $windowEnd; $j++) {
$tailUnion = $tailUnion === null ? $types[$j] : TypeCombinator::union($tailUnion, $types[$j]);
}
$tailEnd = $windowEnd;

$valueTypes[] = $tailUnion === null ? $suffixUnions[$i] : TypeCombinator::union($suffixUnions[$i], $tailUnion);
}
if ($maxIndex >= $count) {
$optionalKeys[] = $i;
}
Expand Down Expand Up @@ -3135,6 +3182,10 @@ public function mergeWith(self $otherArray): self

$otherKeyIndexMap = $otherArray->getKeyIndexMap();
$processed = [];
// Keys written through a union offset share their value type: union
// each pair of value types once, which also keeps the merged keys
// sharing the result.
$mergedValues = [];

foreach ($this->keyTypes as $i => $keyType) {
$keyValue = $keyType->getValue();
Expand All @@ -3144,7 +3195,7 @@ public function mergeWith(self $otherArray): self
if (array_key_exists($keyValue, $otherKeyIndexMap)) {
$j = $otherKeyIndexMap[$keyValue];
$otherValueType = $otherArray->valueTypes[$j];
$mergedValue = TypeCombinator::union($valueType, $otherValueType);
$mergedValue = $mergedValues[spl_object_id($valueType) . ':' . spl_object_id($otherValueType)] ??= TypeCombinator::union($valueType, $otherValueType);
$optional = $this->isOptionalKey($i) || $otherArray->isOptionalKey($j);

$keyTypes[] = $keyType;
Expand Down Expand Up @@ -3218,6 +3269,8 @@ private function legacyMergeWith(self $otherArray): self
{
$valueTypes = $this->valueTypes;
$optionalKeys = $this->optionalKeys;
// union each pair of value types once, see mergeWith()
$mergedValues = [];
foreach ($this->keyTypes as $i => $keyType) {
$otherIndex = $otherArray->getKeyIndex($keyType);
if ($otherIndex === null) {
Expand All @@ -3228,7 +3281,7 @@ private function legacyMergeWith(self $otherArray): self
$optionalKeys[] = $i;
}
$otherValueType = $otherArray->valueTypes[$otherIndex];
$valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $otherValueType);
$valueTypes[$i] = $mergedValues[spl_object_id($valueTypes[$i]) . ':' . spl_object_id($otherValueType)] ??= TypeCombinator::union($valueTypes[$i], $otherValueType);
}

$optionalKeys = array_values(array_unique($optionalKeys));
Expand Down
30 changes: 28 additions & 2 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -1685,13 +1685,28 @@ private static function optimizeConstantArrays(array $types): array
public static function countConstantArrayValueTypes(array $types): int
{
$constantArrayValuesCount = 0;
// A type shared by several positions (array shape keys written
// through a union offset all hold the same value type) is counted
// once per position, but traversed only once. The entries keep the
// visited types alive so that their object ids are not reused.
$countsByType = [];
foreach ($types as $type) {
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$constantArrayValuesCount): Type {
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$constantArrayValuesCount, &$countsByType): Type {
$id = spl_object_id($type);
if (array_key_exists($id, $countsByType)) {
$constantArrayValuesCount += $countsByType[$id][1];
return $type;
}

$countBefore = $constantArrayValuesCount;
if ($type instanceof ConstantArrayType) {
$constantArrayValuesCount += count($type->getValueTypes());
}

return $traverse($type);
$result = $traverse($type);
$countsByType[$id] = [$type, $constantArrayValuesCount - $countBefore];

return $result;
});
}
return $constantArrayValuesCount;
Expand All @@ -1713,14 +1728,25 @@ private static function getConstantScalarValuesByKey(ConstantArrayType $array):

$values = [];
$valueTypes = $array->getValueTypes();
// keys written through a union offset share one value type - inspect it once
$singleValues = [];
foreach ($array->getKeyTypes() as $i => $keyType) {
$id = spl_object_id($valueTypes[$i]);
if (array_key_exists($id, $singleValues)) {
if ($singleValues[$id] !== null) {
$values[$keyType->getValue()] = $singleValues[$id][0];
}
continue;
}
$singleValues[$id] = null;
if (!$valueTypes[$i]->isConstantScalarValue()->yes()) {
continue;
}
$constantScalarValues = $valueTypes[$i]->getConstantScalarValues();
if (count($constantScalarValues) !== 1) {
continue;
}
$singleValues[$id] = [$constantScalarValues[0]];
$values[$keyType->getValue()] = $constantScalarValues[0];
}

Expand Down
8 changes: 8 additions & 0 deletions tests/PHPStan/Analyser/nsrt/array_values.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ public function constantArrayType(): void
assertType("list{0?: 'a'|'b'|'c', 1?: 'b'|'c', 2?: 'c'}", array_values($numbers));
}

/**
* @param array{a: 1, b?: 2, c: 3, d?: 4, e?: 5, f: 6} $shape
*/
public function optionalKeysBetweenRequiredKeys(array $shape): void
{
assertType('list{0: 1, 1: 2|3, 2: 3|4|5|6, 3?: 4|5|6, 4?: 5|6, 5?: 6}', array_values($shape));
}

/**
* @param array<string, non-empty-array<string, int>> $a
*/
Expand Down
Loading
Loading