From 9ebf4a407231d849717e83930e2cae589cce09a5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 20 Dec 2022 09:36:47 +0100 Subject: [PATCH] Extract and test OversizedArrayBuilder --- conf/config.neon | 3 + .../ValidateIgnoredErrorsExtension.php | 3 +- .../InitializerExprTypeResolver.php | 49 +-------- src/Testing/PHPStanTestCase.php | 3 +- src/Type/Constant/ConstantArrayType.php | 14 +-- src/Type/Constant/OversizedArrayBuilder.php | 100 ++++++++++++++++++ .../Constant/OversizedArrayBuilderTest.php | 82 ++++++++++++++ 7 files changed, 194 insertions(+), 60 deletions(-) create mode 100644 src/Type/Constant/OversizedArrayBuilder.php create mode 100644 tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php diff --git a/conf/config.neon b/conf/config.neon index c28be83c4e2..02d66d4fb22 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1797,6 +1797,9 @@ services: arguments: parser: @currentPhpVersionPhpParser + - + class: PHPStan\Type\Constant\OversizedArrayBuilder + exceptionTypeResolver: class: PHPStan\Rules\Exceptions\ExceptionTypeResolver factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index 18779e8b511..543145d46d8 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -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; @@ -97,7 +98,7 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry return new OperatorTypeSpecifyingExtensionRegistry(null, []); } - }), + }, new OversizedArrayBuilder()), ), ), ); diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 6d7a663d05a..9b2b36fb59f 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -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; @@ -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; @@ -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; @@ -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; @@ -87,6 +84,7 @@ public function __construct( private ReflectionProviderProvider $reflectionProviderProvider, private PhpVersion $phpVersion, private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider, + private OversizedArrayBuilder $oversizedArrayBuilder, private bool $usePathConstantsAsConstantString = false, ) { @@ -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) { @@ -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 */ diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 2d3b73db091..62e60958228 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -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; @@ -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, diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 27d9827ebe1..0ac63052dff 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -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 @@ -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(); diff --git a/src/Type/Constant/OversizedArrayBuilder.php b/src/Type/Constant/OversizedArrayBuilder.php new file mode 100644 index 00000000000..ea88de98975 --- /dev/null +++ b/src/Type/Constant/OversizedArrayBuilder.php @@ -0,0 +1,100 @@ +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->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()); + } + +} diff --git a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php new file mode 100644 index 00000000000..e083852496f --- /dev/null +++ b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php @@ -0,0 +1,82 @@ +&oversized-array', + ]; + + yield [ + '[1, 2, 3, ...[1, 2, 3]]', + 'non-empty-list&oversized-array', + ]; + + yield [ + '[1, 2, 3, ...[1, \'foo\' => 2, 3]]', + 'non-empty-array&oversized-array', + ]; + + yield [ + '[1, 2, 2 => 3]', + 'non-empty-list&oversized-array', + ]; + yield [ + '[1, 2, 3 => 3]', + 'non-empty-array&oversized-array', + ]; + yield [ + '[1, 1 => 2, 3]', + 'non-empty-list&oversized-array', + ]; + yield [ + '[1, 2 => 2, 3]', + 'non-empty-array&oversized-array', + ]; + yield [ + '[1, \'foo\' => 2, 3]', + 'non-empty-array&oversized-array', + ]; + } + + /** + * @dataProvider dataBuild + */ + public function testBuild(string $sourceCode, string $expectedTypeDescription): void + { + $parser = self::getParser(); + $ast = $parser->parseString('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', + ]; + } + +}