From e8fa232263514b713100c18fd179f31d05fcf5c8 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 3 Jun 2019 20:17:12 +0200 Subject: [PATCH 1/5] Array shapes support --- doc/grammars/type.abnf | 12 ++ src/Ast/Type/ArrayShapeItemNode.php | 45 +++++++ src/Ast/Type/ArrayShapeNode.php | 22 ++++ src/Lexer/Lexer.php | 6 + src/Parser/TypeParser.php | 69 +++++++++++ tests/PHPStan/Parser/TypeParserTest.php | 154 ++++++++++++++++++++++++ 6 files changed, 308 insertions(+) create mode 100644 src/Ast/Type/ArrayShapeItemNode.php create mode 100644 src/Ast/Type/ArrayShapeNode.php diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 166f819a..2a864bf0 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -52,6 +52,12 @@ CallableReturnType Array = 1*(TokenSquareBracketOpen TokenSquareBracketClose) +ArrayShape + = TokenCurlyBracketOpen ArrayShapeItem *(TokenComma ArrayShapeItem) TokenCurlyBracketClose + +ArrayShapeItem + = (ConstantString / ConstantInt / TokenIdentifier) TokenNullable TokenColon Type + / Type ; ---------------------------------------------------------------------------- ; ; ConstantExpr ; @@ -139,6 +145,12 @@ TokenSquareBracketOpen TokenSquareBracketClose = "]" *ByteHorizontalWs +TokenCurlyBracketOpen + = "{" *ByteHorizontalWs + +TokenCurlyBracketClose + = "}" *ByteHorizontalWs + TokenComma = "," *ByteHorizontalWs diff --git a/src/Ast/Type/ArrayShapeItemNode.php b/src/Ast/Type/ArrayShapeItemNode.php new file mode 100644 index 00000000..9d842a1f --- /dev/null +++ b/src/Ast/Type/ArrayShapeItemNode.php @@ -0,0 +1,45 @@ +keyName = $keyName; + $this->optional = $optional; + $this->valueType = $valueType; + } + + + public function __toString(): string + { + if ($this->keyName !== null) { + return sprintf( + '%s%s: %s', + (string) $this->keyName, + $this->optional ? '?' : '', + (string) $this->valueType + ); + } + + return (string) $this->valueType; + } + +} diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php new file mode 100644 index 00000000..74df4ab3 --- /dev/null +++ b/src/Ast/Type/ArrayShapeNode.php @@ -0,0 +1,22 @@ +items = $items; + } + + + public function __toString(): string + { + return 'array{' . implode(', ', $this->items) . '}'; + } + +} diff --git a/src/Lexer/Lexer.php b/src/Lexer/Lexer.php index 841b0a11..a8fe3d87 100644 --- a/src/Lexer/Lexer.php +++ b/src/Lexer/Lexer.php @@ -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; @@ -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 => '\'...\'', @@ -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 => '\\.\\.\\.', diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 596a0d52..44a4f6fe 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -53,6 +53,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); } } @@ -93,6 +96,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); @@ -167,6 +173,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); } } @@ -208,4 +217,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_COMMA)) { + $items[] = $this->parseArrayShapeItem($tokens); + } + + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); + + return new Ast\Type\ArrayShapeNode($items); + } + + + private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode + { + 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\ArrayShapeItemNode($key, $optional, $value); + } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) { + $tokens->rollback(); + $value = $this->parse($tokens); + + return new Ast\Type\ArrayShapeItemNode(null, false, $value); + } + } + + /** + * @return Ast\ConstExpr\ConstExprStringNode|Ast\ConstExpr\ConstExprIntegerNode|Ast\Type\IdentifierTypeNode + */ + private function parseArrayShapeKey(TokenIterator $tokens) + { + if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { + $key = new Ast\ConstExpr\ConstExprStringNode($tokens->currentTokenValue()); + $tokens->next(); + + } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { + $key = new Ast\ConstExpr\ConstExprStringNode($tokens->currentTokenValue()); + $tokens->next(); + + } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { + $key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue()); + $tokens->next(); + + } else { + $key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue()); + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + } + + return $key; + } + } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index da5852eb..a7e255ab 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -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\ArrayShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; @@ -264,6 +268,142 @@ public function provideParseData(): array ] ), ], + [ + 'array{\'a\': int}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new ConstExprStringNode('\'a\''), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'array{\'a\': ?int}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new ConstExprStringNode('\'a\''), + false, + new NullableTypeNode( + new IdentifierTypeNode('int') + ) + ), + ]), + ], + [ + 'array{\'a\'?: ?int}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new ConstExprStringNode('\'a\''), + true, + new NullableTypeNode( + new IdentifierTypeNode('int') + ) + ), + ]), + ], + [ + 'array{\'a\': int, \'b\': string}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new ConstExprStringNode('\'a\''), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new ConstExprStringNode('\'b\''), + false, + new IdentifierTypeNode('string') + ), + ]), + ], + [ + 'array{int, string, "a": string}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string') + ), + new ArrayShapeItemNode( + new ConstExprStringNode('"a"'), + false, + new IdentifierTypeNode('string') + ), + ]), + ], + [ + 'array{"a"?: int, \'b\': string, 0: int, 1?: DateTime, hello: string}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new ConstExprStringNode('"a"'), + true, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new ConstExprStringNode('\'b\''), + false, + new IdentifierTypeNode('string') + ), + new ArrayShapeItemNode( + new ConstExprIntegerNode('0'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new ConstExprIntegerNode('1'), + true, + new IdentifierTypeNode('DateTime') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('hello'), + false, + new IdentifierTypeNode('string') + ), + ]), + ], + [ + 'array{\'a\': int, \'b\': array{\'c\': callable(): int}}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new ConstExprStringNode('\'a\''), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new ConstExprStringNode('\'b\''), + false, + new ArrayShapeNode([ + new ArrayShapeItemNode( + new ConstExprStringNode('\'c\''), + false, + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [], + new IdentifierTypeNode('int') + ) + ), + ]) + ), + ]), + ], + [ + '?array{\'a\': int}', + new NullableTypeNode( + new ArrayShapeNode([ + new ArrayShapeItemNode( + new ConstExprStringNode('\'a\''), + false, + new IdentifierTypeNode('int') + ), + ]) + ), + ], [ 'callable(): Foo', new CallableTypeNode( @@ -339,6 +479,20 @@ public function provideParseData(): array ]) ), ], + [ + 'callable(): array{\'a\': int}', + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [], + new ArrayShapeNode([ + new ArrayShapeItemNode( + new ConstExprStringNode('\'a\''), + false, + new IdentifierTypeNode('int') + ), + ]) + ), + ], [ 'callable(A&...$a=, B&...=, C): Foo', new CallableTypeNode( From 0d2d90218ff892b8b03a9aa5e91817da2ad6e0c9 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 4 Jun 2019 10:55:52 +0200 Subject: [PATCH 2/5] Disallow whitespaces between 'array' and '{' --- src/Parser/TokenIterator.php | 6 ++++++ src/Parser/TypeParser.php | 6 +++--- tests/PHPStan/Parser/TypeParserTest.php | 6 ++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index b3f22083..05fe5d7f 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -64,6 +64,12 @@ public function isCurrentTokenType(int $tokenType): bool } + public function isPrecededByHorizontalWhitespace(): bool + { + return ($this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] ?? -1) === Lexer::TOKEN_HORIZONTAL_WS; + } + + /** * @param int $tokenType * @throws \PHPStan\PhpDocParser\Parser\ParserException diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 44a4f6fe..905c9fdd 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -54,7 +54,7 @@ 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)) { + } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { $type = $this->parseArrayShape($tokens, $type); } } @@ -97,7 +97,7 @@ 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)) { + } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { $type = $this->parseArrayShape($tokens, $type); } @@ -174,7 +174,7 @@ 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)) { + } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { $type = $this->parseArrayShape($tokens, $type); } } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index a7e255ab..44ea63a4 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -268,6 +268,12 @@ public function provideParseData(): array ] ), ], + [ + 'array {\'a\': int}', + new IdentifierTypeNode('array'), + Lexer::TOKEN_OPEN_CURLY_BRACKET, + ], + [ 'array{\'a\': int}', new ArrayShapeNode([ From db466fd9b0d261ccebf5f177bc964462157966ec Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 4 Jun 2019 12:09:55 +0200 Subject: [PATCH 3/5] Add parse error test --- tests/PHPStan/Parser/TypeParserTest.php | 35 ++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 44ea63a4..219ff558 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -37,17 +37,22 @@ protected function setUp(): void /** * @dataProvider provideParseData - * @param string $input - * @param TypeNode $expectedType - * @param int $nextTokenType + * @param string $input + * @param TypeNode|\Exception $expectedResult + * @param int $nextTokenType */ - public function testParse(string $input, TypeNode $expectedType, int $nextTokenType = Lexer::TOKEN_END): void + public function testParse(string $input, $expectedResult, int $nextTokenType = Lexer::TOKEN_END): void { + if ($expectedResult instanceof \Exception) { + $this->expectException(get_class($expectedResult)); + $this->expectExceptionMessage($expectedResult->getMessage()); + } + $tokens = new TokenIterator($this->lexer->tokenize($input)); $typeNode = $this->typeParser->parse($tokens); - $this->assertSame((string) $expectedType, (string) $typeNode); - $this->assertEquals($expectedType, $typeNode); + $this->assertSame((string) $expectedResult, (string) $typeNode); + $this->assertEquals($expectedResult, $typeNode); $this->assertSame($nextTokenType, $tokens->currentTokenType()); } @@ -410,6 +415,24 @@ public function provideParseData(): array ]) ), ], + [ + 'array{', + new \PHPStan\PhpDocParser\Parser\ParserException( + '', + Lexer::TOKEN_END, + 6, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'array{a => int}', + new \PHPStan\PhpDocParser\Parser\ParserException( + '=>', + Lexer::TOKEN_OTHER, + 8, + Lexer::TOKEN_CLOSE_CURLY_BRACKET + ), + ], [ 'callable(): Foo', new CallableTypeNode( From eeb4a02ed4592a14ee2bca937bb23d8ef7a728fc Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 4 Jun 2019 14:28:08 +0200 Subject: [PATCH 4/5] Allow only identifiers and integers in keys --- src/Ast/Type/ArrayShapeItemNode.php | 5 +- src/Parser/TypeParser.php | 12 +---- tests/PHPStan/Parser/TypeParserTest.php | 66 +++++++++++++++---------- 3 files changed, 44 insertions(+), 39 deletions(-) diff --git a/src/Ast/Type/ArrayShapeItemNode.php b/src/Ast/Type/ArrayShapeItemNode.php index 9d842a1f..ccbab377 100644 --- a/src/Ast/Type/ArrayShapeItemNode.php +++ b/src/Ast/Type/ArrayShapeItemNode.php @@ -3,12 +3,11 @@ namespace PHPStan\PhpDocParser\Ast\Type; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; -use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; class ArrayShapeItemNode implements TypeNode { - /** @var ConstExprStringNode|ConstExprIntegerNode|IdentifierTypeNode|null */ + /** @var ConstExprIntegerNode|IdentifierTypeNode|null */ public $keyName; /** @var bool */ @@ -18,7 +17,7 @@ class ArrayShapeItemNode implements TypeNode public $valueType; /** - * @param ConstExprStringNode|ConstExprIntegerNode|IdentifierTypeNode|null $keyName + * @param ConstExprIntegerNode|IdentifierTypeNode|null $keyName */ public function __construct($keyName, bool $optional, TypeNode $valueType) { diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 905c9fdd..58345175 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -253,19 +253,11 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape } /** - * @return Ast\ConstExpr\ConstExprStringNode|Ast\ConstExpr\ConstExprIntegerNode|Ast\Type\IdentifierTypeNode + * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\Type\IdentifierTypeNode */ private function parseArrayShapeKey(TokenIterator $tokens) { - if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { - $key = new Ast\ConstExpr\ConstExprStringNode($tokens->currentTokenValue()); - $tokens->next(); - - } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { - $key = new Ast\ConstExpr\ConstExprStringNode($tokens->currentTokenValue()); - $tokens->next(); - - } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { + if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { $key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue()); $tokens->next(); diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 219ff558..d09e73e4 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -3,7 +3,6 @@ namespace PHPStan\PhpDocParser\Parser; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; -use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; @@ -280,20 +279,20 @@ public function provideParseData(): array ], [ - 'array{\'a\': int}', + 'array{a: int}', new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('\'a\''), + new IdentifierTypeNode('a'), false, new IdentifierTypeNode('int') ), ]), ], [ - 'array{\'a\': ?int}', + 'array{a: ?int}', new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('\'a\''), + new IdentifierTypeNode('a'), false, new NullableTypeNode( new IdentifierTypeNode('int') @@ -302,10 +301,10 @@ public function provideParseData(): array ]), ], [ - 'array{\'a\'?: ?int}', + 'array{a?: ?int}', new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('\'a\''), + new IdentifierTypeNode('a'), true, new NullableTypeNode( new IdentifierTypeNode('int') @@ -314,22 +313,27 @@ public function provideParseData(): array ]), ], [ - 'array{\'a\': int, \'b\': string}', + 'array{0: int}', new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('\'a\''), + new ConstExprIntegerNode('0'), false, new IdentifierTypeNode('int') ), + ]), + ], + [ + 'array{0?: int}', + new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('\'b\''), - false, - new IdentifierTypeNode('string') + new ConstExprIntegerNode('0'), + true, + new IdentifierTypeNode('int') ), ]), ], [ - 'array{int, string, "a": string}', + 'array{int, int}', new ArrayShapeNode([ new ArrayShapeItemNode( null, @@ -339,25 +343,35 @@ public function provideParseData(): array new ArrayShapeItemNode( null, false, - new IdentifierTypeNode('string') + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'array{a: int, b: string}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') ), new ArrayShapeItemNode( - new ConstExprStringNode('"a"'), + new IdentifierTypeNode('b'), false, new IdentifierTypeNode('string') ), ]), ], [ - 'array{"a"?: int, \'b\': string, 0: int, 1?: DateTime, hello: string}', + 'array{a?: int, b: string, 0: int, 1?: DateTime, hello: string}', new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('"a"'), + new IdentifierTypeNode('a'), true, new IdentifierTypeNode('int') ), new ArrayShapeItemNode( - new ConstExprStringNode('\'b\''), + new IdentifierTypeNode('b'), false, new IdentifierTypeNode('string') ), @@ -379,19 +393,19 @@ public function provideParseData(): array ]), ], [ - 'array{\'a\': int, \'b\': array{\'c\': callable(): int}}', + 'array{a: int, b: array{c: callable(): int}}', new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('\'a\''), + new IdentifierTypeNode('a'), false, new IdentifierTypeNode('int') ), new ArrayShapeItemNode( - new ConstExprStringNode('\'b\''), + new IdentifierTypeNode('b'), false, new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('\'c\''), + new IdentifierTypeNode('c'), false, new CallableTypeNode( new IdentifierTypeNode('callable'), @@ -404,11 +418,11 @@ public function provideParseData(): array ]), ], [ - '?array{\'a\': int}', + '?array{a: int}', new NullableTypeNode( new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('\'a\''), + new IdentifierTypeNode('a'), false, new IdentifierTypeNode('int') ), @@ -509,13 +523,13 @@ public function provideParseData(): array ), ], [ - 'callable(): array{\'a\': int}', + 'callable(): array{a: int}', new CallableTypeNode( new IdentifierTypeNode('callable'), [], new ArrayShapeNode([ new ArrayShapeItemNode( - new ConstExprStringNode('\'a\''), + new IdentifierTypeNode('a'), false, new IdentifierTypeNode('int') ), From c1880010150b6150373d82006f4c560dc8b0f78f Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 4 Jun 2019 20:22:42 +0200 Subject: [PATCH 5/5] Add parse error test --- tests/PHPStan/Parser/TypeParserTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index d09e73e4..a9e5e8df 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -447,6 +447,24 @@ public function provideParseData(): array Lexer::TOKEN_CLOSE_CURLY_BRACKET ), ], + [ + 'array{"a": int}', + new \PHPStan\PhpDocParser\Parser\ParserException( + '"a"', + Lexer::TOKEN_DOUBLE_QUOTED_STRING, + 6, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'array{\'a\': int}', + new \PHPStan\PhpDocParser\Parser\ParserException( + '\'a\'', + Lexer::TOKEN_SINGLE_QUOTED_STRING, + 6, + Lexer::TOKEN_IDENTIFIER + ), + ], [ 'callable(): Foo', new CallableTypeNode(