Skip to content

Commit 6a1b4a7

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents 4255230 + 4390ece commit 6a1b4a7

16 files changed

+410
-74
lines changed

src/Analyser/MutatingScope.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3271,7 +3271,10 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
32713271
unset($scope->expressionTypes[$conditionalExprString]);
32723272
} else {
32733273
if (array_key_exists($conditionalExprString, $scope->expressionTypes)) {
3274-
$type = TypeCombinator::intersect(...array_map(static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getType(), $expressions));
3274+
$type = $expressions[0]->getTypeHolder()->getType();
3275+
for ($i = 1, $count = count($expressions); $i < $count; $i++) {
3276+
$type = TypeCombinator::intersect($type, $expressions[$i]->getTypeHolder()->getType());
3277+
}
32753278

32763279
$scope->expressionTypes[$conditionalExprString] = new ExpressionTypeHolder(
32773280
$scope->expressionTypes[$conditionalExprString]->getExpr(),

src/PhpDoc/TypeNodeResolver.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,11 @@ private function resolveUnionTypeNode(UnionTypeNode $typeNode, NameScope $nameSc
645645
private function resolveIntersectionTypeNode(IntersectionTypeNode $typeNode, NameScope $nameScope): Type
646646
{
647647
$types = $this->resolveMultiple($typeNode->types, $nameScope);
648-
return TypeCombinator::intersect(...$types);
648+
$result = $types[0];
649+
for ($i = 1, $count = count($types); $i < $count; $i++) {
650+
$result = TypeCombinator::intersect($result, $types[$i]);
651+
}
652+
return $result;
649653
}
650654

651655
private function resolveConditionalTypeNode(ConditionalTypeNode $typeNode, NameScope $nameScope): Type

src/Reflection/Type/IntersectionTypeMethodReflection.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,18 @@ public function getVariants(): array
9494
}
9595
}
9696

97-
$returnType = TypeCombinator::intersect(...$returnTypes);
98-
$phpDocReturnType = TypeCombinator::intersect(...$phpDocReturnTypes);
99-
$nativeReturnType = TypeCombinator::intersect(...$nativeReturnTypes);
97+
$returnType = $returnTypes[0];
98+
for ($i = 1, $count = count($returnTypes); $i < $count; $i++) {
99+
$returnType = TypeCombinator::intersect($returnType, $returnTypes[$i]);
100+
}
101+
$phpDocReturnType = $phpDocReturnTypes[0];
102+
for ($i = 1, $count = count($phpDocReturnTypes); $i < $count; $i++) {
103+
$phpDocReturnType = TypeCombinator::intersect($phpDocReturnType, $phpDocReturnTypes[$i]);
104+
}
105+
$nativeReturnType = $nativeReturnTypes[0];
106+
for ($i = 1, $count = count($nativeReturnTypes); $i < $count; $i++) {
107+
$nativeReturnType = TypeCombinator::intersect($nativeReturnType, $nativeReturnTypes[$i]);
108+
}
100109
return array_map(static fn (ExtendedParametersAcceptor $acceptor): ExtendedParametersAcceptor => new ExtendedFunctionVariant(
101110
$acceptor->getTemplateTypeMap(),
102111
$acceptor->getResolvedTemplateTypeMap(),
@@ -188,7 +197,11 @@ public function getThrowType(): ?Type
188197
return null;
189198
}
190199

191-
return TypeCombinator::intersect(...$types);
200+
$result = $types[0];
201+
for ($i = 1, $count = count($types); $i < $count; $i++) {
202+
$result = TypeCombinator::intersect($result, $types[$i]);
203+
}
204+
return $result;
192205
}
193206

194207
public function hasSideEffects(): TrinaryLogic

src/Reflection/Type/IntersectionTypePropertyReflection.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use PHPStan\TrinaryLogic;
1010
use PHPStan\Type\Type;
1111
use PHPStan\Type\TypeCombinator;
12-
use function array_map;
1312
use function count;
1413
use function implode;
1514

@@ -92,7 +91,7 @@ public function hasPhpDocType(): bool
9291

9392
public function getPhpDocType(): Type
9493
{
95-
return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getPhpDocType(), $this->properties));
94+
return $this->pairwiseIntersect(static fn (ExtendedPropertyReflection $property): Type => $property->getPhpDocType());
9695
}
9796

9897
public function hasNativeType(): bool
@@ -102,17 +101,29 @@ public function hasNativeType(): bool
102101

103102
public function getNativeType(): Type
104103
{
105-
return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getNativeType(), $this->properties));
104+
return $this->pairwiseIntersect(static fn (ExtendedPropertyReflection $property): Type => $property->getNativeType());
106105
}
107106

108107
public function getReadableType(): Type
109108
{
110-
return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getReadableType(), $this->properties));
109+
return $this->pairwiseIntersect(static fn (ExtendedPropertyReflection $property): Type => $property->getReadableType());
111110
}
112111

113112
public function getWritableType(): Type
114113
{
115-
return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType(), $this->properties));
114+
return $this->pairwiseIntersect(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType());
115+
}
116+
117+
/**
118+
* @param callable(ExtendedPropertyReflection): Type $getType
119+
*/
120+
private function pairwiseIntersect(callable $getType): Type
121+
{
122+
$result = $getType($this->properties[0]);
123+
for ($i = 1, $count = count($this->properties); $i < $count; $i++) {
124+
$result = TypeCombinator::intersect($result, $getType($this->properties[$i]));
125+
}
126+
return $result;
116127
}
117128

118129
public function canChangeTypeAfterAssignment(): bool

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;

src/Type/IntersectionType.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,7 +1389,11 @@ public function traverse(callable $cb): Type
13891389
}
13901390

13911391
if ($changed) {
1392-
return TypeCombinator::intersect(...$types);
1392+
$result = $types[0];
1393+
for ($i = 1, $count = count($types); $i < $count; $i++) {
1394+
$result = TypeCombinator::intersect($result, $types[$i]);
1395+
}
1396+
return $result;
13931397
}
13941398

13951399
return $this;
@@ -1426,7 +1430,11 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
14261430
return $this;
14271431
}
14281432

1429-
return TypeCombinator::intersect(...$newTypes);
1433+
$result = $newTypes[0];
1434+
for ($i = 1, $count = count($newTypes); $i < $count; $i++) {
1435+
$result = TypeCombinator::intersect($result, $newTypes[$i]);
1436+
}
1437+
return $result;
14301438
}
14311439

14321440
return $this;
@@ -1492,7 +1500,11 @@ private function intersectResults(
14921500
private function intersectTypes(callable $getType): Type
14931501
{
14941502
$operands = array_map($getType, $this->types);
1495-
return TypeCombinator::intersect(...$operands);
1503+
$result = $operands[0];
1504+
for ($i = 1, $count = count($operands); $i < $count; $i++) {
1505+
$result = TypeCombinator::intersect($result, $operands[$i]);
1506+
}
1507+
return $result;
14961508
}
14971509

14981510
public function toPhpDocNode(): TypeNode

src/Type/Php/ImplodeFunctionReturnTypeExtension.php

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use PhpParser\Node\Expr\FuncCall;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\DependencyInjection\AutowiredService;
8-
use PHPStan\Internal\CombinationsHelper;
98
use PHPStan\Reflection\FunctionReflection;
109
use PHPStan\Reflection\InitializerExprTypeResolver;
1110
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
@@ -113,33 +112,45 @@ private function implode(Type $arrayType, Type $separatorType): Type
113112

114113
private function inferConstantType(ConstantArrayType $arrayType, ConstantStringType $separatorType): ?Type
115114
{
116-
$strings = [];
117-
foreach ($arrayType->getAllArrays() as $array) {
118-
$valueTypes = $array->getValueTypes();
119-
120-
$arrayValues = [];
121-
$combinationsCount = 1;
122-
foreach ($valueTypes as $valueType) {
123-
$constScalars = $valueType->getConstantScalarValues();
124-
if (count($constScalars) === 0) {
125-
return null;
126-
}
127-
$arrayValues[] = $constScalars;
128-
$combinationsCount *= count($constScalars);
115+
$sep = $separatorType->getValue();
116+
$valueTypes = $arrayType->getValueTypes();
117+
$limit = InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT;
118+
119+
// Build implode results incrementally, processing one key at a time.
120+
// For optional keys, fork each partial result into with/without variants.
121+
// This avoids generating 2^N ConstantArrayType objects via getAllArrays().
122+
/** @var list<list<scalar>> $partials */
123+
$partials = [[]];
124+
125+
foreach ($valueTypes as $i => $valueType) {
126+
$constScalars = $valueType->getConstantScalarValues();
127+
if (count($constScalars) === 0) {
128+
return null;
129129
}
130130

131-
if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
132-
return null;
131+
$isOptional = $arrayType->isOptionalKey($i);
132+
$newPartials = [];
133+
134+
foreach ($partials as $partial) {
135+
if ($isOptional) {
136+
$newPartials[] = $partial;
137+
}
138+
foreach ($constScalars as $scalar) {
139+
$newPartial = $partial;
140+
$newPartial[] = $scalar;
141+
$newPartials[] = $newPartial;
142+
}
133143
}
134144

135-
$combinations = CombinationsHelper::combinations($arrayValues);
136-
foreach ($combinations as $combination) {
137-
$strings[] = new ConstantStringType(implode($separatorType->getValue(), $combination));
145+
$partials = $newPartials;
146+
if (count($partials) > $limit) {
147+
return null;
138148
}
139149
}
140150

141-
if (count($strings) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
142-
return null;
151+
$strings = [];
152+
foreach ($partials as $partial) {
153+
$strings[] = new ConstantStringType(implode($sep, $partial));
143154
}
144155

145156
return TypeCombinator::union(...$strings);

src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,10 @@ private function computeNeedleNarrowingType(TypeSpecifierContext $context, Type
222222
return null;
223223
}
224224

225-
$guaranteedValueType = TypeCombinator::intersect(...$guaranteedValueTypePerArray);
225+
$guaranteedValueType = $guaranteedValueTypePerArray[0];
226+
for ($i = 1, $count = count($guaranteedValueTypePerArray); $i < $count; $i++) {
227+
$guaranteedValueType = TypeCombinator::intersect($guaranteedValueType, $guaranteedValueTypePerArray[$i]);
228+
}
226229
if (count($guaranteedValueType->getFiniteTypes()) === 0) {
227230
return null;
228231
}

src/Type/TypeCombinator.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1700,7 +1700,11 @@ private static function mergeIntersectionsForUnion(IntersectionType $a, Intersec
17001700
return null;
17011701
}
17021702

1703-
return self::intersect(...$mergedTypes);
1703+
$result = $mergedTypes[0];
1704+
for ($i = 1, $count = count($mergedTypes); $i < $count; $i++) {
1705+
$result = self::intersect($result, $mergedTypes[$i]);
1706+
}
1707+
return $result;
17041708
}
17051709

17061710
public static function removeFalsey(Type $type): Type

src/Type/TypeUtils.php

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
use PHPStan\Type\Generic\TemplateType;
1111
use PHPStan\Type\Generic\TemplateUnionType;
1212
use PHPStan\Type\Traverser\LateResolvableTraverser;
13-
use function array_filter;
14-
use function array_map;
1513
use function array_merge;
16-
use function iterator_to_array;
14+
use function count;
15+
use function max;
16+
use const PHP_INT_MAX;
1717

1818
/**
1919
* @api
@@ -147,18 +147,46 @@ public static function flattenTypes(Type $type): array
147147

148148
$constantArrays = $type->getConstantArrays();
149149
if ($constantArrays !== []) {
150+
// Estimate the total number of power-set variants before expanding.
151+
// Each ConstantArrayType with N optional keys produces 2^N variants
152+
// from getAllArrays(). The cartesian product across multiple constant
153+
// arrays multiplies these counts. Bail out to avoid O(2^N) allocation
154+
// when the total would be large.
155+
$estimatedCount = 1;
156+
$bail = false;
157+
foreach ($constantArrays as $constantArray) {
158+
$optionalCount = count($constantArray->getOptionalKeys());
159+
$arrayCount = $optionalCount <= 20 ? (1 << $optionalCount) : PHP_INT_MAX;
160+
if ($arrayCount > 16384 || $estimatedCount > 16384 / max($arrayCount, 1)) {
161+
$bail = true;
162+
break;
163+
}
164+
$estimatedCount *= $arrayCount;
165+
}
166+
167+
if ($bail) {
168+
return [$type];
169+
}
170+
150171
$newTypes = [];
151172
foreach ($constantArrays as $constantArray) {
152173
$newTypes[] = $constantArray->getAllArrays();
153174
}
154175

155-
return array_filter(
156-
array_map(
157-
static fn (array $types): Type => TypeCombinator::intersect(...$types),
158-
iterator_to_array(CombinationsHelper::combinations($newTypes)),
159-
),
160-
static fn (Type $type): bool => !$type instanceof NeverType,
161-
);
176+
$result = [];
177+
foreach (CombinationsHelper::combinations($newTypes) as $combination) {
178+
$intersected = $combination[0];
179+
for ($i = 1, $count = count($combination); $i < $count; $i++) {
180+
$intersected = TypeCombinator::intersect($intersected, $combination[$i]);
181+
}
182+
if ($intersected instanceof NeverType) {
183+
continue;
184+
}
185+
186+
$result[] = $intersected;
187+
}
188+
189+
return $result;
162190
}
163191

164192
return [$type];

0 commit comments

Comments
 (0)