Skip to content
Merged
21 changes: 18 additions & 3 deletions src/Type/FileTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -394,9 +394,10 @@ private function loadCachedPhpDocNodeMap(string $cacheKey, string $variableCache

/**
* @param array<string, string> $traitMethodAliases
* @param array<string, true> $activeTraitResolutions
* @return array{array<string, IntermediaryNameScope>, list<string>}
*/
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<string, IntermediaryNameScope> $nameScopeMap */
$nameScopeMap = [];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
}

}
7 changes: 7 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Error>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);

namespace BugSelfReferencedTrait;

use BugSelfReferencedTrait\RecursiveTrait;

class Model
{
}

/** @template TModel of Model */
class Builder
{
}

class BelongsTo
{
}

/**
* @method static Builder<static>|BaseModelUseTrait query()
*/
class BaseModelUseTrait extends Model
{
use RecursiveTrait;

public function parent(): BelongsTo
{
return new BelongsTo();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types = 1);

namespace BugSelfReferencedTrait;

trait RecursiveTrait
{
public function getRecursive(): object
{
return new class () {
use RecursiveTrait;
};
}

}
Loading