Skip to content
Merged
19 changes: 17 additions & 2 deletions src/Type/FileTypeMapper.php
Original file line number Diff line number Diff line change
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));
}

}
56 changes: 56 additions & 0 deletions tests/PHPStan/Type/FileTypeMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@

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
{
Expand Down Expand Up @@ -210,4 +217,53 @@ 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);
Comment thread
samsonasik marked this conversation as resolved.
Outdated
self::createReflectionProvider();

/** @var FileTypeMapper $fileTypeMapper */
$fileTypeMapper = $container->getByType(FileTypeMapper::class);

$resolved = $fileTypeMapper->getResolvedPhpDoc(
$realpath,
BaseModelUseTrait::class,
null,
null,
'/** @method static Builder<static>|BaseModelUseTrait query() */',
);

$this->assertArrayHasKey('query', $resolved->getMethodTags());
$returnTypeDescription = $resolved->getMethodTags()['query']->getReturnType()->describe(VerbosityLevel::precise());
$this->assertStringContainsString('BugSelfReferencedTrait\BaseModelUseTrait', $returnTypeDescription);
$this->assertStringContainsString('BugSelfReferencedTrait\Builder<static(BugSelfReferencedTrait\BaseModelUseTrait)>', $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();
}

}
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