diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 2c281f58cad..f2dda82c9c7 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -485,7 +485,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA || ($node instanceof Node\PropertyHook && $node->getAttribute('propertyName') !== null) ) { $docComment = GetLastDocComment::forNode($node); - if ($docComment !== null) { + if ($docComment !== null && $this->shouldParseDocComment($docComment)) { $phpDocNode = $this->phpDocStringResolver->resolve($docComment); } } @@ -751,6 +751,16 @@ private function chooseTemplateTagValueNodesByPriority(array $tags): array return $resolved; } + private function shouldParseDocComment(string $docComment): bool + { + return str_contains($docComment, '@template') + || str_contains($docComment, '-template') + || str_contains($docComment, '@phpstan-type') + || str_contains($docComment, '@psalm-type') + || str_contains($docComment, '@phan-type') + || str_contains($docComment, 'import-type'); + } + /** * @return array */ diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index d613047140b..d195dcefa0d 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -7,7 +7,12 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; use RuntimeException; +use function file_put_contents; +use function microtime; use function realpath; +use function sys_get_temp_dir; +use function tempnam; +use function unlink; class FileTypeMapperTest extends PHPStanTestCase { @@ -196,6 +201,78 @@ public function testFileWithCyclicPhpDocs(): void $this->assertSame('CyclicPhpDocs\Foo|iterable', $returnTag->getType()->describe(VerbosityLevel::precise())); } + public function testLargeStubFileLazyPhpDocParsing(): void + { + /** @var FileTypeMapper $fileTypeMapper */ + $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); + + // Generate a large stub file with many PHPDoc comments, simulating + // WordPress-style stubs where a single file declares many functions. + $tmpFile = tempnam(sys_get_temp_dir(), 'phpstan_stub_perf_'); + if ($tmpFile === false) { + throw new ShouldNotHappenException(); + } + $tmpFile .= '.php'; + + try { + $code = "> \$param1\n"; + $code .= " * @param callable(int, string): array \$param2\n"; + $code .= " * @param list}> \$param3\n"; + $code .= " * @return array}>\n"; + $code .= " */\n"; + $code .= "function stub_{$i}(array \$param1, callable \$param2, array \$param3): array {}\n\n"; + } + + // A class with @template - this PHPDoc must still be parsed + $code .= "/**\n * @template T\n */\n"; + $code .= "class GenericContainer\n{\n"; + $code .= " /** @var T */\n private \$value;\n\n"; + $code .= " /** @param T \$value */\n"; + $code .= " public function __construct(\$value) { \$this->value = \$value; }\n\n"; + $code .= " /** @return T */\n"; + $code .= " public function getValue() { return \$this->value; }\n"; + $code .= "}\n"; + + file_put_contents($tmpFile, $code); + + $start = microtime(true); + + // Resolve the template class - should work despite 10000 other PHPDocs in file + $resolved = $fileTypeMapper->getResolvedPhpDoc( + $tmpFile, + 'StubPhpDocPerformance\\GenericContainer', + null, + 'getValue', + '/** @return T */', + ); + + $elapsed = microtime(true) - $start; + + $returnTag = $resolved->getReturnTag(); + $this->assertNotNull($returnTag); + $this->assertSame( + 'T (class StubPhpDocPerformance\GenericContainer, parameter)', + $returnTag->getType()->describe(VerbosityLevel::precise()), + ); + + // With lazy PHPDoc parsing, only PHPDocs with @template or type-alias + // tags are parsed during name scope map creation. Without the optimization, + // all 10000+ PHPDocs must be parsed, taking >2 seconds on typical hardware. + $this->assertLessThan( + 3.0, + $elapsed, + 'FileTypeMapper should skip PHPDoc parsing for entries without template/type-alias tags', + ); + } finally { + @unlink($tmpFile); + } + } + public function testFilesWithIdenticalPhpDocsUsingDifferentAliases(): void { /** @var FileTypeMapper $fileTypeMapper */