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), ); }