Skip to content

Commit

Permalink
Extract and test OversizedArrayBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Dec 20, 2022
1 parent d78ff13 commit 1573a79
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 60 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
49 changes: 3 additions & 46 deletions src/Reflection/InitializerExprTypeResolver.php
Expand Up @@ -4,7 +4,6 @@

use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
Expand All @@ -28,7 +27,6 @@
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\Accessory\OversizedArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\BooleanType;
Expand All @@ -39,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 All @@ -64,10 +63,8 @@
use PHPStan\Type\TypeUtils;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function array_keys;
use function array_merge;
use function array_values;
use function count;
use function dirname;
use function in_array;
Expand All @@ -87,6 +84,7 @@ public function __construct(
private ReflectionProviderProvider $reflectionProviderProvider,
private PhpVersion $phpVersion,
private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider,
private OversizedArrayBuilder $oversizedArrayBuilder,
private bool $usePathConstantsAsConstantString = false,
)
{
Expand Down Expand Up @@ -454,12 +452,10 @@ public function resolveConcatType(Type $left, Type $right): Type
*/
public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type
{
// degrade oversized constant arrays, supports only basic types. less precise but slim and fast.
if (count($expr->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
return $this->buildDegradedConstantArray($expr, $getTypeCallback);
return $this->oversizedArrayBuilder->build($expr, $getTypeCallback);
}

// more precise but less performant array resolving
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
$isList = null;
foreach ($expr->items as $arrayItem) {
Expand Down Expand Up @@ -514,45 +510,6 @@ public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type
return $arrayType;
}

/**
* @param callable(Expr): Type $getTypeCallback
*/
private function buildDegradedConstantArray(Array_ $node, callable $getTypeCallback): Type
{
$isList = true;
$valueTypes = [];
$keyTypes = [];
foreach ($node->items as $item) {
if ($item === null) {
throw new ShouldNotHappenException();
}

$key = $item->key;
if ($key !== null) {
$isList = false;

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

$value = $item->value;
$itemValueType = $getTypeCallback($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());
}

/**
* @param callable(Expr): Type $getTypeCallback
*/
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
14 changes: 2 additions & 12 deletions src/Type/Constant/ConstantArrayType.php
Expand Up @@ -300,18 +300,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic
$result = $result->and($acceptsValue);
}

$result = $result->and($type->isArray());
if ($result->yes()) {
return $result;
}

if ($type->isOversizedArray()->yes()) {
if ($this->generalizeToArray()->accepts($type, $strictTypes)->yes()) {
return TrinaryLogic::createYes();
}
}

return $result;
return $result->and($type->isArray());
}

public function isSuperTypeOf(Type $type): TrinaryLogic
Expand Down Expand Up @@ -1200,6 +1189,7 @@ public function generalizeValues(): ArrayType
return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList);
}

/** @deprecated */
public function generalizeToArray(): Type
{
$isIterableAtLeastOnce = $this->isIterableAtLeastOnce();
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());
}

}
82 changes: 82 additions & 0 deletions tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php
@@ -0,0 +1,82 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Constant;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Stmt\Expression;
use PHPStan\Reflection\InitializerExprContext;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;

class OversizedArrayBuilderTest extends PHPStanTestCase
{

public function dataBuild(): iterable
{
yield [
'[1, 2, 3]',
'non-empty-list<int>&oversized-array',
];

yield [
'[1, 2, 3, ...[1, 2, 3]]',
'non-empty-list<int>&oversized-array',
];

yield [
'[1, 2, 3, ...[1, \'foo\' => 2, 3]]',
'non-empty-array<int|(literal-string&non-falsy-string), int>&oversized-array',
];

yield [
'[1, 2, 2 => 3]',
'non-empty-list<int>&oversized-array',
];
yield [
'[1, 2, 3 => 3]',
'non-empty-array<int, int>&oversized-array',
];
yield [
'[1, 1 => 2, 3]',
'non-empty-list<int>&oversized-array',
];
yield [
'[1, 2 => 2, 3]',
'non-empty-array<int, int>&oversized-array',
];
yield [
'[1, \'foo\' => 2, 3]',
'non-empty-array<int|(literal-string&non-falsy-string), int>&oversized-array',
];
}

/**
* @dataProvider dataBuild
*/
public function testBuild(string $sourceCode, string $expectedTypeDescription): void
{
$parser = self::getParser();
$ast = $parser->parseString('<?php ' . $sourceCode . ';');
$expr = $ast[0];
$this->assertInstanceOf(Expression::class, $expr);

$array = $expr->expr;
$this->assertInstanceOf(Array_::class, $array);

$builder = new OversizedArrayBuilder();
$initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class);
$arrayType = $builder->build($array, static fn (Expr $expr): Type => $initializerExprTypeResolver->getType($expr, InitializerExprContext::createEmpty()));
$this->assertSame($expectedTypeDescription, $arrayType->describe(VerbosityLevel::precise()));
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../../../conf/bleedingEdge.neon',
];
}

}

0 comments on commit 1573a79

Please sign in to comment.