From 83eab98933c149c8e1a7e91eefa54b955ebded1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Fri, 30 Oct 2026 19:22:51 +0000 Subject: [PATCH] Printer: keep the format of the tag value when the tag name changes Amp-Thread-ID: https://ampcode.com/threads/T-01a17930-0285-73c5-a957-4325c77aa1b2 --- src/Printer/Printer.php | 30 +++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 53 +++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 83e4a9a0..61deed7e 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -977,6 +977,22 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo continue; } + if ($this->isRenamedTag($node, $subNodeName, $originalTokens, $startPos)) { + // the tag name is the first token of the tag; keep the format of the value + $result .= $subNode; + $pos = $startPos + 1; + continue; + } + + if ( + $node instanceof DoctrineAnnotation + && $subNodeName === 'name' + && $node->getAttribute(Attribute::ORIGINAL_NODE)->name !== $originalTokens->getContentBetween($startPos, $startPos + 1) + ) { + // the tokens of the annotation of a Doctrine tag do not contain the name, the tag prints it + continue; + } + if (is_array($subNode) && is_array($origSubNode)) { // Array subnode changed, we might be able to reconstruct it $listResult = $this->printArrayFormatPreserving( @@ -1053,6 +1069,20 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo return $result . $originalTokens->getContentBetween($pos, $endPos + 1); } + private function isRenamedTag(Node $node, string $subNodeName, TokenIterator $originalTokens, int $startPos): bool + { + if (!$node instanceof PhpDocTagNode || $subNodeName !== 'name') { + return false; + } + + $originalNode = $node->getAttribute(Attribute::ORIGINAL_NODE); + if (!$originalNode instanceof PhpDocTagNode) { + return false; + } + + return $originalTokens->getContentBetween($startPos, $startPos + 1) === $originalNode->name; + } + /** * Prints a node that replaces the tokens of the original node. * diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 98682fcb..f69d7e09 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -2200,6 +2200,59 @@ public function enterNode(Node $node) }, ]; + $renameDoctrineTagVisitor = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocTagNode && $node->value instanceof DoctrineTagValueNode) { + $node->name = '@Bar'; + $node->value->annotation->name = '@Bar'; + } + + return $node; + } + + }; + + yield [ + '/**' . PHP_EOL . + ' * @\Foo\Bar(' . PHP_EOL . + ' * name="x",' . PHP_EOL . + ' * cols={@\Foo\Col("a")}' . PHP_EOL . + ' * )' . PHP_EOL . + ' */', + '/**' . PHP_EOL . + ' * @Bar(' . PHP_EOL . + ' * name="x",' . PHP_EOL . + ' * cols={@\Foo\Col("a")}' . PHP_EOL . + ' * )' . PHP_EOL . + ' */', + $renameDoctrineTagVisitor, + ]; + + yield [ + '/** @\Foo\Bar("a") some description */', + '/** @Bar("a") some description */', + $renameDoctrineTagVisitor, + ]; + + yield [ + '/** @param int $a */', + '/** @phpstan-param int $a */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocTagNode) { + $node->name = '@phpstan-param'; + } + + return $node; + } + + }, + ]; + yield [ '/**' . PHP_EOL . ' * @X({' . PHP_EOL .