Skip to content

Commit 0459d2e

Browse files
committed
start on format-preserving printer comment support
1 parent 3efa7c6 commit 0459d2e

File tree

5 files changed

+170
-18
lines changed

5 files changed

+170
-18
lines changed

src/Ast/Comment.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
namespace PHPStan\PhpDocParser\Ast;
44

5+
use function explode;
6+
use function preg_match;
7+
use function preg_quote;
8+
use function preg_replace;
9+
use function strlen;
10+
use function strpos;
11+
use function substr;
12+
use function trim;
13+
use const PHP_INT_MAX;
14+
515
class Comment
616
{
717

@@ -21,4 +31,68 @@ public function __construct(string $text, int $startLine = -1, int $startIndex =
2131
$this->startIndex = $startIndex;
2232
}
2333

34+
public function getReformattedText(): ?string
35+
{
36+
$text = trim($this->text);
37+
$newlinePos = strpos($text, "\n");
38+
if ($newlinePos === false) {
39+
// Single line comments don't need further processing
40+
return $text;
41+
} elseif (preg_match('((*BSR_ANYCRLF)(*ANYCRLF)^.*(?:\R\s+\*.*)+$)', $text) === 1) {
42+
// Multi line comment of the type
43+
//
44+
// /*
45+
// * Some text.
46+
// * Some more text.
47+
// */
48+
//
49+
// is handled by replacing the whitespace sequences before the * by a single space
50+
return preg_replace('(^\s+\*)m', ' *', $this->text);
51+
} elseif (preg_match('(^/\*\*?\s*[\r\n])', $text) === 1 && preg_match('(\n(\s*)\*/$)', $text, $matches) === 1) {
52+
// Multi line comment of the type
53+
//
54+
// /*
55+
// Some text.
56+
// Some more text.
57+
// */
58+
//
59+
// is handled by removing the whitespace sequence on the line before the closing
60+
// */ on all lines. So if the last line is " */", then " " is removed at the
61+
// start of all lines.
62+
return preg_replace('(^' . preg_quote($matches[1]) . ')m', '', $text);
63+
} elseif (preg_match('(^/\*\*?\s*(?!\s))', $text, $matches) === 1) {
64+
// Multi line comment of the type
65+
//
66+
// /* Some text.
67+
// Some more text.
68+
// Indented text.
69+
// Even more text. */
70+
//
71+
// is handled by removing the difference between the shortest whitespace prefix on all
72+
// lines and the length of the "/* " opening sequence.
73+
$prefixLen = $this->getShortestWhitespacePrefixLen(substr($text, $newlinePos + 1));
74+
$removeLen = $prefixLen - strlen($matches[0]);
75+
return preg_replace('(^\s{' . $removeLen . '})m', '', $text);
76+
}
77+
78+
// No idea how to format this comment, so simply return as is
79+
return $text;
80+
}
81+
82+
private function getShortestWhitespacePrefixLen(string $str): int
83+
{
84+
$lines = explode("\n", $str);
85+
$shortestPrefixLen = PHP_INT_MAX;
86+
foreach ($lines as $line) {
87+
preg_match('(^\s*)', $line, $matches);
88+
$prefixLen = strlen($matches[0]);
89+
if ($prefixLen >= $shortestPrefixLen) {
90+
continue;
91+
}
92+
93+
$shortestPrefixLen = $prefixLen;
94+
}
95+
return $shortestPrefixLen;
96+
}
97+
2498
}

src/Ast/Node.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@ public function hasAttribute(string $key): bool;
1919
*/
2020
public function getAttribute(string $key);
2121

22+
/**
23+
* @return array<Comment>
24+
*/
25+
public function getComments(): array;
26+
2227
}

src/Ast/NodeAttributes.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,12 @@ public function getAttribute(string $key)
3535
return null;
3636
}
3737

38+
/**
39+
* @return array<Comment>
40+
*/
41+
public function getComments(): array
42+
{
43+
return $this->attributes[Attribute::COMMENTS] ?? [];
44+
}
45+
3846
}

src/Printer/Printer.php

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use LogicException;
66
use PHPStan\PhpDocParser\Ast\Attribute;
7+
use PHPStan\PhpDocParser\Ast\Comment;
78
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
89
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
910
use PHPStan\PhpDocParser\Ast\Node;
@@ -521,19 +522,22 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes,
521522

522523
foreach ($diff as $i => $diffElem) {
523524
$diffType = $diffElem->type;
524-
$newNode = $diffElem->new;
525-
$originalNode = $diffElem->old;
525+
$arrItem = $diffElem->new;
526+
$origArrayItem = $diffElem->old;
526527
if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) {
527528
$beforeFirstKeepOrReplace = false;
528-
if (!$newNode instanceof Node || !$originalNode instanceof Node) {
529+
if (!$arrItem instanceof Node || !$origArrayItem instanceof Node) {
529530
return null;
530531
}
531-
$itemStartPos = $originalNode->getAttribute(Attribute::START_INDEX);
532-
$itemEndPos = $originalNode->getAttribute(Attribute::END_INDEX);
532+
$itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX);
533+
$itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX);
533534
if ($itemStartPos < 0 || $itemEndPos < 0 || $itemStartPos < $tokenIndex) {
534535
throw new LogicException();
535536
}
536537

538+
// $comments = $arrItem->getComments();
539+
// $origComments = $origArrayItem->getComments();
540+
537541
$result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos);
538542

539543
if (count($delayedAdd) > 0) {
@@ -559,14 +563,14 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes,
559563
}
560564

561565
$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
562-
&& in_array(get_class($newNode), $this->parenthesesListMap[$mapKey], true)
563-
&& !in_array(get_class($originalNode), $this->parenthesesListMap[$mapKey], true);
566+
&& in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true)
567+
&& !in_array(get_class($origArrayItem), $this->parenthesesListMap[$mapKey], true);
564568
$addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($itemStartPos, $itemEndPos);
565569
if ($addParentheses) {
566570
$result .= '(';
567571
}
568572

569-
$result .= $this->printNodeFormatPreserving($newNode, $originalTokens);
573+
$result .= $this->printNodeFormatPreserving($arrItem, $originalTokens);
570574
if ($addParentheses) {
571575
$result .= ')';
572576
}
@@ -576,48 +580,54 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes,
576580
if ($insertStr === null) {
577581
return null;
578582
}
579-
if (!$newNode instanceof Node) {
583+
if (!$arrItem instanceof Node) {
580584
return null;
581585
}
582586

583-
if ($insertStr === ', ' && $isMultiline) {
587+
if ($insertStr === ', ' && $isMultiline || count($arrItem->getComments()) > 0) {
584588
$insertStr = ',';
585589
$insertNewline = true;
586590
}
587591

588592
if ($beforeFirstKeepOrReplace) {
589593
// Will be inserted at the next "replace" or "keep" element
590-
$delayedAdd[] = $newNode;
594+
$delayedAdd[] = $arrItem;
591595
continue;
592596
}
593597

594598
$itemEndPos = $tokenIndex - 1;
595599
if ($insertNewline) {
596-
$result .= $insertStr . sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
600+
$comments = $arrItem->getComments();
601+
$result .= $insertStr;
602+
if (count($comments) > 0) {
603+
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
604+
$result .= $this->pComments($comments);
605+
}
606+
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
597607
} else {
598608
$result .= $insertStr;
599609
}
600610

601611
$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
602-
&& in_array(get_class($newNode), $this->parenthesesListMap[$mapKey], true);
612+
&& in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true);
603613
if ($parenthesesNeeded) {
604614
$result .= '(';
605615
}
606616

607-
$result .= $this->printNodeFormatPreserving($newNode, $originalTokens);
617+
$result .= $this->printNodeFormatPreserving($arrItem, $originalTokens);
608618
if ($parenthesesNeeded) {
609619
$result .= ')';
610620
}
611621

612622
$tokenIndex = $itemEndPos + 1;
613623

614624
} elseif ($diffType === DiffElem::TYPE_REMOVE) {
615-
if (!$originalNode instanceof Node) {
625+
if (!$origArrayItem instanceof Node) {
616626
return null;
617627
}
618628

619-
$itemStartPos = $originalNode->getAttribute(Attribute::START_INDEX);
620-
$itemEndPos = $originalNode->getAttribute(Attribute::END_INDEX);
629+
$itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX);
630+
$itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX);
621631
if ($itemStartPos < 0 || $itemEndPos < 0) {
622632
throw new LogicException();
623633
}
@@ -675,6 +685,20 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes,
675685
return $result;
676686
}
677687

688+
/**
689+
* @param array<Comment> $comments
690+
*/
691+
protected function pComments(array $comments): string
692+
{
693+
$formattedComments = [];
694+
695+
foreach ($comments as $comment) {
696+
$formattedComments[] = $comment->getReformattedText() ?? '';
697+
}
698+
699+
return implode("\n", $formattedComments);
700+
}
701+
678702
/**
679703
* @param Node[] $nodes
680704
* @return array{bool, string, string}

tests/PHPStan/Printer/PrinterTest.php

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor;
66
use PHPStan\PhpDocParser\Ast\Attribute;
7+
use PHPStan\PhpDocParser\Ast\Comment;
78
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayItemNode;
89
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
910
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
@@ -62,7 +63,7 @@ class PrinterTest extends TestCase
6263

6364
protected function setUp(): void
6465
{
65-
$usedAttributes = ['lines' => true, 'indexes' => true];
66+
$usedAttributes = ['lines' => true, 'indexes' => true, 'comments' => true];
6667
$constExprParser = new ConstExprParser(true, true, $usedAttributes);
6768
$this->typeParser = new TypeParser($constExprParser, true, $usedAttributes);
6869
$this->phpDocParser = new PhpDocParser(
@@ -663,6 +664,45 @@ public function enterNode(Node $node)
663664

664665
};
665666

667+
$addItemsToMultilineArrayShape = new class extends AbstractNodeVisitor {
668+
669+
public function enterNode(Node $node)
670+
{
671+
if ($node instanceof ArrayShapeNode) {
672+
$commentedNode = new ArrayShapeItemNode(new IdentifierTypeNode('c'), false, new IdentifierTypeNode('int'));
673+
$commentedNode->setAttribute(Attribute::COMMENTS, [new Comment('// bar')]);
674+
array_splice($node->items, 1, 0, [
675+
$commentedNode,
676+
]);
677+
$node->items[] = new ArrayShapeItemNode(new IdentifierTypeNode('d'), false, new IdentifierTypeNode('string'));
678+
}
679+
680+
return $node;
681+
}
682+
683+
};
684+
685+
yield [
686+
'/**
687+
* @return array{
688+
* // foo
689+
* a: int,
690+
* b: string
691+
* }
692+
*/',
693+
'/**
694+
* @return array{
695+
* // foo
696+
* a: int,
697+
* // bar
698+
* c: int,
699+
* b: string,
700+
* d: string
701+
* }
702+
*/',
703+
$addItemsToMultilineArrayShape,
704+
];
705+
666706
yield [
667707
'/**
668708
* @return array{float}
@@ -1674,6 +1714,7 @@ public function enterNode(Node $node)
16741714
$node->setAttribute(Attribute::START_INDEX, null);
16751715
$node->setAttribute(Attribute::END_INDEX, null);
16761716
$node->setAttribute(Attribute::ORIGINAL_NODE, null);
1717+
$node->setAttribute(Attribute::COMMENTS, null);
16771718

16781719
return $node;
16791720
}

0 commit comments

Comments
 (0)