Skip to content

Commit

Permalink
Array shapes support
Browse files Browse the repository at this point in the history
  • Loading branch information
arnaud-lb committed Jun 3, 2019
1 parent ab518a5 commit 1da2721
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 0 deletions.
12 changes: 12 additions & 0 deletions doc/grammars/type.abnf
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ CallableReturnType
Array
= 1*(TokenSquareBracketOpen TokenSquareBracketClose)

ArrayShape
= TokenCurlyBracketOpen ArrayItem *(TokenComma ArrayItem) TokenCurlyBracketClose

ArrayItem
= (ConstantString / ConstantInt / TokenIdentifier) TokenNullable TokenColon Type
/ Type

; ---------------------------------------------------------------------------- ;
; ConstantExpr ;
Expand Down Expand Up @@ -139,6 +145,12 @@ TokenSquareBracketOpen
TokenSquareBracketClose
= "]" *ByteHorizontalWs

TokenCurlyBracketOpen
= "{" *ByteHorizontalWs

TokenCurlyBracketClose
= "}" *ByteHorizontalWs

TokenComma
= "," *ByteHorizontalWs

Expand Down
6 changes: 6 additions & 0 deletions src/Lexer/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class Lexer
public const TOKEN_CLOSE_ANGLE_BRACKET = 7;
public const TOKEN_OPEN_SQUARE_BRACKET = 8;
public const TOKEN_CLOSE_SQUARE_BRACKET = 9;
public const TOKEN_OPEN_CURLY_BRACKET = 30;
public const TOKEN_CLOSE_CURLY_BRACKET = 31;
public const TOKEN_COMMA = 10;
public const TOKEN_COLON = 29;
public const TOKEN_VARIADIC = 11;
Expand Down Expand Up @@ -50,6 +52,8 @@ class Lexer
self::TOKEN_CLOSE_ANGLE_BRACKET => '\'>\'',
self::TOKEN_OPEN_SQUARE_BRACKET => '\'[\'',
self::TOKEN_CLOSE_SQUARE_BRACKET => '\']\'',
self::TOKEN_OPEN_CURLY_BRACKET => '\'{\'',
self::TOKEN_CLOSE_CURLY_BRACKET => '\'}\'',
self::TOKEN_COMMA => '\',\'',
self::TOKEN_COLON => '\':\'',
self::TOKEN_VARIADIC => '\'...\'',
Expand Down Expand Up @@ -123,6 +127,8 @@ private function initialize(): void
self::TOKEN_CLOSE_ANGLE_BRACKET => '>',
self::TOKEN_OPEN_SQUARE_BRACKET => '\\[',
self::TOKEN_CLOSE_SQUARE_BRACKET => '\\]',
self::TOKEN_OPEN_CURLY_BRACKET => '\\{',
self::TOKEN_CLOSE_CURLY_BRACKET => '\\}',

self::TOKEN_COMMA => ',',
self::TOKEN_VARIADIC => '\\.\\.\\.',
Expand Down
71 changes: 71 additions & 0 deletions src/Parser/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace PHPStan\PhpDocParser\Parser;

use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Lexer\Lexer;

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

} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArray($tokens, $type);

} elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
$type = $this->parseArrayShape($tokens, $type);
}
}

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

if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$type = $this->parseGeneric($tokens, $type);

} elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
$type = $this->parseArrayShape($tokens, $type);
}

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

if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$type = $this->parseGeneric($tokens, $type);

} elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
$type = $this->parseArrayShape($tokens, $type);
}
}

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


private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
$items = [$this->parseArrayShapeItem($tokens)];

while (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
$tokens->consumeTokenType(Lexer::TOKEN_COMMA);
$items[] = $this->parseArrayShapeItem($tokens);
}

return new Ast\Type\ArrayShapeNode($items);
}


private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayItemNode
{
try {
$tokens->pushSavePoint();
$key = $this->parseArrayShapeKey($tokens);
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$value = $this->parse($tokens);
$tokens->dropSavePoint();

return new Ast\Type\ArrayItemNode($key, $optional, $value);
} catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
$tokens->rollback();
$value = $this->parse($tokens);

return new Ast\Type\ArrayItemNode(null, $optional, $value);
}
}

/**
* @return ConstExprStringNode|ConstExprIntegerNode|IdentifierTypeNode
*/
private function parseArrayShapeKey(TokenIterator $tokens)
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
$key = new ConstExprStringNode($tokens->currentTokenValue());
$tokens->next();

} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
$key = new ConstExprStringNode($tokens->currentTokenValue());
$tokens->next();

} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
$key = new ConstExprIntegerNode($tokens->currentTokenValue());
$tokens->next();

} else {
$key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}

return $key;
}


}
154 changes: 154 additions & 0 deletions tests/PHPStan/Parser/TypeParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

namespace PHPStan\PhpDocParser\Parser;

use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayItemNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
Expand Down Expand Up @@ -264,6 +268,142 @@ public function provideParseData(): array
]
),
],
[
'array{\'a\': int}',
new ArrayShapeNode([
new ArrayItemNode(
new ConstExprStringNode('\'a\''),
false,
new IdentifierTypeNode('int')
),
]),
],
[
'array{\'a\': ?int}',
new ArrayShapeNode([
new ArrayItemNode(
new ConstExprStringNode('\'a\''),
false,
new NullableTypeNode(
new IdentifierTypeNode('int')
)
),
]),
],
[
'array{\'a\'?: ?int}',
new ArrayShapeNode([
new ArrayItemNode(
new ConstExprStringNode('\'a\''),
true,
new NullableTypeNode(
new IdentifierTypeNode('int')
)
),
]),
],
[
'array{\'a\': int, \'b\': string}',
new ArrayShapeNode([
new ArrayItemNode(
new ConstExprStringNode('\'a\''),
false,
new IdentifierTypeNode('int')
),
new ArrayItemNode(
new ConstExprStringNode('\'b\''),
false,
new IdentifierTypeNode('string')
),
]),
],
[
'array{int, string, "a": string}',
new ArrayShapeNode([
new ArrayItemNode(
null,
false,
new IdentifierTypeNode('int')
),
new ArrayItemNode(
null,
false,
new IdentifierTypeNode('string')
),
new ArrayItemNode(
new ConstExprStringNode('"a"'),
false,
new IdentifierTypeNode('string')
),
]),
],
[
'array{"a"?: int, \'b\': string, 0: int, 1?: DateTime, hello: string}',
new ArrayShapeNode([
new ArrayItemNode(
new ConstExprStringNode('"a"'),
true,
new IdentifierTypeNode('int')
),
new ArrayItemNode(
new ConstExprStringNode('\'b\''),
false,
new IdentifierTypeNode('string')
),
new ArrayItemNode(
new ConstExprIntegerNode('0'),
false,
new IdentifierTypeNode('int')
),
new ArrayItemNode(
new ConstExprIntegerNode('1'),
true,
new IdentifierTypeNode('DateTime')
),
new ArrayItemNode(
new IdentifierTypeNode('hello'),
false,
new IdentifierTypeNode('string')
),
]),
],
[
'array{\'a\': int, \'b\': array{\'c\': callable(): int}}',
new ArrayShapeNode([
new ArrayItemNode(
new ConstExprStringNode('\'a\''),
false,
new IdentifierTypeNode('int')
),
new ArrayItemNode(
new ConstExprStringNode('\'b\''),
false,
new ArrayShapeNode([
new ArrayItemNode(
new ConstExprStringNode('\'c\''),
false,
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[],
new IdentifierTypeNode('int')
)
),
])
),
]),
],
[
'?array{\'a\': int}',
new NullableTypeNode(
new ArrayShapeNode([
new ArrayItemNode(
new ConstExprStringNode('\'a\''),
false,
new IdentifierTypeNode('int')
),
])
),
],
[
'callable(): Foo',
new CallableTypeNode(
Expand Down Expand Up @@ -339,6 +479,20 @@ public function provideParseData(): array
])
),
],
[
'callable(): array{\'a\': int}',
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[],
new ArrayShapeNode([
new ArrayItemNode(
new ConstExprStringNode('\'a\''),
false,
new IdentifierTypeNode('int')
),
])
),
],
[
'callable(A&...$a=, B&...=, C): Foo',
new CallableTypeNode(
Expand Down

0 comments on commit 1da2721

Please sign in to comment.