Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/Application/FileProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@

final readonly class FileProcessor
{
/**
* Safety cap for the fixed-point loop; a well-behaved rule set settles within a few passes,
* so this only trips on a non-idempotent rule that keeps changing the same node.
*/
private const int MAX_ITERATIONS = 5;

public function __construct(
private BetterStandardPrinter $betterStandardPrinter,
private RectorNodeTraverser $rectorNodeTraverser,
Expand Down Expand Up @@ -59,7 +65,16 @@ public function processFile(File $file, Configuration $configuration): FileProce
$fileHasChanged = false;
$filePath = $file->getFilePath();

$iteration = 0;
do {
// safety cap: a non-idempotent rule can keep changing a node every pass, so the
// fixed-point loop would never settle; stop and report instead of looping forever
if ($iteration >= self::MAX_ITERATIONS) {
return $this->createNotConvergingResult($file, $fileHasChanged);
}

++$iteration;

$file->changeHasChanged(false);

// 1. change nodes with Rector Rules
Expand Down Expand Up @@ -108,6 +123,28 @@ public function processFile(File $file, Configuration $configuration): FileProce
return new FileProcessResult([], $file->getFileDiff(), $file->hasChanged());
}

private function createNotConvergingResult(File $file, bool $fileHasChanged): FileProcessResult
{
$rectorClasses = [];
foreach ($file->getRectorWithLineChanges() as $rectorWithLineChange) {
$rectorClasses[$rectorWithLineChange->getRectorClass()] = true;
}

$message = sprintf(
'File did not stabilize after %d passes, a rule keeps changing it. Rules applied: %s',
self::MAX_ITERATIONS,
implode(', ', array_keys($rectorClasses))
);

// in tests a non-converging file is a rule bug that must surface, not a silent partial result
if (StaticPHPUnitEnvironment::isPHPUnitRun()) {
throw new ShouldNotHappenException($message . ' in ' . $file->getFilePath());
}

$systemError = new SystemError($message, $this->filePathHelper->relativePath($file->getFilePath()));
return new FileProcessResult([$systemError], $file->getFileDiff(), $fileHasChanged);
}

private function parseFileAndDecorateNodes(File $file): ?SystemError
{
try {
Expand Down
Loading