From ec58baf7b3c7f1c81b3b00617c953249fb8cf30c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 31 May 2023 17:46:59 +0200 Subject: [PATCH] Multiple Doctrine tags on a single line --- src/Parser/PhpDocParser.php | 112 +++++++- tests/PHPStan/Parser/PhpDocParserTest.php | 330 +++++++++++++++++++++- 2 files changed, 422 insertions(+), 20 deletions(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 687f7542..33d68968 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -78,10 +78,44 @@ public function parse(TokenIterator $tokens): Ast\PhpDoc\PhpDocNode $children = []; - if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { - $children[] = $this->parseChild($tokens); - while ($tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL) && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { + if ($this->parseDoctrineAnnotations) { + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { + $lastChild = $this->parseChild($tokens); + $children[] = $lastChild; + while (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { + if ( + $lastChild instanceof Ast\PhpDoc\PhpDocTagNode + && ( + $lastChild->value instanceof Doctrine\DoctrineTagValueNode + || $lastChild->value instanceof Ast\PhpDoc\GenericTagValueNode + ) + ) { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { + break; + } + $lastChild = $this->parseChild($tokens); + $children[] = $lastChild; + continue; + } + + if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + break; + } + if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { + break; + } + + $lastChild = $this->parseChild($tokens); + $children[] = $lastChild; + } + } + } else { + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { $children[] = $this->parseChild($tokens); + while ($tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL) && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { + $children[] = $this->parseChild($tokens); + } } } @@ -119,6 +153,7 @@ public function parse(TokenIterator $tokens): Ast\PhpDoc\PhpDocNode } + /** @phpstan-impure */ private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode { if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) { @@ -202,6 +237,63 @@ private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode } + private function parseOptionalDescriptionAfterDoctrineTag(TokenIterator $tokens): string + { + $text = ''; + + while (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + $text .= $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END); + + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + if (!$tokens->isPrecededByHorizontalWhitespace()) { + return trim($text . $this->parseText($tokens)->text, " \t"); + } + if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) { + $tokens->pushSavePoint(); + $child = $this->parseChild($tokens); + if ($child instanceof Ast\PhpDoc\PhpDocTagNode) { + if ( + $child->value instanceof Ast\PhpDoc\GenericTagValueNode + || $child->value instanceof Doctrine\DoctrineTagValueNode + ) { + $tokens->rollback(); + break; + } + if ($child->value instanceof Ast\PhpDoc\InvalidTagValueNode) { + $tokens->rollback(); + $tokens->pushSavePoint(); + $tokens->next(); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { + $tokens->rollback(); + break; + } + $tokens->rollback(); + return trim($text . $this->parseText($tokens)->text, " \t"); + } + } + + $tokens->rollback(); + return trim($text . $this->parseText($tokens)->text, " \t"); + } + break; + } + + $tokens->pushSavePoint(); + $tokens->next(); + + if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END)) { + $tokens->rollback(); + break; + } + + $tokens->dropSavePoint(); + $text .= "\n"; + } + + return trim($text, " \t"); + } + + public function parseTag(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagNode { $tag = $tokens->currentTokenValue(); @@ -333,15 +425,17 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph break; default: - if ( - $this->parseDoctrineAnnotations - && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES) - ) { - $tagValue = $this->parseDoctrineTagValue($tokens, $tag); + if ($this->parseDoctrineAnnotations) { + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { + $tagValue = $this->parseDoctrineTagValue($tokens, $tag); + } else { + $tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescriptionAfterDoctrineTag($tokens)); + } break; } $tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescription($tokens)); + break; } @@ -368,7 +462,7 @@ private function parseDoctrineTagValue(TokenIterator $tokens, string $tag): Ast\ $startLine, $startIndex ), - $this->parseOptionalDescription($tokens) + $this->parseOptionalDescriptionAfterDoctrineTag($tokens) ); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 3c32721a..fc92640b 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -155,8 +155,8 @@ private function executeTestParse(PhpDocParser $phpDocParser, string $label, str $actualPhpDocNode = $phpDocParser->parse($tokens); $this->assertEquals($expectedPhpDocNode, $actualPhpDocNode, $label); - $this->assertSame((string) $expectedPhpDocNode, (string) $actualPhpDocNode); - $this->assertSame(Lexer::TOKEN_END, $tokens->currentTokenType()); + $this->assertSame((string) $expectedPhpDocNode, (string) $actualPhpDocNode, $label); + $this->assertSame(Lexer::TOKEN_END, $tokens->currentTokenType(), $label); } @@ -2544,7 +2544,11 @@ public function provideSingleLinePhpDocData(): Iterator new PhpDocNode([ new PhpDocTagNode( '@foo', - new GenericTagValueNode('lorem @bar ipsum') + new GenericTagValueNode('lorem') + ), + new PhpDocTagNode( + '@bar', + new GenericTagValueNode('ipsum') ), ]), ]; @@ -5767,6 +5771,223 @@ public function provideDoctrineData(): Iterator null, [$x], ]; + + yield [ + 'Multiline tag behaviour 1', + '/**' . PHP_EOL . + ' * @X() test' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@X', new DoctrineTagValueNode(new DoctrineAnnotation( + '@X', + [] + ), 'test')), + ]), + null, + null, + [new Doctrine\X()], + ]; + + yield [ + 'Multiline tag behaviour 2', + '/**' . PHP_EOL . + ' * @X() test' . PHP_EOL . + ' * test2' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@X', new DoctrineTagValueNode(new DoctrineAnnotation( + '@X', + [] + ), "test\ntest2")), + ]), + null, + null, + [new Doctrine\X()], + ]; + yield [ + 'Multiline tag behaviour 3', + '/**' . PHP_EOL . + ' * @X() test' . PHP_EOL . + ' *' . PHP_EOL . + ' * test2' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@X', new DoctrineTagValueNode(new DoctrineAnnotation( + '@X', + [] + ), 'test')), + new PhpDocTextNode(''), + new PhpDocTextNode('test2'), + ]), + null, + null, + [new Doctrine\X()], + ]; + yield [ + 'Multiline tag behaviour 4', + '/**' . PHP_EOL . + ' * @X() test' . PHP_EOL . + ' *' . PHP_EOL . + ' * test2' . PHP_EOL . + ' * @Z()' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@X', new DoctrineTagValueNode(new DoctrineAnnotation( + '@X', + [] + ), 'test')), + new PhpDocTextNode(''), + new PhpDocTextNode('test2'), + new PhpDocTagNode('@Z', new DoctrineTagValueNode(new DoctrineAnnotation( + '@Z', + [] + ), '')), + ]), + null, + null, + [new Doctrine\X(), new Doctrine\Z()], + ]; + + yield [ + 'Multiline generic tag behaviour 1', + '/**' . PHP_EOL . + ' * @X test' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@X', new GenericTagValueNode('test')), + ]), + null, + null, + [new Doctrine\X()], + ]; + + yield [ + 'Multiline generic tag behaviour 2', + '/**' . PHP_EOL . + ' * @X test' . PHP_EOL . + ' * test2' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@X', new GenericTagValueNode("test\ntest2")), + ]), + null, + null, + [new Doctrine\X()], + ]; + yield [ + 'Multiline generic tag behaviour 3', + '/**' . PHP_EOL . + ' * @X test' . PHP_EOL . + ' *' . PHP_EOL . + ' * test2' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@X', new GenericTagValueNode('test')), + new PhpDocTextNode(''), + new PhpDocTextNode('test2'), + ]), + null, + null, + [new Doctrine\X()], + ]; + yield [ + 'Multiline generic tag behaviour 4', + '/**' . PHP_EOL . + ' * @X test' . PHP_EOL . + ' *' . PHP_EOL . + ' * test2' . PHP_EOL . + ' * @Z' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTagNode('@X', new GenericTagValueNode('test')), + new PhpDocTextNode(''), + new PhpDocTextNode('test2'), + new PhpDocTagNode('@Z', new GenericTagValueNode('')), + ]), + null, + null, + [new Doctrine\X(), new Doctrine\Z()], + ]; + + yield [ + 'More tags on the same line', + '/** @X() @Z() */', + new PhpDocNode([ + new PhpDocTagNode('@X', new DoctrineTagValueNode(new DoctrineAnnotation('@X', []), '')), + new PhpDocTagNode('@Z', new DoctrineTagValueNode(new DoctrineAnnotation('@Z', []), '')), + ]), + null, + null, + [new Doctrine\X(), new Doctrine\Z()], + ]; + + yield [ + 'More tags on the same line with description inbetween', + '/** @X() test @Z() */', + new PhpDocNode([ + new PhpDocTagNode('@X', new DoctrineTagValueNode(new DoctrineAnnotation('@X', []), 'test')), + new PhpDocTagNode('@Z', new DoctrineTagValueNode(new DoctrineAnnotation('@Z', []), '')), + ]), + null, + null, + [new Doctrine\X(), new Doctrine\Z()], + ]; + + yield [ + 'More tags on the same line with description inbetween, first one generic', + '/** @X test @Z() */', + new PhpDocNode([ + new PhpDocTagNode('@X', new GenericTagValueNode('test')), + new PhpDocTagNode('@Z', new DoctrineTagValueNode(new DoctrineAnnotation('@Z', []), '')), + ]), + null, + null, + [new Doctrine\X(), new Doctrine\Z()], + ]; + + yield [ + 'More generic tags on the same line with description inbetween, 2nd one @param which should become description', + '/** @X @phpstan-param int $z */', + new PhpDocNode([ + new PhpDocTagNode('@X', new GenericTagValueNode('@phpstan-param int $z')), + ]), + null, + null, + [new Doctrine\X()], + ]; + + yield [ + 'More generic tags on the same line with description inbetween, 2nd one @param which should become description can have a parse error', + '/** @X @phpstan-param |int $z */', + new PhpDocNode([ + new PhpDocTagNode('@X', new GenericTagValueNode('@phpstan-param |int $z')), + ]), + null, + null, + [new Doctrine\X()], + ]; + + yield [ + 'More tags on the same line with description inbetween, 2nd one @param which should become description', + '/** @X() @phpstan-param int $z */', + new PhpDocNode([ + new PhpDocTagNode('@X', new DoctrineTagValueNode(new DoctrineAnnotation('@X', []), '@phpstan-param int $z')), + ]), + null, + null, + [new Doctrine\X()], + ]; + + yield [ + 'More tags on the same line with description inbetween, 2nd one @param which should become description can have a parse error', + '/** @X() @phpstan-param |int $z */', + new PhpDocNode([ + new PhpDocTagNode('@X', new DoctrineTagValueNode(new DoctrineAnnotation('@X', []), '@phpstan-param |int $z')), + ]), + null, + null, + [new Doctrine\X()], + ]; } public function provideDoctrineWithoutDoctrineCheckData(): Iterator @@ -5840,14 +6061,24 @@ public function provideDoctrineWithoutDoctrineCheckData(): Iterator ]), ]; - //yield [ - // 'Multiple on one line', - // '/** - // * @DummyId @DummyColumn(type="integer") @DummyGeneratedValue - // * @var int - // */', - // new PhpDocNode([]), - // ]; + yield [ + 'Multiple on one line', + '/** + * @DummyId @DummyColumn(type="integer") @DummyGeneratedValue + * @var int + */', + new PhpDocNode([ + new PhpDocTagNode('@DummyId', new GenericTagValueNode('')), + new PhpDocTagNode('@DummyColumn', new DoctrineTagValueNode( + new DoctrineAnnotation('@DummyColumn', [ + new DoctrineArgument(new IdentifierTypeNode('type'), new ConstExprStringNode('integer')), + ]), + '' + )), + new PhpDocTagNode('@DummyGeneratedValue', new GenericTagValueNode('')), + new PhpDocTagNode('@var', new VarTagValueNode(new IdentifierTypeNode('int'), '', '')), + ]), + ]; yield [ 'Parse error with dashes', @@ -5958,6 +6189,83 @@ public function provideDoctrineWithoutDoctrineCheckData(): Iterator // '/** @Doctrine\Tests\Common\Annotations\Name(foo="""bar""") */', // new PhpDocNode([]), //]; + + yield [ + 'More tags on the same line with description inbetween, second Doctrine one cannot have parse error', + '/** @X test @Z(test= */', + new PhpDocNode([ + new PhpDocTagNode('@X', new GenericTagValueNode('test')), + new PhpDocTagNode('@Z', new InvalidTagValueNode('(test=', new ParserException( + '=', + 14, + 19, + 5, + null, + 1 + ))), + ]), + null, + null, + [new Doctrine\X()], + ]; + + yield [ + 'More tags on the same line with description inbetween, second Doctrine one cannot have parse error 2', + '/** @X() test @Z(test= */', + new PhpDocNode([ + new PhpDocTagNode('@X', new DoctrineTagValueNode(new DoctrineAnnotation('@X', []), 'test')), + new PhpDocTagNode('@Z', new InvalidTagValueNode('(test=', new ParserException( + '=', + 14, + 21, + 5, + null, + 1 + ))), + ]), + null, + null, + [new Doctrine\X()], + ]; + + yield [ + 'Doctrine tag after common tag is just a description', + '/** @phpstan-param int $z @X() */', + new PhpDocNode([ + new PhpDocTagNode('@phpstan-param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$z', + '@X()' + )), + ]), + ]; + + yield [ + 'Doctrine tag after common tag is just a description 2', + '/** @phpstan-param int $z @\X\Y() */', + new PhpDocNode([ + new PhpDocTagNode('@phpstan-param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$z', + '@\X\Y()' + )), + ]), + ]; + + yield [ + 'Generic tag after common tag is just a description', + '/** @phpstan-param int $z @X */', + new PhpDocNode([ + new PhpDocTagNode('@phpstan-param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$z', + '@X' + )), + ]), + ]; } public function provideSpecializedTags(): Iterator