Skip to content

Commit be145d9

Browse files
authored
Use PhpDocParser from phpstan/phpdoc-parser without subclass (#8871)
* Use PhpDocParser from phpstan/phpdoc-parser without subclass BetterPhpDocParser now calls PhpDocParser::parse() and then runs the decorators. Positions come from the START_INDEX and END_INDEX attributes. Tags after an inline tag are now parsed. Before, "@internal Use @see Foo" ended the docblock and the next tags were lost. CloningPhpDocNodeVisitor is replaced with CloningVisitor. Amp-Thread-ID: https://ampcode.com/threads/T-01a17930-0285-73c5-a957-4325c77aa1b2 * Skip incomplete Doctrine annotation with nested annotations @Orm\Table('user', indexes={...}) with a single-quoted argument gets InvalidTagValueNode. Its value ends before the nested @Orm\Index lines. NestedAnnotationToAttributeRector then threw ShouldNotHappenException. The decorator now keeps such annotation and its next children unchanged. Amp-Thread-ID: https://ampcode.com/threads/T-01a17930-0285-73c5-a957-4325c77aa1b2
1 parent 4cf0bdc commit be145d9

7 files changed

Lines changed: 127 additions & 119 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\Property\RemoveUselessVarTagRector\Fixture;
4+
5+
final class AfterInlineTag
6+
{
7+
/**
8+
* @internal Use @see \Rector\Config\RectorConfig::paths() instead
9+
* @var string
10+
*/
11+
public const string NAME = 'value';
12+
}
13+
14+
?>
15+
-----
16+
<?php
17+
18+
namespace Rector\Tests\DeadCode\Rector\Property\RemoveUselessVarTagRector\Fixture;
19+
20+
final class AfterInlineTag
21+
{
22+
/**
23+
* @internal Use @see \Rector\Config\RectorConfig::paths() instead
24+
*/
25+
public const string NAME = 'value';
26+
}
27+
28+
?>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
7+
/**
8+
* @ORM\Entity()
9+
* @ORM\Table('user', indexes={
10+
* @ORM\Index(name="name_index", columns={"name"}),
11+
* @ORM\Index(name="surname_index", columns={"surname"}),
12+
* })
13+
*/
14+
class SingleQuoteAlias
15+
{
16+
}

‎src/BetterPhpDocParser/NodeDecorator/DoctrineAnnotationDecorator.php‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,19 @@ private function transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes(
202202
PhpDocNode $phpDocNode,
203203
Node $currentPhpNode
204204
): void {
205+
$incompleteAnnotationContent = null;
206+
205207
foreach ($phpDocNode->children as $key => $phpDocChildNode) {
208+
// the next children are the rest of an incomplete annotation, e.g. its nested annotations; keep them
209+
if ($incompleteAnnotationContent !== null) {
210+
$incompleteAnnotationContent .= "\n" . $phpDocChildNode;
211+
if ($this->isClosedContent($incompleteAnnotationContent)) {
212+
$incompleteAnnotationContent = null;
213+
}
214+
215+
continue;
216+
}
217+
206218
// the @\FQN use case
207219
if ($phpDocChildNode instanceof PhpDocTextNode) {
208220
$this->processTextSpacelessInTextNode($phpDocNode, $phpDocChildNode, $currentPhpNode, $key);
@@ -215,6 +227,12 @@ private function transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes(
215227

216228
// single quoted got invalid tag, keep process
217229
if ($phpDocChildNode->value instanceof InvalidTagValueNode) {
230+
// the value ends before a nested annotation on the next line
231+
if (! $this->isClosedContent($phpDocChildNode->value->value)) {
232+
$incompleteAnnotationContent = $phpDocChildNode->value->value;
233+
continue;
234+
}
235+
218236
$name = ltrim($phpDocChildNode->name, '@');
219237

220238
$values = $phpDocChildNode->value->value;
@@ -313,9 +331,6 @@ private function transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes(
313331
$this->attributeMirrorer->mirror($phpDocNode->children[$key], $spacelessPhpDocTagNode);
314332
$phpDocNode->children[$key] = $spacelessPhpDocTagNode;
315333

316-
// require to reprint the generic
317-
$phpDocNode->children[$key]->value->setAttribute(PhpDocAttributeKey::ORIG_NODE, null);
318-
319334
array_splice($phpDocNode->children, $key + 1, 0, $spacelessPhpDocTagNodes);
320335
}
321336
}

‎src/BetterPhpDocParser/PhpDocNodeMapper.php‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
namespace Rector\BetterPhpDocParser;
66

77
use PHPStan\PhpDocParser\Ast\NodeTraverser;
8+
use PHPStan\PhpDocParser\Ast\NodeVisitor\CloningVisitor;
89
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
9-
use Rector\PhpDocParser\PhpDocParser\PhpDocNodeVisitor\CloningPhpDocNodeVisitor;
1010
use Rector\PhpDocParser\PhpDocParser\PhpDocNodeVisitor\ParentConnectingPhpDocNodeVisitor;
1111
use Webmozart\Assert\Assert;
1212

@@ -18,11 +18,10 @@
1818
private NodeTraverser $phpDocNodeTraverser;
1919

2020
public function __construct(
21-
CloningPhpDocNodeVisitor $cloningPhpDocNodeVisitor,
2221
ParentConnectingPhpDocNodeVisitor $parentConnectingPhpDocNodeVisitor,
2322
) {
2423
$this->phpDocNodeTraverser = new NodeTraverser([
25-
$cloningPhpDocNodeVisitor,
24+
new CloningVisitor(),
2625
$parentConnectingPhpDocNodeVisitor,
2726
]);
2827
}

‎src/BetterPhpDocParser/PhpDocParser/BetterPhpDocParser.php‎

Lines changed: 63 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,52 +6,56 @@
66

77
use Nette\Utils\Strings;
88
use PhpParser\Node;
9+
use PHPStan\PhpDocParser\Ast\Attribute;
10+
use PHPStan\PhpDocParser\Ast\Node as PhpDocParserNode;
911
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
10-
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
1112
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
1213
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
13-
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
1414
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
1515
use PHPStan\PhpDocParser\Lexer\Lexer;
16-
use PHPStan\PhpDocParser\Parser\ConstExprParser;
1716
use PHPStan\PhpDocParser\Parser\ParserException;
1817
use PHPStan\PhpDocParser\Parser\PhpDocParser;
1918
use PHPStan\PhpDocParser\Parser\TokenIterator;
20-
use PHPStan\PhpDocParser\Parser\TypeParser;
21-
use PHPStan\PhpDocParser\ParserConfig;
2219
use Rector\BetterPhpDocParser\Contract\PhpDocParser\PhpDocNodeDecoratorInterface;
2320
use Rector\BetterPhpDocParser\NodeDecorator\ArrayItemClassNameDecorator;
2421
use Rector\BetterPhpDocParser\NodeDecorator\ConstExprClassNameDecorator;
2522
use Rector\BetterPhpDocParser\NodeDecorator\DoctrineAnnotationDecorator;
2623
use Rector\BetterPhpDocParser\NodeDecorator\PhpDocTagGenericUsesDecorator;
2724
use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey;
2825
use Rector\BetterPhpDocParser\ValueObject\StartAndEnd;
29-
use Rector\Util\Reflection\PrivatesAccessor;
3026

3127
/**
3228
* @see \Rector\Tests\BetterPhpDocParser\PhpDocParser\TagValueNodeReprint\TagValueNodeReprintTest
3329
*/
34-
final class BetterPhpDocParser extends PhpDocParser
30+
final readonly class BetterPhpDocParser
3531
{
3632
/**
3733
* @see https://regex101.com/r/JOKSmr/5
3834
*/
3935
private const string MULTI_NEW_LINES_REGEX = '#(?<new_line>\r\n|\n){2,}#';
4036

37+
/**
38+
* @var int[]
39+
*/
40+
private const array NO_TEXT_TOKEN_TYPES = [
41+
Lexer::TOKEN_OPEN_PHPDOC,
42+
Lexer::TOKEN_CLOSE_PHPDOC,
43+
Lexer::TOKEN_PHPDOC_EOL,
44+
Lexer::TOKEN_HORIZONTAL_WS,
45+
Lexer::TOKEN_END,
46+
];
47+
4148
/**
4249
* @var PhpDocNodeDecoratorInterface[]
4350
*/
44-
private readonly array $phpDocNodeDecorators;
51+
private array $phpDocNodeDecorators;
4552

4653
public function __construct(
47-
ParserConfig $parserConfig,
48-
TypeParser $typeParser,
49-
ConstExprParser $constExprParser,
54+
private PhpDocParser $phpDocParser,
5055
ConstExprClassNameDecorator $constExprClassNameDecorator,
5156
DoctrineAnnotationDecorator $doctrineAnnotationDecorator,
5257
ArrayItemClassNameDecorator $arrayItemClassNameDecorator,
5358
PhpDocTagGenericUsesDecorator $phpDocTagGenericUsesDecorator,
54-
private readonly PrivatesAccessor $privatesAccessor,
5559
) {
5660
// The decorator order below is significant; keep it as is. DoctrineAnnotationDecorator must
5761
// run before ArrayItemClassNameDecorator, which resolves class names inside the array items the
@@ -63,84 +67,81 @@ public function __construct(
6367
$arrayItemClassNameDecorator,
6468
$phpDocTagGenericUsesDecorator,
6569
];
66-
67-
parent::__construct(
68-
// ParserConfig
69-
$parserConfig,
70-
// TypeParser
71-
$typeParser,
72-
// ConstExprParser
73-
$constExprParser,
74-
);
7570
}
7671

7772
public function parseWithNode(TokenIterator $tokenIterator, Node $node): PhpDocNode
7873
{
79-
$tokenIterator->consumeTokenType(Lexer::TOKEN_OPEN_PHPDOC);
80-
$tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
74+
try {
75+
$phpDocNode = $this->phpDocParser->parse($tokenIterator);
76+
} catch (ParserException) {
77+
// invalid Doctrine annotation arguments; keep the docblock as it is
78+
return $this->createTextPhpDocNode($tokenIterator);
79+
}
8180

82-
$children = [];
83-
if (! $tokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
84-
$children[] = $this->parseChildAndStoreItsPositions($tokenIterator);
81+
foreach ($phpDocNode->children as $phpDocChildNode) {
82+
$this->setStartAndEnd($phpDocChildNode);
8583

86-
while ($tokenIterator->tryConsumeTokenType(
87-
Lexer::TOKEN_PHPDOC_EOL
88-
) && ! $tokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
89-
$children[] = $this->parseChildAndStoreItsPositions($tokenIterator);
84+
if (! $phpDocChildNode instanceof PhpDocTagNode) {
85+
continue;
9086
}
91-
}
9287

93-
// might be in the middle of annotations
94-
$tokenIterator->tryConsumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC);
88+
$this->setStartAndEnd($phpDocChildNode->value);
89+
90+
if ($phpDocChildNode->value instanceof GenericTagValueNode) {
91+
$phpDocChildNode->value->value = Strings::replace(
92+
$phpDocChildNode->value->value,
93+
self::MULTI_NEW_LINES_REGEX,
94+
static fn (array $match): string => (string) $match['new_line']
95+
);
96+
}
97+
}
9598

96-
$phpDocNode = new PhpDocNode($children);
9799
foreach ($this->phpDocNodeDecorators as $phpDocNodeDecorator) {
98100
$phpDocNodeDecorator->decorate($phpDocNode, $node);
99101
}
100102

101103
return $phpDocNode;
102104
}
103105

104-
public function parseTagValue(TokenIterator $tokenIterator, string $tag): PhpDocTagValueNode
106+
/**
107+
* The Doctrine annotation decorator moves these positions to the nodes it creates
108+
*/
109+
private function setStartAndEnd(PhpDocParserNode $phpDocParserNode): void
105110
{
106-
$startPosition = $tokenIterator->currentTokenIndex();
107-
$phpDocTagValueNode = parent::parseTagValue($tokenIterator, $tag);
108-
109-
$endPosition = $tokenIterator->currentTokenIndex();
110-
111-
$startAndEnd = new StartAndEnd($startPosition, $endPosition);
112-
$phpDocTagValueNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd);
113-
114-
if ($phpDocTagValueNode instanceof GenericTagValueNode) {
115-
$phpDocTagValueNode->value = Strings::replace(
116-
$phpDocTagValueNode->value,
117-
self::MULTI_NEW_LINES_REGEX,
118-
static fn (array $match): string => (string) $match['new_line']
119-
);
111+
$startIndex = $phpDocParserNode->getAttribute(Attribute::START_INDEX);
112+
$endIndex = $phpDocParserNode->getAttribute(Attribute::END_INDEX);
113+
if (! is_int($startIndex) || ! is_int($endIndex)) {
114+
return;
120115
}
121116

122-
return $phpDocTagValueNode;
117+
// an empty node has no tokens, its end index is before its start index
118+
$phpDocParserNode->setAttribute(
119+
PhpDocAttributeKey::START_AND_END,
120+
new StartAndEnd($startIndex, max($startIndex, $endIndex + 1))
121+
);
123122
}
124123

125124
/**
126-
* @return PhpDocTextNode|PhpDocTagNode
125+
* One text node with the content between "/**" and "*\/"
127126
*/
128-
private function parseChildAndStoreItsPositions(TokenIterator $tokenIterator): PhpDocChildNode
127+
private function createTextPhpDocNode(TokenIterator $tokenIterator): PhpDocNode
129128
{
130-
$startPosition = $tokenIterator->currentTokenIndex();
129+
$tokens = $tokenIterator->getTokens();
130+
$startIndex = 0;
131+
$endIndex = count($tokens) - 1;
131132

132-
try {
133-
/** @var PhpDocTextNode|PhpDocTagNode $phpDocNode */
134-
$phpDocNode = $this->privatesAccessor->callPrivateMethod($this, 'parseChild', [$tokenIterator]);
135-
} catch (ParserException) {
136-
$phpDocNode = new PhpDocTextNode('');
133+
while ($startIndex < $endIndex && in_array($tokens[$startIndex][Lexer::TYPE_OFFSET], self::NO_TEXT_TOKEN_TYPES, true)) {
134+
++$startIndex;
137135
}
138136

139-
$endPosition = $tokenIterator->currentTokenIndex();
137+
while ($endIndex > $startIndex && in_array($tokens[$endIndex][Lexer::TYPE_OFFSET], self::NO_TEXT_TOKEN_TYPES, true)) {
138+
--$endIndex;
139+
}
140140

141-
$startAndEnd = new StartAndEnd($startPosition, $endPosition);
142-
$phpDocNode->setAttribute(PhpDocAttributeKey::START_AND_END, $startAndEnd);
141+
$phpDocTextNode = new PhpDocTextNode($tokenIterator->getContentBetween($startIndex, $endIndex + 1));
142+
$phpDocTextNode->setAttribute(Attribute::START_INDEX, $startIndex);
143+
$phpDocTextNode->setAttribute(Attribute::END_INDEX, $endIndex);
143144

144-
return $phpDocNode;
145+
return new PhpDocNode([$phpDocTextNode]);
145146
}
146147
}

0 commit comments

Comments
 (0)