diff --git a/src/BetterPhpDocParser/ValueObject/Type/BracketsAwareUnionTypeNode.php b/src/BetterPhpDocParser/ValueObject/Type/BracketsAwareUnionTypeNode.php index 6d8d8e617ae..5eeff42351e 100644 --- a/src/BetterPhpDocParser/ValueObject/Type/BracketsAwareUnionTypeNode.php +++ b/src/BetterPhpDocParser/ValueObject/Type/BracketsAwareUnionTypeNode.php @@ -20,25 +20,13 @@ public function __construct( parent::__construct($types); } - /** - * Preserve common format - */ public function __toString(): string { - $types = []; - - // get the actual strings first before array_unique - // to avoid similar object but different printing to be treated as unique - foreach ($this->types as $type) { - $types[] = (string) $type; - } - - $types = array_unique($types); if (! $this->isWrappedInBrackets) { - return implode('|', $types); + return implode('|', $this->types); } - return '(' . implode('|', $types) . ')'; + return '(' . implode('|', $this->types) . ')'; } public function isWrappedInBrackets(): bool diff --git a/src/BetterPhpDocParser/ValueObject/Type/SpacingAwareArrayTypeNode.php b/src/BetterPhpDocParser/ValueObject/Type/SpacingAwareArrayTypeNode.php index 4e33184ab26..02ead02ceb4 100644 --- a/src/BetterPhpDocParser/ValueObject/Type/SpacingAwareArrayTypeNode.php +++ b/src/BetterPhpDocParser/ValueObject/Type/SpacingAwareArrayTypeNode.php @@ -6,10 +6,6 @@ use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; -use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; -use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; -use Rector\PHPStanStaticTypeMapper\TypeMapper\ArrayTypeMapper; use Stringable; final class SpacingAwareArrayTypeNode extends ArrayTypeNode implements Stringable @@ -20,89 +16,10 @@ public function __toString(): string return sprintf('(%s)[]', (string) $this->type); } - $typeAsString = (string) $this->type; - - if ($this->isGenericArrayCandidate($this->type)) { - return sprintf('array<%s>', $typeAsString); - } - - if ($this->type instanceof ArrayTypeNode) { - return $this->printArrayType($this->type); - } - - if ($this->type instanceof BracketsAwareUnionTypeNode) { - return $this->printUnionType($this->type); - } - - return $typeAsString . '[]'; - } - - private function isGenericArrayCandidate(TypeNode $typeNode): bool - { - $hasGenericTypeParent = (bool) $this->getAttribute(ArrayTypeMapper::HAS_GENERIC_TYPE_PARENT); - if (! $hasGenericTypeParent) { - return false; - } - - return $typeNode instanceof UnionTypeNode || $typeNode instanceof ArrayTypeNode; - } - - private function printArrayType(ArrayTypeNode $arrayTypeNode): string - { - $typeAsString = (string) $arrayTypeNode; - - $singleTypesAsString = explode('|', $typeAsString); - foreach ($singleTypesAsString as $key => $singleTypeAsString) { - $singleTypesAsString[$key] = $singleTypeAsString . '[]'; - } - - return implode('|', $singleTypesAsString); - } - - private function printUnionType(BracketsAwareUnionTypeNode $bracketsAwareUnionTypeNode): string - { - if ($bracketsAwareUnionTypeNode->isWrappedInBrackets()) { - return $bracketsAwareUnionTypeNode . '[]'; - } - - // If all types in the union are GenericTypeNode, use array syntax - $allGeneric = true; - $firstGenericTypeName = null; - - foreach ($bracketsAwareUnionTypeNode->types as $unionedType) { - if (! $unionedType instanceof GenericTypeNode) { - $allGeneric = false; - break; - } - - // ensure only check on base level - // avoid mix usage without [] added - if (count($unionedType->genericTypes) !== 1) { - $allGeneric = false; - break; - } - - // ensure all generic types has the same base type - $currentTypeName = $unionedType->type->name; - - if ($firstGenericTypeName === null) { - $firstGenericTypeName = $currentTypeName; - } elseif ($firstGenericTypeName !== $currentTypeName) { - // Different generic base types (e.g., class-string vs array) - $allGeneric = false; - break; - } - } - - if ($allGeneric) { - return sprintf('array', (string) $bracketsAwareUnionTypeNode); - } - - $unionedTypes = []; - foreach ($bracketsAwareUnionTypeNode->types as $unionedType) { - $unionedTypes[] = $unionedType . '[]'; + if ($this->type instanceof BracketsAwareUnionTypeNode && ! $this->type->isWrappedInBrackets()) { + return sprintf('(%s)[]', (string) $this->type); } - return implode('|', $unionedTypes); + return $this->type . '[]'; } } diff --git a/src/PHPStanStaticTypeMapper/TypeMapper/ArrayTypeMapper.php b/src/PHPStanStaticTypeMapper/TypeMapper/ArrayTypeMapper.php index 871f95d4ce3..f22f299933d 100644 --- a/src/PHPStanStaticTypeMapper/TypeMapper/ArrayTypeMapper.php +++ b/src/PHPStanStaticTypeMapper/TypeMapper/ArrayTypeMapper.php @@ -5,9 +5,11 @@ namespace Rector\PHPStanStaticTypeMapper\TypeMapper; use PhpParser\Node\Identifier; +use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\Type\ArrayType; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantArrayType; @@ -32,8 +34,6 @@ */ final class ArrayTypeMapper implements TypeMapperInterface { - public const string HAS_GENERIC_TYPE_PARENT = 'has_generic_type_parent'; - private PHPStanStaticTypeMapper $phpStanStaticTypeMapper; public function __construct( @@ -61,6 +61,22 @@ public function getNodeClasses(): array * @param ArrayType $type */ public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode + { + return $this->splitUnionItemType($this->createArrayTypeNode($type)); + } + + /** + * @param ArrayType $type + */ + public function mapToPhpParserNode(Type $type, string $typeKind): Identifier + { + return new Identifier('array'); + } + + /** + * Returns "(A|B)[]" as is, the union item type is split later + */ + private function createArrayTypeNode(ArrayType $type): TypeNode { // this cannot be handled by PHPStan $type->toPhpDocNode() as requires space removal around "|" in union type // then e.g. "int" instead of explicit number, and nice arrays @@ -68,10 +84,7 @@ public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode $isGenericArray = $this->isGenericArrayCandidate($type); - if ($itemType instanceof UnionType - && ! $type instanceof ConstantArrayType - && ! $isGenericArray - ) { + if ($itemType instanceof UnionType && ! $isGenericArray) { return $this->createArrayTypeNodeFromUnionType($itemType); } @@ -99,11 +112,68 @@ public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode } /** - * @param ArrayType $type + * "(A|B)[]" becomes "A[]|B[]", or "array|A>" when all items are generics of the same type */ - public function mapToPhpParserNode(Type $type, string $typeKind): Identifier + private function splitUnionItemType(TypeNode $typeNode): TypeNode { - return new Identifier('array'); + if (! $typeNode instanceof ArrayTypeNode || ! $typeNode->type instanceof UnionTypeNode) { + return $typeNode; + } + + $unionTypeNode = $typeNode->type; + if ($this->hasOnlySameGenericTypes($unionTypeNode)) { + return new GenericTypeNode(new IdentifierTypeNode('array'), [ + new IdentifierTypeNode('int'), + $unionTypeNode, + ]); + } + + $arrayTypeNodes = []; + foreach ($unionTypeNode->types as $unionedTypeNode) { + $arrayTypeNodes[] = new SpacingAwareArrayTypeNode($unionedTypeNode); + } + + return new BracketsAwareUnionTypeNode($arrayTypeNodes); + } + + private function hasOnlySameGenericTypes(UnionTypeNode $unionTypeNode): bool + { + $genericTypeName = null; + + foreach ($unionTypeNode->types as $unionedTypeNode) { + if (! $unionedTypeNode instanceof GenericTypeNode) { + return false; + } + + if (count($unionedTypeNode->genericTypes) !== 1) { + return false; + } + + $genericTypeName ??= $unionedTypeNode->type->name; + if ($genericTypeName !== $unionedTypeNode->type->name) { + return false; + } + } + + return true; + } + + /** + * Value type in "array" uses "array" instead of "A[]|B[]" + */ + private function createGenericArrayValueTypeNode(Type $itemType): TypeNode + { + if (! $itemType instanceof ArrayType) { + return $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($itemType); + } + + $arrayTypeNode = $this->createArrayTypeNode($itemType); + if ($arrayTypeNode instanceof ArrayTypeNode + && ($arrayTypeNode->type instanceof UnionTypeNode || $arrayTypeNode->type instanceof ArrayTypeNode)) { + return new GenericTypeNode(new IdentifierTypeNode('array'), [$arrayTypeNode->type]); + } + + return $this->splitUnionItemType($arrayTypeNode); } private function createArrayTypeNodeFromUnionType(UnionType $unionType): SpacingAwareArrayTypeNode @@ -164,7 +234,6 @@ private function isGenericArrayCandidate(ArrayType $arrayType): bool private function createGenericArrayType(ArrayType $arrayType, bool $withKey = false): GenericTypeNode { $itemType = $arrayType->getIterableValueType(); - $itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($itemType); $identifierTypeNode = new IdentifierTypeNode('array'); // is class-string[] list only @@ -172,30 +241,19 @@ private function createGenericArrayType(ArrayType $arrayType, bool $withKey = fa $withKey = false; } - if ($withKey) { - $keyTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($arrayType->getKeyType()); - - if ($itemTypeNode instanceof BracketsAwareUnionTypeNode && $this->isPairClassTooDetailed($itemType)) { - $genericTypes = [$keyTypeNode, $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode( - new ClassStringType() - )]; - } else { - $genericTypes = [$keyTypeNode, $itemTypeNode]; - } - } else { - $genericTypes = [$itemTypeNode]; + if (! $withKey) { + $itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($itemType); + return new GenericTypeNode($identifierTypeNode, [$itemTypeNode]); } - // @see https://github.com/phpstan/phpdoc-parser/blob/98a088b17966bdf6ee25c8a4b634df313d8aa531/tests/PHPStan/Parser/PhpDocParserTest.php#L2692-L2696 + $keyTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($arrayType->getKeyType()); + $itemTypeNode = $this->createGenericArrayValueTypeNode($itemType); - foreach ($genericTypes as $genericType) { - /** @var TypeNode $genericType */ - $genericType->setAttribute(self::HAS_GENERIC_TYPE_PARENT, $withKey); + if ($itemTypeNode instanceof BracketsAwareUnionTypeNode && $this->isPairClassTooDetailed($itemType)) { + $itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode(new ClassStringType()); } - $identifierTypeNode->setAttribute(self::HAS_GENERIC_TYPE_PARENT, $withKey); - - return new GenericTypeNode($identifierTypeNode, $genericTypes); + return new GenericTypeNode($identifierTypeNode, [$keyTypeNode, $itemTypeNode]); } private function isPairClassTooDetailed(Type $itemType): bool diff --git a/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php b/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php index 9563dcffeb2..edf4d2df523 100644 --- a/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php +++ b/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php @@ -12,13 +12,13 @@ use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\NullableType; use PhpParser\Node\UnionType as PhpParserUnionType; +use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\Type\ArrayType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareUnionTypeNode; -use Rector\BetterPhpDocParser\ValueObject\Type\SpacingAwareArrayTypeNode; use Rector\NodeAnalyzer\PropertyAnalyzer; use Rector\Php\PhpVersionProvider; use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface; @@ -60,35 +60,24 @@ public function getNodeClasses(): array public function mapToPHPStanPhpDocTypeNode(Type $type): BracketsAwareUnionTypeNode { $unionTypesNodes = []; - $existingTypes = []; foreach ($type->getTypes() as $unionedType) { if ($unionedType instanceof ArrayType && $unionedType->getItemType() instanceof NeverType) { $unionedType = new ArrayType($unionedType->getKeyType(), new MixedType()); } - $unionedType = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($unionedType); + $unionedTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($unionedType); - if ($unionedType instanceof SpacingAwareArrayTypeNode && $unionedType->type instanceof BracketsAwareUnionTypeNode) { - foreach ($unionedType->type->types as $key => $innerTypeNode) { - $printedInnerType = (string) $innerTypeNode; - if (in_array($printedInnerType, $existingTypes, true)) { - unset($unionedType->type->types[$key]); - continue; - } + // an array of union item type is mapped to "A[]|B[]", merge it into this union + $typeNodes = $unionedTypeNode instanceof UnionTypeNode ? $unionedTypeNode->types : [$unionedTypeNode]; - $existingTypes[] = $printedInnerType; - } - - if ($unionedType->type->types === []) { - continue; - } + // remove duplicates, e.g. "int[]|int[]" + foreach ($typeNodes as $typeNode) { + $unionTypesNodes[(string) $typeNode] = $typeNode; } - - $unionTypesNodes[] = $unionedType; } - return new BracketsAwareUnionTypeNode($unionTypesNodes); + return new BracketsAwareUnionTypeNode(array_values($unionTypesNodes)); } /**