2020-01-15 13:51:15 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace Rector\PHPStanStaticTypeMapper\TypeMapper;
|
|
|
|
|
|
|
|
use PhpParser\Node;
|
2020-05-10 21:02:46 +00:00
|
|
|
use PhpParser\Node\Name;
|
2020-01-15 13:51:15 +00:00
|
|
|
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
|
|
|
|
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
|
2021-02-28 07:47:48 +00:00
|
|
|
use PHPStan\Reflection\ReflectionProvider;
|
2020-01-15 13:51:15 +00:00
|
|
|
use PHPStan\Type\ArrayType;
|
2020-08-11 11:58:03 +00:00
|
|
|
use PHPStan\Type\Constant\ConstantArrayType;
|
|
|
|
use PHPStan\Type\Constant\ConstantIntegerType;
|
2021-02-20 00:14:58 +00:00
|
|
|
use PHPStan\Type\Generic\GenericClassStringType;
|
2020-08-28 05:00:03 +00:00
|
|
|
use PHPStan\Type\IntegerType;
|
2020-08-11 11:58:03 +00:00
|
|
|
use PHPStan\Type\MixedType;
|
|
|
|
use PHPStan\Type\NeverType;
|
2021-02-20 00:14:58 +00:00
|
|
|
use PHPStan\Type\ObjectType;
|
2020-01-15 13:51:15 +00:00
|
|
|
use PHPStan\Type\Type;
|
|
|
|
use PHPStan\Type\UnionType;
|
2020-08-29 08:41:19 +00:00
|
|
|
use Rector\AttributeAwarePhpDoc\Ast\Type\AttributeAwareArrayTypeNode;
|
2020-08-11 11:58:03 +00:00
|
|
|
use Rector\AttributeAwarePhpDoc\Ast\Type\AttributeAwareGenericTypeNode;
|
|
|
|
use Rector\AttributeAwarePhpDoc\Ast\Type\AttributeAwareIdentifierTypeNode;
|
2020-01-15 13:51:15 +00:00
|
|
|
use Rector\AttributeAwarePhpDoc\Ast\Type\AttributeAwareUnionTypeNode;
|
2020-08-29 14:20:27 +00:00
|
|
|
use Rector\BetterPhpDocParser\Contract\PhpDocNode\AttributeAwareNodeInterface;
|
2020-01-15 13:51:15 +00:00
|
|
|
use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
|
|
|
|
use Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper;
|
2021-02-20 00:14:58 +00:00
|
|
|
use Rector\PHPStanStaticTypeMapper\TypeAnalyzer\UnionTypeCommonTypeNarrower;
|
2020-05-05 15:22:54 +00:00
|
|
|
use Rector\TypeDeclaration\TypeNormalizer;
|
2020-01-15 13:51:15 +00:00
|
|
|
|
2020-09-01 17:56:30 +00:00
|
|
|
/**
|
|
|
|
* @see \Rector\PHPStanStaticTypeMapper\Tests\TypeMapper\ArrayTypeMapperTest
|
|
|
|
*/
|
2020-01-15 13:51:15 +00:00
|
|
|
final class ArrayTypeMapper implements TypeMapperInterface
|
|
|
|
{
|
2020-08-29 14:20:27 +00:00
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public const HAS_GENERIC_TYPE_PARENT = 'has_generic_type_parent';
|
|
|
|
|
2020-01-15 13:51:15 +00:00
|
|
|
/**
|
|
|
|
* @var PHPStanStaticTypeMapper
|
|
|
|
*/
|
|
|
|
private $phpStanStaticTypeMapper;
|
|
|
|
|
2020-05-05 15:22:54 +00:00
|
|
|
/**
|
|
|
|
* @var TypeNormalizer
|
|
|
|
*/
|
|
|
|
private $typeNormalizer;
|
|
|
|
|
2021-02-20 00:14:58 +00:00
|
|
|
/**
|
|
|
|
* @var UnionTypeCommonTypeNarrower
|
|
|
|
*/
|
|
|
|
private $unionTypeCommonTypeNarrower;
|
|
|
|
|
|
|
|
/**
|
2021-02-28 07:47:48 +00:00
|
|
|
* @var ReflectionProvider
|
2021-02-20 00:14:58 +00:00
|
|
|
*/
|
2021-02-28 07:47:48 +00:00
|
|
|
private $reflectionProvider;
|
2021-02-20 00:14:58 +00:00
|
|
|
|
2020-01-28 16:39:05 +00:00
|
|
|
/**
|
2021-02-28 07:47:48 +00:00
|
|
|
* To avoid circular dependency
|
2020-01-28 16:39:05 +00:00
|
|
|
* @required
|
|
|
|
*/
|
2020-05-05 15:22:54 +00:00
|
|
|
public function autowireArrayTypeMapper(
|
|
|
|
PHPStanStaticTypeMapper $phpStanStaticTypeMapper,
|
2021-02-20 00:14:58 +00:00
|
|
|
TypeNormalizer $typeNormalizer,
|
|
|
|
UnionTypeCommonTypeNarrower $unionTypeCommonTypeNarrower,
|
2021-02-28 07:47:48 +00:00
|
|
|
ReflectionProvider $reflectionProvider
|
2020-05-05 15:22:54 +00:00
|
|
|
): void {
|
2020-01-28 16:39:05 +00:00
|
|
|
$this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper;
|
2020-05-05 15:22:54 +00:00
|
|
|
$this->typeNormalizer = $typeNormalizer;
|
2021-02-20 00:14:58 +00:00
|
|
|
$this->unionTypeCommonTypeNarrower = $unionTypeCommonTypeNarrower;
|
2021-02-28 07:47:48 +00:00
|
|
|
$this->reflectionProvider = $reflectionProvider;
|
2020-01-28 16:39:05 +00:00
|
|
|
}
|
|
|
|
|
2021-03-06 21:16:18 +00:00
|
|
|
/**
|
|
|
|
* @return class-string<Type>
|
|
|
|
*/
|
2020-01-15 13:51:15 +00:00
|
|
|
public function getNodeClass(): string
|
|
|
|
{
|
|
|
|
return ArrayType::class;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ArrayType $type
|
|
|
|
*/
|
|
|
|
public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode
|
|
|
|
{
|
2020-08-29 08:41:19 +00:00
|
|
|
$itemType = $type->getItemType();
|
2020-08-29 14:20:27 +00:00
|
|
|
if ($itemType instanceof UnionType && ! $type instanceof ConstantArrayType) {
|
2021-02-20 00:14:58 +00:00
|
|
|
return $this->createArrayTypeNodeFromUnionType($itemType);
|
2020-01-15 13:51:15 +00:00
|
|
|
}
|
|
|
|
|
2021-02-20 00:14:58 +00:00
|
|
|
if ($itemType instanceof ArrayType && $this->isGenericArrayCandidate($itemType)) {
|
|
|
|
return $this->createGenericArrayType($type, true);
|
2020-08-29 14:20:27 +00:00
|
|
|
}
|
|
|
|
|
2021-02-20 00:14:58 +00:00
|
|
|
if ($this->isGenericArrayCandidate($type)) {
|
2020-08-29 14:20:27 +00:00
|
|
|
return $this->createGenericArrayType($type, true);
|
2020-08-11 11:58:03 +00:00
|
|
|
}
|
|
|
|
|
2021-02-20 00:14:58 +00:00
|
|
|
$narrowedTypeNode = $this->narrowConstantArrayTypeOfUnionType($type, $itemType);
|
|
|
|
if ($narrowedTypeNode instanceof TypeNode) {
|
|
|
|
return $narrowedTypeNode;
|
|
|
|
}
|
|
|
|
|
2020-08-29 08:41:19 +00:00
|
|
|
$itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($itemType);
|
2021-02-20 00:14:58 +00:00
|
|
|
|
2020-08-29 14:20:27 +00:00
|
|
|
return new AttributeAwareArrayTypeNode($itemTypeNode);
|
2020-01-15 13:51:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ArrayType $type
|
|
|
|
*/
|
|
|
|
public function mapToPhpParserNode(Type $type, ?string $kind = null): ?Node
|
|
|
|
{
|
2020-05-10 21:02:46 +00:00
|
|
|
return new Name('array');
|
2020-01-15 13:51:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ArrayType $type
|
|
|
|
*/
|
|
|
|
public function mapToDocString(Type $type, ?Type $parentType = null): string
|
|
|
|
{
|
|
|
|
$itemType = $type->getItemType();
|
|
|
|
|
2020-05-05 15:22:54 +00:00
|
|
|
$normalizedType = $this->typeNormalizer->normalizeArrayOfUnionToUnionArray($type);
|
|
|
|
if ($normalizedType instanceof UnionType) {
|
|
|
|
return $this->mapArrayUnionTypeToDocString($type, $normalizedType);
|
2020-01-15 13:51:15 +00:00
|
|
|
}
|
|
|
|
|
2020-05-05 15:22:54 +00:00
|
|
|
return $this->phpStanStaticTypeMapper->mapToDocString($itemType, $parentType) . '[]';
|
2020-01-15 13:51:15 +00:00
|
|
|
}
|
|
|
|
|
2021-02-20 00:14:58 +00:00
|
|
|
private function createArrayTypeNodeFromUnionType(UnionType $unionType): ArrayTypeNode
|
2020-08-29 08:41:19 +00:00
|
|
|
{
|
2020-01-15 13:51:15 +00:00
|
|
|
$unionedArrayType = [];
|
2020-08-29 08:41:19 +00:00
|
|
|
foreach ($unionType->getTypes() as $unionedType) {
|
|
|
|
$typeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($unionedType);
|
|
|
|
$unionedArrayType[(string) $typeNode] = $typeNode;
|
|
|
|
}
|
2020-01-15 13:51:15 +00:00
|
|
|
|
2020-08-29 08:41:19 +00:00
|
|
|
if (count($unionedArrayType) > 1) {
|
|
|
|
return new AttributeAwareArrayTypeNode(new AttributeAwareUnionTypeNode($unionedArrayType));
|
2020-01-15 13:51:15 +00:00
|
|
|
}
|
|
|
|
|
2020-08-29 08:41:19 +00:00
|
|
|
/** @var TypeNode $arrayType */
|
|
|
|
$arrayType = array_shift($unionedArrayType);
|
|
|
|
return new AttributeAwareArrayTypeNode($arrayType);
|
2020-01-15 13:51:15 +00:00
|
|
|
}
|
2020-02-01 16:04:38 +00:00
|
|
|
|
2020-08-11 11:58:03 +00:00
|
|
|
private function isGenericArrayCandidate(ArrayType $arrayType): bool
|
|
|
|
{
|
|
|
|
if ($arrayType->getKeyType() instanceof MixedType) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-03-09 23:00:45 +00:00
|
|
|
if ($this->isClassStringArrayType($arrayType)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-08-28 05:00:03 +00:00
|
|
|
// skip simple arrays, like "string[]", from converting to obvious "array<int, string>"
|
|
|
|
if ($this->isIntegerKeyAndNonNestedArray($arrayType)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-08-11 11:58:03 +00:00
|
|
|
if ($arrayType->getKeyType() instanceof NeverType) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// make sure the integer key type is not natural/implicit array int keys
|
|
|
|
$keysArrayType = $arrayType->getKeysArray();
|
|
|
|
if (! $keysArrayType instanceof ConstantArrayType) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($keysArrayType->getValueTypes() as $key => $keyType) {
|
|
|
|
if (! $keyType instanceof ConstantIntegerType) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($key !== $keyType->getValue()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-08-29 08:41:19 +00:00
|
|
|
private function createGenericArrayType(ArrayType $arrayType, bool $withKey = false): AttributeAwareGenericTypeNode
|
2020-08-11 11:58:03 +00:00
|
|
|
{
|
2020-08-29 08:41:19 +00:00
|
|
|
$itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($arrayType->getItemType());
|
2020-08-11 11:58:03 +00:00
|
|
|
$attributeAwareIdentifierTypeNode = new AttributeAwareIdentifierTypeNode('array');
|
2021-03-09 23:00:45 +00:00
|
|
|
|
|
|
|
// is class-string[] list only
|
|
|
|
if ($this->isClassStringArrayType($arrayType)) {
|
|
|
|
$withKey = false;
|
|
|
|
}
|
|
|
|
|
2020-08-29 08:41:19 +00:00
|
|
|
if ($withKey) {
|
|
|
|
$keyTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($arrayType->getKeyType());
|
|
|
|
$genericTypes = [$keyTypeNode, $itemTypeNode];
|
|
|
|
} else {
|
|
|
|
$genericTypes = [$itemTypeNode];
|
|
|
|
}
|
|
|
|
|
2020-08-11 11:58:03 +00:00
|
|
|
// @see https://github.com/phpstan/phpdoc-parser/blob/98a088b17966bdf6ee25c8a4b634df313d8aa531/tests/PHPStan/Parser/PhpDocParserTest.php#L2692-L2696
|
2020-08-29 14:20:27 +00:00
|
|
|
|
|
|
|
foreach ($genericTypes as $genericType) {
|
|
|
|
/** @var AttributeAwareNodeInterface $genericType */
|
|
|
|
$genericType->setAttribute(self::HAS_GENERIC_TYPE_PARENT, $withKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
$attributeAwareIdentifierTypeNode->setAttribute(self::HAS_GENERIC_TYPE_PARENT, $withKey);
|
|
|
|
|
2020-08-11 11:58:03 +00:00
|
|
|
return new AttributeAwareGenericTypeNode($attributeAwareIdentifierTypeNode, $genericTypes);
|
|
|
|
}
|
|
|
|
|
2020-02-01 16:04:38 +00:00
|
|
|
private function mapArrayUnionTypeToDocString(ArrayType $arrayType, UnionType $unionType): string
|
|
|
|
{
|
|
|
|
$unionedTypesAsString = [];
|
|
|
|
|
|
|
|
foreach ($unionType->getTypes() as $unionedArrayItemType) {
|
|
|
|
$unionedTypesAsString[] = $this->phpStanStaticTypeMapper->mapToDocString(
|
|
|
|
$unionedArrayItemType,
|
|
|
|
$arrayType
|
2020-05-05 15:22:54 +00:00
|
|
|
);
|
2020-02-01 16:04:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$unionedTypesAsString = array_values($unionedTypesAsString);
|
|
|
|
$unionedTypesAsString = array_unique($unionedTypesAsString);
|
|
|
|
|
|
|
|
return implode('|', $unionedTypesAsString);
|
|
|
|
}
|
2020-08-28 05:00:03 +00:00
|
|
|
|
|
|
|
private function isIntegerKeyAndNonNestedArray(ArrayType $arrayType): bool
|
|
|
|
{
|
|
|
|
if (! $arrayType->getKeyType() instanceof IntegerType) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ! $arrayType->getItemType() instanceof ArrayType;
|
|
|
|
}
|
2021-02-20 00:14:58 +00:00
|
|
|
|
|
|
|
private function narrowConstantArrayTypeOfUnionType(ArrayType $arrayType, Type $itemType): ?TypeNode
|
|
|
|
{
|
|
|
|
if ($arrayType instanceof ConstantArrayType && $itemType instanceof UnionType) {
|
|
|
|
$narrowedItemType = $this->unionTypeCommonTypeNarrower->narrowToSharedObjectType($itemType);
|
|
|
|
if ($narrowedItemType instanceof ObjectType) {
|
|
|
|
$itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($narrowedItemType);
|
|
|
|
return new AttributeAwareArrayTypeNode($itemTypeNode);
|
|
|
|
}
|
|
|
|
|
|
|
|
$narrowedItemType = $this->unionTypeCommonTypeNarrower->narrowToGenericClassStringType($itemType);
|
|
|
|
if ($narrowedItemType instanceof GenericClassStringType) {
|
|
|
|
return $this->createTypeNodeFromGenericClassStringType($narrowedItemType);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function createTypeNodeFromGenericClassStringType(
|
|
|
|
GenericClassStringType $genericClassStringType
|
2021-02-23 01:25:34 +00:00
|
|
|
): AttributeAwareNodeInterface {
|
2021-02-20 00:14:58 +00:00
|
|
|
$genericType = $genericClassStringType->getGenericType();
|
2021-02-28 07:47:48 +00:00
|
|
|
if ($genericType instanceof ObjectType && ! $this->reflectionProvider->hasClass($genericType->getClassName())) {
|
2021-02-20 00:14:58 +00:00
|
|
|
return new AttributeAwareIdentifierTypeNode($genericType->getClassName());
|
|
|
|
}
|
|
|
|
|
|
|
|
$itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($genericClassStringType);
|
|
|
|
|
|
|
|
return new AttributeAwareGenericTypeNode(new AttributeAwareIdentifierTypeNode('array'), [$itemTypeNode]);
|
|
|
|
}
|
2021-03-09 23:00:45 +00:00
|
|
|
|
|
|
|
private function isClassStringArrayType(ArrayType $arrayType): bool
|
|
|
|
{
|
|
|
|
if ($arrayType->getKeyType() instanceof MixedType) {
|
|
|
|
return $arrayType->getItemType() instanceof GenericClassStringType;
|
|
|
|
}
|
|
|
|
if ($arrayType->getKeyType() instanceof ConstantIntegerType) {
|
|
|
|
return $arrayType->getItemType() instanceof GenericClassStringType;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2020-01-15 13:51:15 +00:00
|
|
|
}
|