From 5ce9a14cabed38ce922354108da5af6e6d1900ec Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Wed, 30 Mar 2022 11:34:46 +0200 Subject: [PATCH] Support multiline parenthesized types This includes union, intersection, and conditional types --- src/Parser/TypeParser.php | 59 +++++++++-- tests/PHPStan/Parser/PhpDocParserTest.php | 113 ++++++++++++++++++++++ tests/PHPStan/Parser/TypeParserTest.php | 74 ++++++++++++++ 3 files changed, 237 insertions(+), 9 deletions(-) diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 3afc206f..95825ca4 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -51,13 +51,17 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode } else { $type = $this->parseAtomic($tokens); - if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { - $type = $this->parseUnion($tokens, $type); - - } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { - $type = $this->parseIntersection($tokens, $type); - } elseif ($tokens->isCurrentTokenValue('is')) { + if ($tokens->isCurrentTokenValue('is')) { $type = $this->parseConditional($tokens, $type); + } else { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { + $type = $this->subParseUnion($tokens, $type); + + } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { + $type = $this->subParseIntersection($tokens, $type); + } } } @@ -69,7 +73,10 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode { if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $type = $this->subParse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { @@ -169,6 +176,21 @@ private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast } + /** @phpstan-impure */ + private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode + { + $types = [$type]; + + while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $types[] = $this->parseAtomic($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } + + return new Ast\Type\UnionTypeNode($types); + } + + /** @phpstan-impure */ private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode { @@ -182,6 +204,21 @@ private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $typ } + /** @phpstan-impure */ + private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode + { + $types = [$type]; + + while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $types[] = $this->parseAtomic($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } + + return new Ast\Type\IntersectionTypeNode($types); + } + + /** @phpstan-impure */ private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode { @@ -193,15 +230,19 @@ private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subj $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); } - $targetType = $this->parseAtomic($tokens); + $targetType = $this->parse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - $ifType = $this->parseAtomic($tokens); + $ifType = $this->parse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_COLON); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - $elseType = $this->parseAtomic($tokens); + $elseType = $this->parse($tokens); return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $ifType, $elseType, $negated); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 6ffecc3d..b8a3fa42 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -2818,6 +2818,119 @@ public function provideMultiLinePhpDocData(): array ), ]), ], + [ + 'OK with multiline conditional return type', + '/** + * @template TRandKey as array-key + * @template TRandVal + * @template TRandList as array|XIterator|Traversable + * + * @param TRandList $list + * + * @return ( + * TRandList is array ? array : ( + * TRandList is XIterator ? XIterator : + * IteratorIterator|LimitIterator + * )) + */', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new TemplateTagValueNode('TRandKey', new IdentifierTypeNode('array-key'), '') + ), + new PhpDocTagNode( + '@template', + new TemplateTagValueNode('TRandVal', null, '') + ), + new PhpDocTagNode( + '@template', + new TemplateTagValueNode( + 'TRandList', + new UnionTypeNode([ + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + new GenericTypeNode( + new IdentifierTypeNode('XIterator'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + new GenericTypeNode( + new IdentifierTypeNode('Traversable'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + ]), + '' + ) + ), + new PhpDocTextNode(''), + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + new IdentifierTypeNode('TRandList'), + false, + '$list', + '' + ) + ), + new PhpDocTextNode(''), + new PhpDocTagNode( + '@return', + new ReturnTagValueNode( + new ConditionalTypeNode( + new IdentifierTypeNode('TRandList'), + new IdentifierTypeNode('array'), + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + new ConditionalTypeNode( + new IdentifierTypeNode('TRandList'), + new IdentifierTypeNode('XIterator'), + new GenericTypeNode( + new IdentifierTypeNode('XIterator'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + new UnionTypeNode([ + new GenericTypeNode( + new IdentifierTypeNode('IteratorIterator'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + new GenericTypeNode( + new IdentifierTypeNode('LimitIterator'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + ]), + false + ), + false + ), + '' + ) + ), + ]), + ], ]; } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index a852c0cf..8d9ebf2e 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -149,6 +149,17 @@ public function provideParseData(): array new IdentifierTypeNode('int'), ]), ], + [ + '(' . PHP_EOL . + ' string' . PHP_EOL . + ' &' . PHP_EOL . + ' int' . PHP_EOL . + ')', + new IntersectionTypeNode([ + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int'), + ]), + ], [ 'string & int & float', new IntersectionTypeNode([ @@ -1136,6 +1147,69 @@ public function provideParseData(): array false ), ], + [ + '(Foo is Bar|Baz ? never : int|string)', + new ConditionalTypeNode( + new IdentifierTypeNode('Foo'), + new UnionTypeNode([ + new IdentifierTypeNode('Bar'), + new IdentifierTypeNode('Baz'), + ]), + new IdentifierTypeNode('never'), + new UnionTypeNode([ + new IdentifierTypeNode('int'), + new IdentifierTypeNode('string'), + ]), + false + ), + ], + [ + '(' . PHP_EOL . + ' TRandList is array ? array : (' . PHP_EOL . + ' TRandList is XIterator ? XIterator :' . PHP_EOL . + ' IteratorIterator|LimitIterator' . PHP_EOL . + '))', + new ConditionalTypeNode( + new IdentifierTypeNode('TRandList'), + new IdentifierTypeNode('array'), + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + new ConditionalTypeNode( + new IdentifierTypeNode('TRandList'), + new IdentifierTypeNode('XIterator'), + new GenericTypeNode( + new IdentifierTypeNode('XIterator'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + new UnionTypeNode([ + new GenericTypeNode( + new IdentifierTypeNode('IteratorIterator'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + new GenericTypeNode( + new IdentifierTypeNode('LimitIterator'), + [ + new IdentifierTypeNode('TRandKey'), + new IdentifierTypeNode('TRandVal'), + ] + ), + ]), + false + ), + false + ), + ], ]; }