Skip to content

Commit d5c7122

Browse files
committed
Array shapes support
1 parent ab518a5 commit d5c7122

File tree

6 files changed

+312
-0
lines changed

6 files changed

+312
-0
lines changed

doc/grammars/type.abnf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ CallableReturnType
5252
Array
5353
= 1*(TokenSquareBracketOpen TokenSquareBracketClose)
5454

55+
ArrayShape
56+
= TokenCurlyBracketOpen ArrayItem *(TokenComma ArrayItem) TokenCurlyBracketClose
57+
58+
ArrayItem
59+
= (ConstantString / ConstantInt / TokenIdentifier) TokenNullable TokenColon Type
60+
/ Type
5561

5662
; ---------------------------------------------------------------------------- ;
5763
; ConstantExpr ;
@@ -139,6 +145,12 @@ TokenSquareBracketOpen
139145
TokenSquareBracketClose
140146
= "]" *ByteHorizontalWs
141147

148+
TokenCurlyBracketOpen
149+
= "{" *ByteHorizontalWs
150+
151+
TokenCurlyBracketClose
152+
= "}" *ByteHorizontalWs
153+
142154
TokenComma
143155
= "," *ByteHorizontalWs
144156

src/Ast/Type/ArrayItemNode.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
6+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
7+
8+
class ArrayItemNode implements TypeNode
9+
{
10+
11+
/** @var ConstExprStringNode|ConstExprIntegerNode|IdentifierTypeNode|null */
12+
public $keyName;
13+
14+
/** @var bool */
15+
public $keyOptional;
16+
17+
/** @var TypeNode */
18+
public $valueType;
19+
20+
/**
21+
* @param ConstExprStringNode|ConstExprIntegerNode|IdentifierTypeNode|null $keyName
22+
*/
23+
public function __construct($keyName, bool $keyOptional, TypeNode $valueType)
24+
{
25+
$this->keyName = $keyName;
26+
$this->keyOptional = $keyOptional;
27+
$this->valueType = $valueType;
28+
}
29+
30+
31+
public function __toString(): string
32+
{
33+
if ($this->keyName !== null) {
34+
return sprintf(
35+
'%s%s: %s',
36+
(string) $this->keyName,
37+
$this->keyOptional ? '?' : '',
38+
(string) $this->valueType
39+
);
40+
}
41+
42+
return (string) $this->valueType;
43+
}
44+
45+
}

src/Ast/Type/ArrayShapeNode.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
class ArrayShapeNode implements TypeNode
6+
{
7+
8+
/** @var ArrayItemNode[] */
9+
public $items;
10+
11+
public function __construct(array $items)
12+
{
13+
$this->items = $items;
14+
}
15+
16+
17+
public function __toString(): string
18+
{
19+
return 'array{' . implode(', ', array_map(static function (ArrayItemNode $node): string {
20+
return (string) $node;
21+
}, $this->items)) . '}';
22+
}
23+
24+
}

src/Lexer/Lexer.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class Lexer
1818
public const TOKEN_CLOSE_ANGLE_BRACKET = 7;
1919
public const TOKEN_OPEN_SQUARE_BRACKET = 8;
2020
public const TOKEN_CLOSE_SQUARE_BRACKET = 9;
21+
public const TOKEN_OPEN_CURLY_BRACKET = 30;
22+
public const TOKEN_CLOSE_CURLY_BRACKET = 31;
2123
public const TOKEN_COMMA = 10;
2224
public const TOKEN_COLON = 29;
2325
public const TOKEN_VARIADIC = 11;
@@ -50,6 +52,8 @@ class Lexer
5052
self::TOKEN_CLOSE_ANGLE_BRACKET => '\'>\'',
5153
self::TOKEN_OPEN_SQUARE_BRACKET => '\'[\'',
5254
self::TOKEN_CLOSE_SQUARE_BRACKET => '\']\'',
55+
self::TOKEN_OPEN_CURLY_BRACKET => '\'{\'',
56+
self::TOKEN_CLOSE_CURLY_BRACKET => '\'}\'',
5357
self::TOKEN_COMMA => '\',\'',
5458
self::TOKEN_COLON => '\':\'',
5559
self::TOKEN_VARIADIC => '\'...\'',
@@ -123,6 +127,8 @@ private function initialize(): void
123127
self::TOKEN_CLOSE_ANGLE_BRACKET => '>',
124128
self::TOKEN_OPEN_SQUARE_BRACKET => '\\[',
125129
self::TOKEN_CLOSE_SQUARE_BRACKET => '\\]',
130+
self::TOKEN_OPEN_CURLY_BRACKET => '\\{',
131+
self::TOKEN_CLOSE_CURLY_BRACKET => '\\}',
126132

127133
self::TOKEN_COMMA => ',',
128134
self::TOKEN_VARIADIC => '\\.\\.\\.',

src/Parser/TypeParser.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace PHPStan\PhpDocParser\Parser;
44

55
use PHPStan\PhpDocParser\Ast;
6+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
7+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
68
use PHPStan\PhpDocParser\Lexer\Lexer;
79

810
class TypeParser
@@ -53,6 +55,9 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
5355

5456
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
5557
$type = $this->tryParseArray($tokens, $type);
58+
59+
} elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
60+
$type = $this->parseArrayShape($tokens, $type);
5661
}
5762
}
5863

@@ -93,6 +98,9 @@ private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode
9398

9499
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
95100
$type = $this->parseGeneric($tokens, $type);
101+
102+
} elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
103+
$type = $this->parseArrayShape($tokens, $type);
96104
}
97105

98106
return new Ast\Type\NullableTypeNode($type);
@@ -167,6 +175,9 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo
167175

168176
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
169177
$type = $this->parseGeneric($tokens, $type);
178+
179+
} elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
180+
$type = $this->parseArrayShape($tokens, $type);
170181
}
171182
}
172183

@@ -208,4 +219,64 @@ private function tryParseArray(TokenIterator $tokens, Ast\Type\TypeNode $type):
208219
return $type;
209220
}
210221

222+
223+
private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
224+
{
225+
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
226+
$items = [$this->parseArrayShapeItem($tokens)];
227+
228+
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
229+
$items[] = $this->parseArrayShapeItem($tokens);
230+
}
231+
232+
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
233+
234+
return new Ast\Type\ArrayShapeNode($items);
235+
}
236+
237+
238+
private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayItemNode
239+
{
240+
try {
241+
$tokens->pushSavePoint();
242+
$key = $this->parseArrayShapeKey($tokens);
243+
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
244+
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
245+
$value = $this->parse($tokens);
246+
$tokens->dropSavePoint();
247+
248+
return new Ast\Type\ArrayItemNode($key, $optional, $value);
249+
} catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
250+
$tokens->rollback();
251+
$value = $this->parse($tokens);
252+
253+
return new Ast\Type\ArrayItemNode(null, false, $value);
254+
}
255+
}
256+
257+
/**
258+
* @return ConstExprStringNode|ConstExprIntegerNode|Ast\Type\IdentifierTypeNode
259+
*/
260+
private function parseArrayShapeKey(TokenIterator $tokens)
261+
{
262+
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
263+
$key = new ConstExprStringNode($tokens->currentTokenValue());
264+
$tokens->next();
265+
266+
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
267+
$key = new ConstExprStringNode($tokens->currentTokenValue());
268+
$tokens->next();
269+
270+
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
271+
$key = new ConstExprIntegerNode($tokens->currentTokenValue());
272+
$tokens->next();
273+
274+
} else {
275+
$key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
276+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
277+
}
278+
279+
return $key;
280+
}
281+
211282
}

0 commit comments

Comments
 (0)