From 70bd0427d27730dc6699634fcb631cad5829772a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 27 Oct 2026 20:41:03 +0000 Subject: [PATCH] Printer: remove the separator after a removed first list item Removing the first type from A|B|C printed |B|C. Removing the first type from (A & B) printed ( & B). Amp-Thread-ID: https://ampcode.com/threads/T-01a17930-0285-73c5-a957-4325c77aa1b2 --- src/Printer/Printer.php | 24 +++++++++---- tests/PHPStan/Printer/PrinterTest.php | 49 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 83fc14df..c80315eb 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -603,6 +603,7 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, $insertStr = $this->listInsertionMap[$mapKey] ?? null; $result = ''; $beforeFirstKeepOrReplace = true; + $skipRemovedNode = false; $delayedAdd = []; $insertNewline = false; @@ -643,7 +644,12 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, $commentStartPos = count($origComments) > 0 ? $origComments[0]->startIndex : $itemStartPos; assert($commentStartPos >= 0); - $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); + if ($skipRemovedNode) { + // the separator after the removed first item belongs to it + $skipRemovedNode = false; + } else { + $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); + } if (count($delayedAdd) > 0) { foreach ($delayedAdd as $delayedAddNode) { @@ -760,12 +766,18 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, if ($i === 0) { // If we're removing from the start, keep the tokens before the node and drop those after it, // instead of the other way around. - $originalTokensArray = $originalTokens->getTokens(); - for ($j = $tokenIndex; $j < $itemStartPos; $j++) { - if ($originalTokensArray[$j][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) { - break; + if ($mapKey === PhpDocNode::class . '->children') { + // the line of the removed child is dropped, the next child keeps its own line + $originalTokensArray = $originalTokens->getTokens(); + for ($j = $tokenIndex; $j < $itemStartPos; $j++) { + if ($originalTokensArray[$j][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) { + break; + } + $result .= $originalTokensArray[$j][Lexer::VALUE_OFFSET]; } - $result .= $originalTokensArray[$j][Lexer::VALUE_OFFSET]; + } else { + $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); + $skipRemovedNode = true; } } diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 860d6b5f..2b6fa576 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -58,6 +58,7 @@ use PHPUnit\Framework\TestCase; use function array_map; use function array_pop; +use function array_shift; use function array_slice; use function array_splice; use function array_unshift; @@ -2850,6 +2851,54 @@ public function enterNode(Node $node) */'), $addCommentToObjectShapeItemMiddle, ]; + + $removeFirstType = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof UnionTypeNode || $node instanceof IntersectionTypeNode) { + array_shift($node->types); + } + + return $node; + } + + }; + + yield [ + '/** @var A|B|C */', + '/** @var B|C */', + $removeFirstType, + ]; + + yield [ + '/** @var A | B | C */', + '/** @var B | C */', + $removeFirstType, + ]; + + yield [ + '/** @var (A & B & C) */', + '/** @var (B & C) */', + $removeFirstType, + ]; + + yield [ + '/** @var A|B|C|D */', + '/** @var C|D */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof UnionTypeNode) { + $node->types = [$node->types[2], $node->types[3]]; + } + + return $node; + } + + }, + ]; } /**