Skip to content

Commit b48dbfd

Browse files
authored
Move union dedup and array printing to type mappers (#8858)
Amp-Thread-ID: https://ampcode.com/threads/T-01a17930-0285-73c5-a957-4325c77aa1b2
1 parent 0add245 commit b48dbfd

4 files changed

Lines changed: 100 additions & 148 deletions

File tree

‎src/BetterPhpDocParser/ValueObject/Type/BracketsAwareUnionTypeNode.php‎

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,13 @@ public function __construct(
2020
parent::__construct($types);
2121
}
2222

23-
/**
24-
* Preserve common format
25-
*/
2623
public function __toString(): string
2724
{
28-
$types = [];
29-
30-
// get the actual strings first before array_unique
31-
// to avoid similar object but different printing to be treated as unique
32-
foreach ($this->types as $type) {
33-
$types[] = (string) $type;
34-
}
35-
36-
$types = array_unique($types);
3725
if (! $this->isWrappedInBrackets) {
38-
return implode('|', $types);
26+
return implode('|', $this->types);
3927
}
4028

41-
return '(' . implode('|', $types) . ')';
29+
return '(' . implode('|', $this->types) . ')';
4230
}
4331

4432
public function isWrappedInBrackets(): bool

‎src/BetterPhpDocParser/ValueObject/Type/SpacingAwareArrayTypeNode.php‎

Lines changed: 3 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66

77
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
88
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
9-
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
10-
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
11-
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
12-
use Rector\PHPStanStaticTypeMapper\TypeMapper\ArrayTypeMapper;
139
use Stringable;
1410

1511
final class SpacingAwareArrayTypeNode extends ArrayTypeNode implements Stringable
@@ -20,89 +16,10 @@ public function __toString(): string
2016
return sprintf('(%s)[]', (string) $this->type);
2117
}
2218

23-
$typeAsString = (string) $this->type;
24-
25-
if ($this->isGenericArrayCandidate($this->type)) {
26-
return sprintf('array<%s>', $typeAsString);
27-
}
28-
29-
if ($this->type instanceof ArrayTypeNode) {
30-
return $this->printArrayType($this->type);
31-
}
32-
33-
if ($this->type instanceof BracketsAwareUnionTypeNode) {
34-
return $this->printUnionType($this->type);
35-
}
36-
37-
return $typeAsString . '[]';
38-
}
39-
40-
private function isGenericArrayCandidate(TypeNode $typeNode): bool
41-
{
42-
$hasGenericTypeParent = (bool) $this->getAttribute(ArrayTypeMapper::HAS_GENERIC_TYPE_PARENT);
43-
if (! $hasGenericTypeParent) {
44-
return false;
45-
}
46-
47-
return $typeNode instanceof UnionTypeNode || $typeNode instanceof ArrayTypeNode;
48-
}
49-
50-
private function printArrayType(ArrayTypeNode $arrayTypeNode): string
51-
{
52-
$typeAsString = (string) $arrayTypeNode;
53-
54-
$singleTypesAsString = explode('|', $typeAsString);
55-
foreach ($singleTypesAsString as $key => $singleTypeAsString) {
56-
$singleTypesAsString[$key] = $singleTypeAsString . '[]';
57-
}
58-
59-
return implode('|', $singleTypesAsString);
60-
}
61-
62-
private function printUnionType(BracketsAwareUnionTypeNode $bracketsAwareUnionTypeNode): string
63-
{
64-
if ($bracketsAwareUnionTypeNode->isWrappedInBrackets()) {
65-
return $bracketsAwareUnionTypeNode . '[]';
66-
}
67-
68-
// If all types in the union are GenericTypeNode, use array<union> syntax
69-
$allGeneric = true;
70-
$firstGenericTypeName = null;
71-
72-
foreach ($bracketsAwareUnionTypeNode->types as $unionedType) {
73-
if (! $unionedType instanceof GenericTypeNode) {
74-
$allGeneric = false;
75-
break;
76-
}
77-
78-
// ensure only check on base level
79-
// avoid mix usage without [] added
80-
if (count($unionedType->genericTypes) !== 1) {
81-
$allGeneric = false;
82-
break;
83-
}
84-
85-
// ensure all generic types has the same base type
86-
$currentTypeName = $unionedType->type->name;
87-
88-
if ($firstGenericTypeName === null) {
89-
$firstGenericTypeName = $currentTypeName;
90-
} elseif ($firstGenericTypeName !== $currentTypeName) {
91-
// Different generic base types (e.g., class-string vs array)
92-
$allGeneric = false;
93-
break;
94-
}
95-
}
96-
97-
if ($allGeneric) {
98-
return sprintf('array<int, %s>', (string) $bracketsAwareUnionTypeNode);
99-
}
100-
101-
$unionedTypes = [];
102-
foreach ($bracketsAwareUnionTypeNode->types as $unionedType) {
103-
$unionedTypes[] = $unionedType . '[]';
19+
if ($this->type instanceof BracketsAwareUnionTypeNode && ! $this->type->isWrappedInBrackets()) {
20+
return sprintf('(%s)[]', (string) $this->type);
10421
}
10522

106-
return implode('|', $unionedTypes);
23+
return $this->type . '[]';
10724
}
10825
}

‎src/PHPStanStaticTypeMapper/TypeMapper/ArrayTypeMapper.php‎

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
namespace Rector\PHPStanStaticTypeMapper\TypeMapper;
66

77
use PhpParser\Node\Identifier;
8+
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
89
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
910
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1011
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
12+
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
1113
use PHPStan\Type\ArrayType;
1214
use PHPStan\Type\ClassStringType;
1315
use PHPStan\Type\Constant\ConstantArrayType;
@@ -32,8 +34,6 @@
3234
*/
3335
final class ArrayTypeMapper implements TypeMapperInterface
3436
{
35-
public const string HAS_GENERIC_TYPE_PARENT = 'has_generic_type_parent';
36-
3737
private PHPStanStaticTypeMapper $phpStanStaticTypeMapper;
3838

3939
public function __construct(
@@ -61,17 +61,30 @@ public function getNodeClasses(): array
6161
* @param ArrayType $type
6262
*/
6363
public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode
64+
{
65+
return $this->splitUnionItemType($this->createArrayTypeNode($type));
66+
}
67+
68+
/**
69+
* @param ArrayType $type
70+
*/
71+
public function mapToPhpParserNode(Type $type, string $typeKind): Identifier
72+
{
73+
return new Identifier('array');
74+
}
75+
76+
/**
77+
* Returns "(A|B)[]" as is, the union item type is split later
78+
*/
79+
private function createArrayTypeNode(ArrayType $type): TypeNode
6480
{
6581
// this cannot be handled by PHPStan $type->toPhpDocNode() as requires space removal around "|" in union type
6682
// then e.g. "int" instead of explicit number, and nice arrays
6783
$itemType = $type->getIterableValueType();
6884

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

71-
if ($itemType instanceof UnionType
72-
&& ! $type instanceof ConstantArrayType
73-
&& ! $isGenericArray
74-
) {
87+
if ($itemType instanceof UnionType && ! $isGenericArray) {
7588
return $this->createArrayTypeNodeFromUnionType($itemType);
7689
}
7790

@@ -99,11 +112,68 @@ public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode
99112
}
100113

101114
/**
102-
* @param ArrayType $type
115+
* "(A|B)[]" becomes "A[]|B[]", or "array<int, A<X>|A<Y>>" when all items are generics of the same type
103116
*/
104-
public function mapToPhpParserNode(Type $type, string $typeKind): Identifier
117+
private function splitUnionItemType(TypeNode $typeNode): TypeNode
105118
{
106-
return new Identifier('array');
119+
if (! $typeNode instanceof ArrayTypeNode || ! $typeNode->type instanceof UnionTypeNode) {
120+
return $typeNode;
121+
}
122+
123+
$unionTypeNode = $typeNode->type;
124+
if ($this->hasOnlySameGenericTypes($unionTypeNode)) {
125+
return new GenericTypeNode(new IdentifierTypeNode('array'), [
126+
new IdentifierTypeNode('int'),
127+
$unionTypeNode,
128+
]);
129+
}
130+
131+
$arrayTypeNodes = [];
132+
foreach ($unionTypeNode->types as $unionedTypeNode) {
133+
$arrayTypeNodes[] = new SpacingAwareArrayTypeNode($unionedTypeNode);
134+
}
135+
136+
return new BracketsAwareUnionTypeNode($arrayTypeNodes);
137+
}
138+
139+
private function hasOnlySameGenericTypes(UnionTypeNode $unionTypeNode): bool
140+
{
141+
$genericTypeName = null;
142+
143+
foreach ($unionTypeNode->types as $unionedTypeNode) {
144+
if (! $unionedTypeNode instanceof GenericTypeNode) {
145+
return false;
146+
}
147+
148+
if (count($unionedTypeNode->genericTypes) !== 1) {
149+
return false;
150+
}
151+
152+
$genericTypeName ??= $unionedTypeNode->type->name;
153+
if ($genericTypeName !== $unionedTypeNode->type->name) {
154+
return false;
155+
}
156+
}
157+
158+
return true;
159+
}
160+
161+
/**
162+
* Value type in "array<string, ...>" uses "array<A|B>" instead of "A[]|B[]"
163+
*/
164+
private function createGenericArrayValueTypeNode(Type $itemType): TypeNode
165+
{
166+
if (! $itemType instanceof ArrayType) {
167+
return $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($itemType);
168+
}
169+
170+
$arrayTypeNode = $this->createArrayTypeNode($itemType);
171+
if ($arrayTypeNode instanceof ArrayTypeNode
172+
&& ($arrayTypeNode->type instanceof UnionTypeNode || $arrayTypeNode->type instanceof ArrayTypeNode)) {
173+
return new GenericTypeNode(new IdentifierTypeNode('array'), [$arrayTypeNode->type]);
174+
}
175+
176+
return $this->splitUnionItemType($arrayTypeNode);
107177
}
108178

109179
private function createArrayTypeNodeFromUnionType(UnionType $unionType): SpacingAwareArrayTypeNode
@@ -164,38 +234,26 @@ private function isGenericArrayCandidate(ArrayType $arrayType): bool
164234
private function createGenericArrayType(ArrayType $arrayType, bool $withKey = false): GenericTypeNode
165235
{
166236
$itemType = $arrayType->getIterableValueType();
167-
$itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($itemType);
168237
$identifierTypeNode = new IdentifierTypeNode('array');
169238

170239
// is class-string[] list only
171240
if ($this->isClassStringArrayType($arrayType)) {
172241
$withKey = false;
173242
}
174243

175-
if ($withKey) {
176-
$keyTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($arrayType->getKeyType());
177-
178-
if ($itemTypeNode instanceof BracketsAwareUnionTypeNode && $this->isPairClassTooDetailed($itemType)) {
179-
$genericTypes = [$keyTypeNode, $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode(
180-
new ClassStringType()
181-
)];
182-
} else {
183-
$genericTypes = [$keyTypeNode, $itemTypeNode];
184-
}
185-
} else {
186-
$genericTypes = [$itemTypeNode];
244+
if (! $withKey) {
245+
$itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($itemType);
246+
return new GenericTypeNode($identifierTypeNode, [$itemTypeNode]);
187247
}
188248

189-
// @see https://github.com/phpstan/phpdoc-parser/blob/98a088b17966bdf6ee25c8a4b634df313d8aa531/tests/PHPStan/Parser/PhpDocParserTest.php#L2692-L2696
249+
$keyTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($arrayType->getKeyType());
250+
$itemTypeNode = $this->createGenericArrayValueTypeNode($itemType);
190251

191-
foreach ($genericTypes as $genericType) {
192-
/** @var TypeNode $genericType */
193-
$genericType->setAttribute(self::HAS_GENERIC_TYPE_PARENT, $withKey);
252+
if ($itemTypeNode instanceof BracketsAwareUnionTypeNode && $this->isPairClassTooDetailed($itemType)) {
253+
$itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode(new ClassStringType());
194254
}
195255

196-
$identifierTypeNode->setAttribute(self::HAS_GENERIC_TYPE_PARENT, $withKey);
197-
198-
return new GenericTypeNode($identifierTypeNode, $genericTypes);
256+
return new GenericTypeNode($identifierTypeNode, [$keyTypeNode, $itemTypeNode]);
199257
}
200258

201259
private function isPairClassTooDetailed(Type $itemType): bool

‎src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php‎

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
use PhpParser\Node\Name\FullyQualified;
1313
use PhpParser\Node\NullableType;
1414
use PhpParser\Node\UnionType as PhpParserUnionType;
15+
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
1516
use PHPStan\Type\ArrayType;
1617
use PHPStan\Type\MixedType;
1718
use PHPStan\Type\NeverType;
1819
use PHPStan\Type\Type;
1920
use PHPStan\Type\UnionType;
2021
use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareUnionTypeNode;
21-
use Rector\BetterPhpDocParser\ValueObject\Type\SpacingAwareArrayTypeNode;
2222
use Rector\NodeAnalyzer\PropertyAnalyzer;
2323
use Rector\Php\PhpVersionProvider;
2424
use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
@@ -60,35 +60,24 @@ public function getNodeClasses(): array
6060
public function mapToPHPStanPhpDocTypeNode(Type $type): BracketsAwareUnionTypeNode
6161
{
6262
$unionTypesNodes = [];
63-
$existingTypes = [];
6463

6564
foreach ($type->getTypes() as $unionedType) {
6665
if ($unionedType instanceof ArrayType && $unionedType->getItemType() instanceof NeverType) {
6766
$unionedType = new ArrayType($unionedType->getKeyType(), new MixedType());
6867
}
6968

70-
$unionedType = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($unionedType);
69+
$unionedTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($unionedType);
7170

72-
if ($unionedType instanceof SpacingAwareArrayTypeNode && $unionedType->type instanceof BracketsAwareUnionTypeNode) {
73-
foreach ($unionedType->type->types as $key => $innerTypeNode) {
74-
$printedInnerType = (string) $innerTypeNode;
75-
if (in_array($printedInnerType, $existingTypes, true)) {
76-
unset($unionedType->type->types[$key]);
77-
continue;
78-
}
71+
// an array of union item type is mapped to "A[]|B[]", merge it into this union
72+
$typeNodes = $unionedTypeNode instanceof UnionTypeNode ? $unionedTypeNode->types : [$unionedTypeNode];
7973

80-
$existingTypes[] = $printedInnerType;
81-
}
82-
83-
if ($unionedType->type->types === []) {
84-
continue;
85-
}
74+
// remove duplicates, e.g. "int[]|int[]"
75+
foreach ($typeNodes as $typeNode) {
76+
$unionTypesNodes[(string) $typeNode] = $typeNode;
8677
}
87-
88-
$unionTypesNodes[] = $unionedType;
8978
}
9079

91-
return new BracketsAwareUnionTypeNode($unionTypesNodes);
80+
return new BracketsAwareUnionTypeNode(array_values($unionTypesNodes));
9281
}
9382

9483
/**

0 commit comments

Comments
 (0)