From d084f5787505d8f192a90f3bbc6fcfc38ed65767 Mon Sep 17 00:00:00 2001 From: tmdk Date: Wed, 25 Feb 2026 23:08:12 +0000 Subject: [PATCH 1/4] Fix duplicate star on continuation lines starting with * in tag descriptions When a tag description contains continuation lines beginning with *, the * was duplicated in the parsed output. This affected block-style @param descriptions (e.g. WordPress-style docblocks with @type lines and star- prefixed list items). Root cause: tokenizeLine() split TOKEN_PHPDOC_EOL tokens with trailing whitespace into a bare EOL + TOKEN_HORIZONTAL_WS pair, placing * in both token values. When PHPStan rolled back past such a token on encountering a tag-like token (@type), joinUntil() concatenated both raw values, doubling the star. Fix: prefix every continuation line with "* " before tokenizing. The lexer always consumes the inserted "* " as part of TOKEN_PHPDOC_EOL, leaving original indentation as a separate subsequent token. An unconditional trim(..., "* \t") on every EOL token value strips the inserted "* " back out. The manual split into TOKEN_HORIZONTAL_WS is no longer needed. Co-Authored-By: Claude Sonnet 4.6 --- .../Tags/Factory/AbstractPHPStanFactory.php | 22 ++++------- .../integration/InterpretingDocBlocksTest.php | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php index 35a981e5..456a2645 100644 --- a/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php +++ b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php @@ -61,7 +61,7 @@ public function __construct(PHPStanFactory ...$factories) public function create(string $tagLine, ?TypeContext $context = null): Tag { try { - $tokens = $this->tokenizeLine($tagLine . "\n"); + $tokens = $this->tokenizeLine($tagLine); $ast = $this->parser->parseTag($tokens); if (property_exists($ast->value, 'description') === true) { $ast->value->setAttribute( @@ -104,21 +104,15 @@ public function create(string $tagLine, ?TypeContext $context = null): Tag */ private function tokenizeLine(string $tagLine): TokenIterator { - $tokens = $this->lexer->tokenize($tagLine); + // Prefix continuation lines with "* ", which is consumed by the phpstan parser as TOKEN_PHPDOC_EOL. + $tagLine = str_replace("\n", "\n* ", $tagLine); + $tokens = $this->lexer->tokenize($tagLine . "\n"); $fixed = []; foreach ($tokens as $token) { - if (($token[1] === Lexer::TOKEN_PHPDOC_EOL) && rtrim($token[0], " \t") !== $token[0]) { - $fixed[] = [ - rtrim($token[Lexer::VALUE_OFFSET], " \t"), - Lexer::TOKEN_PHPDOC_EOL, - $token[2] ?? 0, - ]; - $fixed[] = [ - ltrim($token[Lexer::VALUE_OFFSET], "\n\r"), - Lexer::TOKEN_HORIZONTAL_WS, - ($token[2] ?? 0) + 1, - ]; - continue; + if ($token[Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) { + // Strip "* " prefix (and other horizontal whitespace) again so it doesn't and up in the + // description when we joinUntil() in create(). + $token[Lexer::VALUE_OFFSET] = trim($token[Lexer::VALUE_OFFSET], "* \t"); } $fixed[] = $token; diff --git a/tests/integration/InterpretingDocBlocksTest.php b/tests/integration/InterpretingDocBlocksTest.php index 4ea86680..64644f42 100644 --- a/tests/integration/InterpretingDocBlocksTest.php +++ b/tests/integration/InterpretingDocBlocksTest.php @@ -489,4 +489,42 @@ public function testParamTagDescriptionIsCorrectly(): void self::assertSame('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas varius, tellus in cursus dictum, justo odio sagittis velit, id iaculis mi dui id nisi.', (string) $paramTags->getDescription()); } + + public function testParamBlockDescriptionPreservesStarContinuationLines(): void + { + $docComment = <<create($docComment); + + self::assertEquals( + [ + new Param( + 'foo', + new Array_(), + false, + new Description( + '{' . "\n" . + ' Description of foo.' . "\n" . + "\n" . + ' @type string $bar Description of bar with' . "\n" . + ' * a list' . "\n" . + ' * spanning *multiple* lines' . "\n" . + '}' + ), + ), + ], + $docblock->getTags() + ); + } } From cd0c39367e6f0c4df85b5e693acc6f3a3d7f03c3 Mon Sep 17 00:00:00 2001 From: Tobias van Dijk Date: Sun, 1 Mar 2026 14:38:30 +0100 Subject: [PATCH 2/4] fix code style issues --- src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php index 456a2645..99ba99dd 100644 --- a/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php +++ b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php @@ -25,9 +25,10 @@ use PHPStan\PhpDocParser\ParserConfig; use RuntimeException; -use function ltrim; use function property_exists; use function rtrim; +use function str_replace; +use function trim; /** * Factory class creating tags using phpstan's parser From 7a64be90afc131cf2b8d97fd9e4a4e3890459f8b Mon Sep 17 00:00:00 2001 From: Tobias van Dijk Date: Sun, 1 Mar 2026 14:39:06 +0100 Subject: [PATCH 3/4] convert description in test to heredoc --- tests/integration/InterpretingDocBlocksTest.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/integration/InterpretingDocBlocksTest.php b/tests/integration/InterpretingDocBlocksTest.php index 64644f42..4d5e9dba 100644 --- a/tests/integration/InterpretingDocBlocksTest.php +++ b/tests/integration/InterpretingDocBlocksTest.php @@ -513,14 +513,15 @@ public function testParamBlockDescriptionPreservesStarContinuationLines(): void 'foo', new Array_(), false, - new Description( - '{' . "\n" . - ' Description of foo.' . "\n" . - "\n" . - ' @type string $bar Description of bar with' . "\n" . - ' * a list' . "\n" . - ' * spanning *multiple* lines' . "\n" . - '}' + new Description(<<<'DESCRIPTION' +{ + Description of foo. + + @type string $bar Description of bar with + * a list + * spanning *multiple* lines +} +DESCRIPTION ), ), ], From bc1d9aa432d784e73609c42a7e5d7c8dbb866a35 Mon Sep 17 00:00:00 2001 From: Tobias van Dijk Date: Sun, 1 Mar 2026 14:47:44 +0100 Subject: [PATCH 4/4] modify copy of token instead of original token to fix value --- src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php index 99ba99dd..770a09a3 100644 --- a/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php +++ b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php @@ -113,7 +113,13 @@ private function tokenizeLine(string $tagLine): TokenIterator if ($token[Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) { // Strip "* " prefix (and other horizontal whitespace) again so it doesn't and up in the // description when we joinUntil() in create(). - $token[Lexer::VALUE_OFFSET] = trim($token[Lexer::VALUE_OFFSET], "* \t"); + $fixed[] = [ + Lexer::VALUE_OFFSET => trim($token[Lexer::VALUE_OFFSET], "* \t"), + Lexer::TYPE_OFFSET => $token[Lexer::TYPE_OFFSET], + Lexer::LINE_OFFSET => $token[Lexer::LINE_OFFSET] ?? 0, + ]; + + continue; } $fixed[] = $token;