Skip to content

Commit 736df6a

Browse files
committed
Printer: keep the "*" prefix on continuation lines of multi-line text
PhpDocParser::parseText() and the description parsers join continuation lines with a bare newline and drop the " * " prefix. Printer::print() emitted that text verbatim, so a changed or inserted multi-line PhpDocTextNode or tag description printed continuation lines without the asterisk. printNodeFormatPreserving() falls back to print() when a string sub-node changes, so printFormatPreserving() had the same problem. print() now re-inserts the line prefix at every newline inside a text node or tag value. The default prefix is "\n * ". printFormatPreserving() uses the newline and before-asterisk indentation detected from the original tokens. The lexer strips exactly one space after the asterisk, so the prefix ends with a single space and any deeper indentation stays in the text. Blank lines print as " *" without a trailing space. Fixes #317 Co-Authored-By: Claude Code
1 parent 1fc50c2 commit 736df6a

2 files changed

Lines changed: 149 additions & 10 deletions

File tree

src/Printer/Printer.php

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
use PHPStan\PhpDocParser\Parser\TokenIterator;
7070
use function array_keys;
7171
use function array_map;
72+
use function array_shift;
7273
use function assert;
7374
use function count;
7475
use function get_class;
@@ -77,6 +78,8 @@
7778
use function in_array;
7879
use function is_array;
7980
use function preg_match_all;
81+
use function preg_split;
82+
use function rtrim;
8083
use function sprintf;
8184
use function str_replace;
8285
use function strlen;
@@ -96,6 +99,12 @@ final class Printer
9699
/** @var Differ<Node> */
97100
private Differ $differ;
98101

102+
/**
103+
* Inserted before every continuation line of multi-line text inside a printed PhpDocTextNode or tag value.
104+
* printFormatPreserving() sets it to the indentation detected in the original PHPDoc.
105+
*/
106+
private string $continuationLinePrefix = "\n * ";
107+
99108
/**
100109
* Map From "{$class}->{$subNode}" to string that should be inserted
101110
* between elements of this list subnode
@@ -187,14 +196,18 @@ public function printFormatPreserving(PhpDocNode $node, PhpDocNode $originalNode
187196
});
188197

189198
$tokenIndex = 0;
190-
$result = $this->printArrayFormatPreserving(
191-
$node->children,
192-
$originalNode->children,
193-
$originalTokens,
194-
$tokenIndex,
195-
PhpDocNode::class,
196-
'children',
197-
);
199+
try {
200+
$result = $this->printArrayFormatPreserving(
201+
$node->children,
202+
$originalNode->children,
203+
$originalTokens,
204+
$tokenIndex,
205+
PhpDocNode::class,
206+
'children',
207+
);
208+
} finally {
209+
$this->continuationLinePrefix = "\n * ";
210+
}
198211
if ($result !== null) {
199212
return $result . $originalTokens->getContentBetween($tokenIndex, $originalTokens->getTokenCount());
200213
}
@@ -214,7 +227,7 @@ function (PhpDocChildNode $child): string {
214227
)) . "\n */";
215228
}
216229
if ($node instanceof PhpDocTextNode) {
217-
return $node->text;
230+
return $this->printMultilineText($node->text);
218231
}
219232
if ($node instanceof PhpDocTagNode) {
220233
if ($node->value instanceof DoctrineTagValueNode) {
@@ -224,7 +237,7 @@ function (PhpDocChildNode $child): string {
224237
return trim(sprintf('%s %s', $node->name, $this->print($node->value)));
225238
}
226239
if ($node instanceof PhpDocTagValueNode) {
227-
return $this->printTagValue($node);
240+
return $this->printMultilineText($this->printTagValue($node));
228241
}
229242
if ($node instanceof TypeNode) {
230243
return $this->printType($node);
@@ -292,6 +305,25 @@ function (PhpDocChildNode $child): string {
292305
throw new LogicException(sprintf('Unknown node type %s', get_class($node)));
293306
}
294307

308+
private function printMultilineText(string $text): string
309+
{
310+
if (strpos($text, "\n") === false) {
311+
return $text;
312+
}
313+
314+
$lines = preg_split('~\r?\n~', $text);
315+
if ($lines === false) {
316+
return $text;
317+
}
318+
319+
$result = array_shift($lines);
320+
foreach ($lines as $line) {
321+
$result .= $line === '' ? rtrim($this->continuationLinePrefix) : $this->continuationLinePrefix . $line;
322+
}
323+
324+
return $result;
325+
}
326+
295327
private function printTagValue(PhpDocTagValueNode $node): string
296328
{
297329
// only nodes that contain another node are handled here
@@ -590,6 +622,8 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes,
590622

591623
if ($insertStr === "\n * ") {
592624
$insertStr = sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
625+
// the lexer strips exactly one space after the asterisk, any further indentation stays in the text
626+
$this->continuationLinePrefix = sprintf('%s%s* ', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent);
593627
}
594628

595629
foreach ($diff as $i => $diffElem) {

tests/PHPStan/Printer/PrinterTest.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
2525
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
2626
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
27+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
2728
use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode;
2829
use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessParameterIsPassedTagValueNode;
2930
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
@@ -2690,6 +2691,88 @@ public function enterNode(Node $node)
26902691
*/'),
26912692
$addCommentToObjectShapeItemMiddle,
26922693
];
2694+
$changeMultilineText = new class extends AbstractNodeVisitor {
2695+
2696+
public function enterNode(Node $node)
2697+
{
2698+
if ($node instanceof PhpDocTextNode) {
2699+
$node->text = str_replace('Foo', 'Bar', $node->text);
2700+
}
2701+
if ($node instanceof ParamTagValueNode) {
2702+
$node->description = str_replace('Foo', 'Bar', $node->description);
2703+
}
2704+
2705+
return $node;
2706+
}
2707+
2708+
};
2709+
2710+
yield [
2711+
self::nowdoc('
2712+
/**
2713+
* First line Foo
2714+
* second line Foo
2715+
*
2716+
* Third line Foo
2717+
*
2718+
* @param int $a Foo description
2719+
* continues Foo
2720+
* @param int $b
2721+
*/'),
2722+
self::nowdoc('
2723+
/**
2724+
* First line Bar
2725+
* second line Bar
2726+
*
2727+
* Third line Bar
2728+
*
2729+
* @param int $a Bar description
2730+
* continues Bar
2731+
* @param int $b
2732+
*/'),
2733+
$changeMultilineText,
2734+
];
2735+
2736+
yield [
2737+
self::nowdoc('
2738+
/**
2739+
* First line Foo
2740+
* second line Foo
2741+
*/'),
2742+
self::nowdoc('
2743+
/**
2744+
* First line Bar
2745+
* second line Bar
2746+
*/'),
2747+
$changeMultilineText,
2748+
];
2749+
2750+
$addMultilineText = new class extends AbstractNodeVisitor {
2751+
2752+
public function enterNode(Node $node)
2753+
{
2754+
if ($node instanceof PhpDocNode) {
2755+
array_unshift($node->children, new PhpDocTextNode("Added first\nadded second"));
2756+
}
2757+
2758+
return $node;
2759+
}
2760+
2761+
};
2762+
2763+
yield [
2764+
self::nowdoc('
2765+
/**
2766+
* @param int $a
2767+
*/'),
2768+
self::nowdoc('
2769+
/**
2770+
* Added first
2771+
* added second
2772+
* @param int $a
2773+
*/'),
2774+
$addMultilineText,
2775+
];
26932776
}
26942777

26952778
/**
@@ -2951,6 +3034,28 @@ public function dataPrintPhpDocNode(): iterable
29513034
]),
29523035
'/**
29533036
* @param int $a
3037+
*/',
3038+
];
3039+
yield [
3040+
new PhpDocNode([
3041+
new PhpDocTextNode("First line\nsecond line\n\nthird line"),
3042+
new PhpDocTextNode(''),
3043+
new PhpDocTagNode('@param', new ParamTagValueNode(
3044+
new IdentifierTypeNode('int'),
3045+
false,
3046+
'$a',
3047+
"description\n continues",
3048+
false,
3049+
)),
3050+
]),
3051+
'/**
3052+
* First line
3053+
* second line
3054+
*
3055+
* third line
3056+
*
3057+
* @param int $a description
3058+
* continues
29543059
*/',
29553060
];
29563061
}

0 commit comments

Comments
 (0)