Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit c6ffc4b

Browse files
arnaud-lbondrejmirtes
authored andcommitted
Array shapes support
1 parent ab518a5 commit c6ffc4b

File tree

6 files changed

+308
-0
lines changed

6 files changed

+308
-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 ArrayShapeItem *(TokenComma ArrayShapeItem) TokenCurlyBracketClose
57+
58+
ArrayShapeItem
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/ArrayShapeItemNode.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 ArrayShapeItemNode implements TypeNode
9+
{
10+
11+
/** @var ConstExprStringNode|ConstExprIntegerNode|IdentifierTypeNode|null */
12+
public $keyName;
13+
14+
/** @var bool */
15+
public $optional;
16+
17+
/** @var TypeNode */
18+
public $valueType;
19+
20+
/**
21+
* @param ConstExprStringNode|ConstExprIntegerNode|IdentifierTypeNode|null $keyName
22+
*/
23+
public function __construct($keyName, bool $optional, TypeNode $valueType)
24+
{
25+
$this->keyName = $keyName;
26+
$this->optional = $optional;
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->optional ? '?' : '',
38+
(string) $this->valueType
39+
);
40+
}
41+
42+
return (string) $this->valueType;
43+
}
44+
45+
}

‎src/Ast/Type/ArrayShapeNode.php‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
class ArrayShapeNode implements TypeNode
6+
{
7+
8+
/** @var ArrayShapeItemNode[] */
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(', ', $this->items) . '}';
20+
}
21+
22+
}

‎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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
5353

5454
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
5555
$type = $this->tryParseArray($tokens, $type);
56+
57+
} elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
58+
$type = $this->parseArrayShape($tokens, $type);
5659
}
5760
}
5861

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

9497
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
9598
$type = $this->parseGeneric($tokens, $type);
99+
100+
} elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
101+
$type = $this->parseArrayShape($tokens, $type);
96102
}
97103

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

168174
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
169175
$type = $this->parseGeneric($tokens, $type);
176+
177+
} elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
178+
$type = $this->parseArrayShape($tokens, $type);
170179
}
171180
}
172181

@@ -208,4 +217,64 @@ private function tryParseArray(TokenIterator $tokens, Ast\Type\TypeNode $type):
208217
return $type;
209218
}
210219

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

‎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\ArrayShapeItemNode;
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 ArrayShapeItemNode(
275+
new ConstExprStringNode('\'a\''),
276+
false,
277+
new IdentifierTypeNode('int')
278+
),
279+
]),
280+
],
281+
[
282+
'array{\'a\': ?int}',
283+
new ArrayShapeNode([
284+
new ArrayShapeItemNode(
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 ArrayShapeItemNode(
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 ArrayShapeItemNode(
309+
new ConstExprStringNode('\'a\''),
310+
false,
311+
new IdentifierTypeNode('int')
312+
),
313+
new ArrayShapeItemNode(
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 ArrayShapeItemNode(
324+
null,
325+
false,
326+
new IdentifierTypeNode('int')
327+
),
328+
new ArrayShapeItemNode(
329+
null,
330+
false,
331+
new IdentifierTypeNode('string')
332+
),
333+
new ArrayShapeItemNode(
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 ArrayShapeItemNode(
344+
new ConstExprStringNode('"a"'),
345+
true,
346+
new IdentifierTypeNode('int')
347+
),
348+
new ArrayShapeItemNode(
349+
new ConstExprStringNode('\'b\''),
350+
false,
351+
new IdentifierTypeNode('string')
352+
),
353+
new ArrayShapeItemNode(
354+
new ConstExprIntegerNode('0'),
355+
false,
356+
new IdentifierTypeNode('int')
357+
),
358+
new ArrayShapeItemNode(
359+
new ConstExprIntegerNode('1'),
360+
true,
361+
new IdentifierTypeNode('DateTime')
362+
),
363+
new ArrayShapeItemNode(
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 ArrayShapeItemNode(
374+
new ConstExprStringNode('\'a\''),
375+
false,
376+
new IdentifierTypeNode('int')
377+
),
378+
new ArrayShapeItemNode(
379+
new ConstExprStringNode('\'b\''),
380+
false,
381+
new ArrayShapeNode([
382+
new ArrayShapeItemNode(
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 ArrayShapeItemNode(
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 ArrayShapeItemNode(
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)

AltStyle によって変換されたページ (->オリジナル) /