From bad08ef7754732408c4643df35619e850dd37044 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Sep 2026 12:38:36 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01GvDxWpzXzticTTm5eLCsb2 --- src/Type/Constant/ConstantArrayType.php | 58 ++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index fcb35f63c85..2f3471bbd67 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -67,6 +67,7 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function array_flip; use function array_key_exists; use function array_keys; use function array_map; @@ -129,6 +130,9 @@ class ConstantArrayType implements Type /** @var array|null */ private ?array $keyIndexMap = null; + /** @var array|null */ + private ?array $optionalKeySet = null; + /** * @api * @param list $keyTypes @@ -480,7 +484,9 @@ public function getValueTypes(): array public function isOptionalKey(int $i): bool { - return in_array($i, $this->optionalKeys, true); + $this->optionalKeySet ??= array_flip($this->optionalKeys); + + return isset($this->optionalKeySet[$i]); } public function sortKeys(): self @@ -1186,6 +1192,17 @@ private function recursiveHasOffsetValueType(Type $offsetType): TrinaryLogic } } + // Constant offsets against constant keys resolve by value: a hit at the + // indexed slot is verified before use, a miss falls through to the scan + // (which also covers the unsealed extras below). + $index = $this->findVerifiedKeyIndex($offsetType); + if ($index !== null) { + if ($this->isOptionalKey($index)) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createYes(); + } + $result = TrinaryLogic::createNo(); foreach ($this->keyTypes as $i => $keyType) { // PHP coerces decimal-integer strings to int when used as array @@ -1240,6 +1257,21 @@ public function getOffsetValueType(Type $offsetType): Type } $offsetType = $offsetType->toArrayKey(); + if (count($this->keyTypes) > 1) { + // Same result as the scan below for a verified hit: exactly one explicit + // key matches a constant offset, so neither the all-keys nor the + // maybe-all fallbacks apply and the unsealed extras cannot contribute. + $index = $this->findVerifiedKeyIndex($offsetType); + if ($index !== null) { + $type = TypeCombinator::union($this->valueTypes[$index]); + if ($type instanceof ErrorType) { + return new MixedType(); + } + + return $type; + } + } + $matchingValueTypes = []; $all = true; $maybeAll = true; @@ -3161,6 +3193,30 @@ private function getKeyIndexMap(): array return $this->keyIndexMap = $map; } + /** + * Index of the explicit key a constant scalar offset (already passed through + * toArrayKey()) resolves to, or null when the offset is not a constant scalar + * or no explicit key is a supertype of it. + */ + private function findVerifiedKeyIndex(Type $offsetType): ?int + { + if (!$offsetType->isConstantScalarValue()->yes()) { + return null; + } + + $value = $offsetType->getConstantScalarValues()[0] ?? null; + if (!is_int($value) && !is_string($value)) { + return null; + } + + $index = $this->getKeyIndexMap()[$value] ?? null; + if ($index === null || !$this->keyTypes[$index]->isSuperTypeOf($offsetType)->yes()) { + return null; + } + + return $index; + } + /** * @param ConstantIntegerType|ConstantStringType $otherKeyType */