diff --git a/src/Application/FileProcessor.php b/src/Application/FileProcessor.php index 91f2ec44a1c..a2589af176e 100644 --- a/src/Application/FileProcessor.php +++ b/src/Application/FileProcessor.php @@ -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, @@ -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 @@ -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 {