|
13 | 13 | use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode; |
14 | 14 | use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; |
15 | 15 | use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; |
| 16 | +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; |
16 | 17 | use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; |
17 | 18 | use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; |
18 | 19 | use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; |
@@ -107,6 +108,7 @@ public function parse(TypeNode $node): Type |
107 | 108 | $node instanceof UnionTypeNode => orT(...array_map($this->parse(...), $node->types)), |
108 | 109 | $node instanceof IntersectionTypeNode => andT(...array_map($this->parse(...), $node->types)), |
109 | 110 | $node instanceof ArrayTypeNode => arrayT(value: $this->parse($node->type)), |
| 111 | + $node instanceof ArrayShapeNode => $this->arrayShape($node), |
110 | 112 | $node instanceof OffsetAccessTypeNode => offsetT($this->parse($node->type), $this->parse($node->offset)), |
111 | 113 | $node instanceof CallableTypeNode => $this->callable($node), |
112 | 114 | default => throw new \LogicException(\sprintf('`%s` is not supported', $node::class)), |
@@ -303,6 +305,56 @@ private function list(array $templateArguments, bool $isNonEmpty = false): ListT |
303 | 305 | }; |
304 | 306 | } |
305 | 307 |
|
| 308 | + private function arrayShape(ArrayShapeNode $node): Type |
| 309 | + { |
| 310 | + $elements = []; |
| 311 | + |
| 312 | + foreach ($node->items as $item) { |
| 313 | + $elements[] = new Type\ArrayElement( |
| 314 | + key: match (true) { |
| 315 | + $item->keyName === null => \count($elements), |
| 316 | + $item->keyName instanceof ConstExprIntegerNode => (int) $item->keyName->value, |
| 317 | + $item->keyName instanceof ConstExprStringNode => $item->keyName->value, |
| 318 | + $item->keyName instanceof IdentifierTypeNode => $item->keyName->name, |
| 319 | + default => throw new \LogicException(), |
| 320 | + }, |
| 321 | + type: $this->parse($item->valueType), |
| 322 | + isOptional: $item->optional, |
| 323 | + ); |
| 324 | + } |
| 325 | + |
| 326 | + $valueType = match (true) { |
| 327 | + $node->sealed => neverT, |
| 328 | + $node->unsealedType === null => mixedT, |
| 329 | + default => $this->parse($node->unsealedType->valueType), |
| 330 | + }; |
| 331 | + |
| 332 | + if (($node->kind === ArrayShapeNode::KIND_NON_EMPTY_LIST || $node->kind === ArrayShapeNode::KIND_LIST) |
| 333 | + && $node->unsealedType?->keyType === null |
| 334 | + && array_column($elements, 'key') === array_keys($elements) |
| 335 | + && array_all($elements, static fn(Type\ArrayElement $e): bool => !$e->isOptional) |
| 336 | + ) { |
| 337 | + return new ListT( |
| 338 | + valueType: $valueType, |
| 339 | + elementTypes: array_column($elements, 'type'), |
| 340 | + isNonEmpty: $node->kind === ArrayShapeNode::KIND_NON_EMPTY_LIST, |
| 341 | + ); |
| 342 | + } |
| 343 | + |
| 344 | + $keyType = match (true) { |
| 345 | + $node->sealed => neverT, |
| 346 | + $node->unsealedType?->keyType === null => arrayKeyT, |
| 347 | + default => $this->parse($node->unsealedType->keyType), |
| 348 | + }; |
| 349 | + |
| 350 | + return new ArrayT( |
| 351 | + keyType: $keyType, |
| 352 | + valueType: $valueType, |
| 353 | + elements: $elements, |
| 354 | + isNonEmpty: $node->kind === ArrayShapeNode::KIND_NON_EMPTY_LIST, |
| 355 | + ); |
| 356 | + } |
| 357 | + |
306 | 358 | /** |
307 | 359 | * @param list<Type> $templateArguments |
308 | 360 | */ |
|
0 commit comments