Skip to content

Commit 70e8d11

Browse files
SanderMullerclaude
andcommitted
Reuse parsed PHPDocs of equal strings
FileTypeMapper, InvalidPhpDocTagValueRule and InvalidPHPStanDocTagRule parse the same PHPDoc strings, and the two rules parse each one directly after the other. PhpDocStringResolver now keeps the last 1024 parsed nodes, and the two rules use it instead of their own lexer and parser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 184d34e commit 70e8d11

6 files changed

Lines changed: 75 additions & 29 deletions

File tree

‎src/PhpDoc/PhpDocStringResolver.php‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,44 @@
77
use PHPStan\PhpDocParser\Lexer\Lexer;
88
use PHPStan\PhpDocParser\Parser\PhpDocParser;
99
use PHPStan\PhpDocParser\Parser\TokenIterator;
10+
use function array_key_first;
11+
use function count;
1012

1113
#[AutowiredService]
1214
final class PhpDocStringResolver
1315
{
1416

17+
/**
18+
* FileTypeMapper and the rules that validate PHPDocs parse the same strings, often one right after
19+
* another. The parsed nodes are not modified afterwards, so equal strings can share one node.
20+
*/
21+
private const PARSED_PHPDOC_CACHE_LIMIT = 1024;
22+
23+
/** @var array<string, PhpDocNode> least recently used first */
24+
private array $parsedPhpDocs = [];
25+
1526
public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser)
1627
{
1728
}
1829

1930
public function resolve(string $phpDocString): PhpDocNode
2031
{
32+
if (isset($this->parsedPhpDocs[$phpDocString])) {
33+
$phpDocNode = $this->parsedPhpDocs[$phpDocString];
34+
unset($this->parsedPhpDocs[$phpDocString]);
35+
36+
return $this->parsedPhpDocs[$phpDocString] = $phpDocNode;
37+
}
38+
2139
$tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString));
2240
$phpDocNode = $this->phpDocParser->parse($tokens);
2341
$tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore missingType.checkedException
2442

25-
return $phpDocNode;
43+
if (count($this->parsedPhpDocs) >= self::PARSED_PHPDOC_CACHE_LIMIT) {
44+
unset($this->parsedPhpDocs[array_key_first($this->parsedPhpDocs)]);
45+
}
46+
47+
return $this->parsedPhpDocs[$phpDocString] = $phpDocNode;
2648
}
2749

2850
}

‎src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
use PHPStan\DependencyInjection\RegisteredRule;
99
use PHPStan\DependencyInjection\ValidatesStubFiles;
1010
use PHPStan\Node\VirtualNode;
11-
use PHPStan\PhpDocParser\Lexer\Lexer;
12-
use PHPStan\PhpDocParser\Parser\PhpDocParser;
13-
use PHPStan\PhpDocParser\Parser\TokenIterator;
11+
use PHPStan\PhpDoc\PhpDocStringResolver;
1412
use PHPStan\Rules\MultipleNodeTypesRule;
1513
use PHPStan\Rules\RuleErrorBuilder;
1614
use function in_array;
@@ -70,10 +68,7 @@ final class InvalidPHPStanDocTagRule implements MultipleNodeTypesRule
7068
'@phpstan-all-methods-impure',
7169
];
7270

73-
public function __construct(
74-
private Lexer $phpDocLexer,
75-
private PhpDocParser $phpDocParser,
76-
)
71+
public function __construct(private PhpDocStringResolver $phpDocStringResolver)
7772
{
7873
}
7974

@@ -111,8 +106,7 @@ public function processNode(Node $node, Scope $scope): array
111106
return [];
112107
}
113108
$phpDocString = $docComment->getText();
114-
$tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString));
115-
$phpDocNode = $this->phpDocParser->parse($tokens);
109+
$phpDocNode = $this->phpDocStringResolver->resolve($phpDocString);
116110

117111
$errors = [];
118112
foreach ($phpDocNode->getTags() as $phpDocTag) {

‎src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@
88
use PHPStan\DependencyInjection\RegisteredRule;
99
use PHPStan\DependencyInjection\ValidatesStubFiles;
1010
use PHPStan\Node\VirtualNode;
11+
use PHPStan\PhpDoc\PhpDocStringResolver;
1112
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
1213
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
1314
use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode;
14-
use PHPStan\PhpDocParser\Lexer\Lexer;
15-
use PHPStan\PhpDocParser\Parser\PhpDocParser;
16-
use PHPStan\PhpDocParser\Parser\TokenIterator;
1715
use PHPStan\Rules\MultipleNodeTypesRule;
1816
use PHPStan\Rules\RuleErrorBuilder;
1917
use function sprintf;
@@ -27,10 +25,7 @@
2725
final class InvalidPhpDocTagValueRule implements MultipleNodeTypesRule
2826
{
2927

30-
public function __construct(
31-
private Lexer $phpDocLexer,
32-
private PhpDocParser $phpDocParser,
33-
)
28+
public function __construct(private PhpDocStringResolver $phpDocStringResolver)
3429
{
3530
}
3631

@@ -69,8 +64,7 @@ public function processNode(Node $node, Scope $scope): array
6964
}
7065

7166
$phpDocString = $docComment->getText();
72-
$tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString));
73-
$phpDocNode = $this->phpDocParser->parse($tokens);
67+
$phpDocNode = $this->phpDocStringResolver->resolve($phpDocString);
7468

7569
$errors = [];
7670
foreach ($phpDocNode->getTags() as $phpDocTag) {

‎tests/PHPStan/PhpDoc/PhpDocStringResolverTest.php‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
namespace PHPStan\PhpDoc;
44

5+
use PHPStan\PhpDocParser\Lexer\Lexer;
56
use PHPStan\PhpDocParser\Parser\ParserException;
7+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
68
use PHPStan\Testing\PHPStanTestCase;
9+
use ReflectionClassConstant;
10+
use function sprintf;
711

812
class PhpDocStringResolverTest extends PHPStanTestCase
913
{
@@ -17,4 +21,42 @@ public function testConsumeEnd(): void
1721
$phpDocStringResolver->resolve('/** @param int $test */ test');
1822
}
1923

24+
public function testEqualStringsShareOneNode(): void
25+
{
26+
$phpDocStringResolver = $this->createPhpDocStringResolver();
27+
28+
$node = $phpDocStringResolver->resolve('/** @param int $test */');
29+
$this->assertSame($node, $phpDocStringResolver->resolve('/** @param int $test */'));
30+
$this->assertNotSame($node, $phpDocStringResolver->resolve('/** @param string $test */'));
31+
}
32+
33+
public function testLeastRecentlyUsedNodeIsEvicted(): void
34+
{
35+
$limit = (new ReflectionClassConstant(PhpDocStringResolver::class, 'PARSED_PHPDOC_CACHE_LIMIT'))->getValue();
36+
$phpDocStringResolver = $this->createPhpDocStringResolver();
37+
38+
$first = $phpDocStringResolver->resolve('/** @var int */');
39+
$second = $phpDocStringResolver->resolve('/** @var string */');
40+
for ($i = 2; $i < $limit; $i++) {
41+
$phpDocStringResolver->resolve(sprintf('/** @var %d */', $i));
42+
}
43+
44+
// a hit makes the first node the most recently used, so the next miss evicts the second one
45+
$this->assertSame($first, $phpDocStringResolver->resolve('/** @var int */'));
46+
$phpDocStringResolver->resolve('/** @var float */');
47+
48+
$this->assertSame($first, $phpDocStringResolver->resolve('/** @var int */'));
49+
$this->assertNotSame($second, $phpDocStringResolver->resolve('/** @var string */'));
50+
}
51+
52+
private function createPhpDocStringResolver(): PhpDocStringResolver
53+
{
54+
$container = self::getContainer();
55+
56+
return new PhpDocStringResolver(
57+
$container->getByType(Lexer::class),
58+
$container->getByType(PhpDocParser::class),
59+
);
60+
}
61+
2062
}

‎tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
namespace PHPStan\Rules\PhpDoc;
44

5-
use PHPStan\PhpDocParser\Lexer\Lexer;
6-
use PHPStan\PhpDocParser\Parser\PhpDocParser;
5+
use PHPStan\PhpDoc\PhpDocStringResolver;
76
use PHPStan\Rules\Rule;
87
use PHPStan\Testing\RuleTestCase;
98
use PHPUnit\Framework\Attributes\RequiresPhp;
@@ -16,10 +15,8 @@ class InvalidPHPStanDocTagRuleTest extends RuleTestCase
1615

1716
protected function getRule(): Rule
1817
{
19-
$container = self::getContainer();
2018
return new InvalidPHPStanDocTagRule(
21-
$container->getByType(Lexer::class),
22-
$container->getByType(PhpDocParser::class),
19+
self::getContainer()->getByType(PhpDocStringResolver::class),
2320
);
2421
}
2522

‎tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
namespace PHPStan\Rules\PhpDoc;
44

5-
use PHPStan\PhpDocParser\Lexer\Lexer;
6-
use PHPStan\PhpDocParser\Parser\PhpDocParser;
5+
use PHPStan\PhpDoc\PhpDocStringResolver;
76
use PHPStan\Rules\Rule;
87
use PHPStan\Testing\RuleTestCase;
98
use PHPUnit\Framework\Attributes\RequiresPhp;
@@ -16,10 +15,8 @@ class InvalidPhpDocTagValueRuleTest extends RuleTestCase
1615

1716
protected function getRule(): Rule
1817
{
19-
$container = self::getContainer();
2018
return new InvalidPhpDocTagValueRule(
21-
$container->getByType(Lexer::class),
22-
$container->getByType(PhpDocParser::class),
19+
self::getContainer()->getByType(PhpDocStringResolver::class),
2320
);
2421
}
2522

0 commit comments

Comments
 (0)