Bug Report
| Question |
Answer |
| Rector version |
2.3.8 |
| PHP version |
8.4 |
| Laravel version |
12.x |
Description
Rector hangs indefinitely (infinite recursion in PHPStan's FileTypeMapper) when processing a Laravel Eloquent Model that combines:
- A trait containing an anonymous class that
uses itself (self-referencing trait)
- A
@method static Builder<static>|ClassName PHPDoc annotation
- Any Eloquent relationship method (e.g.
belongsTo, hasMany, morphMany)
Removing any one of these three components makes it work. PHPStan itself does not exhibit this issue, it only occurs through Rector's PHPStanNodeScopeResolver wrapper which doesn't have cycle detection when walking trait AST nodes.
Related: #3836 (similar symptom via @mixin, but different trigger - this repro uses no @mixin)
Minimal Reproduction
composer create-project laravel/laravel rector-hang-repro
cd rector-hang-repro
composer require --dev rector/rector
app/Traits/RecursiveTrait.php:
<?php
declare(strict_types=1);
namespace App\Traits;
trait RecursiveTrait {
public function getRecursive(): object {
return new class () {
use RecursiveTrait;
};
}
}
app/Models/BaseModel.php:
<?php
namespace App\Models;
use App\Traits\RecursiveTrait;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @method static Builder<static>|BaseModel query()
*/
class BaseModel extends Model {
use RecursiveTrait;
public function parent(): BelongsTo {
return $this->belongsTo(self::class);
}
}
rector.php:
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector;
return RectorConfig::configure()
->withPaths([__DIR__ . '/app'])
->withoutParallel()
->withRules([RemoveUnusedPrivateMethodRector::class]);
Run:
php vendor/bin/rector process app/Models/BaseModel.php --dry-run
Results in:
PHP Fatal error: Maximum execution time of 0 seconds exceeded in
phar://.../vendor/phpstan/phpstan/phpstan.phar/src/Type/FileTypeMapper.php on line 332
(Line number varies between runs the recursion hits different points in FileTypeMapper, CachedParser, FileReader, etc.)
Expected Behavior
Rector should detect the cycle and stop recursing, or set a depth limit on trait processing.
The recursion originates in PHPStanNodeScopeResolver::processTrait() (line ~582):
- Rector processes
BaseModel.php -> finds use RecursiveTrait -> calls processTrait()
processTrait() processes the trait's statements -> finds the anonymous class containing use RecursiveTrait
- Rector enters the anonymous class -> finds
use RecursiveTrait -> calls processTrait() again
- Infinite loop - at each level, PHPStan's
FileTypeMapper re-resolves the Builder<static> generic types from the PHPDoc, which is where the timeout occurs
Standalone PHPStan handles this correctly because its NodeScopeResolver has built-in cycle detection and result caching. Rector's PHPStanNodeScopeResolver wrapper defeats this by manually walking into trait AST nodes without tracking already-visited traits.
Bug Report
Description
Rector hangs indefinitely (infinite recursion in PHPStan's
FileTypeMapper) when processing a Laravel Eloquent Model that combines:uses itself (self-referencing trait)@method static Builder<static>|ClassNamePHPDoc annotationbelongsTo,hasMany,morphMany)Removing any one of these three components makes it work. PHPStan itself does not exhibit this issue, it only occurs through Rector's
PHPStanNodeScopeResolverwrapper which doesn't have cycle detection when walking trait AST nodes.Related: #3836 (similar symptom via
@mixin, but different trigger - this repro uses no@mixin)Minimal Reproduction
app/Traits/RecursiveTrait.php:app/Models/BaseModel.php:rector.php:Run:
Results in:
(Line number varies between runs the recursion hits different points in
FileTypeMapper,CachedParser,FileReader, etc.)Expected Behavior
Rector should detect the cycle and stop recursing, or set a depth limit on trait processing.
The recursion originates in
PHPStanNodeScopeResolver::processTrait()(line ~582):BaseModel.php-> findsuse RecursiveTrait-> callsprocessTrait()processTrait()processes the trait's statements -> finds the anonymous class containinguse RecursiveTraituse RecursiveTrait-> callsprocessTrait()againFileTypeMapperre-resolves theBuilder<static>generic types from the PHPDoc, which is where the timeout occursStandalone PHPStan handles this correctly because its
NodeScopeResolverhas built-in cycle detection and result caching. Rector'sPHPStanNodeScopeResolverwrapper defeats this by manually walking into trait AST nodes without tracking already-visited traits.