From b18e158ec0f13092bce6c0446c9c857c9dd0db0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Mon, 26 Oct 2026 18:07:29 +0000 Subject: [PATCH] Printer: indent a new tag like the other tags When the printer added a child to a PHPDoc, it used the largest indentation after the asterisk in the PHPDoc. The next lines of a multi-line description often have a larger indentation, so the new tag got it too. Now the printer uses the indentation of the line where the last existing child starts. Amp-Thread-ID: https://ampcode.com/threads/T-01a17930-0285-73c5-a957-4325c77aa1b2 --- src/Printer/Printer.php | 39 +++++++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 46 +++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index fe049aee..83fc14df 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -76,6 +76,7 @@ use function implode; use function in_array; use function is_array; +use function preg_match; use function preg_match_all; use function preg_replace_callback; use function sprintf; @@ -608,6 +609,11 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, [$isMultiline, $beforeAsteriskIndent, $afterAsteriskIndent] = $this->isMultiline($tokenIndex, $originalNodes, $originalTokens); if ($insertStr === "\n * ") { + $lastChildIndent = $this->detectLastChildIndent($originalNodes, $originalTokens); + if ($lastChildIndent !== null) { + [$beforeAsteriskIndent, $afterAsteriskIndent] = $lastChildIndent; + } + $insertStr = sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); } @@ -869,6 +875,39 @@ private function isMultiline(int $initialIndex, array $nodes, TokenIterator $ori return [$isMultiline, $before, $after]; } + /** + * Indentation before and after the asterisk on the line where the last child of a PHPDoc starts. + * Other lines, e.g. the next lines of a multi-line description, can have a different indentation. + * + * @param Node[] $originalNodes + * @return array{string, string}|null + */ + private function detectLastChildIndent(array $originalNodes, TokenIterator $originalTokens): ?array + { + $tokens = $originalTokens->getTokens(); + for ($i = count($originalNodes) - 1; $i >= 0; $i--) { + $index = $originalNodes[$i]->getAttribute(Attribute::START_INDEX) - 1; + $lineStart = ''; + while (isset($tokens[$index]) && $tokens[$index][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) { + $lineStart = $tokens[$index][Lexer::VALUE_OFFSET] . $lineStart; + $index--; + } + + if (!isset($tokens[$index]) || $tokens[$index][Lexer::TYPE_OFFSET] !== Lexer::TOKEN_PHPDOC_EOL) { + continue; + } + + $lineStart = $tokens[$index][Lexer::VALUE_OFFSET] . $lineStart; + if (preg_match('~\n(?[\x09\x20]*)\*(?[\x09\x20]*)$~', $lineStart, $match) !== 1) { + continue; + } + + return [$match['before'], $match['after']]; + } + + return null; + } + private function printNodeFormatPreserving(Node $node, TokenIterator $originalTokens): string { /** @var Node|null $originalNode */ diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 4f6c2c9c..860d6b5f 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -30,6 +30,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessParameterIsPassedTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; @@ -195,6 +196,51 @@ public function enterNode(Node $node) $removeFirst, ]; + $addThrows = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocNode) { + $node->children[] = new PhpDocTagNode('@throws', new ThrowsTagValueNode(new IdentifierTypeNode('Exception'), '')); + } + + return $node; + } + + }; + + yield [ + self::nowdoc(' + /** + * @param int $foo multi + * line description + */'), + self::nowdoc(' + /** + * @param int $foo multi + * line description + * @throws Exception + */'), + $addThrows, + ]; + + yield [ + self::nowdoc(' + /** + * @param int $foo multi + * line description + * @return void + */'), + self::nowdoc(' + /** + * @param int $foo multi + * line description + * @return void + * @throws Exception + */'), + $addThrows, + ]; + $removeLast = new class extends AbstractNodeVisitor { public function enterNode(Node $node)