diff --git a/src/NodeTypeResolver/PHPStan/Type/TypeFactory.php b/src/NodeTypeResolver/PHPStan/Type/TypeFactory.php index 49d431e8d37..2572eeb7694 100644 --- a/src/NodeTypeResolver/PHPStan/Type/TypeFactory.php +++ b/src/NodeTypeResolver/PHPStan/Type/TypeFactory.php @@ -16,7 +16,6 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use Rector\NodeTypeResolver\PHPStan\ObjectWithoutClassTypeWithParentTypes; use Rector\NodeTypeResolver\PHPStan\TypeHasher; @@ -127,7 +126,7 @@ private function unwrapUnionedTypes(array $types): array // unwrap union types $unwrappedTypes = []; foreach ($types as $type) { - $flattenTypes = TypeUtils::flattenTypes($type); + $flattenTypes = TypeFlattener::flatten($type); foreach ($flattenTypes as $flattenType) { if ($flattenType instanceof ConstantArrayType) { @@ -202,8 +201,8 @@ private function unwrapConstantArrayTypes(ConstantArrayType $constantArrayType): { $unwrappedTypes = []; - $flattenKeyTypes = TypeUtils::flattenTypes($constantArrayType->getIterableKeyType()); - $flattenItemTypes = TypeUtils::flattenTypes($constantArrayType->getIterableValueType()); + $flattenKeyTypes = TypeFlattener::flatten($constantArrayType->getIterableKeyType()); + $flattenItemTypes = TypeFlattener::flatten($constantArrayType->getIterableValueType()); foreach ($flattenItemTypes as $position => $nestedFlattenItemType) { $nestedFlattenKeyType = $flattenKeyTypes[$position] ?? null; diff --git a/src/NodeTypeResolver/PHPStan/Type/TypeFlattener.php b/src/NodeTypeResolver/PHPStan/Type/TypeFlattener.php new file mode 100644 index 00000000000..f3d41aad187 --- /dev/null +++ b/src/NodeTypeResolver/PHPStan/Type/TypeFlattener.php @@ -0,0 +1,95 @@ +getTypes() as $innerType) { + foreach (self::flatten($innerType) as $flattenType) { + $types[] = $flattenType; + } + } + + return $types; + } + + $constantArrays = $type->getConstantArrays(); + if ($constantArrays === []) { + return [$type]; + } + + // each ConstantArrayType with N optional keys expands to 2^N variants via getAllArrays(), + // and the cartesian product across arrays multiplies those counts - bail out before it explodes + $estimatedCount = 1; + foreach ($constantArrays as $constantArray) { + $optionalCount = count($constantArray->getOptionalKeys()); + $arrayCount = $optionalCount <= 20 ? 1 << $optionalCount : PHP_INT_MAX; + + if ($arrayCount > 16384 || $estimatedCount > 16384 / max($arrayCount, 1)) { + return [$type]; + } + + $estimatedCount *= $arrayCount; + } + + $allArrays = []; + foreach ($constantArrays as $constantArray) { + $allArrays[] = $constantArray->getAllArrays(); + } + + $result = []; + foreach (self::combinations($allArrays) as $combination) { + $intersected = $combination[0]; + for ($i = 1, $count = count($combination); $i < $count; ++$i) { + $intersected = TypeCombinator::intersect($intersected, $combination[$i]); + } + + if ($intersected instanceof NeverType) { + continue; + } + + $result[] = $intersected; + } + + return $result; + } + + /** + * @param array> $arrays + * @return iterable> + */ + private static function combinations(array $arrays): iterable + { + if ($arrays === []) { + yield []; + return; + } + + $last = array_pop($arrays); + foreach (self::combinations($arrays) as $combination) { + foreach ($last as $value) { + $next = $combination; + $next[] = $value; + + yield $next; + } + } + } +}