From 24166323b5d203c2f205b36d2c91b8312f5cb86f Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 09:39:12 +0700 Subject: [PATCH 01/14] add test for infinite loop on recursive trait --- tests/PHPStan/Type/FileTypeMapperTest.php | 91 ++++++++++++++++++- .../PHPStan/Type/data/bug-9684/BaseModel.php | 31 +++++++ .../Type/data/bug-9684/RecursiveTrait.php | 14 +++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Type/data/bug-9684/BaseModel.php create mode 100644 tests/PHPStan/Type/data/bug-9684/RecursiveTrait.php diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index d613047140b..ee4ec66935f 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use DependentPhpDocs\Foo; +use PhpParser\Node; use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; @@ -11,7 +12,6 @@ class FileTypeMapperTest extends PHPStanTestCase { - public function testGetResolvedPhpDoc(): void { /** @var FileTypeMapper $fileTypeMapper */ @@ -210,4 +210,93 @@ public function testFilesWithIdenticalPhpDocsUsingDifferentAliases(): void $this->assertSame('AliasCollisionNamespace2\Foo', $doc2->getVarTags()['x']->getType()->describe(VerbosityLevel::precise())); } + public function testNodeCallbackLikeRectorInfiniteRecursionSimulation(): void + { + self::createReflectionProvider(); + + /** @var FileTypeMapper $fileTypeMapper */ + $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); + + // Use nikic/php-parser directly to parse the file + $parserFactory = new \PhpParser\ParserFactory(); + $parser = $parserFactory->createForNewestSupportedVersion(); + $baseModelPath = __DIR__ . '/data/bug-9684/BaseModel.php'; + $code = file_get_contents($baseModelPath); + if ($code === false) { + $this->fail('Failed to read BaseModel.php'); + } + $ast = $parser->parse($code); + if (!is_array($ast)) { + $this->fail('Failed to parse BaseModel.php'); + } + + $traitUsages = []; + $nodeCallback = function (Node $node) use (&$traitUsages, &$nodeCallback) { + if ($node instanceof \PhpParser\Node\Stmt\TraitUse) { + foreach ($node->traits as $trait) { + $traitUsages[] = $trait->toString(); + } + } + + foreach ($node->getSubNodeNames() as $name) { + $subNode = $node->{$name}; + if (is_array($subNode)) { + foreach ($subNode as $child) { + if ($child instanceof \PhpParser\Node) { + $nodeCallback($child); + } + } + } elseif ($subNode instanceof \PhpParser\Node) { + $nodeCallback($subNode); + } + } + }; + + foreach ($ast as $astNode) { + $nodeCallback($astNode); + } + + // Simulate Rector's repeated trait processing + $traitFile = __DIR__ . '/data/bug-9684/RecursiveTrait.php'; + foreach ($traitUsages as $traitName) { + // Simulate Rector's use: both className and traitName set, repeatedly, as Rector does when walking traits + for ($i = 0; $i < 20; $i++) { + $fileTypeMapper->getResolvedPhpDoc( + $traitFile, + 'Bug9684\\BaseModel', + 'Bug9684\\RecursiveTrait', + null, + null + ); + } + } + $this->assertNotEmpty($traitUsages, 'Trait usages should be found'); + } + + public function testRecursiveTraitUsedInAnonymousClassDoesNotLoopIndefinitely(): void + { + self::createReflectionProvider(); + + /** @var FileTypeMapper $fileTypeMapper */ + $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); + + $realpath = realpath(__DIR__ . '/data/bug-9684/BaseModel.php'); + if ($realpath === false) { + throw new ShouldNotHappenException(); + } + + $resolved = $fileTypeMapper->getResolvedPhpDoc( + $realpath, + \Bug9684\BaseModel::class, + null, + null, + '/** @method static Builder|BaseModel query() */', + ); + + $this->assertArrayHasKey('query', $resolved->getMethodTags()); + $returnTypeDescription = $resolved->getMethodTags()['query']->getReturnType()->describe(VerbosityLevel::precise()); + $this->assertStringContainsString('Bug9684\BaseModel', $returnTypeDescription); + $this->assertStringContainsString('Bug9684\Builder', $returnTypeDescription); + } + } diff --git a/tests/PHPStan/Type/data/bug-9684/BaseModel.php b/tests/PHPStan/Type/data/bug-9684/BaseModel.php new file mode 100644 index 00000000000..3622b0fbd89 --- /dev/null +++ b/tests/PHPStan/Type/data/bug-9684/BaseModel.php @@ -0,0 +1,31 @@ +|BaseModel query() + */ +class BaseModel extends Model +{ + use RecursiveTrait; + + public function parent(): BelongsTo + { + return new BelongsTo(); + } +} diff --git a/tests/PHPStan/Type/data/bug-9684/RecursiveTrait.php b/tests/PHPStan/Type/data/bug-9684/RecursiveTrait.php new file mode 100644 index 00000000000..e5b9934d8e8 --- /dev/null +++ b/tests/PHPStan/Type/data/bug-9684/RecursiveTrait.php @@ -0,0 +1,14 @@ + Date: Sat, 18 Apr 2026 09:47:14 +0700 Subject: [PATCH 02/14] keep spacing --- tests/PHPStan/Type/FileTypeMapperTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index ee4ec66935f..e1e54ae73e5 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -12,6 +12,7 @@ class FileTypeMapperTest extends PHPStanTestCase { + public function testGetResolvedPhpDoc(): void { /** @var FileTypeMapper $fileTypeMapper */ From e175dc4f4904e4a7e39e0612f83c5815d970b72c Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 09:48:30 +0700 Subject: [PATCH 03/14] clean up test --- tests/PHPStan/Type/FileTypeMapperTest.php | 63 ----------------------- 1 file changed, 63 deletions(-) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index e1e54ae73e5..a0af6d93a2f 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -211,69 +211,6 @@ public function testFilesWithIdenticalPhpDocsUsingDifferentAliases(): void $this->assertSame('AliasCollisionNamespace2\Foo', $doc2->getVarTags()['x']->getType()->describe(VerbosityLevel::precise())); } - public function testNodeCallbackLikeRectorInfiniteRecursionSimulation(): void - { - self::createReflectionProvider(); - - /** @var FileTypeMapper $fileTypeMapper */ - $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); - - // Use nikic/php-parser directly to parse the file - $parserFactory = new \PhpParser\ParserFactory(); - $parser = $parserFactory->createForNewestSupportedVersion(); - $baseModelPath = __DIR__ . '/data/bug-9684/BaseModel.php'; - $code = file_get_contents($baseModelPath); - if ($code === false) { - $this->fail('Failed to read BaseModel.php'); - } - $ast = $parser->parse($code); - if (!is_array($ast)) { - $this->fail('Failed to parse BaseModel.php'); - } - - $traitUsages = []; - $nodeCallback = function (Node $node) use (&$traitUsages, &$nodeCallback) { - if ($node instanceof \PhpParser\Node\Stmt\TraitUse) { - foreach ($node->traits as $trait) { - $traitUsages[] = $trait->toString(); - } - } - - foreach ($node->getSubNodeNames() as $name) { - $subNode = $node->{$name}; - if (is_array($subNode)) { - foreach ($subNode as $child) { - if ($child instanceof \PhpParser\Node) { - $nodeCallback($child); - } - } - } elseif ($subNode instanceof \PhpParser\Node) { - $nodeCallback($subNode); - } - } - }; - - foreach ($ast as $astNode) { - $nodeCallback($astNode); - } - - // Simulate Rector's repeated trait processing - $traitFile = __DIR__ . '/data/bug-9684/RecursiveTrait.php'; - foreach ($traitUsages as $traitName) { - // Simulate Rector's use: both className and traitName set, repeatedly, as Rector does when walking traits - for ($i = 0; $i < 20; $i++) { - $fileTypeMapper->getResolvedPhpDoc( - $traitFile, - 'Bug9684\\BaseModel', - 'Bug9684\\RecursiveTrait', - null, - null - ); - } - } - $this->assertNotEmpty($traitUsages, 'Trait usages should be found'); - } - public function testRecursiveTraitUsedInAnonymousClassDoesNotLoopIndefinitely(): void { self::createReflectionProvider(); From 67727b0923c99c82dffc198c136fc6446fbf659d Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 10:06:51 +0700 Subject: [PATCH 04/14] remove unused use --- tests/PHPStan/Type/FileTypeMapperTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index a0af6d93a2f..31add892cd0 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -3,7 +3,6 @@ namespace PHPStan\Type; use DependentPhpDocs\Foo; -use PhpParser\Node; use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; From 35e30bdbf39e9f6c2ac6054f4841228499c5b28f Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 10:26:45 +0700 Subject: [PATCH 05/14] fix with detect nested trait --- src/Type/FileTypeMapper.php | 19 +++++++++++++++++-- tests/PHPStan/Type/FileTypeMapperTest.php | 9 +++++---- .../BaseModel.php | 4 ++-- .../RecursiveTrait.php | 2 +- 4 files changed, 25 insertions(+), 9 deletions(-) rename tests/PHPStan/Type/data/{bug-9684 => bug-self-referenced-trait}/BaseModel.php (80%) rename tests/PHPStan/Type/data/{bug-9684 => bug-self-referenced-trait}/RecursiveTrait.php (82%) diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 2c281f58cad..aef64676399 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -394,9 +394,10 @@ private function loadCachedPhpDocNodeMap(string $cacheKey, string $variableCache /** * @param array $traitMethodAliases + * @param array $activeTraitResolutions * @return array{array, list} */ - private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName): array + private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName, array $activeTraitResolutions = []): array { /** @var array $nameScopeMap */ $nameScopeMap = []; @@ -425,7 +426,7 @@ private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?s $constUses = []; $this->processNodes( $this->phpParser->parseFile($fileName), - function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$typeMapStack, &$typeAliasStack, &$classStack, &$namespace, &$functionStack, &$uses, &$constUses, &$files): ?int { + function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, $activeTraitResolutions, &$nameScopeMap, &$typeMapStack, &$typeAliasStack, &$classStack, &$namespace, &$functionStack, &$uses, &$constUses, &$files): ?int { if ($node instanceof Node\Stmt\ClassLike) { if ($traitFound && $fileName === $originalClassFileName) { return self::SKIP_NODE; @@ -635,12 +636,21 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA throw new ShouldNotHappenException(); } + $traitResolutionKey = $this->getTraitResolutionKey($traitReflection->getFileName(), $traitName, $className, $originalClassFileName); + if (isset($activeTraitResolutions[$traitResolutionKey])) { + continue; + } + + $nestedActiveTraitResolutions = $activeTraitResolutions; + $nestedActiveTraitResolutions[$traitResolutionKey] = true; + [$traitNameScopeMap, $traitFiles] = $this->createPhpDocNodeMap( $traitReflection->getFileName(), $traitName, $className, $traitMethodAliases[$traitName] ?? [], $originalClassFileName, + $nestedActiveTraitResolutions, ); $nameScopeMap = array_merge($nameScopeMap, array_map(static fn ($originalNameScope) => $originalNameScope->getTraitData() === null ? $originalNameScope->withTraitData($originalClassFileName, $className, $traitName, $lookForTrait, $docComment) : $originalNameScope, $traitNameScopeMap)); $files = array_merge($files, $traitFiles); @@ -818,4 +828,9 @@ private function getPhpDocKey(string $nameScopeKey, string $docComment): string return md5(sprintf('%s-%s', $nameScopeKey, $doc->getReformattedText())); } + private function getTraitResolutionKey(string $fileName, string $traitName, string $className, string $originalClassFileName): string + { + return md5(sprintf('%s-%s-%s-%s', $fileName, $traitName, $className, $originalClassFileName)); + } + } diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index 31add892cd0..4d8721cbf5b 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use BugSelfReferencedTrait\BaseModel; use DependentPhpDocs\Foo; use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\ShouldNotHappenException; @@ -217,14 +218,14 @@ public function testRecursiveTraitUsedInAnonymousClassDoesNotLoopIndefinitely(): /** @var FileTypeMapper $fileTypeMapper */ $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); - $realpath = realpath(__DIR__ . '/data/bug-9684/BaseModel.php'); + $realpath = realpath(__DIR__ . '/data/bug-self-referenced-trait/BaseModel.php'); if ($realpath === false) { throw new ShouldNotHappenException(); } $resolved = $fileTypeMapper->getResolvedPhpDoc( $realpath, - \Bug9684\BaseModel::class, + BaseModel::class, null, null, '/** @method static Builder|BaseModel query() */', @@ -232,8 +233,8 @@ public function testRecursiveTraitUsedInAnonymousClassDoesNotLoopIndefinitely(): $this->assertArrayHasKey('query', $resolved->getMethodTags()); $returnTypeDescription = $resolved->getMethodTags()['query']->getReturnType()->describe(VerbosityLevel::precise()); - $this->assertStringContainsString('Bug9684\BaseModel', $returnTypeDescription); - $this->assertStringContainsString('Bug9684\Builder', $returnTypeDescription); + $this->assertStringContainsString('BugSelfReferencedTrait\BaseModel', $returnTypeDescription); + $this->assertStringContainsString('BugSelfReferencedTrait\Builder', $returnTypeDescription); } } diff --git a/tests/PHPStan/Type/data/bug-9684/BaseModel.php b/tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModel.php similarity index 80% rename from tests/PHPStan/Type/data/bug-9684/BaseModel.php rename to tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModel.php index 3622b0fbd89..466381fbf29 100644 --- a/tests/PHPStan/Type/data/bug-9684/BaseModel.php +++ b/tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModel.php @@ -1,8 +1,8 @@ Date: Sat, 18 Apr 2026 10:53:15 +0700 Subject: [PATCH 06/14] avoid duplicated class BaseModel to other class --- tests/PHPStan/Type/FileTypeMapperTest.php | 12 ++++++------ .../{BaseModel.php => BaseModelUseTrait.php} | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) rename tests/PHPStan/Type/data/bug-self-referenced-trait/{BaseModel.php => BaseModelUseTrait.php} (75%) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index 4d8721cbf5b..c1f390fc17f 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -use BugSelfReferencedTrait\BaseModel; +use BugSelfReferencedTrait\BaseModelUseTrait; use DependentPhpDocs\Foo; use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\ShouldNotHappenException; @@ -218,23 +218,23 @@ public function testRecursiveTraitUsedInAnonymousClassDoesNotLoopIndefinitely(): /** @var FileTypeMapper $fileTypeMapper */ $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); - $realpath = realpath(__DIR__ . '/data/bug-self-referenced-trait/BaseModel.php'); + $realpath = realpath(__DIR__ . '/data/bug-self-referenced-trait/BaseModelUseTrait.php'); if ($realpath === false) { throw new ShouldNotHappenException(); } $resolved = $fileTypeMapper->getResolvedPhpDoc( $realpath, - BaseModel::class, + BaseModelUseTrait::class, null, null, - '/** @method static Builder|BaseModel query() */', + '/** @method static Builder|BaseModelUseTrait query() */', ); $this->assertArrayHasKey('query', $resolved->getMethodTags()); $returnTypeDescription = $resolved->getMethodTags()['query']->getReturnType()->describe(VerbosityLevel::precise()); - $this->assertStringContainsString('BugSelfReferencedTrait\BaseModel', $returnTypeDescription); - $this->assertStringContainsString('BugSelfReferencedTrait\Builder', $returnTypeDescription); + $this->assertStringContainsString('BugSelfReferencedTrait\BaseModelUseTrait', $returnTypeDescription); + $this->assertStringContainsString('BugSelfReferencedTrait\Builder', $returnTypeDescription); } } diff --git a/tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModel.php b/tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModelUseTrait.php similarity index 75% rename from tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModel.php rename to tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModelUseTrait.php index 466381fbf29..2f19c3086ef 100644 --- a/tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModel.php +++ b/tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModelUseTrait.php @@ -18,9 +18,9 @@ class BelongsTo } /** - * @method static Builder|BaseModel query() + * @method static Builder|BaseModelUseTrait query() */ -class BaseModel extends Model +class BaseModelUseTrait extends Model { use RecursiveTrait; From 1a0e7456805bc6802046e5b84bb24b8b7607c770 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 11:27:20 +0700 Subject: [PATCH 07/14] final touch: ensure result test consistent --- tests/PHPStan/Type/FileTypeMapperTest.php | 34 +++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index c1f390fc17f..05944594494 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -213,16 +213,18 @@ public function testFilesWithIdenticalPhpDocsUsingDifferentAliases(): void public function testRecursiveTraitUsedInAnonymousClassDoesNotLoopIndefinitely(): void { - self::createReflectionProvider(); - - /** @var FileTypeMapper $fileTypeMapper */ - $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); - $realpath = realpath(__DIR__ . '/data/bug-self-referenced-trait/BaseModelUseTrait.php'); if ($realpath === false) { throw new ShouldNotHappenException(); } + $container = self::getContainer(); + $this->clearFileTypeMapperCache($container->getParameter('tmpDir'), $realpath); + self::createReflectionProvider(); + + /** @var FileTypeMapper $fileTypeMapper */ + $fileTypeMapper = $container->getByType(FileTypeMapper::class); + $resolved = $fileTypeMapper->getResolvedPhpDoc( $realpath, BaseModelUseTrait::class, @@ -237,4 +239,26 @@ public function testRecursiveTraitUsedInAnonymousClassDoesNotLoopIndefinitely(): $this->assertStringContainsString('BugSelfReferencedTrait\Builder', $returnTypeDescription); } + /** + * This ensure test result consistent regardless FileTypeMapper when just rolled back to verify back + */ + private function clearFileTypeMapperCache(string $tmpDir, string $fileName): void + { + $cacheKeyHash = hash('sha256', sprintf('ftm-%s', $fileName)); + $cacheFilePath = sprintf( + '%s/cache/PHPStan/%s/%s/%s.php', + $tmpDir, + substr($cacheKeyHash, 0, 2), + substr($cacheKeyHash, 2, 2), + $cacheKeyHash, + ); + + if (!is_file($cacheFilePath)) { + return; + } + + unlink($cacheFilePath); + clearstatcache(); + } + } From db816232a86f60779106e586660bc57ca90e3afd Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 11:33:11 +0700 Subject: [PATCH 08/14] final touch: fix cs --- tests/PHPStan/Type/FileTypeMapperTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index 05944594494..f1b20354a4e 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -8,7 +8,13 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; use RuntimeException; +use function clearstatcache; +use function hash; +use function is_file; use function realpath; +use function sprintf; +use function substr; +use function unlink; class FileTypeMapperTest extends PHPStanTestCase { From 248527fedac3af487ea76dc53bd2e70c5426e0c5 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 11:40:27 +0700 Subject: [PATCH 09/14] final touch: fix phpstan notice on php 7.4 --- tests/PHPStan/Type/FileTypeMapperTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index f1b20354a4e..4fc76437179 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -254,8 +254,8 @@ private function clearFileTypeMapperCache(string $tmpDir, string $fileName): voi $cacheFilePath = sprintf( '%s/cache/PHPStan/%s/%s/%s.php', $tmpDir, - substr($cacheKeyHash, 0, 2), - substr($cacheKeyHash, 2, 2), + (string) substr($cacheKeyHash, 0, 2), + (string) substr($cacheKeyHash, 2, 2), $cacheKeyHash, ); From eba0376535a02588650f428b65e44db235e808a6 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 11:48:36 +0700 Subject: [PATCH 10/14] fix phpstan on php 8+ --- tests/PHPStan/Type/FileTypeMapperTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index 4fc76437179..743accc93ec 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -251,11 +251,15 @@ public function testRecursiveTraitUsedInAnonymousClassDoesNotLoopIndefinitely(): private function clearFileTypeMapperCache(string $tmpDir, string $fileName): void { $cacheKeyHash = hash('sha256', sprintf('ftm-%s', $fileName)); + + $directory1 = substr($cacheKeyHash, 0, 2); + $directory2 = substr($cacheKeyHash, 2, 2); + $cacheFilePath = sprintf( '%s/cache/PHPStan/%s/%s/%s.php', $tmpDir, - (string) substr($cacheKeyHash, 0, 2), - (string) substr($cacheKeyHash, 2, 2), + $directory1, + $directory2, $cacheKeyHash, ); From f090089f98e3804a35ea7d05f200e74ea55823fd Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 11:51:45 +0700 Subject: [PATCH 11/14] fix phpstan on php 7.4 --- tests/PHPStan/Type/FileTypeMapperTest.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index 743accc93ec..17a5251c6f8 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -255,13 +255,8 @@ private function clearFileTypeMapperCache(string $tmpDir, string $fileName): voi $directory1 = substr($cacheKeyHash, 0, 2); $directory2 = substr($cacheKeyHash, 2, 2); - $cacheFilePath = sprintf( - '%s/cache/PHPStan/%s/%s/%s.php', - $tmpDir, - $directory1, - $directory2, - $cacheKeyHash, - ); + $cacheTmpRootDir = sprintf('%s/cache/PHPStan', $tmpDir); + $cacheFilePath = $cacheTmpRootDir . '/' . $directory1 . '/' . $directory2 . $cacheKeyHash; if (!is_file($cacheFilePath)) { return; From b6b9117c52dbbeda8051b93db47eb0dc9adf343f Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 11:55:22 +0700 Subject: [PATCH 12/14] fix cache concat key --- tests/PHPStan/Type/FileTypeMapperTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index 17a5251c6f8..7869104d8f1 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -256,7 +256,7 @@ private function clearFileTypeMapperCache(string $tmpDir, string $fileName): voi $directory2 = substr($cacheKeyHash, 2, 2); $cacheTmpRootDir = sprintf('%s/cache/PHPStan', $tmpDir); - $cacheFilePath = $cacheTmpRootDir . '/' . $directory1 . '/' . $directory2 . $cacheKeyHash; + $cacheFilePath = $cacheTmpRootDir . '/' . $directory1 . '/' . $directory2 . '/' . $cacheKeyHash . '.php'; if (!is_file($cacheFilePath)) { return; From 44d4c03930e31a74f991076cfd02a246245629a2 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 18 Apr 2026 11:57:50 +0700 Subject: [PATCH 13/14] final touch: cs fix --- tests/PHPStan/Type/FileTypeMapperTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index 7869104d8f1..ae5cd89ae32 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -256,7 +256,7 @@ private function clearFileTypeMapperCache(string $tmpDir, string $fileName): voi $directory2 = substr($cacheKeyHash, 2, 2); $cacheTmpRootDir = sprintf('%s/cache/PHPStan', $tmpDir); - $cacheFilePath = $cacheTmpRootDir . '/' . $directory1 . '/' . $directory2 . '/' . $cacheKeyHash . '.php'; + $cacheFilePath = $cacheTmpRootDir . '/' . $directory1 . '/' . $directory2 . '/' . $cacheKeyHash . '.php'; if (!is_file($cacheFilePath)) { return; From 10f93cdfb0d21181fb7a05f80ff667c5973fa1fa Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sun, 19 Apr 2026 18:02:51 +0700 Subject: [PATCH 14/14] move to AnalyserIntegrationTest and increase cache key --- src/Type/FileTypeMapper.php | 2 +- .../Analyser/AnalyserIntegrationTest.php | 7 +++ .../BaseModelUseTrait.php | 0 .../RecursiveTrait.php | 0 tests/PHPStan/Type/FileTypeMapperTest.php | 56 ------------------- 5 files changed, 8 insertions(+), 57 deletions(-) rename tests/PHPStan/{Type => Analyser}/data/bug-self-referenced-trait/BaseModelUseTrait.php (100%) rename tests/PHPStan/{Type => Analyser}/data/bug-self-referenced-trait/RecursiveTrait.php (100%) diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index aef64676399..ddd2032d62f 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -329,7 +329,7 @@ private function getNameScopeMap(string $fileName): array { if (!isset($this->memoryCache[$fileName])) { $cacheKey = sprintf('ftm-%s', $fileName); - $variableCacheKey = sprintf('v4-%s', ComposerHelper::getPhpDocParserVersion()); + $variableCacheKey = sprintf('v5-%s', ComposerHelper::getPhpDocParserVersion()); $cached = $this->loadCachedPhpDocNodeMap($cacheKey, $variableCacheKey); if ($cached === null) { [$nameScopeMap, $files] = $this->createPhpDocNodeMap($fileName, null, null, [], $fileName); diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index e7c5a9d37ff..9949f3f9a25 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1529,6 +1529,13 @@ public function testBug14439(): void $this->assertNoErrors($errors); } + public function testBugInfiniteLoopOnFileTypeMapper(): void + { + // endless loop crash on self referencing trait + $errors = $this->runAnalyse(__DIR__ . '/data/bug-self-referenced-trait/BaseModelUseTrait.php'); + $this->assertCount(0, $errors); + } + /** * @param string[]|null $allAnalysedFiles * @return list diff --git a/tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModelUseTrait.php b/tests/PHPStan/Analyser/data/bug-self-referenced-trait/BaseModelUseTrait.php similarity index 100% rename from tests/PHPStan/Type/data/bug-self-referenced-trait/BaseModelUseTrait.php rename to tests/PHPStan/Analyser/data/bug-self-referenced-trait/BaseModelUseTrait.php diff --git a/tests/PHPStan/Type/data/bug-self-referenced-trait/RecursiveTrait.php b/tests/PHPStan/Analyser/data/bug-self-referenced-trait/RecursiveTrait.php similarity index 100% rename from tests/PHPStan/Type/data/bug-self-referenced-trait/RecursiveTrait.php rename to tests/PHPStan/Analyser/data/bug-self-referenced-trait/RecursiveTrait.php diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index ae5cd89ae32..d613047140b 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -2,19 +2,12 @@ namespace PHPStan\Type; -use BugSelfReferencedTrait\BaseModelUseTrait; use DependentPhpDocs\Foo; use PHPStan\PhpDoc\Tag\ReturnTag; use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; use RuntimeException; -use function clearstatcache; -use function hash; -use function is_file; use function realpath; -use function sprintf; -use function substr; -use function unlink; class FileTypeMapperTest extends PHPStanTestCase { @@ -217,53 +210,4 @@ public function testFilesWithIdenticalPhpDocsUsingDifferentAliases(): void $this->assertSame('AliasCollisionNamespace2\Foo', $doc2->getVarTags()['x']->getType()->describe(VerbosityLevel::precise())); } - public function testRecursiveTraitUsedInAnonymousClassDoesNotLoopIndefinitely(): void - { - $realpath = realpath(__DIR__ . '/data/bug-self-referenced-trait/BaseModelUseTrait.php'); - if ($realpath === false) { - throw new ShouldNotHappenException(); - } - - $container = self::getContainer(); - $this->clearFileTypeMapperCache($container->getParameter('tmpDir'), $realpath); - self::createReflectionProvider(); - - /** @var FileTypeMapper $fileTypeMapper */ - $fileTypeMapper = $container->getByType(FileTypeMapper::class); - - $resolved = $fileTypeMapper->getResolvedPhpDoc( - $realpath, - BaseModelUseTrait::class, - null, - null, - '/** @method static Builder|BaseModelUseTrait query() */', - ); - - $this->assertArrayHasKey('query', $resolved->getMethodTags()); - $returnTypeDescription = $resolved->getMethodTags()['query']->getReturnType()->describe(VerbosityLevel::precise()); - $this->assertStringContainsString('BugSelfReferencedTrait\BaseModelUseTrait', $returnTypeDescription); - $this->assertStringContainsString('BugSelfReferencedTrait\Builder', $returnTypeDescription); - } - - /** - * This ensure test result consistent regardless FileTypeMapper when just rolled back to verify back - */ - private function clearFileTypeMapperCache(string $tmpDir, string $fileName): void - { - $cacheKeyHash = hash('sha256', sprintf('ftm-%s', $fileName)); - - $directory1 = substr($cacheKeyHash, 0, 2); - $directory2 = substr($cacheKeyHash, 2, 2); - - $cacheTmpRootDir = sprintf('%s/cache/PHPStan', $tmpDir); - $cacheFilePath = $cacheTmpRootDir . '/' . $directory1 . '/' . $directory2 . '/' . $cacheKeyHash . '.php'; - - if (!is_file($cacheFilePath)) { - return; - } - - unlink($cacheFilePath); - clearstatcache(); - } - }