Skip to content

Commit

Permalink
Implement OversizedArrayBuilder to improve huge constant array perfor…
Browse files Browse the repository at this point in the history
…mance
  • Loading branch information
staabm authored and ondrejmirtes committed Dec 20, 2022
1 parent 25dbb8f commit f1e0ed2
Show file tree
Hide file tree
Showing 15 changed files with 24,374 additions and 8 deletions.
3 changes: 3 additions & 0 deletions conf/config.neon
Expand Up @@ -1797,6 +1797,9 @@ services:
arguments:
parser: @currentPhpVersionPhpParser

-
class: PHPStan\Type\Constant\OversizedArrayBuilder

exceptionTypeResolver:
class: PHPStan\Rules\Exceptions\ExceptionTypeResolver
factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver
Expand Down
3 changes: 2 additions & 1 deletion src/DependencyInjection/ValidateIgnoredErrorsExtension.php
Expand Up @@ -23,6 +23,7 @@
use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider;
use PHPStan\Reflection\ReflectionProvider\DummyReflectionProvider;
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
use PHPStan\Type\Constant\OversizedArrayBuilder;
use PHPStan\Type\DirectTypeAliasResolverProvider;
use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry;
use PHPStan\Type\Type;
Expand Down Expand Up @@ -97,7 +98,7 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry
return new OperatorTypeSpecifyingExtensionRegistry(null, []);
}

}),
}, new OversizedArrayBuilder()),
),
),
);
Expand Down
13 changes: 9 additions & 4 deletions src/Reflection/InitializerExprTypeResolver.php
Expand Up @@ -5,6 +5,8 @@
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\DNumber;
Expand Down Expand Up @@ -35,6 +37,7 @@
use PHPStan\Type\Constant\ConstantFloatType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Constant\OversizedArrayBuilder;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\ConstantTypeHelper;
use PHPStan\Type\Enum\EnumCaseObjectType;
Expand Down Expand Up @@ -81,6 +84,7 @@ public function __construct(
private ReflectionProviderProvider $reflectionProviderProvider,
private PhpVersion $phpVersion,
private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider,
private OversizedArrayBuilder $oversizedArrayBuilder,
private bool $usePathConstantsAsConstantString = false,
)
{
Expand All @@ -101,7 +105,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type
if ($expr instanceof String_) {
return new ConstantStringType($expr->value);
}
if ($expr instanceof Expr\ConstFetch) {
if ($expr instanceof ConstFetch) {
$constName = (string) $expr->name;
$loweredConstName = strtolower($constName);
if ($loweredConstName === 'true') {
Expand Down Expand Up @@ -153,7 +157,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type
$dim = $this->getType($expr->dim, $context);
return $var->getOffsetValueType($dim);
}
if ($expr instanceof Expr\ClassConstFetch && $expr->name instanceof Identifier) {
if ($expr instanceof ClassConstFetch && $expr->name instanceof Identifier) {
return $this->getClassConstFetchType($expr->class, $expr->name->toString(), $context->getClassName(), fn (Expr $expr): Type => $this->getType($expr, $context));
}
if ($expr instanceof Expr\UnaryPlus) {
Expand Down Expand Up @@ -448,10 +452,11 @@ public function resolveConcatType(Type $left, Type $right): Type
*/
public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type
{
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
if (count($expr->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
$arrayBuilder->degradeToGeneralArray();
return $this->oversizedArrayBuilder->build($expr, $getTypeCallback);
}

$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
$isList = null;
foreach ($expr->items as $arrayItem) {
if ($arrayItem === null) {
Expand Down
3 changes: 2 additions & 1 deletion src/Testing/PHPStanTestCase.php
Expand Up @@ -29,6 +29,7 @@
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\Type\Constant\OversizedArrayBuilder;
use PHPStan\Type\TypeAliasResolver;
use PHPStan\Type\UsefulTypeAliasResolver;
use PHPUnit\Framework\ExpectationFailedException;
Expand Down Expand Up @@ -168,7 +169,7 @@ public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeS
new DirectInternalScopeFactory(
MutatingScope::class,
$reflectionProvider,
new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), $container->getParameter('usePathConstantsAsConstantString')),
new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), new OversizedArrayBuilder(), $container->getParameter('usePathConstantsAsConstantString')),
$container->getByType(DynamicReturnTypeExtensionRegistryProvider::class),
$container->getByType(ExprPrinter::class),
$typeSpecifier,
Expand Down
107 changes: 107 additions & 0 deletions src/Type/Constant/OversizedArrayBuilder.php
@@ -0,0 +1,107 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Constant;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Accessory\AccessoryArrayListType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\Accessory\OversizedArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\VerbosityLevel;
use function array_splice;
use function array_values;
use function count;

class OversizedArrayBuilder
{

/**
* @param callable(Expr): Type $getTypeCallback
*/
public function build(Array_ $expr, callable $getTypeCallback): Type
{
$isList = true;
$valueTypes = [];
$keyTypes = [];
$nextAutoIndex = 0;
$items = $expr->items;
for ($i = 0; $i < count($items); $i++) {
$item = $items[$i];
if ($item === null) {
continue;
}
if (!$item->unpack) {
continue;
}

$valueType = $getTypeCallback($item->value);
if ($valueType instanceof ConstantArrayType) {
array_splice($items, $i, 1);
foreach ($valueType->getKeyTypes() as $j => $innerKeyType) {
$innerValueType = $valueType->getValueTypes()[$j];
if ($innerKeyType->isString()->no()) {
$keyExpr = null;
} else {
$keyExpr = new TypeExpr($innerKeyType);
}
array_splice($items, $i++, 0, [new Expr\ArrayItem(
new TypeExpr($innerValueType),
$keyExpr,
)]);
}
} else {
array_splice($items, $i, 1, [new Expr\ArrayItem(
new TypeExpr($valueType->getIterableValueType()),
new TypeExpr($valueType->getIterableKeyType()),
)]);
}
}
foreach ($items as $item) {
if ($item === null) {
continue;
}
if ($item->unpack) {
throw new ShouldNotHappenException();
}
if ($item->key !== null) {
$itemKeyType = $getTypeCallback($item->key);
if (!$itemKeyType instanceof ConstantIntegerType) {
$isList = false;
} elseif ($itemKeyType->getValue() !== $nextAutoIndex) {
$isList = false;
$nextAutoIndex = $itemKeyType->getValue() + 1;
} else {
$itemKeyType = new ConstantIntegerType($nextAutoIndex);
$nextAutoIndex++;
}
} else {
$itemKeyType = new ConstantIntegerType($nextAutoIndex);
$nextAutoIndex++;
}

$generalizedKeyType = $itemKeyType->generalize(GeneralizePrecision::moreSpecific());
$keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType;

$itemValueType = $getTypeCallback($item->value);
$generalizedValueType = $itemValueType->generalize(GeneralizePrecision::moreSpecific());
$valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType;
}

$keyType = TypeCombinator::union(...array_values($keyTypes));
$valueType = TypeCombinator::union(...array_values($valueTypes));

$arrayType = new ArrayType($keyType, $valueType);
if ($isList) {
$arrayType = AccessoryArrayListType::intersectWith($arrayType);
}

return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType());
}

}
18 changes: 16 additions & 2 deletions src/Type/IntersectionType.php
Expand Up @@ -145,12 +145,26 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic
return $otherType->isSuperTypeOf($this);
}

return TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType));
$result = TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType));
if ($this->isOversizedArray()->yes()) {
if (!$result->no()) {
return TrinaryLogic::createYes();
}
}

return $result;
}

public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic
{
return TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes));
$result = TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes));
if ($this->isOversizedArray()->yes()) {
if (!$result->no()) {
return TrinaryLogic::createYes();
}
}

return $result;
}

public function equals(Type $type): bool
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Expand Up @@ -1085,6 +1085,18 @@ public function testBug8537(): void
$this->assertNoErrors($errors);
}

public function testBug8146(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/bug-8146b.php');
$this->assertNoErrors($errors);
}

public function testBug8215(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/bug-8215.php');
$this->assertNoErrors($errors);
}

/**
* @param string[]|null $allAnalysedFiles
* @return Error[]
Expand Down

0 comments on commit f1e0ed2

Please sign in to comment.