diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 2c281f58ca..ddd2032d62 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); @@ -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/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index e7c5a9d37f..9949f3f9a2 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/Analyser/data/bug-self-referenced-trait/BaseModelUseTrait.php b/tests/PHPStan/Analyser/data/bug-self-referenced-trait/BaseModelUseTrait.php new file mode 100644 index 0000000000..2f19c3086e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-self-referenced-trait/BaseModelUseTrait.php @@ -0,0 +1,31 @@ +|BaseModelUseTrait query() + */ +class BaseModelUseTrait extends Model +{ + use RecursiveTrait; + + public function parent(): BelongsTo + { + return new BelongsTo(); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-self-referenced-trait/RecursiveTrait.php b/tests/PHPStan/Analyser/data/bug-self-referenced-trait/RecursiveTrait.php new file mode 100644 index 0000000000..685b7b14bd --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-self-referenced-trait/RecursiveTrait.php @@ -0,0 +1,14 @@ +