From 5164f16dcfb1e86b506239d7d27f3cb25cbbca91 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 21 Jul 2023 15:41:44 +0200 Subject: [PATCH] New option to attach text between tags as description to the tag above --- src/Parser/PhpDocParser.php | 21 +++-- tests/PHPStan/Parser/PhpDocParserTest.php | 99 +++++++++++++++++++++++ 2 files changed, 115 insertions(+), 5 deletions(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 92804c57..c407c8b0 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -49,6 +49,9 @@ class PhpDocParser /** @var bool */ private $useIndexAttributes; + /** @var bool */ + private $textBetweenTagsBelongsToDescription; + /** * @param array{lines?: bool, indexes?: bool} $usedAttributes */ @@ -58,7 +61,8 @@ public function __construct( bool $requireWhitespaceBeforeDescription = false, bool $preserveTypeAliasesWithInvalidTypes = false, array $usedAttributes = [], - bool $parseDoctrineAnnotations = false + bool $parseDoctrineAnnotations = false, + bool $textBetweenTagsBelongsToDescription = false ) { $this->typeParser = $typeParser; @@ -68,6 +72,7 @@ public function __construct( $this->parseDoctrineAnnotations = $parseDoctrineAnnotations; $this->useLinesAttributes = $usedAttributes['lines'] ?? false; $this->useIndexAttributes = $usedAttributes['indexes'] ?? false; + $this->textBetweenTagsBelongsToDescription = $textBetweenTagsBelongsToDescription; } @@ -215,10 +220,13 @@ private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode $text = ''; $endTokens = [Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END]; + if ($this->textBetweenTagsBelongsToDescription) { + $endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END]; + } // if the next token is EOL, everything below is skipped and empty string is returned - while (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { - $text .= $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(...$endTokens); + while ($this->textBetweenTagsBelongsToDescription || !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + $text .= $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, ...$endTokens); // stop if we're not at EOL - meaning it's the end of PHPDoc if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { @@ -250,10 +258,13 @@ private function parseOptionalDescriptionAfterDoctrineTag(TokenIterator $tokens) $text = ''; $endTokens = [Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END]; + if ($this->textBetweenTagsBelongsToDescription) { + $endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END]; + } // if the next token is EOL, everything below is skipped and empty string is returned - while (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { - $text .= $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens); + while ($this->textBetweenTagsBelongsToDescription || !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + $text .= $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, Lexer::TOKEN_PHPDOC_EOL, ...$endTokens); // stop if we're not at EOL - meaning it's the end of PHPDoc if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 817ada88..32eb518b 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -6682,4 +6682,103 @@ public function testDoctrine( $this->assertEquals($expectedAnnotations, $parser->parse($input, $label), $label); } + /** + * @return iterable + */ + public function dataTextBetweenTagsBelongsToDescription(): iterable + { + yield [ + '/**' . PHP_EOL . + ' * Real description' . PHP_EOL . + ' * @param int $a' . PHP_EOL . + ' * paramA description' . PHP_EOL . + ' * @param int $b' . PHP_EOL . + ' * paramB description' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTextNode('Real description'), + new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$a', PHP_EOL . ' paramA description')), + new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$b', PHP_EOL . ' paramB description')), + ]), + ]; + + yield [ + '/**' . PHP_EOL . + ' * Real description' . PHP_EOL . + ' * @param int $a aaaa' . PHP_EOL . + ' * bbbb' . PHP_EOL . + ' *' . PHP_EOL . + ' * ccc' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTextNode('Real description'), + new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$a', 'aaaa' . PHP_EOL . ' bbbb' . PHP_EOL . PHP_EOL . 'ccc')), + ]), + ]; + + yield [ + '/**' . PHP_EOL . + ' * Real description' . PHP_EOL . + ' * @ORM\Column()' . PHP_EOL . + ' * bbbb' . PHP_EOL . + ' *' . PHP_EOL . + ' * ccc' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTextNode('Real description'), + new PhpDocTagNode('@ORM\Column', new DoctrineTagValueNode(new DoctrineAnnotation('@ORM\Column', []), PHP_EOL . ' bbbb' . PHP_EOL . PHP_EOL . 'ccc')), + ]), + ]; + + yield [ + '/**' . PHP_EOL . + ' * Real description' . PHP_EOL . + ' * @ORM\Column() aaaa' . PHP_EOL . + ' * bbbb' . PHP_EOL . + ' *' . PHP_EOL . + ' * ccc' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTextNode('Real description'), + new PhpDocTagNode('@ORM\Column', new DoctrineTagValueNode(new DoctrineAnnotation('@ORM\Column', []), 'aaaa' . PHP_EOL . ' bbbb' . PHP_EOL . PHP_EOL . 'ccc')), + ]), + ]; + + yield [ + '/**' . PHP_EOL . + ' * Real description' . PHP_EOL . + ' * @ORM\Column() aaaa' . PHP_EOL . + ' * bbbb' . PHP_EOL . + ' *' . PHP_EOL . + ' * ccc' . PHP_EOL . + ' * @param int $b' . PHP_EOL . + ' */', + new PhpDocNode([ + new PhpDocTextNode('Real description'), + new PhpDocTagNode('@ORM\Column', new DoctrineTagValueNode(new DoctrineAnnotation('@ORM\Column', []), 'aaaa' . PHP_EOL . ' bbbb' . PHP_EOL . PHP_EOL . 'ccc')), + new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$b', '')), + ]), + ]; + } + + /** + * @dataProvider dataTextBetweenTagsBelongsToDescription + */ + public function testTextBetweenTagsBelongsToDescription( + string $input, + PhpDocNode $expectedPhpDocNode + ): void + { + $constExprParser = new ConstExprParser(); + $typeParser = new TypeParser($constExprParser); + $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, [], true, true); + + $tokens = new TokenIterator($this->lexer->tokenize($input)); + $actualPhpDocNode = $phpDocParser->parse($tokens); + + $this->assertEquals($expectedPhpDocNode, $actualPhpDocNode); + $this->assertSame((string) $expectedPhpDocNode, (string) $actualPhpDocNode); + $this->assertSame(Lexer::TOKEN_END, $tokens->currentTokenType()); + } + }