Skip to content

Commit e3fbd5d

Browse files
committed
Printer: put back asterisks in multi-line texts and descriptions
The parser removes the asterisk at the start of each line of a text or description. When the printer printed such a node, the lines after the first had no asterisk. The format-preserving printer now uses the line start of the original PHPDoc, so the indentation stays the same. Amp-Thread-ID: https://ampcode.com/threads/T-01a17930-0285-73c5-a957-4325c77aa1b2
1 parent f26818e commit e3fbd5d

2 files changed

Lines changed: 138 additions & 3 deletions

File tree

‎src/Printer/Printer.php‎

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,12 @@
7777
use function in_array;
7878
use function is_array;
7979
use function preg_match_all;
80+
use function preg_replace_callback;
8081
use function sprintf;
8182
use function str_replace;
8283
use function strlen;
8384
use function strpos;
85+
use function substr;
8486
use function trim;
8587
use const PREG_SET_ORDER;
8688

@@ -96,6 +98,12 @@ final class Printer
9698
/** @var Differ<Node> */
9799
private Differ $differ;
98100

101+
/**
102+
* Newline, indentation and asterisk at the start of a PHPDoc line.
103+
* Written after each newline in a printed text or description.
104+
*/
105+
private string $lineStart = "\n *";
106+
99107
/**
100108
* Map From "{$class}->{$subNode}" to string that should be inserted
101109
* between elements of this list subnode
@@ -177,6 +185,17 @@ final class Printer
177185
];
178186

179187
public function printFormatPreserving(PhpDocNode $node, PhpDocNode $originalNode, TokenIterator $originalTokens): string
188+
{
189+
$lineStart = $this->lineStart;
190+
$this->lineStart = $this->detectLineStart($originalTokens);
191+
try {
192+
return $this->printFormatPreservingWithLineStart($node, $originalNode, $originalTokens);
193+
} finally {
194+
$this->lineStart = $lineStart;
195+
}
196+
}
197+
198+
private function printFormatPreservingWithLineStart(PhpDocNode $node, PhpDocNode $originalNode, TokenIterator $originalTokens): string
180199
{
181200
$this->differ = new Differ(static function ($a, $b) {
182201
if ($a instanceof Node && $b instanceof Node) {
@@ -207,7 +226,7 @@ public function print(Node $node): string
207226
if ($node instanceof PhpDocNode) {
208227
return "/**\n *" . implode("\n *", array_map(
209228
function (PhpDocChildNode $child): string {
210-
$s = $this->print($child);
229+
$s = $this->addLineStarts($this->print($child));
211230
return $s === '' ? '' : ' ' . $s;
212231
},
213232
$node->children,
@@ -970,7 +989,7 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo
970989
private function printReplacement(Node $node): string
971990
{
972991
if (!$node instanceof DoctrineTagValueNode) {
973-
return $this->print($node);
992+
return $this->addLineStarts($this->print($node));
974993
}
975994

976995
$arguments = '(' . implode(', ', $node->annotation->arguments) . ')';
@@ -984,7 +1003,40 @@ private function printReplacement(Node $node): string
9841003
}
9851004
}
9861005

987-
return trim($arguments . ' ' . $node->description);
1006+
return $this->addLineStarts(trim($arguments . ' ' . $node->description));
1007+
}
1008+
1009+
/**
1010+
* The parser removes the asterisk at the start of each line of a text or description.
1011+
* This puts it back.
1012+
*/
1013+
private function addLineStarts(string $s): string
1014+
{
1015+
return preg_replace_callback(
1016+
'~\r?\n(?=(.?))~',
1017+
fn (array $matches): string => $matches[1] === '' || $matches[1] === "\r" || $matches[1] === "\n"
1018+
? $this->lineStart
1019+
: $this->lineStart . ' ',
1020+
$s,
1021+
) ?? $s;
1022+
}
1023+
1024+
private function detectLineStart(TokenIterator $originalTokens): string
1025+
{
1026+
foreach ($originalTokens->getTokens() as $token) {
1027+
if ($token[Lexer::TYPE_OFFSET] !== Lexer::TOKEN_PHPDOC_EOL) {
1028+
continue;
1029+
}
1030+
1031+
$asteriskPosition = strpos($token[Lexer::VALUE_OFFSET], '*');
1032+
if ($asteriskPosition === false) {
1033+
continue;
1034+
}
1035+
1036+
return substr($token[Lexer::VALUE_OFFSET], 0, $asteriskPosition + 1);
1037+
}
1038+
1039+
return ($originalTokens->getDetectedNewline() ?? "\n") . ' *';
9881040
}
9891041

9901042
}

‎tests/PHPStan/Printer/PrinterTest.php‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
2626
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
2727
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
28+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
2829
use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode;
2930
use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessParameterIsPassedTagValueNode;
3031
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
@@ -1988,6 +1989,67 @@ public function enterNode(Node $node)
19881989
},
19891990
];
19901991

1992+
yield [
1993+
self::nowdoc('
1994+
/**
1995+
* @param int $foo multi
1996+
* line description
1997+
* @return void
1998+
*/'),
1999+
self::nowdoc('
2000+
/**
2001+
* @param int $bar multi
2002+
* line description
2003+
* @return void
2004+
*/'),
2005+
new class extends AbstractNodeVisitor {
2006+
2007+
public function enterNode(Node $node)
2008+
{
2009+
if ($node instanceof ParamTagValueNode) {
2010+
$node->parameterName = '$bar';
2011+
}
2012+
2013+
return $node;
2014+
}
2015+
2016+
},
2017+
];
2018+
2019+
yield [
2020+
"/**\n * First line\n * second line\n *\n * @param int \$a\n */",
2021+
"/**\n * Changed line\n * second line\n *\n * @param int \$a\n */",
2022+
new class extends AbstractNodeVisitor {
2023+
2024+
public function enterNode(Node $node)
2025+
{
2026+
if ($node instanceof PhpDocTextNode && $node->text !== '') {
2027+
$node->text = "Changed line\nsecond line";
2028+
}
2029+
2030+
return $node;
2031+
}
2032+
2033+
},
2034+
];
2035+
2036+
yield [
2037+
"/**\n * @param int \$a\n */",
2038+
"/**\n * @param int \$a\n * @return int first\n *\n * second\n */",
2039+
new class extends AbstractNodeVisitor {
2040+
2041+
public function enterNode(Node $node)
2042+
{
2043+
if ($node instanceof PhpDocNode) {
2044+
$node->children[] = new PhpDocTagNode('@return', new ReturnTagValueNode(new IdentifierTypeNode('int'), "first\n\n second"));
2045+
}
2046+
2047+
return $node;
2048+
}
2049+
2050+
},
2051+
];
2052+
19912053
yield [
19922054
'/** @\Foo\Id */',
19932055
'/** @\Foo\Id */',
@@ -3003,6 +3065,27 @@ public function dataPrintPhpDocNode(): iterable
30033065
]),
30043066
'/**
30053067
* @param int $a
3068+
*/',
3069+
];
3070+
3071+
yield [
3072+
new PhpDocNode([
3073+
new PhpDocTextNode("First line\nsecond line"),
3074+
new PhpDocTagNode('@param', new ParamTagValueNode(
3075+
new IdentifierTypeNode('int'),
3076+
false,
3077+
'$a',
3078+
"multi\n line\n\nwith empty line",
3079+
false,
3080+
)),
3081+
]),
3082+
'/**
3083+
* First line
3084+
* second line
3085+
* @param int $a multi
3086+
* line
3087+
*
3088+
* with empty line
30063089
*/',
30073090
];
30083091
}

0 commit comments

Comments
 (0)