Skip to content

Commit

Permalink
Implement OversizedArrayVisitor to improve huge constant array perfor…
Browse files Browse the repository at this point in the history
…mance
  • Loading branch information
staabm committed Dec 16, 2022
1 parent 2a61ebc commit a732639
Show file tree
Hide file tree
Showing 11 changed files with 18,822 additions and 32 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Expand Up @@ -460,6 +460,11 @@ services:
tags:
- phpstan.parser.richParserNodeVisitor

-
class: PHPStan\Parser\OversizedConstantArrayVisitor
tags:
- phpstan.parser.richParserNodeVisitor

-
class: PHPStan\Parser\ClosureArgVisitor
tags:
Expand Down
51 changes: 51 additions & 0 deletions src/Analyser/GetTypeHelper.php
@@ -0,0 +1,51 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\ResourceType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;

final class GetTypeHelper
{

public static function typeFromString(string $typeName): ?Type
{
$type = null;

if ($typeName === 'string') {
$type = new StringType();
}
if ($typeName === 'array') {
$type = new ArrayType(new MixedType(), new MixedType());
}
if ($typeName === 'boolean') {
$type = new BooleanType();
}
if ($typeName === 'resource' || $typeName === 'resource (closed)') {
$type = new ResourceType();
}
if ($typeName === 'integer') {
$type = new IntegerType();
}
if ($typeName === 'double') {
$type = new FloatType();
}
if ($typeName === 'NULL') {
$type = new NullType();
}
if ($typeName === 'object') {
$type = new ObjectWithoutClassType();
}

return $type;
}

}
30 changes: 1 addition & 29 deletions src/Analyser/TypeSpecifier.php
Expand Up @@ -31,8 +31,6 @@
use PHPStan\Type\Accessory\HasOffsetType;
use PHPStan\Type\Accessory\HasPropertyType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\ConditionalTypeForParameter;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
Expand All @@ -56,11 +54,9 @@
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\ResourceType;
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
use PHPStan\Type\StaticType;
use PHPStan\Type\StaticTypeFactory;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
Expand Down Expand Up @@ -1080,31 +1076,7 @@ private function specifyTypesForConstantStringBinaryExpression(
&& strtolower($exprNode->name->toString()) === 'gettype'
&& isset($exprNode->getArgs()[0])
) {
$type = null;
if ($constantType->getValue() === 'string') {
$type = new StringType();
}
if ($constantType->getValue() === 'array') {
$type = new ArrayType(new MixedType(), new MixedType());
}
if ($constantType->getValue() === 'boolean') {
$type = new BooleanType();
}
if ($constantType->getValue() === 'resource' || $constantType->getValue() === 'resource (closed)') {
$type = new ResourceType();
}
if ($constantType->getValue() === 'integer') {
$type = new IntegerType();
}
if ($constantType->getValue() === 'double') {
$type = new FloatType();
}
if ($constantType->getValue() === 'NULL') {
$type = new NullType();
}
if ($constantType->getValue() === 'object') {
$type = new ObjectWithoutClassType();
}
$type = GetTypeHelper::typeFromString($constantType->getValue());

if ($type !== null) {
return $this->create($exprNode->getArgs()[0]->value, $type, $context, false, $scope, $rootExpr);
Expand Down
10 changes: 10 additions & 0 deletions src/Parser/OversizedArrayTypeException.php
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace PHPStan\Parser;

use Exception;

class OversizedArrayTypeException extends Exception
{

}
171 changes: 171 additions & 0 deletions src/Parser/OversizedConstantArrayVisitor.php
@@ -0,0 +1,171 @@
<?php declare(strict_types = 1);

namespace PHPStan\Parser;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Scalar\DNumber;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\GetTypeHelper;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\IntegerType;
use PHPStan\Type\StringType;
use PHPStan\Type\TypeCombinator;
use function array_keys;
use function array_shift;
use function count;
use function get_class;
use function gettype;
use function is_array;
use function sprintf;
use function strtolower;

class OversizedConstantArrayVisitor extends NodeVisitorAbstract
{

public const IS_OVERSIZED = 'oversized';
public const ARRAY_TYPES = 'oversizedArrayTypes';

private ?Node $inOversizedArray = null;

private bool $leavingOversized = false;

public function enterNode(Node $node): ?Node
{
if ($node instanceof Array_ && $node->items !== null && count($node->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
$this->inOversizedArray = $node;
}

return null;
}

public function leaveNode(Node $node): ?Node
{
if ($node instanceof Array_ && $this->inOversizedArray !== null) {
try {
$this->inspectArrayItems($node);
} catch (OversizedArrayTypeException) {
// the array contains values we are not able to inspect just using AST.
// -> skip the optimization
$this->inOversizedArray = null;
$this->leavingOversized = false;
return null;
}
}

if ($node === $this->inOversizedArray) {
$this->inOversizedArray = null;
$this->leavingOversized = true;
}

if ($this->leavingOversized) {
if ($node instanceof Array_) {
$node->setAttribute(self::IS_OVERSIZED, true);
} elseif (!$node instanceof Expr) {
// we reached the end of the expression
$this->leavingOversized = false;
}
}

return null;
}

/**
* @throws OversizedArrayTypeException
*/
private function inspectArrayItems(Array_ $node): void
{
$valueType = [];

$itemKeyTypes = [];
$itemValueTypes = [];
foreach ($node->items as $item) {
if ($item === null) {
continue;
}

$key = $item->key;
if ($key === null) {
continue;
}
$keyValue = $this->getValueFromExpr($key);

$value = $item->value;
if ($value instanceof Array_) {
$arrayTypes = $value->getAttribute(self::ARRAY_TYPES);

if (!is_array($arrayTypes)) {
throw new ShouldNotHappenException();
}

if (count($value->items) > 0) {
$valueType[] = TypeCombinator::intersect(
new ArrayType($arrayTypes[0], $arrayTypes[1]),
new NonEmptyArrayType(),
);

continue;
}

$valueType[] = new ArrayType($arrayTypes[0], $arrayTypes[1]);
continue;
}
$valueValue = $this->getValueFromExpr($value);

// de-duplicate values
$itemKeyTypes[gettype($keyValue)] = true;
$itemValueTypes[gettype($valueValue)] = true;
}

$keyType = new BenevolentUnionType([new IntegerType(), new StringType()]);
if (count($itemKeyTypes) === 1) {
$itemKeyTypes = array_keys($itemKeyTypes);
$type = GetTypeHelper::typeFromString(array_shift($itemKeyTypes));
if ($type === null) {
throw new ShouldNotHappenException();
}
$keyType = $type;
}

foreach (array_keys($itemValueTypes) as $itemValueType) {
$type = GetTypeHelper::typeFromString($itemValueType);
if ($type === null) {
throw new ShouldNotHappenException();
}
$valueType[] = $type;
}

$node->setAttribute(self::ARRAY_TYPES, [$keyType, TypeCombinator::union(...$valueType)]);
}

private function getValueFromExpr(Expr $expr)
{
if ($expr instanceof LNumber || $expr instanceof DNumber || $expr instanceof String_) {
return $expr->value;
}

if ($expr instanceof ConstFetch) {
$constName = (string) $expr->name;
$loweredConstName = strtolower($constName);

if ($loweredConstName === 'true') {
return true;
} elseif ($loweredConstName === 'false') {
return false;
} elseif ($loweredConstName === 'null') {
return null;
}
}

throw new OversizedArrayTypeException(sprintf('Unexpected expression type %s', get_class($expr)));
}

}
8 changes: 8 additions & 0 deletions src/Reflection/InitializerExprTypeResolver.php
Expand Up @@ -17,6 +17,7 @@
use PHPStan\Analyser\ConstantResolver;
use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Parser\OversizedConstantArrayVisitor;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;
use PHPStan\ShouldNotHappenException;
Expand Down Expand Up @@ -64,6 +65,7 @@
use function count;
use function dirname;
use function in_array;
use function is_array;
use function is_float;
use function is_int;
use function max;
Expand Down Expand Up @@ -440,6 +442,12 @@ public function getConcatType(Expr $left, Expr $right, callable $getTypeCallback
*/
public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type
{
// degrade oversized constant arrays
$oversizedArrayTypes = $expr->getAttribute(OversizedConstantArrayVisitor::ARRAY_TYPES);
if (is_array($oversizedArrayTypes)) {
return new ArrayType($oversizedArrayTypes[0], $oversizedArrayTypes[1]);
}

$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
if (count($expr->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
$arrayBuilder->degradeToGeneralArray();
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/FunctionReturnTypeCheck.php
Expand Up @@ -6,6 +6,7 @@
use PhpParser\Node;
use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
use PHPStan\Parser\OversizedConstantArrayVisitor;
use PHPStan\Type\GenericTypeVariableResolver;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
Expand Down Expand Up @@ -93,7 +94,7 @@ public function checkReturnType(
];
}

if (!$this->ruleLevelHelper->accepts($returnType, $returnValueType, $scope->isDeclareStrictTypes())) {
if (!$returnValue->hasAttribute(OversizedConstantArrayVisitor::IS_OVERSIZED) && !$this->ruleLevelHelper->accepts($returnType, $returnValueType, $scope->isDeclareStrictTypes())) {
return [
RuleErrorBuilder::message(sprintf(
$typeMismatchMessage,
Expand Down
8 changes: 6 additions & 2 deletions src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Parser\OversizedConstantArrayVisitor;
use PHPStan\Reflection\ClassConstantReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\InitializerExprContext;
Expand Down Expand Up @@ -78,10 +79,13 @@ private function processSingleConstant(ClassReflection $classReflection, string
$constantName,
))->build();
} else {
$nativeType = $this->initializerExprTypeResolver->getType($constantReflection->getValueExpr(), InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass()));
$valueExpr = $constantReflection->getValueExpr();
$nativeType = $this->initializerExprTypeResolver->getType($valueExpr, InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass()));
$isSuperType = $phpDocType->isSuperTypeOf($nativeType);
$verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $nativeType);
if ($isSuperType->no()) {
if ($valueExpr->hasAttribute(OversizedConstantArrayVisitor::IS_OVERSIZED)) {
// skip oversized constant arrays
} elseif ($isSuperType->no()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'PHPDoc tag @var for constant %s::%s with type %s is incompatible with value %s.',
$constantReflection->getDeclaringClass()->getDisplayName(),
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Expand Up @@ -1081,6 +1081,18 @@ public function testBug8503(): 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);
}

/**
* @dataProvider dataExplicitNever
*
Expand Down

0 comments on commit a732639

Please sign in to comment.