diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index 530d3fb4..27d3fb92 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -5,7 +5,7 @@ name: "Deploy API Reference" on: push: branches: - - "2.0.x" + - "2.1.x" concurrency: group: "pages" @@ -21,6 +21,7 @@ jobs: branch: - "1.23.x" - "2.0.x" + - "2.1.x" steps: - name: "Checkout" diff --git a/.github/workflows/send-pr.yml b/.github/workflows/send-pr.yml index 8f389817..2503256e 100644 --- a/.github/workflows/send-pr.yml +++ b/.github/workflows/send-pr.yml @@ -23,7 +23,7 @@ jobs: repository: phpstan/phpstan-src path: phpstan-src token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - ref: 2.0.x + ref: 2.1.x - name: "Install dependencies" working-directory: ./phpstan-src diff --git a/LICENSE b/LICENSE index 98a854e4..e5f34e60 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2016 Ondřej Mirtes +Copyright (c) 2025 PHPStan s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +19,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index 87ded21a..a9738d62 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -205,6 +205,19 @@ 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); + } + + private function detectNewline(): void { $value = $this->currentTokenValue(); diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 84a3880d..b24561b6 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); - } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { - $type = $this->parseIntersection($tokens, $type); + } catch (ParserException $parserException) { + $enrichedType = null; + } + + if ($enrichedType !== null) { + $type = $enrichedType; + $tokens->dropSavePoint(); + + } 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 @@ -90,7 +117,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 +139,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); @@ -244,6 +271,14 @@ private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) { $types[] = $this->parseAtomic($tokens); + $tokens->pushSavePoint(); + $tokens->skipNewLineTokens(); + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { + $tokens->rollback(); + break; + } + + $tokens->dropSavePoint(); } return new Ast\Type\UnionTypeNode($types); @@ -256,9 +291,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); @@ -272,6 +307,14 @@ private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $typ while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) { $types[] = $this->parseAtomic($tokens); + $tokens->pushSavePoint(); + $tokens->skipNewLineTokens(); + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { + $tokens->rollback(); + break; + } + + $tokens->dropSavePoint(); } return new Ast\Type\IntersectionTypeNode($types); @@ -284,9 +327,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 +349,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 +378,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 +452,11 @@ 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->skipNewLineTokens(); // trailing comma case if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { @@ -419,7 +465,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 +556,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 +596,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 +605,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 +876,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 +885,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 +901,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 +991,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 +1024,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 +1049,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 +1057,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 7626580e..49ae1873 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -60,8 +60,11 @@ use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; 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; @@ -69,6 +72,7 @@ use PHPUnit\Framework\TestCase; use function count; use function sprintf; +use const DIRECTORY_SEPARATOR; use const PHP_EOL; class PhpDocParserTest extends TestCase @@ -122,6 +126,7 @@ protected function setUp(): void * @dataProvider provideDoctrineData * @dataProvider provideDoctrineWithoutDoctrineCheckData * @dataProvider provideCommentLikeDescriptions + * @dataProvider provideInlineTags */ public function testParse( string $label, @@ -4026,6 +4031,398 @@ 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'), + 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, + 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'), + 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, + 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 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, + ], + ), + )), + ]), + ]; + + yield [ + 'Multiline PHPDoc with multiple new line within union 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'), + 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')), + ]), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo2'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('foo3'), false, new IdentifierTypeNode('bool')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar2'), true, new IdentifierTypeNode('true')), + ]), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('foo3'), false, new IdentifierTypeNode('true')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar3'), true, new IdentifierTypeNode('false')), + ]), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + )), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + ), + false, + '$a', + '', + false, + )), + ]), + ]; + + yield [ + 'Multiline PHPDoc with multiple new line within intersection 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'), + new IntersectionTypeNode([ + 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')), + ]), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo2'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('foo3'), false, new IdentifierTypeNode('bool')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar2'), true, new IdentifierTypeNode('true')), + ]), + ArrayShapeNode::createSealed([ + new ArrayShapeItemNode(new IdentifierTypeNode('foo1'), false, new IdentifierTypeNode('int')), + new ArrayShapeItemNode(new IdentifierTypeNode('foo3'), false, new IdentifierTypeNode('true')), + new ArrayShapeItemNode(new IdentifierTypeNode('bar3'), true, new IdentifierTypeNode('false')), + ]), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + )), + ]), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ], + ), + false, + '$a', + '', + false, + )), + ]), + ]; + + yield [ + 'Multiline PHPDoc with multiple new line being invalid due to union and intersection type declaration', + '/**' . PHP_EOL . + ' * @param array,' . PHP_EOL . + ' * }> $a' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@param', new InvalidTagValueNode( + 'array,' . PHP_EOL . + '}> $a', + new ParserException( + '?', + Lexer::TOKEN_NULLABLE, + DIRECTORY_SEPARATOR === '\\' ? 65 : 62, + Lexer::TOKEN_CLOSE_CURLY_BRACKET, + null, + 4, + ), + )), + ]), + ]; + + /** + * @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 @@ -6024,6 +6421,27 @@ public function provideCommentLikeDescriptions(): Iterator ]; } + public function provideInlineTags(): Iterator + { + yield [ + 'Inline @link tag in @copyright', + '/**' . PHP_EOL . + ' * Unit tests for stored_progress_bar_cleanup' . PHP_EOL . + ' *' . PHP_EOL . + ' * @package core' . PHP_EOL . + ' * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}' . PHP_EOL . + ' * @\ORM\Entity() 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTextNode('Unit tests for stored_progress_bar_cleanup'), + new PhpDocTextNode(''), + new PhpDocTagNode('@package', new GenericTagValueNode('core')), + new PhpDocTagNode('@copyright', new GenericTagValueNode('2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}')), + new PhpDocTagNode('@\ORM\Entity', new DoctrineTagValueNode(new DoctrineAnnotation('@\ORM\Entity', []), '2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}')), + ]), + ]; + } + public function provideParamOutTagsData(): Iterator { yield [