Skip to content

Commit 7f977db

Browse files
committed
Array shapes support
1 parent ab518a5 commit 7f977db

File tree

4 files changed

+205
-0
lines changed

4 files changed

+205
-0
lines changed

doc/grammars/type.abnf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ 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
5560

5661
; ---------------------------------------------------------------------------- ;
5762
; ConstantExpr ;
@@ -139,6 +144,12 @@ TokenSquareBracketOpen
139144
TokenSquareBracketClose
140145
= "]" *ByteHorizontalWs
141146

147+
TokenCurlyBracketOpen
148+
= "{" *ByteHorizontalWs
149+
150+
TokenCurlyBracketClose
151+
= "}" *ByteHorizontalWs
152+
142153
TokenComma
143154
= "," *ByteHorizontalWs
144155

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: 54 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,47 @@ 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+
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
240+
$key = new ConstExprStringNode($tokens->currentTokenValue());
241+
$tokens->next();
242+
243+
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
244+
$key = new ConstExprStringNode($tokens->currentTokenValue());
245+
$tokens->next();
246+
247+
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
248+
$key = new ConstExprIntegerNode($tokens->currentTokenValue());
249+
$tokens->next();
250+
251+
} else {
252+
$key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
253+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
254+
}
255+
256+
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
257+
258+
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
259+
260+
$value = $this->parse($tokens);
261+
262+
return new Ast\Type\ArrayItemNode($key, $optional, $value);
263+
}
264+
211265
}

tests/PHPStan/Parser/TypeParserTest.php

Lines changed: 134 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,122 @@ 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{"a"?: int, \'b\': string, 0: int, 1?: DateTime, hello: string}',
322+
new ArrayShapeNode([
323+
new ArrayItemNode(
324+
new ConstExprStringNode('"a"'),
325+
true,
326+
new IdentifierTypeNode('int')
327+
),
328+
new ArrayItemNode(
329+
new ConstExprStringNode('\'b\''),
330+
false,
331+
new IdentifierTypeNode('string')
332+
),
333+
new ArrayItemNode(
334+
new ConstExprIntegerNode('0'),
335+
false,
336+
new IdentifierTypeNode('int')
337+
),
338+
new ArrayItemNode(
339+
new ConstExprIntegerNode('1'),
340+
true,
341+
new IdentifierTypeNode('DateTime')
342+
),
343+
new ArrayItemNode(
344+
new IdentifierTypeNode('hello'),
345+
false,
346+
new IdentifierTypeNode('string')
347+
),
348+
]),
349+
],
350+
[
351+
'array{\'a\': int, \'b\': array{\'c\': callable(): int}}',
352+
new ArrayShapeNode([
353+
new ArrayItemNode(
354+
new ConstExprStringNode('\'a\''),
355+
false,
356+
new IdentifierTypeNode('int')
357+
),
358+
new ArrayItemNode(
359+
new ConstExprStringNode('\'b\''),
360+
false,
361+
new ArrayShapeNode([
362+
new ArrayItemNode(
363+
new ConstExprStringNode('\'c\''),
364+
false,
365+
new CallableTypeNode(
366+
new IdentifierTypeNode('callable'),
367+
[],
368+
new IdentifierTypeNode('int')
369+
)
370+
),
371+
])
372+
),
373+
]),
374+
],
375+
[
376+
'?array{\'a\': int}',
377+
new NullableTypeNode(
378+
new ArrayShapeNode([
379+
new ArrayItemNode(
380+
new ConstExprStringNode('\'a\''),
381+
false,
382+
new IdentifierTypeNode('int')
383+
),
384+
])
385+
),
386+
],
267387
[
268388
'callable(): Foo',
269389
new CallableTypeNode(
@@ -339,6 +459,20 @@ public function provideParseData(): array
339459
])
340460
),
341461
],
462+
[
463+
'callable(): array{\'a\': int}',
464+
new CallableTypeNode(
465+
new IdentifierTypeNode('callable'),
466+
[],
467+
new ArrayShapeNode([
468+
new ArrayItemNode(
469+
new ConstExprStringNode('\'a\''),
470+
false,
471+
new IdentifierTypeNode('int')
472+
),
473+
])
474+
),
475+
],
342476
[
343477
'callable(A&...$a=, B&...=, C): Foo',
344478
new CallableTypeNode(

0 commit comments

Comments
 (0)