|
7 | 7 | use PHPStan\ShouldNotHappenException; |
8 | 8 | use PHPStan\Testing\PHPStanTestCase; |
9 | 9 | use RuntimeException; |
| 10 | +use function file_put_contents; |
| 11 | +use function microtime; |
10 | 12 | use function realpath; |
| 13 | +use function sys_get_temp_dir; |
| 14 | +use function tempnam; |
| 15 | +use function unlink; |
11 | 16 |
|
12 | 17 | class FileTypeMapperTest extends PHPStanTestCase |
13 | 18 | { |
@@ -196,6 +201,78 @@ public function testFileWithCyclicPhpDocs(): void |
196 | 201 | $this->assertSame('CyclicPhpDocs\Foo|iterable<CyclicPhpDocs\Foo>', $returnTag->getType()->describe(VerbosityLevel::precise())); |
197 | 202 | } |
198 | 203 |
|
| 204 | + public function testLargeStubFileLazyPhpDocParsing(): void |
| 205 | + { |
| 206 | + /** @var FileTypeMapper $fileTypeMapper */ |
| 207 | + $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); |
| 208 | + |
| 209 | + // Generate a large stub file with many PHPDoc comments, simulating |
| 210 | + // WordPress-style stubs where a single file declares many functions. |
| 211 | + $tmpFile = tempnam(sys_get_temp_dir(), 'phpstan_stub_perf_'); |
| 212 | + if ($tmpFile === false) { |
| 213 | + throw new ShouldNotHappenException(); |
| 214 | + } |
| 215 | + $tmpFile .= '.php'; |
| 216 | + |
| 217 | + try { |
| 218 | + $code = "<?php declare(strict_types = 1);\n\nnamespace StubPhpDocPerformance;\n\n"; |
| 219 | + |
| 220 | + // 10000 functions with complex PHPDocs but no template/type-alias tags |
| 221 | + for ($i = 1; $i <= 10000; $i++) { |
| 222 | + $code .= "/**\n"; |
| 223 | + $code .= " * @param array<string, array<int, string>> \$param1\n"; |
| 224 | + $code .= " * @param callable(int, string): array<string, mixed> \$param2\n"; |
| 225 | + $code .= " * @param list<array{id: int, name: string, data: array<string, mixed>}> \$param3\n"; |
| 226 | + $code .= " * @return array<int, array{key: string, value: mixed, metadata: array<string, string>}>\n"; |
| 227 | + $code .= " */\n"; |
| 228 | + $code .= "function stub_{$i}(array \$param1, callable \$param2, array \$param3): array {}\n\n"; |
| 229 | + } |
| 230 | + |
| 231 | + // A class with @template - this PHPDoc must still be parsed |
| 232 | + $code .= "/**\n * @template T\n */\n"; |
| 233 | + $code .= "class GenericContainer\n{\n"; |
| 234 | + $code .= " /** @var T */\n private \$value;\n\n"; |
| 235 | + $code .= " /** @param T \$value */\n"; |
| 236 | + $code .= " public function __construct(\$value) { \$this->value = \$value; }\n\n"; |
| 237 | + $code .= " /** @return T */\n"; |
| 238 | + $code .= " public function getValue() { return \$this->value; }\n"; |
| 239 | + $code .= "}\n"; |
| 240 | + |
| 241 | + file_put_contents($tmpFile, $code); |
| 242 | + |
| 243 | + $start = microtime(true); |
| 244 | + |
| 245 | + // Resolve the template class - should work despite 10000 other PHPDocs in file |
| 246 | + $resolved = $fileTypeMapper->getResolvedPhpDoc( |
| 247 | + $tmpFile, |
| 248 | + 'StubPhpDocPerformance\\GenericContainer', |
| 249 | + null, |
| 250 | + 'getValue', |
| 251 | + '/** @return T */', |
| 252 | + ); |
| 253 | + |
| 254 | + $elapsed = microtime(true) - $start; |
| 255 | + |
| 256 | + $returnTag = $resolved->getReturnTag(); |
| 257 | + $this->assertNotNull($returnTag); |
| 258 | + $this->assertSame( |
| 259 | + 'T (class StubPhpDocPerformance\GenericContainer, parameter)', |
| 260 | + $returnTag->getType()->describe(VerbosityLevel::precise()), |
| 261 | + ); |
| 262 | + |
| 263 | + // With lazy PHPDoc parsing, only PHPDocs with @template or type-alias |
| 264 | + // tags are parsed during name scope map creation. Without the optimization, |
| 265 | + // all 10000+ PHPDocs must be parsed, taking >2 seconds on typical hardware. |
| 266 | + $this->assertLessThan( |
| 267 | + 3.0, |
| 268 | + $elapsed, |
| 269 | + 'FileTypeMapper should skip PHPDoc parsing for entries without template/type-alias tags', |
| 270 | + ); |
| 271 | + } finally { |
| 272 | + @unlink($tmpFile); |
| 273 | + } |
| 274 | + } |
| 275 | + |
199 | 276 | public function testFilesWithIdenticalPhpDocsUsingDifferentAliases(): void |
200 | 277 | { |
201 | 278 | /** @var FileTypeMapper $fileTypeMapper */ |
|
0 commit comments