Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement OversizedArrayVisitor to improve huge constant array performance #2116

Merged
merged 1 commit into from Dec 20, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {
ondrejmirtes marked this conversation as resolved.
Show resolved Hide resolved
$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