mirror of
https://github.com/rectorphp/rector.git
synced 2025-01-17 21:38:22 +01:00
fix union too many types
This commit is contained in:
parent
d56c23dbb3
commit
1ea9bd1fe6
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
174
packages/TypeDeclaration/src/TypeNormalizer.php
Normal file
174
packages/TypeDeclaration/src/TypeNormalizer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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'];
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
62
packages/TypeDeclaration/tests/TypeNormalizerTest.php
Normal file
62
packages/TypeDeclaration/tests/TypeNormalizerTest.php
Normal 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[][]'];
|
||||
}
|
||||
}
|
@ -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\>#'
|
||||
|
Loading…
x
Reference in New Issue
Block a user