Skip to content

Commit 70d5b9d

Browse files
ondrejmirtesclaude
andcommitted
Resolve constant offsets on constant arrays through the key index
ConstantArrayType::isSuperTypeOf() asks the other array about every key with hasOffsetValueType() and getOffsetValueType(), and both scanned the whole key list with isSuperTypeOf() per key, so comparing two array shapes was quadratic in their size. The scope's conditional-expression matching compares the shape of a variable against every recorded condition on it, which made a method with dozens of `$params['key'] ?? null` reads over a large optional array shape spend seconds in ConstantStringType::isSuperTypeOf. Constant integer and string offsets now resolve through the existing value-keyed index: a hit is verified with isSuperTypeOf() before it is used, a miss keeps the full scan, and isOptionalKey() reads a set instead of in_array() over the optional keys. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvDxWpzXzticTTm5eLCsb2
1 parent 5071596 commit 70d5b9d

1 file changed

Lines changed: 57 additions & 1 deletion

File tree

‎src/Type/Constant/ConstantArrayType.php‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
use PHPStan\Type\TypeCombinator;
6868
use PHPStan\Type\UnionType;
6969
use PHPStan\Type\VerbosityLevel;
70+
use function array_flip;
7071
use function array_key_exists;
7172
use function array_keys;
7273
use function array_map;
@@ -129,6 +130,9 @@ class ConstantArrayType implements Type
129130
/** @var array<int|string, int>|null */
130131
private ?array $keyIndexMap = null;
131132

133+
/** @var array<int, int>|null */
134+
private ?array $optionalKeySet = null;
135+
132136
/**
133137
* @api
134138
* @param list<ConstantIntegerType|ConstantStringType> $keyTypes
@@ -480,7 +484,9 @@ public function getValueTypes(): array
480484

481485
public function isOptionalKey(int $i): bool
482486
{
483-
return in_array($i, $this->optionalKeys, true);
487+
$this->optionalKeySet ??= array_flip($this->optionalKeys);
488+
489+
return isset($this->optionalKeySet[$i]);
484490
}
485491

486492
public function sortKeys(): self
@@ -1186,6 +1192,17 @@ private function recursiveHasOffsetValueType(Type $offsetType): TrinaryLogic
11861192
}
11871193
}
11881194

1195+
// Constant offsets against constant keys resolve by value: a hit at the
1196+
// indexed slot is verified before use, a miss falls through to the scan
1197+
// (which also covers the unsealed extras below).
1198+
$index = $this->findVerifiedKeyIndex($offsetType);
1199+
if ($index !== null) {
1200+
if ($this->isOptionalKey($index)) {
1201+
return TrinaryLogic::createMaybe();
1202+
}
1203+
return TrinaryLogic::createYes();
1204+
}
1205+
11891206
$result = TrinaryLogic::createNo();
11901207
foreach ($this->keyTypes as $i => $keyType) {
11911208
// PHP coerces decimal-integer strings to int when used as array
@@ -1240,6 +1257,21 @@ public function getOffsetValueType(Type $offsetType): Type
12401257
}
12411258

12421259
$offsetType = $offsetType->toArrayKey();
1260+
if (count($this->keyTypes) > 1) {
1261+
// Same result as the scan below for a verified hit: exactly one explicit
1262+
// key matches a constant offset, so neither the all-keys nor the
1263+
// maybe-all fallbacks apply and the unsealed extras cannot contribute.
1264+
$index = $this->findVerifiedKeyIndex($offsetType);
1265+
if ($index !== null) {
1266+
$type = TypeCombinator::union($this->valueTypes[$index]);
1267+
if ($type instanceof ErrorType) {
1268+
return new MixedType();
1269+
}
1270+
1271+
return $type;
1272+
}
1273+
}
1274+
12431275
$matchingValueTypes = [];
12441276
$all = true;
12451277
$maybeAll = true;
@@ -3161,6 +3193,30 @@ private function getKeyIndexMap(): array
31613193
return $this->keyIndexMap = $map;
31623194
}
31633195

3196+
/**
3197+
* Index of the explicit key a constant scalar offset (already passed through
3198+
* toArrayKey()) resolves to, or null when the offset is not a constant scalar
3199+
* or no explicit key is a supertype of it.
3200+
*/
3201+
private function findVerifiedKeyIndex(Type $offsetType): ?int
3202+
{
3203+
if (!$offsetType->isConstantScalarValue()->yes()) {
3204+
return null;
3205+
}
3206+
3207+
$value = $offsetType->getConstantScalarValues()[0] ?? null;
3208+
if (!is_int($value) && !is_string($value)) {
3209+
return null;
3210+
}
3211+
3212+
$index = $this->getKeyIndexMap()[$value] ?? null;
3213+
if ($index === null || !$this->keyTypes[$index]->isSuperTypeOf($offsetType)->yes()) {
3214+
return null;
3215+
}
3216+
3217+
return $index;
3218+
}
3219+
31643220
/**
31653221
* @param ConstantIntegerType|ConstantStringType $otherKeyType
31663222
*/

0 commit comments

Comments
 (0)