Skip to content

Commit 5ceb0e3

Browse files
authored
Support typing extra items in unsealed array shapes
1 parent cc2b26c commit 5ceb0e3

File tree

6 files changed

+645
-4
lines changed

6 files changed

+645
-4
lines changed

src/Ast/Type/ArrayShapeNode.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,24 @@ class ArrayShapeNode implements TypeNode
2222
/** @var self::KIND_* */
2323
public $kind;
2424

25+
/** @var ArrayShapeUnsealedTypeNode|null */
26+
public $unsealedType;
27+
2528
/**
2629
* @param ArrayShapeItemNode[] $items
2730
* @param self::KIND_* $kind
2831
*/
29-
public function __construct(array $items, bool $sealed = true, string $kind = self::KIND_ARRAY)
32+
public function __construct(
33+
array $items,
34+
bool $sealed = true,
35+
string $kind = self::KIND_ARRAY,
36+
?ArrayShapeUnsealedTypeNode $unsealedType = null
37+
)
3038
{
3139
$this->items = $items;
3240
$this->sealed = $sealed;
3341
$this->kind = $kind;
42+
$this->unsealedType = $unsealedType;
3443
}
3544

3645

@@ -39,7 +48,7 @@ public function __toString(): string
3948
$items = $this->items;
4049

4150
if (! $this->sealed) {
42-
$items[] = '...';
51+
$items[] = '...' . $this->unsealedType;
4352
}
4453

4554
return $this->kind . '{' . implode(', ', $items) . '}';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\Node;
6+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
7+
use function sprintf;
8+
9+
class ArrayShapeUnsealedTypeNode implements Node
10+
{
11+
12+
use NodeAttributes;
13+
14+
/** @var TypeNode */
15+
public $valueType;
16+
17+
/** @var TypeNode|null */
18+
public $keyType;
19+
20+
public function __construct(TypeNode $valueType, ?TypeNode $keyType)
21+
{
22+
$this->valueType = $valueType;
23+
$this->keyType = $keyType;
24+
}
25+
26+
public function __toString(): string
27+
{
28+
if ($this->keyType !== null) {
29+
return sprintf('<%s, %s>', $this->keyType, $this->valueType);
30+
}
31+
return sprintf('<%s>', $this->valueType);
32+
}
33+
34+
}

src/Parser/TypeParser.php

+70-1
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type,
848848

849849
$items = [];
850850
$sealed = true;
851+
$unsealedType = null;
851852

852853
do {
853854
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
@@ -858,6 +859,17 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type,
858859

859860
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) {
860861
$sealed = false;
862+
863+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
864+
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
865+
if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) {
866+
$unsealedType = $this->parseArrayShapeUnsealedType($tokens);
867+
} else {
868+
$unsealedType = $this->parseListShapeUnsealedType($tokens);
869+
}
870+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
871+
}
872+
861873
$tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA);
862874
break;
863875
}
@@ -870,7 +882,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type,
870882
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
871883
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
872884

873-
return new Ast\Type\ArrayShapeNode($items, $sealed, $kind);
885+
return new Ast\Type\ArrayShapeNode($items, $sealed, $kind, $unsealedType);
874886
}
875887

876888

@@ -949,6 +961,63 @@ private function parseArrayShapeKey(TokenIterator $tokens)
949961
);
950962
}
951963

964+
/**
965+
* @phpstan-impure
966+
*/
967+
private function parseArrayShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode
968+
{
969+
$startLine = $tokens->currentTokenLine();
970+
$startIndex = $tokens->currentTokenIndex();
971+
972+
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
973+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
974+
975+
$valueType = $this->parse($tokens);
976+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
977+
978+
$keyType = null;
979+
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
980+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
981+
982+
$keyType = $valueType;
983+
$valueType = $this->parse($tokens);
984+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
985+
}
986+
987+
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
988+
989+
return $this->enrichWithAttributes(
990+
$tokens,
991+
new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, $keyType),
992+
$startLine,
993+
$startIndex
994+
);
995+
}
996+
997+
/**
998+
* @phpstan-impure
999+
*/
1000+
private function parseListShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode
1001+
{
1002+
$startLine = $tokens->currentTokenLine();
1003+
$startIndex = $tokens->currentTokenIndex();
1004+
1005+
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
1006+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
1007+
1008+
$valueType = $this->parse($tokens);
1009+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
1010+
1011+
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
1012+
1013+
return $this->enrichWithAttributes(
1014+
$tokens,
1015+
new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, null),
1016+
$startLine,
1017+
$startIndex
1018+
);
1019+
}
1020+
9521021
/**
9531022
* @phpstan-impure
9541023
*/

src/Printer/Printer.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
4444
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
4545
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
46+
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode;
4647
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
4748
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
4849
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
@@ -229,6 +230,12 @@ function (PhpDocChildNode $child): string {
229230
$isOptional = $node->isOptional ? '=' : '';
230231
return trim("{$type}{$isReference}{$isVariadic}{$node->parameterName}") . $isOptional;
231232
}
233+
if ($node instanceof ArrayShapeUnsealedTypeNode) {
234+
if ($node->keyType !== null) {
235+
return sprintf('<%s, %s>', $this->printType($node->keyType), $this->printType($node->valueType));
236+
}
237+
return sprintf('<%s>', $this->printType($node->valueType));
238+
}
232239
if ($node instanceof DoctrineAnnotation) {
233240
return (string) $node;
234241
}
@@ -366,7 +373,7 @@ private function printType(TypeNode $node): string
366373
}, $node->items);
367374

368375
if (! $node->sealed) {
369-
$items[] = '...';
376+
$items[] = '...' . ($node->unsealedType === null ? '' : $this->print($node->unsealedType));
370377
}
371378

372379
return $node->kind . '{' . implode(', ', $items) . '}';

0 commit comments

Comments
 (0)