Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<union> 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<int, %s>', (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 . '[]';
}
}
116 changes: 87 additions & 29 deletions src/PHPStanStaticTypeMapper/TypeMapper/ArrayTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -61,17 +61,30 @@ 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
$itemType = $type->getIterableValueType();

$isGenericArray = $this->isGenericArrayCandidate($type);

if ($itemType instanceof UnionType
&& ! $type instanceof ConstantArrayType
&& ! $isGenericArray
) {
if ($itemType instanceof UnionType && ! $isGenericArray) {
return $this->createArrayTypeNodeFromUnionType($itemType);
}

Expand Down Expand Up @@ -99,11 +112,68 @@ public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode
}

/**
* @param ArrayType $type
* "(A|B)[]" becomes "A[]|B[]", or "array<int, A<X>|A<Y>>" 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<string, ...>" uses "array<A|B>" 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
Expand Down Expand Up @@ -164,38 +234,26 @@ 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
if ($this->isClassStringArrayType($arrayType)) {
$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
Expand Down
27 changes: 8 additions & 19 deletions src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}

/**
Expand Down
Loading