Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3266,7 +3266,10 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
unset($scope->expressionTypes[$conditionalExprString]);
} else {
if (array_key_exists($conditionalExprString, $scope->expressionTypes)) {
$type = TypeCombinator::intersect(...array_map(static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getType(), $expressions));
$type = $expressions[0]->getTypeHolder()->getType();
for ($i = 1, $count = count($expressions); $i < $count; $i++) {
$type = TypeCombinator::intersect($type, $expressions[$i]->getTypeHolder()->getType());
}

$scope->expressionTypes[$conditionalExprString] = new ExpressionTypeHolder(
$scope->expressionTypes[$conditionalExprString]->getExpr(),
Expand Down
6 changes: 5 additions & 1 deletion src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,11 @@ private function resolveUnionTypeNode(UnionTypeNode $typeNode, NameScope $nameSc
private function resolveIntersectionTypeNode(IntersectionTypeNode $typeNode, NameScope $nameScope): Type
{
$types = $this->resolveMultiple($typeNode->types, $nameScope);
return TypeCombinator::intersect(...$types);
$result = $types[0];
for ($i = 1, $count = count($types); $i < $count; $i++) {
$result = TypeCombinator::intersect($result, $types[$i]);
}
return $result;
}

private function resolveConditionalTypeNode(ConditionalTypeNode $typeNode, NameScope $nameScope): Type
Expand Down
21 changes: 17 additions & 4 deletions src/Reflection/Type/IntersectionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,18 @@ public function getVariants(): array
}
}

$returnType = TypeCombinator::intersect(...$returnTypes);
$phpDocReturnType = TypeCombinator::intersect(...$phpDocReturnTypes);
$nativeReturnType = TypeCombinator::intersect(...$nativeReturnTypes);
$returnType = $returnTypes[0];
for ($i = 1, $count = count($returnTypes); $i < $count; $i++) {
$returnType = TypeCombinator::intersect($returnType, $returnTypes[$i]);
}
$phpDocReturnType = $phpDocReturnTypes[0];
for ($i = 1, $count = count($phpDocReturnTypes); $i < $count; $i++) {
$phpDocReturnType = TypeCombinator::intersect($phpDocReturnType, $phpDocReturnTypes[$i]);
}
$nativeReturnType = $nativeReturnTypes[0];
for ($i = 1, $count = count($nativeReturnTypes); $i < $count; $i++) {
$nativeReturnType = TypeCombinator::intersect($nativeReturnType, $nativeReturnTypes[$i]);
}
return array_map(static fn (ExtendedParametersAcceptor $acceptor): ExtendedParametersAcceptor => new ExtendedFunctionVariant(
$acceptor->getTemplateTypeMap(),
$acceptor->getResolvedTemplateTypeMap(),
Expand Down Expand Up @@ -188,7 +197,11 @@ public function getThrowType(): ?Type
return null;
}

return TypeCombinator::intersect(...$types);
$result = $types[0];
for ($i = 1, $count = count($types); $i < $count; $i++) {
$result = TypeCombinator::intersect($result, $types[$i]);
}
return $result;
}

public function hasSideEffects(): TrinaryLogic
Expand Down
21 changes: 16 additions & 5 deletions src/Reflection/Type/IntersectionTypePropertyReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_map;
use function count;
use function implode;

Expand Down Expand Up @@ -92,7 +91,7 @@ public function hasPhpDocType(): bool

public function getPhpDocType(): Type
{
return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getPhpDocType(), $this->properties));
return $this->pairwiseIntersect(static fn (ExtendedPropertyReflection $property): Type => $property->getPhpDocType());
}

public function hasNativeType(): bool
Expand All @@ -102,17 +101,29 @@ public function hasNativeType(): bool

public function getNativeType(): Type
{
return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getNativeType(), $this->properties));
return $this->pairwiseIntersect(static fn (ExtendedPropertyReflection $property): Type => $property->getNativeType());
}

public function getReadableType(): Type
{
return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getReadableType(), $this->properties));
return $this->pairwiseIntersect(static fn (ExtendedPropertyReflection $property): Type => $property->getReadableType());
}

public function getWritableType(): Type
{
return TypeCombinator::intersect(...array_map(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType(), $this->properties));
return $this->pairwiseIntersect(static fn (ExtendedPropertyReflection $property): Type => $property->getWritableType());
}

/**
* @param callable(ExtendedPropertyReflection): Type $getType
*/
private function pairwiseIntersect(callable $getType): Type
{
$result = $getType($this->properties[0]);
for ($i = 1, $count = count($this->properties); $i < $count; $i++) {
$result = TypeCombinator::intersect($result, $getType($this->properties[$i]));
}
return $result;
}

public function canChangeTypeAfterAssignment(): bool
Expand Down
58 changes: 31 additions & 27 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Nette\Utils\Strings;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Internal\CombinationsHelper;
use PHPStan\Php\PhpVersion;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
Expand Down Expand Up @@ -1997,38 +1996,43 @@ public static function isValidIdentifier(string $value): bool

public function getFiniteTypes(): array
{
$arraysArraysForCombinations = [];
$count = 0;
foreach ($this->getAllArrays() as $array) {
$values = $array->getValueTypes();
$arraysForCombinations = [];
$combinationCount = 1;
foreach ($values as $valueType) {
$finiteTypes = $valueType->getFiniteTypes();
if ($finiteTypes === []) {
return [];
$limit = InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT;

// Build finite array types incrementally, processing one key at a time.
// For optional keys, fork each partial result into with/without variants.
// This avoids generating 2^N ConstantArrayType objects via getAllArrays().
/** @var list<ConstantArrayTypeBuilder> $partials */
$partials = [ConstantArrayTypeBuilder::createEmpty()];

foreach ($this->keyTypes as $i => $keyType) {
$finiteValueTypes = $this->valueTypes[$i]->getFiniteTypes();
if ($finiteValueTypes === []) {
return [];
}

$isOptional = $this->isOptionalKey($i);
$newPartials = [];

foreach ($partials as $partial) {
if ($isOptional) {
$newPartials[] = clone $partial;
}
foreach ($finiteValueTypes as $finiteValueType) {
$newPartial = clone $partial;
$newPartial->setOffsetValueType($keyType, $finiteValueType);
$newPartials[] = $newPartial;
}
$arraysForCombinations[] = $finiteTypes;
$combinationCount *= count($finiteTypes);
}
$arraysArraysForCombinations[] = $arraysForCombinations;
$count += $combinationCount;
}

if ($count > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
return [];
$partials = $newPartials;
if (count($partials) > $limit) {
return [];
}
}

$finiteTypes = [];
foreach ($arraysArraysForCombinations as $arraysForCombinations) {
$combinations = CombinationsHelper::combinations($arraysForCombinations);
foreach ($combinations as $combination) {
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($combination as $i => $v) {
$builder->setOffsetValueType($this->keyTypes[$i], $v);
}
$finiteTypes[] = $builder->getArray();
}
foreach ($partials as $partial) {
$finiteTypes[] = $partial->getArray();
}

return $finiteTypes;
Expand Down
18 changes: 15 additions & 3 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -1378,7 +1378,11 @@ public function traverse(callable $cb): Type
}

if ($changed) {
return TypeCombinator::intersect(...$types);
$result = $types[0];
for ($i = 1, $count = count($types); $i < $count; $i++) {
$result = TypeCombinator::intersect($result, $types[$i]);
}
return $result;
}

return $this;
Expand Down Expand Up @@ -1415,7 +1419,11 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
return $this;
}

return TypeCombinator::intersect(...$newTypes);
$result = $newTypes[0];
for ($i = 1, $count = count($newTypes); $i < $count; $i++) {
$result = TypeCombinator::intersect($result, $newTypes[$i]);
}
return $result;
}

return $this;
Expand Down Expand Up @@ -1481,7 +1489,11 @@ private function intersectResults(
private function intersectTypes(callable $getType): Type
{
$operands = array_map($getType, $this->types);
return TypeCombinator::intersect(...$operands);
$result = $operands[0];
for ($i = 1, $count = count($operands); $i < $count; $i++) {
$result = TypeCombinator::intersect($result, $operands[$i]);
}
return $result;
}

public function toPhpDocNode(): TypeNode
Expand Down
53 changes: 32 additions & 21 deletions src/Type/Php/ImplodeFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Internal\CombinationsHelper;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
Expand Down Expand Up @@ -113,33 +112,45 @@ private function implode(Type $arrayType, Type $separatorType): Type

private function inferConstantType(ConstantArrayType $arrayType, ConstantStringType $separatorType): ?Type
{
$strings = [];
foreach ($arrayType->getAllArrays() as $array) {
$valueTypes = $array->getValueTypes();

$arrayValues = [];
$combinationsCount = 1;
foreach ($valueTypes as $valueType) {
$constScalars = $valueType->getConstantScalarValues();
if (count($constScalars) === 0) {
return null;
}
$arrayValues[] = $constScalars;
$combinationsCount *= count($constScalars);
$sep = $separatorType->getValue();
$valueTypes = $arrayType->getValueTypes();
$limit = InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT;

// Build implode results incrementally, processing one key at a time.
// For optional keys, fork each partial result into with/without variants.
// This avoids generating 2^N ConstantArrayType objects via getAllArrays().
/** @var list<list<scalar>> $partials */
$partials = [[]];

foreach ($valueTypes as $i => $valueType) {
$constScalars = $valueType->getConstantScalarValues();
if (count($constScalars) === 0) {
return null;
}

if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
return null;
$isOptional = $arrayType->isOptionalKey($i);
$newPartials = [];

foreach ($partials as $partial) {
if ($isOptional) {
$newPartials[] = $partial;
}
foreach ($constScalars as $scalar) {
$newPartial = $partial;
$newPartial[] = $scalar;
$newPartials[] = $newPartial;
}
}

$combinations = CombinationsHelper::combinations($arrayValues);
foreach ($combinations as $combination) {
$strings[] = new ConstantStringType(implode($separatorType->getValue(), $combination));
$partials = $newPartials;
if (count($partials) > $limit) {
return null;
}
}

if (count($strings) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
return null;
$strings = [];
foreach ($partials as $partial) {
$strings[] = new ConstantStringType(implode($sep, $partial));
}

return TypeCombinator::union(...$strings);
Expand Down
5 changes: 4 additions & 1 deletion src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,10 @@ private function computeNeedleNarrowingType(TypeSpecifierContext $context, Type
return null;
}

$guaranteedValueType = TypeCombinator::intersect(...$guaranteedValueTypePerArray);
$guaranteedValueType = $guaranteedValueTypePerArray[0];
for ($i = 1, $count = count($guaranteedValueTypePerArray); $i < $count; $i++) {
$guaranteedValueType = TypeCombinator::intersect($guaranteedValueType, $guaranteedValueTypePerArray[$i]);
}
if (count($guaranteedValueType->getFiniteTypes()) === 0) {
return null;
}
Expand Down
6 changes: 5 additions & 1 deletion src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -1668,7 +1668,11 @@ private static function mergeIntersectionsForUnion(IntersectionType $a, Intersec
return null;
}

return self::intersect(...$mergedTypes);
$result = $mergedTypes[0];
for ($i = 1, $count = count($mergedTypes); $i < $count; $i++) {
$result = self::intersect($result, $mergedTypes[$i]);
}
return $result;
}

public static function removeFalsey(Type $type): Type
Expand Down
48 changes: 38 additions & 10 deletions src/Type/TypeUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateUnionType;
use PHPStan\Type\Traverser\LateResolvableTraverser;
use function array_filter;
use function array_map;
use function array_merge;
use function iterator_to_array;
use function count;
use function max;
use const PHP_INT_MAX;

/**
* @api
Expand Down Expand Up @@ -147,18 +147,46 @@ public static function flattenTypes(Type $type): array

$constantArrays = $type->getConstantArrays();
if ($constantArrays !== []) {
// Estimate the total number of power-set variants before expanding.
// Each ConstantArrayType with N optional keys produces 2^N variants
// from getAllArrays(). The cartesian product across multiple constant
// arrays multiplies these counts. Bail out to avoid O(2^N) allocation
// when the total would be large.
$estimatedCount = 1;
$bail = false;
foreach ($constantArrays as $constantArray) {
$optionalCount = count($constantArray->getOptionalKeys());
$arrayCount = $optionalCount <= 20 ? (1 << $optionalCount) : PHP_INT_MAX;
if ($arrayCount > 16384 || $estimatedCount > 16384 / max($arrayCount, 1)) {
$bail = true;
break;
}
$estimatedCount *= $arrayCount;
}

if ($bail) {
return [$type];
}

$newTypes = [];
foreach ($constantArrays as $constantArray) {
$newTypes[] = $constantArray->getAllArrays();
}

return array_filter(
array_map(
static fn (array $types): Type => TypeCombinator::intersect(...$types),
iterator_to_array(CombinationsHelper::combinations($newTypes)),
),
static fn (Type $type): bool => !$type instanceof NeverType,
);
$result = [];
foreach (CombinationsHelper::combinations($newTypes) 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;
}

return [$type];
Expand Down
Loading
Loading