fix union too many types

This commit is contained in:
Tomas Votruba 2019-10-23 14:34:20 +02:00
parent d56c23dbb3
commit 1ea9bd1fe6
9 changed files with 373 additions and 48 deletions

View File

@ -27,6 +27,7 @@ use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\CallableType;
use PHPStan\Type\ClosureType;
use PHPStan\Type\ConstantType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
@ -480,19 +481,17 @@ final class StaticTypeMapper
return $type->getClassName();
}
if ($type instanceof ConstantType) {
if (method_exists($type, 'getValue')) {
return get_class($type) . $type->getValue();
}
throw new ShouldNotHappenException();
}
return $this->mapPHPStanTypeToDocString($type);
}
public function mapStringToPHPStanType(string $newSimpleType): Type
{
$phpParserNode = $this->mapStringToPhpParserNode($newSimpleType);
if ($phpParserNode === null) {
return new MixedType();
}
return $this->mapPhpParserNodePHPStanType($phpParserNode);
}
/**
* @return Identifier|Name|NullableType|null
*/

View File

@ -127,16 +127,14 @@ PHP
private function shouldSkipType(Type $newType, ClassMethod $classMethod): bool
{
if (! $newType instanceof ArrayType && ! $newType instanceof UnionType) {
return true;
}
if ($newType instanceof ArrayType) {
if ($this->isNewAndCurrentTypeBothCallable($newType, $classMethod)) {
if ($this->shouldSkipArrayType($newType, $classMethod)) {
return true;
}
}
if ($this->isMixedOfSpecificOverride($newType, $classMethod)) {
if ($newType instanceof UnionType) {
if (count($newType->getTypes()) > self::MAX_NUMBER_OF_TYPES) {
return true;
}
}
@ -191,4 +189,17 @@ PHP
return true;
}
private function shouldSkipArrayType(ArrayType $arrayType, ClassMethod $classMethod): bool
{
if ($this->isNewAndCurrentTypeBothCallable($arrayType, $classMethod)) {
return true;
}
if ($this->isMixedOfSpecificOverride($arrayType, $classMethod)) {
return true;
}
return false;
}
}

View File

@ -5,14 +5,11 @@ declare(strict_types=1);
namespace Rector\TypeDeclaration\TypeInferer;
use PhpParser\Node\FunctionLike;
use PHPStan\Type\ArrayType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
use Rector\TypeDeclaration\Contract\TypeInferer\ReturnTypeInfererInterface;
use Rector\TypeDeclaration\TypeNormalizer;
final class ReturnTypeInferer extends AbstractPriorityAwareTypeInferer
{
@ -22,17 +19,17 @@ final class ReturnTypeInferer extends AbstractPriorityAwareTypeInferer
private $returnTypeInferers = [];
/**
* @var TypeFactory
* @var TypeNormalizer
*/
private $typeFactory;
private $typeNormalizer;
/**
* @param ReturnTypeInfererInterface[] $returnTypeInferers
*/
public function __construct(TypeFactory $typeFactory, array $returnTypeInferers)
public function __construct(array $returnTypeInferers, TypeNormalizer $typeNormalizer)
{
$this->returnTypeInferers = $this->sortTypeInferersByPriority($returnTypeInferers);
$this->typeFactory = $typeFactory;
$this->typeNormalizer = $typeNormalizer;
}
public function inferFunctionLike(FunctionLike $functionLike): Type
@ -51,7 +48,10 @@ final class ReturnTypeInferer extends AbstractPriorityAwareTypeInferer
}
$type = $returnTypeInferer->inferFunctionLike($functionLike);
$type = $this->normalizeArrayTypeAndArrayNever($type);
$type = $this->typeNormalizer->normalizeArrayTypeAndArrayNever($type);
$type = $this->typeNormalizer->uniqueateConstantArrayType($type);
$type = $this->typeNormalizer->normalizeArrayOfUnionToUnionArray($type);
if (! $type instanceof MixedType) {
return $type;
@ -87,29 +87,4 @@ final class ReturnTypeInferer extends AbstractPriorityAwareTypeInferer
throw new ShouldNotHappenException();
}
/**
* From "string[]|mixed[]" based on empty array to to "string[]"
*/
private function normalizeArrayTypeAndArrayNever(Type $type): Type
{
if (! $type instanceof UnionType) {
return $type;
}
$nonNeverTypes = [];
foreach ($type->getTypes() as $unionedType) {
if (! $unionedType instanceof ArrayType) {
return $type;
}
if ($unionedType->getItemType() instanceof NeverType) {
continue;
}
$nonNeverTypes[] = $unionedType;
}
return $this->typeFactory->createMixedPassedOrUnionType($nonNeverTypes);
}
}

View File

@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
use Rector\NodeTypeResolver\StaticTypeMapper;
use Rector\PHPStan\TypeFactoryStaticHelper;
use Rector\TypeDeclaration\ValueObject\NestedArrayTypeValueObject;
final class TypeNormalizer
{
/**
* @var StaticTypeMapper
*/
private $staticTypeMapper;
/**
* @var TypeFactory
*/
private $typeFactory;
/**
* @var NestedArrayTypeValueObject[]
*/
private $collectedNestedArrayTypes = [];
public function __construct(StaticTypeMapper $staticTypeMapper, TypeFactory $typeFactory)
{
$this->staticTypeMapper = $staticTypeMapper;
$this->typeFactory = $typeFactory;
}
/**
* Turn nested array union types to unique ones:
* e.g. int[]|string[][]|bool[][]|string[][]
*
* int[]|string[][]|bool[][]
*/
public function normalizeArrayOfUnionToUnionArray(Type $type, int $arrayNesting = 1): Type
{
if (! $type instanceof ArrayType) {
return $type;
}
// first collection of types
if ($arrayNesting === 1) {
$this->collectedNestedArrayTypes = [];
}
if ($type->getItemType() instanceof ArrayType) {
++$arrayNesting;
$this->normalizeArrayOfUnionToUnionArray($type->getItemType(), $arrayNesting);
} elseif ($type->getItemType() instanceof UnionType) {
$this->collectNestedArrayTypeFromUnionType($type->getItemType(), $arrayNesting);
} else {
$this->collectedNestedArrayTypes[] = new NestedArrayTypeValueObject(
$type->getItemType(),
$arrayNesting
);
}
return $this->createUnionedTypesFromArrayTypes($this->collectedNestedArrayTypes);
}
public function uniqueateConstantArrayType(Type $type): Type
{
if (! $type instanceof ConstantArrayType) {
return $type;
}
// nothing to normalize
if ($type->getValueTypes() === []) {
return $type;
}
$uniqueTypes = [];
$removedKeys = [];
foreach ($type->getValueTypes() as $key => $valueType) {
$typeHash = $this->staticTypeMapper->createTypeHash($valueType);
$valueType = $this->uniqueateConstantArrayType($valueType);
$valueType = $this->normalizeArrayOfUnionToUnionArray($valueType);
if (! isset($uniqueTypes[$typeHash])) {
$uniqueTypes[$typeHash] = $valueType;
} else {
$removedKeys[] = $key;
}
}
// re-index keys
$uniqueTypes = array_values($uniqueTypes);
$keyTypes = [];
foreach ($type->getKeyTypes() as $key => $keyType) {
if (in_array($key, $removedKeys, true)) {
// remove it
continue;
}
$keyTypes[$key] = $keyType;
}
return new ConstantArrayType($keyTypes, $uniqueTypes);
}
/**
* From "string[]|mixed[]" based on empty array to to "string[]"
*/
public function normalizeArrayTypeAndArrayNever(Type $type): Type
{
if (! $type instanceof UnionType) {
return $type;
}
$nonNeverTypes = [];
foreach ($type->getTypes() as $unionedType) {
if (! $unionedType instanceof ArrayType) {
return $type;
}
if ($unionedType->getItemType() instanceof NeverType) {
continue;
}
$nonNeverTypes[] = $unionedType;
}
return $this->typeFactory->createMixedPassedOrUnionType($nonNeverTypes);
}
/**
* @param NestedArrayTypeValueObject[] $collectedNestedArrayTypes
*/
private function createUnionedTypesFromArrayTypes(array $collectedNestedArrayTypes): Type
{
$unionedTypes = [];
foreach ($collectedNestedArrayTypes as $collectedNestedArrayType) {
$arrayType = $collectedNestedArrayType->getType();
for ($i = 0; $i < $collectedNestedArrayType->getArrayNestingLevel(); ++$i) {
$arrayType = new ArrayType(new MixedType(), $arrayType);
}
/** @var ArrayType $arrayType */
$unionedTypes[] = $arrayType;
}
if (count($unionedTypes) > 1) {
return TypeFactoryStaticHelper::createUnionObjectType($unionedTypes);
}
return $unionedTypes[0];
}
private function collectNestedArrayTypeFromUnionType(UnionType $unionType, int $arrayNesting): void
{
foreach ($unionType->getTypes() as $unionedType) {
if ($unionedType instanceof ArrayType) {
++$arrayNesting;
$this->normalizeArrayOfUnionToUnionArray($unionedType, $arrayNesting);
} else {
$this->collectedNestedArrayTypes[] = new NestedArrayTypeValueObject($unionedType, $arrayNesting);
}
}
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\ValueObject;
use PHPStan\Type\Type;
final class NestedArrayTypeValueObject
{
/**
* @var Type
*/
private $type;
/**
* @var int
*/
private $arrayNestingLevel;
public function __construct(Type $type, int $arrayNestingLevel)
{
$this->type = $type;
$this->arrayNestingLevel = $arrayNestingLevel;
}
public function getType(): Type
{
return $this->type;
}
public function getArrayNestingLevel(): int
{
return $this->arrayNestingLevel;
}
}

View File

@ -32,6 +32,7 @@ final class AddArrayReturnDocTypeRectorTest extends AbstractRectorTestCase
// skip
yield [__DIR__ . '/Fixture/skip_too_many.php.inc'];
yield [__DIR__ . '/Fixture/skip_too_many_2.php.inc'];
yield [__DIR__ . '/Fixture/skip_mixed_of_specific_override.php.inc'];
yield [__DIR__ . '/Fixture/skip_closure_callable_override.php.inc'];
yield [__DIR__ . '/Fixture/skip_shorten_class_name.php.inc'];

View File

@ -0,0 +1,66 @@
<?php
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddArrayReturnDocTypeRector\Fixture;
use Nette\Utils\Strings;
final class SkipToOMany2
{
/**
* @param string[] $packageNames
*/
public function createPackagesData(array $packageNames): array
{
$packagesData = [];
foreach ($packageNames as $packageName) {
$packageKey = $this->createPackageKey($packageName);
$packageDownloads = $this->provideForPackage($packageName);
if ($packageDownloads === []) {
continue;
}
// complete relative number of downloads
$totalDownloads = array_sum($packageDownloads[1]);
foreach ($packageDownloads[1] as $version => $absoluteDownloads) {
$relativeRate = 100 * ($absoluteDownloads / $totalDownloads);
$packageDownloads[1][$version] = [
'absolute_downloads' => $absoluteDownloads,
'relative_downloads' => round($relativeRate, 1),
'version_publish_date' => $this->provideForPackageAndVersion(
$packageName,
$version
),
];
}
$packagesData[$packageKey] = [
'package_name' => $packageName,
'package_short_name' => Strings::after($packageName, '/'),
] + $packageDownloads;
}
return $packagesData;
}
private function createPackageKey(string $packageName): string
{
return Strings::replace($packageName, '#(/|-)#', '_');
}
/**
* @return int[][]
*/
private function provideForPackage(string $packageName): array
{
return [[1, 3]];
}
private function provideForPackageAndVersion(): ?string
{
return '' ? null : 'string';
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\Tests;
use Iterator;
use PHPStan\Type\ArrayType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPStan\Type\UnionType;
use Rector\HttpKernel\RectorKernel;
use Rector\NodeTypeResolver\StaticTypeMapper;
use Rector\TypeDeclaration\TypeNormalizer;
use Symplify\PackageBuilder\Tests\AbstractKernelTestCase;
final class TypeNormalizerTest extends AbstractKernelTestCase
{
/**
* @var TypeNormalizer
*/
private $typeNormalizer;
/**
* @var StaticTypeMapper
*/
private $staticTypeMapper;
protected function setUp(): void
{
self::bootKernel(RectorKernel::class);
$this->typeNormalizer = self::$container->get(TypeNormalizer::class);
$this->staticTypeMapper = self::$container->get(StaticTypeMapper::class);
}
/**
* @dataProvider provideDataForTestNormalizeArrayOfUnionToUnionArray()
*/
public function testNormalizeArrayOfUnionToUnionArray(ArrayType $arrayType, string $expectedDocString): void
{
$arrayDocString = $this->staticTypeMapper->mapPHPStanTypeToDocString($arrayType);
$this->assertSame($expectedDocString, $arrayDocString);
$unionType = $this->typeNormalizer->normalizeArrayOfUnionToUnionArray($arrayType);
$this->assertInstanceOf(UnionType::class, $unionType);
$unionDocString = $this->staticTypeMapper->mapPHPStanTypeToDocString($unionType);
$this->assertSame($expectedDocString, $unionDocString);
}
public function provideDataForTestNormalizeArrayOfUnionToUnionArray(): Iterator
{
$arrayType = new ArrayType(new MixedType(), new UnionType([new StringType(), new IntegerType()]));
yield [$arrayType, 'int[]|string[]'];
$arrayType = new ArrayType(new MixedType(), new UnionType([new StringType(), new IntegerType()]));
$moreNestedArrayType = new ArrayType(new MixedType(), $arrayType);
yield [$moreNestedArrayType, 'int[][]|string[][]'];
}
}

View File

@ -209,3 +209,4 @@ parameters:
- '#PHPDoc tag @param for parameter \$symfonyStyle with type array<Rector\\Contract\\PhpParser\\Node\\CommanderInterface\> is incompatible with native type Symfony\\Component\\Console\\Style\\SymfonyStyle#'
- '#In method "Rector\\BetterPhpDocParser\\AnnotationReader\\NodeAnnotationReader\:\:createPropertyReflectionFromPropertyNode", caught "Throwable" must be rethrown\. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception\. More info\: http\://bit\.ly/failloud#'
- '#Method Rector\\TypeDeclaration\\TypeNormalizer\:\:createUnionedTypesFromArrayTypes\(\) should return array<PHPStan\\Type\\ArrayType\> but returns array<int, PHPStan\\Type\\Type\>#'