Skip to content

Commit 03c33d2

Browse files
ondrejmirtesclaude
andcommitted
Avoid 2^N getAllArrays() expansion in ConstantArrayType::getFiniteTypes()
Process keys incrementally instead of generating all power-set variants upfront via `getAllArrays()`. For each key, fork partial `ConstantArrayTypeBuilder` instances for optional keys and finite value variants, bailing early when the count exceeds `CALCULATE_SCALARS_LIMIT`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent df03be2 commit 03c33d2

File tree

2 files changed

+54
-27
lines changed

2 files changed

+54
-27
lines changed

src/Type/Constant/ConstantArrayType.php

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Nette\Utils\Strings;
66
use PHPStan\Analyser\OutOfClassScope;
7-
use PHPStan\Internal\CombinationsHelper;
87
use PHPStan\Php\PhpVersion;
98
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
109
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
@@ -1997,38 +1996,43 @@ public static function isValidIdentifier(string $value): bool
19971996

19981997
public function getFiniteTypes(): array
19991998
{
2000-
$arraysArraysForCombinations = [];
2001-
$count = 0;
2002-
foreach ($this->getAllArrays() as $array) {
2003-
$values = $array->getValueTypes();
2004-
$arraysForCombinations = [];
2005-
$combinationCount = 1;
2006-
foreach ($values as $valueType) {
2007-
$finiteTypes = $valueType->getFiniteTypes();
2008-
if ($finiteTypes === []) {
2009-
return [];
1999+
$limit = InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT;
2000+
2001+
// Build finite array types incrementally, processing one key at a time.
2002+
// For optional keys, fork each partial result into with/without variants.
2003+
// This avoids generating 2^N ConstantArrayType objects via getAllArrays().
2004+
/** @var list<ConstantArrayTypeBuilder> $partials */
2005+
$partials = [ConstantArrayTypeBuilder::createEmpty()];
2006+
2007+
foreach ($this->keyTypes as $i => $keyType) {
2008+
$finiteValueTypes = $this->valueTypes[$i]->getFiniteTypes();
2009+
if ($finiteValueTypes === []) {
2010+
return [];
2011+
}
2012+
2013+
$isOptional = $this->isOptionalKey($i);
2014+
$newPartials = [];
2015+
2016+
foreach ($partials as $partial) {
2017+
if ($isOptional) {
2018+
$newPartials[] = clone $partial;
2019+
}
2020+
foreach ($finiteValueTypes as $finiteValueType) {
2021+
$newPartial = clone $partial;
2022+
$newPartial->setOffsetValueType($keyType, $finiteValueType);
2023+
$newPartials[] = $newPartial;
20102024
}
2011-
$arraysForCombinations[] = $finiteTypes;
2012-
$combinationCount *= count($finiteTypes);
20132025
}
2014-
$arraysArraysForCombinations[] = $arraysForCombinations;
2015-
$count += $combinationCount;
2016-
}
20172026

2018-
if ($count > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
2019-
return [];
2027+
$partials = $newPartials;
2028+
if (count($partials) > $limit) {
2029+
return [];
2030+
}
20202031
}
20212032

20222033
$finiteTypes = [];
2023-
foreach ($arraysArraysForCombinations as $arraysForCombinations) {
2024-
$combinations = CombinationsHelper::combinations($arraysForCombinations);
2025-
foreach ($combinations as $combination) {
2026-
$builder = ConstantArrayTypeBuilder::createEmpty();
2027-
foreach ($combination as $i => $v) {
2028-
$builder->setOffsetValueType($this->keyTypes[$i], $v);
2029-
}
2030-
$finiteTypes[] = $builder->getArray();
2031-
}
2034+
foreach ($partials as $partial) {
2035+
$finiteTypes[] = $partial->getArray();
20322036
}
20332037

20342038
return $finiteTypes;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace BenchFiniteTypesOptionalKeysBlowup;
4+
5+
/**
6+
* Regression test for getFiniteTypes() calling getAllArrays() on arrays with many optional keys.
7+
* getAllArrays() generates 2^N ConstantArrayType objects for N optional keys.
8+
* The fix processes keys incrementally, bailing early when partial count exceeds the limit.
9+
*
10+
* @param array{
11+
* a?: 'x', b?: 'x', c?: 'x', d?: 'x', e?: 'x',
12+
* f?: 'x', g?: 'x', h?: 'x', i?: 'x', j?: 'x',
13+
* k?: 'x', l?: 'x', m?: 'x', n?: 'x', o?: 'x',
14+
* p?: 'x', q?: 'x', r?: 'x'
15+
* } $data
16+
* @param array{a?: 'x', b?: 'x', c?: 'x', d?: 'x', e?: 'x', f?: 'x', g?: 'x', h?: 'x', i?: 'x', j?: 'x', k?: 'x', l?: 'x', m?: 'x', n?: 'x', o?: 'x', p?: 'x', q?: 'x', r?: 'x'} $other
17+
*/
18+
function testFinite(array $data, array $other): void
19+
{
20+
if ($data === $other) {
21+
echo 'same';
22+
}
23+
}

0 commit comments

Comments
 (0)