diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index db7d3db7..00060544 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -69,6 +69,7 @@ use PHPStan\PhpDocParser\Parser\TokenIterator; use function array_keys; use function array_map; +use function array_shift; use function assert; use function count; use function get_class; @@ -77,6 +78,8 @@ use function in_array; use function is_array; use function preg_match_all; +use function preg_split; +use function rtrim; use function sprintf; use function str_replace; use function strlen; @@ -96,6 +99,12 @@ final class Printer /** @var Differ */ private Differ $differ; + /** + * Inserted before every continuation line of multi-line text inside a printed PhpDocTextNode or tag value. + * printFormatPreserving() sets it to the indentation detected in the original PHPDoc. + */ + private string $continuationLinePrefix = "\n * "; + /** * Map From "{$class}->{$subNode}" to string that should be inserted * between elements of this list subnode @@ -187,14 +196,18 @@ public function printFormatPreserving(PhpDocNode $node, PhpDocNode $originalNode }); $tokenIndex = 0; - $result = $this->printArrayFormatPreserving( - $node->children, - $originalNode->children, - $originalTokens, - $tokenIndex, - PhpDocNode::class, - 'children', - ); + try { + $result = $this->printArrayFormatPreserving( + $node->children, + $originalNode->children, + $originalTokens, + $tokenIndex, + PhpDocNode::class, + 'children', + ); + } finally { + $this->continuationLinePrefix = "\n * "; + } if ($result !== null) { return $result . $originalTokens->getContentBetween($tokenIndex, $originalTokens->getTokenCount()); } @@ -214,7 +227,7 @@ function (PhpDocChildNode $child): string { )) . "\n */"; } if ($node instanceof PhpDocTextNode) { - return $node->text; + return $this->printMultilineText($node->text); } if ($node instanceof PhpDocTagNode) { if ($node->value instanceof DoctrineTagValueNode) { @@ -224,7 +237,7 @@ function (PhpDocChildNode $child): string { return trim(sprintf('%s %s', $node->name, $this->print($node->value))); } if ($node instanceof PhpDocTagValueNode) { - return $this->printTagValue($node); + return $this->printMultilineText($this->printTagValue($node)); } if ($node instanceof TypeNode) { return $this->printType($node); @@ -292,6 +305,25 @@ function (PhpDocChildNode $child): string { throw new LogicException(sprintf('Unknown node type %s', get_class($node))); } + private function printMultilineText(string $text): string + { + if (strpos($text, "\n") === false) { + return $text; + } + + $lines = preg_split('~\r?\n~', $text); + if ($lines === false) { + return $text; + } + + $result = array_shift($lines); + foreach ($lines as $line) { + $result .= $line === '' ? rtrim($this->continuationLinePrefix) : $this->continuationLinePrefix . $line; + } + + return $result; + } + private function printTagValue(PhpDocTagValueNode $node): string { // only nodes that contain another node are handled here @@ -590,6 +622,8 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, if ($insertStr === "\n * ") { $insertStr = sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); + // the lexer strips exactly one space after the asterisk, any further indentation stays in the text + $this->continuationLinePrefix = sprintf('%s%s* ', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent); } foreach ($diff as $i => $diffElem) { diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 3f791927..70ef7ff1 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -24,6 +24,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessParameterIsPassedTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; @@ -2690,6 +2691,88 @@ public function enterNode(Node $node) */'), $addCommentToObjectShapeItemMiddle, ]; + $changeMultilineText = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocTextNode) { + $node->text = str_replace('Foo', 'Bar', $node->text); + } + if ($node instanceof ParamTagValueNode) { + $node->description = str_replace('Foo', 'Bar', $node->description); + } + + return $node; + } + + }; + + yield [ + self::nowdoc(' + /** + * First line Foo + * second line Foo + * + * Third line Foo + * + * @param int $a Foo description + * continues Foo + * @param int $b + */'), + self::nowdoc(' + /** + * First line Bar + * second line Bar + * + * Third line Bar + * + * @param int $a Bar description + * continues Bar + * @param int $b + */'), + $changeMultilineText, + ]; + + yield [ + self::nowdoc(' + /** + * First line Foo + * second line Foo + */'), + self::nowdoc(' + /** + * First line Bar + * second line Bar + */'), + $changeMultilineText, + ]; + + $addMultilineText = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof PhpDocNode) { + array_unshift($node->children, new PhpDocTextNode("Added first\nadded second")); + } + + return $node; + } + + }; + + yield [ + self::nowdoc(' + /** + * @param int $a + */'), + self::nowdoc(' + /** + * Added first + * added second + * @param int $a + */'), + $addMultilineText, + ]; } /** @@ -2951,6 +3034,28 @@ public function dataPrintPhpDocNode(): iterable ]), '/** * @param int $a + */', + ]; + yield [ + new PhpDocNode([ + new PhpDocTextNode("First line\nsecond line\n\nthird line"), + new PhpDocTextNode(''), + new PhpDocTagNode('@param', new ParamTagValueNode( + new IdentifierTypeNode('int'), + false, + '$a', + "description\n continues", + false, + )), + ]), + '/** + * First line + * second line + * + * third line + * + * @param int $a description + * continues */', ]; }