From 122bb31ae30f67d056a44158a4a373e821ba71b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismail=20=C3=96zg=C3=BCn=20Turan?= Date: Tue, 11 Feb 2025 16:32:56 +0100 Subject: [PATCH 1/2] Allow new lines in between everything and handle them correctly --- src/Parser/TokenIterator.php | 9 + src/Parser/TypeParser.php | 81 ++++---- tests/PHPStan/Parser/PhpDocParserTest.php | 221 ++++++++++++++++++++++ 3 files changed, 273 insertions(+), 38 deletions(-) diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index 87ded21a..5285d48a 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -205,6 +205,15 @@ public function tryConsumeTokenType(int $tokenType): bool } + /** @phpstan-impure */ + public function skipNewLineTokens(): void + { + do { + $foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } while ($foundNewLine === true); + } + + private function detectNewline(): void { $value = $this->currentTokenValue(); diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 84a3880d..1b2080e6 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -90,7 +90,7 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode if ($tokens->isCurrentTokenValue('is')) { $type = $this->parseConditional($tokens, $type); } else { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { $type = $this->subParseUnion($tokens, $type); @@ -112,9 +112,9 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode $startIndex = $tokens->currentTokenIndex(); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $type = $this->subParse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); @@ -256,9 +256,9 @@ private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): $types = [$type]; while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $types[] = $this->parseAtomic($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } return new Ast\Type\UnionTypeNode($types); @@ -284,9 +284,9 @@ private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $ $types = [$type]; while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $types[] = $this->parseAtomic($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } return new Ast\Type\IntersectionTypeNode($types); @@ -306,15 +306,15 @@ private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subj $targetType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $ifType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_COLON); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $elseType = $this->subParse($tokens); @@ -335,15 +335,15 @@ private function parseConditionalForParameter(TokenIterator $tokens, string $par $targetType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $ifType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_COLON); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $elseType = $this->subParse($tokens); @@ -409,8 +409,13 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $variances = []; $isFirst = true; - while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + while ( + $isFirst + || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) + || $tokens->tryConsumeTokenType(Lexer::TOKEN_UNION) + || $tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION) + ) { + $tokens->skipNewLineTokens(); // trailing comma case if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { @@ -419,7 +424,7 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $isFirst = false; [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } $type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); @@ -510,19 +515,19 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod : []; $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $parameters = []; if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { $parameters[] = $this->parseCallableParameter($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { break; } $parameters[] = $this->parseCallableParameter($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } } @@ -550,7 +555,7 @@ private function parseCallableTemplates(TokenIterator $tokens): array $isFirst = true; while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); // trailing comma case if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { @@ -559,7 +564,7 @@ private function parseCallableTemplates(TokenIterator $tokens): array $isFirst = false; $templates[] = $this->parseCallableTemplateArgument($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); @@ -830,7 +835,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $unsealedType = null; do { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { return Ast\Type\ArrayShapeNode::createSealed($items, $kind); @@ -839,14 +844,14 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { $sealed = false; - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) { $unsealedType = $this->parseArrayShapeUnsealedType($tokens); } else { $unsealedType = $this->parseListShapeUnsealedType($tokens); } - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA); @@ -855,10 +860,10 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $items[] = $this->parseArrayShapeItem($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); if ($sealed) { @@ -945,18 +950,18 @@ private function parseArrayShapeUnsealedType(TokenIterator $tokens): Ast\Type\Ar $startIndex = $tokens->currentTokenIndex(); $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $valueType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $keyType = null; if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $keyType = $valueType; $valueType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); @@ -978,10 +983,10 @@ private function parseListShapeUnsealedType(TokenIterator $tokens): Ast\Type\Arr $startIndex = $tokens->currentTokenIndex(); $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $valueType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); @@ -1003,7 +1008,7 @@ private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNo $items = []; do { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { return new Ast\Type\ObjectShapeNode($items); @@ -1011,10 +1016,10 @@ private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNo $items[] = $this->parseObjectShapeItem($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + $tokens->skipNewLineTokens(); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); return new Ast\Type\ObjectShapeNode($items); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 8e98d639..f859e9f6 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -62,6 +62,8 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; @@ -4027,6 +4029,225 @@ public function provideMultiLinePhpDocData(): iterable new PhpDocTextNode(''), ]), ]; + + yield [ + 'Multiline PHPDoc with new line across generic type declaration', + '/**' . PHP_EOL . + ' * @param array,' . PHP_EOL . + ' * }> $a' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar'), true, new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('foo2'), false, new IdentifierTypeNode('true')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + )), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + ), + false, + '$a', + '', + false, + )), + ]), + ]; + + yield [ + 'Multiline PHPDoc with new line within type declaration', + '/**' . PHP_EOL . + ' * @param array,' . PHP_EOL . + ' * }> $a' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@param', new ParamTagValueNode( + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar'), true, new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('foo2'), false, new IdentifierTypeNode('true')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + )), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + ), + false, + '$a', + '', + false, + )), + ]), + ]; + + yield [ + 'Multiline PHPDoc with new line within type declaration including usage of braces', + '/**' . PHP_EOL . + ' * @phpstan-type FactoriesConfigurationType = array<' . PHP_EOL . + ' * string,' . PHP_EOL . + ' * (class-string|Factory\FactoryInterface)' . PHP_EOL . + ' * |callable(ContainerInterface,?string,array|null):object' . PHP_EOL . + ' * >' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@phpstan-type', new TypeAliasTagValueNode( + 'FactoriesConfigurationType', + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + new UnionTypeNode([ + new GenericTypeNode( + new IdentifierTypeNode('class-string'), + [new IdentifierTypeNode('Factory\\FactoryInterface')], + [GenericTypeNode::VARIANCE_INVARIANT], + ), + new IdentifierTypeNode('Factory\\FactoryInterface'), + ]), + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [ + new CallableTypeParameterNode(new IdentifierTypeNode('ContainerInterface'), false, false, '', false), + new CallableTypeParameterNode( + new NullableTypeNode( + new IdentifierTypeNode('string'), + ), + false, + false, + '', + false, + ), + new CallableTypeParameterNode( + new UnionTypeNode([ + new GenericTypeNode( + new IdentifierTypeNode('array'), + [new IdentifierTypeNode('mixed')], + [GenericTypeNode::VARIANCE_INVARIANT], + ), + new IdentifierTypeNode('null'), + ]), + false, + false, + '', + false, + ), + ], + new IdentifierTypeNode('object'), + [], + ), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + ), + )), + ]), + ]; + + /** + * @return object{ + * a: int, + * + * b: int, + * } + */ + + yield [ + 'Multiline PHPDoc with new line within object type declaration', + '/**' . PHP_EOL . + ' * @return object{' . PHP_EOL . + ' * a: int,' . PHP_EOL . + ' *' . PHP_EOL . + ' * b: int,' . PHP_EOL . + ' * }' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode( + '@return', + new ReturnTagValueNode( + new ObjectShapeNode( + [ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int'), + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('int'), + ), + ], + ), + '', + ), + ), + ]), + ]; } public function provideTemplateTagsData(): Iterator From 61ead7b837377b018a3166b0cbfd9a461010af3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismail=20=C3=96zg=C3=BCn=20Turan?= Date: Wed, 12 Feb 2025 16:50:12 +0100 Subject: [PATCH 2/2] Move determining the union and intersection with new lines to a proper place and handle them --- src/Parser/TokenIterator.php | 4 + src/Parser/TypeParser.php | 37 +++++-- tests/PHPStan/Parser/PhpDocParserTest.php | 113 +++++++++++----------- 3 files changed, 93 insertions(+), 61 deletions(-) diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index 5285d48a..a9738d62 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -208,6 +208,10 @@ public function tryConsumeTokenType(int $tokenType): bool /** @phpstan-impure */ public function skipNewLineTokens(): void { + if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + return; + } + do { $foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); } while ($foundNewLine === true); diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 1b2080e6..d5f9217c 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -40,17 +40,44 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode } else { $type = $this->parseAtomic($tokens); - if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { - $type = $this->parseUnion($tokens, $type); + $tokens->pushSavePoint(); + $tokens->skipNewLineTokens(); + + try { + $enrichedType = $this->enrichTypeOnUnionOrIntersection($tokens, $type); + + } catch (ParserException $parserException) { + $enrichedType = null; + } + + if ($enrichedType !== null) { + $type = $enrichedType; + $tokens->dropSavePoint(); - } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { - $type = $this->parseIntersection($tokens, $type); + } else { + $tokens->rollback(); + $type = $this->enrichTypeOnUnionOrIntersection($tokens, $type) ?? $type; } } return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); } + /** @phpstan-impure */ + private function enrichTypeOnUnionOrIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): ?Ast\Type\TypeNode + { + if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { + return $this->parseUnion($tokens, $type); + + } + + if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { + return $this->parseIntersection($tokens, $type); + } + + return null; + } + /** * @internal * @template T of Ast\Node @@ -412,8 +439,6 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode while ( $isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) - || $tokens->tryConsumeTokenType(Lexer::TOKEN_UNION) - || $tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION) ) { $tokens->skipNewLineTokens(); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index f859e9f6..2cbde3c6 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -4053,20 +4053,21 @@ public function provideMultiLinePhpDocData(): iterable new IdentifierTypeNode('array'), [ new IdentifierTypeNode('string'), - ArrayShapeNode::createSealed([ - new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), - new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), - ]), - ArrayShapeNode::createSealed([ - new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), - new ArrayShapeItemNode(new IdentifierTypeNode('foo2'), false, new IdentifierTypeNode('true')), - new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + new UnionTypeNode([ + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('foo2'), false, new IdentifierTypeNode('true')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), ]), ], [ GenericTypeNode::VARIANCE_INVARIANT, GenericTypeNode::VARIANCE_INVARIANT, - GenericTypeNode::VARIANCE_INVARIANT, ], )), ]), @@ -4111,20 +4112,21 @@ public function provideMultiLinePhpDocData(): iterable new IdentifierTypeNode('array'), [ new IdentifierTypeNode('string'), - ArrayShapeNode::createSealed([ - new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), - new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), - ]), - ArrayShapeNode::createSealed([ - new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), - new ArrayShapeItemNode(new IdentifierTypeNode('foo2'), false, new IdentifierTypeNode('true')), - new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + new UnionTypeNode([ + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('foo2'), false, new IdentifierTypeNode('true')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar1'), true, new IdentifierTypeNode('true')), + ]), ]), ], [ GenericTypeNode::VARIANCE_INVARIANT, GenericTypeNode::VARIANCE_INVARIANT, - GenericTypeNode::VARIANCE_INVARIANT, ], )), ]), @@ -4159,49 +4161,50 @@ public function provideMultiLinePhpDocData(): iterable [ new IdentifierTypeNode('string'), new UnionTypeNode([ - new GenericTypeNode( - new IdentifierTypeNode('class-string'), - [new IdentifierTypeNode('Factory\\FactoryInterface')], - [GenericTypeNode::VARIANCE_INVARIANT], - ), - new IdentifierTypeNode('Factory\\FactoryInterface'), - ]), - new CallableTypeNode( - new IdentifierTypeNode('callable'), - [ - new CallableTypeParameterNode(new IdentifierTypeNode('ContainerInterface'), false, false, '', false), - new CallableTypeParameterNode( - new NullableTypeNode( - new IdentifierTypeNode('string'), - ), - false, - false, - '', - false, + new UnionTypeNode([ + new GenericTypeNode( + new IdentifierTypeNode('class-string'), + [new IdentifierTypeNode('Factory\\FactoryInterface')], + [GenericTypeNode::VARIANCE_INVARIANT], ), - new CallableTypeParameterNode( - new UnionTypeNode([ - new GenericTypeNode( - new IdentifierTypeNode('array'), - [new IdentifierTypeNode('mixed')], - [GenericTypeNode::VARIANCE_INVARIANT], + new IdentifierTypeNode('Factory\\FactoryInterface'), + ]), + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [ + new CallableTypeParameterNode(new IdentifierTypeNode('ContainerInterface'), false, false, '', false), + new CallableTypeParameterNode( + new NullableTypeNode( + new IdentifierTypeNode('string'), ), - new IdentifierTypeNode('null'), - ]), - false, - false, - '', - false, - ), - ], - new IdentifierTypeNode('object'), - [], - ), + false, + false, + '', + false, + ), + new CallableTypeParameterNode( + new UnionTypeNode([ + new GenericTypeNode( + new IdentifierTypeNode('array'), + [new IdentifierTypeNode('mixed')], + [GenericTypeNode::VARIANCE_INVARIANT], + ), + new IdentifierTypeNode('null'), + ]), + false, + false, + '', + false, + ), + ], + new IdentifierTypeNode('object'), + [], + ), + ]), ], [ GenericTypeNode::VARIANCE_INVARIANT, GenericTypeNode::VARIANCE_INVARIANT, - GenericTypeNode::VARIANCE_INVARIANT, ], ), )),