|
5 | 5 | namespace Rector\PHPStanStaticTypeMapper\TypeMapper; |
6 | 6 |
|
7 | 7 | use PhpParser\Node\Identifier; |
| 8 | +use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; |
8 | 9 | use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; |
9 | 10 | use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; |
10 | 11 | use PHPStan\PhpDocParser\Ast\Type\TypeNode; |
| 12 | +use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; |
11 | 13 | use PHPStan\Type\ArrayType; |
12 | 14 | use PHPStan\Type\ClassStringType; |
13 | 15 | use PHPStan\Type\Constant\ConstantArrayType; |
|
32 | 34 | */ |
33 | 35 | final class ArrayTypeMapper implements TypeMapperInterface |
34 | 36 | { |
35 | | - public const string HAS_GENERIC_TYPE_PARENT = 'has_generic_type_parent'; |
36 | | - |
37 | 37 | private PHPStanStaticTypeMapper $phpStanStaticTypeMapper; |
38 | 38 |
|
39 | 39 | public function __construct( |
@@ -61,17 +61,30 @@ public function getNodeClasses(): array |
61 | 61 | * @param ArrayType $type |
62 | 62 | */ |
63 | 63 | 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 |
64 | 80 | { |
65 | 81 | // this cannot be handled by PHPStan $type->toPhpDocNode() as requires space removal around "|" in union type |
66 | 82 | // then e.g. "int" instead of explicit number, and nice arrays |
67 | 83 | $itemType = $type->getIterableValueType(); |
68 | 84 |
|
69 | 85 | $isGenericArray = $this->isGenericArrayCandidate($type); |
70 | 86 |
|
71 | | - if ($itemType instanceof UnionType |
72 | | - && ! $type instanceof ConstantArrayType |
73 | | - && ! $isGenericArray |
74 | | - ) { |
| 87 | + if ($itemType instanceof UnionType && ! $isGenericArray) { |
75 | 88 | return $this->createArrayTypeNodeFromUnionType($itemType); |
76 | 89 | } |
77 | 90 |
|
@@ -99,11 +112,68 @@ public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode |
99 | 112 | } |
100 | 113 |
|
101 | 114 | /** |
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 |
103 | 116 | */ |
104 | | - public function mapToPhpParserNode(Type $type, string $typeKind): Identifier |
| 117 | + private function splitUnionItemType(TypeNode $typeNode): TypeNode |
105 | 118 | { |
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); |
107 | 177 | } |
108 | 178 |
|
109 | 179 | private function createArrayTypeNodeFromUnionType(UnionType $unionType): SpacingAwareArrayTypeNode |
@@ -164,38 +234,26 @@ private function isGenericArrayCandidate(ArrayType $arrayType): bool |
164 | 234 | private function createGenericArrayType(ArrayType $arrayType, bool $withKey = false): GenericTypeNode |
165 | 235 | { |
166 | 236 | $itemType = $arrayType->getIterableValueType(); |
167 | | - $itemTypeNode = $this->phpStanStaticTypeMapper->mapToPHPStanPhpDocTypeNode($itemType); |
168 | 237 | $identifierTypeNode = new IdentifierTypeNode('array'); |
169 | 238 |
|
170 | 239 | // is class-string[] list only |
171 | 240 | if ($this->isClassStringArrayType($arrayType)) { |
172 | 241 | $withKey = false; |
173 | 242 | } |
174 | 243 |
|
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]); |
187 | 247 | } |
188 | 248 |
|
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); |
190 | 251 |
|
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()); |
194 | 254 | } |
195 | 255 |
|
196 | | - $identifierTypeNode->setAttribute(self::HAS_GENERIC_TYPE_PARENT, $withKey); |
197 | | - |
198 | | - return new GenericTypeNode($identifierTypeNode, $genericTypes); |
| 256 | + return new GenericTypeNode($identifierTypeNode, [$keyTypeNode, $itemTypeNode]); |
199 | 257 | } |
200 | 258 |
|
201 | 259 | private function isPairClassTooDetailed(Type $itemType): bool |
|
0 commit comments