Skip to content

Commit 1da2721

Browse files
committed
Array shapes support
1 parent ab518a5 commit 1da2721

File tree

4 files changed

+243
-0
lines changed

4 files changed

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

tests/PHPStan/Parser/TypeParserTest.php

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
namespace PHPStan\PhpDocParser\Parser;
44

5+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
6+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
7+
use PHPStan\PhpDocParser\Ast\Type\ArrayItemNode;
8+
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
59
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
610
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
711
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
@@ -264,6 +268,142 @@ public function provideParseData(): array
264268
]
265269
),
266270
],
271+
[
272+
'array{\'a\': int}',
273+
new ArrayShapeNode([
274+
new ArrayItemNode(
275+
new ConstExprStringNode('\'a\''),
276+
false,
277+
new IdentifierTypeNode('int')
278+
),
279+
]),
280+
],
281+
[
282+
'array{\'a\': ?int}',
283+
new ArrayShapeNode([
284+
new ArrayItemNode(
285+
new ConstExprStringNode('\'a\''),
286+
false,
287+
new NullableTypeNode(
288+
new IdentifierTypeNode('int')
289+
)
290+
),
291+
]),
292+
],
293+
[
294+
'array{\'a\'?: ?int}',
295+
new ArrayShapeNode([
296+
new ArrayItemNode(
297+
new ConstExprStringNode('\'a\''),
298+
true,
299+
new NullableTypeNode(
300+
new IdentifierTypeNode('int')
301+
)
302+
),
303+
]),
304+
],
305+
[
306+
'array{\'a\': int, \'b\': string}',
307+
new ArrayShapeNode([
308+
new ArrayItemNode(
309+
new ConstExprStringNode('\'a\''),
310+
false,
311+
new IdentifierTypeNode('int')
312+
),
313+
new ArrayItemNode(
314+
new ConstExprStringNode('\'b\''),
315+
false,
316+
new IdentifierTypeNode('string')
317+
),
318+
]),
319+
],
320+
[
321+
'array{int, string, "a": string}',
322+
new ArrayShapeNode([
323+
new ArrayItemNode(
324+
null,
325+
false,
326+
new IdentifierTypeNode('int')
327+
),
328+
new ArrayItemNode(
329+
null,
330+
false,
331+
new IdentifierTypeNode('string')
332+
),
333+
new ArrayItemNode(
334+
new ConstExprStringNode('"a"'),
335+
false,
336+
new IdentifierTypeNode('string')
337+
),
338+
]),
339+
],
340+
[
341+
'array{"a"?: int, \'b\': string, 0: int, 1?: DateTime, hello: string}',
342+
new ArrayShapeNode([
343+
new ArrayItemNode(
344+
new ConstExprStringNode('"a"'),
345+
true,
346+
new IdentifierTypeNode('int')
347+
),
348+
new ArrayItemNode(
349+
new ConstExprStringNode('\'b\''),
350+
false,
351+
new IdentifierTypeNode('string')
352+
),
353+
new ArrayItemNode(
354+
new ConstExprIntegerNode('0'),
355+
false,
356+
new IdentifierTypeNode('int')
357+
),
358+
new ArrayItemNode(
359+
new ConstExprIntegerNode('1'),
360+
true,
361+
new IdentifierTypeNode('DateTime')
362+
),
363+
new ArrayItemNode(
364+
new IdentifierTypeNode('hello'),
365+
false,
366+
new IdentifierTypeNode('string')
367+
),
368+
]),
369+
],
370+
[
371+
'array{\'a\': int, \'b\': array{\'c\': callable(): int}}',
372+
new ArrayShapeNode([
373+
new ArrayItemNode(
374+
new ConstExprStringNode('\'a\''),
375+
false,
376+
new IdentifierTypeNode('int')
377+
),
378+
new ArrayItemNode(
379+
new ConstExprStringNode('\'b\''),
380+
false,
381+
new ArrayShapeNode([
382+
new ArrayItemNode(
383+
new ConstExprStringNode('\'c\''),
384+
false,
385+
new CallableTypeNode(
386+
new IdentifierTypeNode('callable'),
387+
[],
388+
new IdentifierTypeNode('int')
389+
)
390+
),
391+
])
392+
),
393+
]),
394+
],
395+
[
396+
'?array{\'a\': int}',
397+
new NullableTypeNode(
398+
new ArrayShapeNode([
399+
new ArrayItemNode(
400+
new ConstExprStringNode('\'a\''),
401+
false,
402+
new IdentifierTypeNode('int')
403+
),
404+
])
405+
),
406+
],
267407
[
268408
'callable(): Foo',
269409
new CallableTypeNode(
@@ -339,6 +479,20 @@ public function provideParseData(): array
339479
])
340480
),
341481
],
482+
[
483+
'callable(): array{\'a\': int}',
484+
new CallableTypeNode(
485+
new IdentifierTypeNode('callable'),
486+
[],
487+
new ArrayShapeNode([
488+
new ArrayItemNode(
489+
new ConstExprStringNode('\'a\''),
490+
false,
491+
new IdentifierTypeNode('int')
492+
),
493+
])
494+
),
495+
],
342496
[
343497
'callable(A&...$a=, B&...=, C): Foo',
344498
new CallableTypeNode(

0 commit comments

Comments
 (0)