From fbcde4bf9caa53cc2430669948359afc8ad1e110 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Sun, 11 Oct 2026 20:10:56 +0200 Subject: [PATCH] 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 most recently parsed nodes in an LruCache, and the two rules use it instead of their own lexer and parser. The new cache.parsedPhpDocCountMax parameter sets how many nodes it keeps, 1024 by default. Co-Authored-By: Claude Opus 5.5 (1M context) --- conf/config.neon | 1 + conf/parametersSchema.neon | 3 ++- src/PhpDoc/PhpDocStringResolver.php | 25 ++++++++++++++++++- src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php | 12 +++------ .../PhpDoc/InvalidPhpDocTagValueRule.php | 12 +++------ .../PhpDoc/PhpDocStringResolverTest.php | 22 ++++++++++++++++ .../PhpDoc/InvalidPHPStanDocTagRuleTest.php | 7 ++---- .../PhpDoc/InvalidPhpDocTagValueRuleTest.php | 7 ++---- 8 files changed, 59 insertions(+), 30 deletions(-) diff --git a/conf/config.neon b/conf/config.neon index f623528506a..b31c9dd4a14 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -146,6 +146,7 @@ parameters: phpStormStubsNodesCountMax: 128 memberCacheKeysMax: 2048 resolvedLocalTypeAliasesCountMax: 2048 + parsedPhpDocCountMax: 1024 reportUnmatchedIgnoredErrors: true reportIgnoresWithoutComments: false typeAliases: [] diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 154054ccb1f..fce20ad82fe 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -164,7 +164,8 @@ parametersSchema: nameScopeMapMemoryCacheCountMax: int(), phpStormStubsNodesCountMax: int(), memberCacheKeysMax: int(), - resolvedLocalTypeAliasesCountMax: int() + resolvedLocalTypeAliasesCountMax: int(), + parsedPhpDocCountMax: int() ]) reportUnmatchedIgnoredErrors: bool() reportIgnoresWithoutComments: bool() diff --git a/src/PhpDoc/PhpDocStringResolver.php b/src/PhpDoc/PhpDocStringResolver.php index 30a6e1df0e6..03fb1941787 100644 --- a/src/PhpDoc/PhpDocStringResolver.php +++ b/src/PhpDoc/PhpDocStringResolver.php @@ -2,7 +2,9 @@ namespace PHPStan\PhpDoc; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Internal\LruCache; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; @@ -12,16 +14,37 @@ final class PhpDocStringResolver { - public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) + /** + * FileTypeMapper and the rules that validate PHPDocs parse the same strings, often one right after + * another. The parsed nodes are not modified afterwards, so equal strings can share one node. + * + * @var LruCache + */ + private LruCache $parsedPhpDocs; + + public function __construct( + private Lexer $phpDocLexer, + private PhpDocParser $phpDocParser, + #[AutowiredParameter(ref: '%cache.parsedPhpDocCountMax%')] + int $parsedPhpDocCountMax, + ) { + $this->parsedPhpDocs = new LruCache($parsedPhpDocCountMax); } public function resolve(string $phpDocString): PhpDocNode { + $phpDocNode = $this->parsedPhpDocs->get($phpDocString); + if ($phpDocNode !== null) { + return $phpDocNode; + } + $tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString)); $phpDocNode = $this->phpDocParser->parse($tokens); $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore missingType.checkedException + $this->parsedPhpDocs->set($phpDocString, $phpDocNode, 0); + return $phpDocNode; } diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index e7f58e41bbc..73922ec0566 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -8,9 +8,7 @@ use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\DependencyInjection\ValidatesStubFiles; use PHPStan\Node\VirtualNode; -use PHPStan\PhpDocParser\Lexer\Lexer; -use PHPStan\PhpDocParser\Parser\PhpDocParser; -use PHPStan\PhpDocParser\Parser\TokenIterator; +use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\Rules\MultipleNodeTypesRule; use PHPStan\Rules\RuleErrorBuilder; use function in_array; @@ -70,10 +68,7 @@ final class InvalidPHPStanDocTagRule implements MultipleNodeTypesRule '@phpstan-all-methods-impure', ]; - public function __construct( - private Lexer $phpDocLexer, - private PhpDocParser $phpDocParser, - ) + public function __construct(private PhpDocStringResolver $phpDocStringResolver) { } @@ -111,8 +106,7 @@ public function processNode(Node $node, Scope $scope): array return []; } $phpDocString = $docComment->getText(); - $tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString)); - $phpDocNode = $this->phpDocParser->parse($tokens); + $phpDocNode = $this->phpDocStringResolver->resolve($phpDocString); $errors = []; foreach ($phpDocNode->getTags() as $phpDocTag) { diff --git a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php index bf1cbd4c796..8d96606409b 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php @@ -8,12 +8,10 @@ use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\DependencyInjection\ValidatesStubFiles; use PHPStan\Node\VirtualNode; +use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode; use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode; -use PHPStan\PhpDocParser\Lexer\Lexer; -use PHPStan\PhpDocParser\Parser\PhpDocParser; -use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\Rules\MultipleNodeTypesRule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; @@ -27,10 +25,7 @@ final class InvalidPhpDocTagValueRule implements MultipleNodeTypesRule { - public function __construct( - private Lexer $phpDocLexer, - private PhpDocParser $phpDocParser, - ) + public function __construct(private PhpDocStringResolver $phpDocStringResolver) { } @@ -69,8 +64,7 @@ public function processNode(Node $node, Scope $scope): array } $phpDocString = $docComment->getText(); - $tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString)); - $phpDocNode = $this->phpDocParser->parse($tokens); + $phpDocNode = $this->phpDocStringResolver->resolve($phpDocString); $errors = []; foreach ($phpDocNode->getTags() as $phpDocTag) { diff --git a/tests/PHPStan/PhpDoc/PhpDocStringResolverTest.php b/tests/PHPStan/PhpDoc/PhpDocStringResolverTest.php index ba59b491888..83283aee119 100644 --- a/tests/PHPStan/PhpDoc/PhpDocStringResolverTest.php +++ b/tests/PHPStan/PhpDoc/PhpDocStringResolverTest.php @@ -2,7 +2,9 @@ namespace PHPStan\PhpDoc; +use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\ParserException; +use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\Testing\PHPStanTestCase; class PhpDocStringResolverTest extends PHPStanTestCase @@ -17,4 +19,24 @@ public function testConsumeEnd(): void $phpDocStringResolver->resolve('/** @param int $test */ test'); } + public function testEqualStringsShareOneNode(): void + { + $phpDocStringResolver = $this->createPhpDocStringResolver(); + + $node = $phpDocStringResolver->resolve('/** @param int $test */'); + $this->assertSame($node, $phpDocStringResolver->resolve('/** @param int $test */')); + $this->assertNotSame($node, $phpDocStringResolver->resolve('/** @param string $test */')); + } + + private function createPhpDocStringResolver(): PhpDocStringResolver + { + $container = self::getContainer(); + + return new PhpDocStringResolver( + $container->getByType(Lexer::class), + $container->getByType(PhpDocParser::class), + 1024, + ); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php index d3bab774130..5c7425f2561 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php @@ -2,8 +2,7 @@ namespace PHPStan\Rules\PhpDoc; -use PHPStan\PhpDocParser\Lexer\Lexer; -use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -16,10 +15,8 @@ class InvalidPHPStanDocTagRuleTest extends RuleTestCase protected function getRule(): Rule { - $container = self::getContainer(); return new InvalidPHPStanDocTagRule( - $container->getByType(Lexer::class), - $container->getByType(PhpDocParser::class), + self::getContainer()->getByType(PhpDocStringResolver::class), ); } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php index 7010ec568fa..6b5896643bc 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php @@ -2,8 +2,7 @@ namespace PHPStan\Rules\PhpDoc; -use PHPStan\PhpDocParser\Lexer\Lexer; -use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -16,10 +15,8 @@ class InvalidPhpDocTagValueRuleTest extends RuleTestCase protected function getRule(): Rule { - $container = self::getContainer(); return new InvalidPhpDocTagValueRule( - $container->getByType(Lexer::class), - $container->getByType(PhpDocParser::class), + self::getContainer()->getByType(PhpDocStringResolver::class), ); }