Skip to content

Commit 5ab83ce

Browse files
authored
Fix infinite loop on recursive trait
1 parent 3cb3109 commit 5ab83ce

File tree

4 files changed

+70
-3
lines changed

4 files changed

+70
-3
lines changed

src/Type/FileTypeMapper.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ private function getNameScopeMap(string $fileName): array
329329
{
330330
if (!isset($this->memoryCache[$fileName])) {
331331
$cacheKey = sprintf('ftm-%s', $fileName);
332-
$variableCacheKey = sprintf('v4-%s', ComposerHelper::getPhpDocParserVersion());
332+
$variableCacheKey = sprintf('v5-%s', ComposerHelper::getPhpDocParserVersion());
333333
$cached = $this->loadCachedPhpDocNodeMap($cacheKey, $variableCacheKey);
334334
if ($cached === null) {
335335
[$nameScopeMap, $files] = $this->createPhpDocNodeMap($fileName, null, null, [], $fileName);
@@ -394,9 +394,10 @@ private function loadCachedPhpDocNodeMap(string $cacheKey, string $variableCache
394394

395395
/**
396396
* @param array<string, string> $traitMethodAliases
397+
* @param array<string, true> $activeTraitResolutions
397398
* @return array{array<string, IntermediaryNameScope>, list<string>}
398399
*/
399-
private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName): array
400+
private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName, array $activeTraitResolutions = []): array
400401
{
401402
/** @var array<string, IntermediaryNameScope> $nameScopeMap */
402403
$nameScopeMap = [];
@@ -425,7 +426,7 @@ private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?s
425426
$constUses = [];
426427
$this->processNodes(
427428
$this->phpParser->parseFile($fileName),
428-
function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$typeMapStack, &$typeAliasStack, &$classStack, &$namespace, &$functionStack, &$uses, &$constUses, &$files): ?int {
429+
function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, $activeTraitResolutions, &$nameScopeMap, &$typeMapStack, &$typeAliasStack, &$classStack, &$namespace, &$functionStack, &$uses, &$constUses, &$files): ?int {
429430
if ($node instanceof Node\Stmt\ClassLike) {
430431
if ($traitFound && $fileName === $originalClassFileName) {
431432
return self::SKIP_NODE;
@@ -635,12 +636,21 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
635636
throw new ShouldNotHappenException();
636637
}
637638

639+
$traitResolutionKey = $this->getTraitResolutionKey($traitReflection->getFileName(), $traitName, $className, $originalClassFileName);
640+
if (isset($activeTraitResolutions[$traitResolutionKey])) {
641+
continue;
642+
}
643+
644+
$nestedActiveTraitResolutions = $activeTraitResolutions;
645+
$nestedActiveTraitResolutions[$traitResolutionKey] = true;
646+
638647
[$traitNameScopeMap, $traitFiles] = $this->createPhpDocNodeMap(
639648
$traitReflection->getFileName(),
640649
$traitName,
641650
$className,
642651
$traitMethodAliases[$traitName] ?? [],
643652
$originalClassFileName,
653+
$nestedActiveTraitResolutions,
644654
);
645655
$nameScopeMap = array_merge($nameScopeMap, array_map(static fn ($originalNameScope) => $originalNameScope->getTraitData() === null ? $originalNameScope->withTraitData($originalClassFileName, $className, $traitName, $lookForTrait, $docComment) : $originalNameScope, $traitNameScopeMap));
646656
$files = array_merge($files, $traitFiles);
@@ -818,4 +828,9 @@ private function getPhpDocKey(string $nameScopeKey, string $docComment): string
818828
return md5(sprintf('%s-%s', $nameScopeKey, $doc->getReformattedText()));
819829
}
820830

831+
private function getTraitResolutionKey(string $fileName, string $traitName, string $className, string $originalClassFileName): string
832+
{
833+
return md5(sprintf('%s-%s-%s-%s', $fileName, $traitName, $className, $originalClassFileName));
834+
}
835+
821836
}

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,13 @@ public function testBug14439(): void
15291529
$this->assertNoErrors($errors);
15301530
}
15311531

1532+
public function testBugInfiniteLoopOnFileTypeMapper(): void
1533+
{
1534+
// endless loop crash on self referencing trait
1535+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-self-referenced-trait/BaseModelUseTrait.php');
1536+
$this->assertCount(0, $errors);
1537+
}
1538+
15321539
/**
15331540
* @param string[]|null $allAnalysedFiles
15341541
* @return list<Error>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace BugSelfReferencedTrait;
4+
5+
use BugSelfReferencedTrait\RecursiveTrait;
6+
7+
class Model
8+
{
9+
}
10+
11+
/** @template TModel of Model */
12+
class Builder
13+
{
14+
}
15+
16+
class BelongsTo
17+
{
18+
}
19+
20+
/**
21+
* @method static Builder<static>|BaseModelUseTrait query()
22+
*/
23+
class BaseModelUseTrait extends Model
24+
{
25+
use RecursiveTrait;
26+
27+
public function parent(): BelongsTo
28+
{
29+
return new BelongsTo();
30+
}
31+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace BugSelfReferencedTrait;
4+
5+
trait RecursiveTrait
6+
{
7+
public function getRecursive(): object
8+
{
9+
return new class () {
10+
use RecursiveTrait;
11+
};
12+
}
13+
14+
}

0 commit comments

Comments
 (0)