|
32 | 32 |
|
33 | 33 | final readonly class FileProcessor |
34 | 34 | { |
| 35 | + /** |
| 36 | + * Safety cap for the fixed-point loop; a well-behaved rule set settles within a few passes, |
| 37 | + * so this only trips on a non-idempotent rule that keeps changing the same node. |
| 38 | + */ |
| 39 | + private const int MAX_ITERATIONS = 5; |
| 40 | + |
35 | 41 | public function __construct( |
36 | 42 | private BetterStandardPrinter $betterStandardPrinter, |
37 | 43 | private RectorNodeTraverser $rectorNodeTraverser, |
@@ -59,7 +65,16 @@ public function processFile(File $file, Configuration $configuration): FileProce |
59 | 65 | $fileHasChanged = false; |
60 | 66 | $filePath = $file->getFilePath(); |
61 | 67 |
|
| 68 | + $iteration = 0; |
62 | 69 | do { |
| 70 | + // safety cap: a non-idempotent rule can keep changing a node every pass, so the |
| 71 | + // fixed-point loop would never settle; stop and report instead of looping forever |
| 72 | + if ($iteration >= self::MAX_ITERATIONS) { |
| 73 | + return $this->createNotConvergingResult($file, $fileHasChanged); |
| 74 | + } |
| 75 | + |
| 76 | + ++$iteration; |
| 77 | + |
63 | 78 | $file->changeHasChanged(false); |
64 | 79 |
|
65 | 80 | // 1. change nodes with Rector Rules |
@@ -108,6 +123,28 @@ public function processFile(File $file, Configuration $configuration): FileProce |
108 | 123 | return new FileProcessResult([], $file->getFileDiff(), $file->hasChanged()); |
109 | 124 | } |
110 | 125 |
|
| 126 | + private function createNotConvergingResult(File $file, bool $fileHasChanged): FileProcessResult |
| 127 | + { |
| 128 | + $rectorClasses = []; |
| 129 | + foreach ($file->getRectorWithLineChanges() as $rectorWithLineChange) { |
| 130 | + $rectorClasses[$rectorWithLineChange->getRectorClass()] = true; |
| 131 | + } |
| 132 | + |
| 133 | + $message = sprintf( |
| 134 | + 'File did not stabilize after %d passes, a rule keeps changing it. Rules applied: %s', |
| 135 | + self::MAX_ITERATIONS, |
| 136 | + implode(', ', array_keys($rectorClasses)) |
| 137 | + ); |
| 138 | + |
| 139 | + // in tests a non-converging file is a rule bug that must surface, not a silent partial result |
| 140 | + if (StaticPHPUnitEnvironment::isPHPUnitRun()) { |
| 141 | + throw new ShouldNotHappenException($message . ' in ' . $file->getFilePath()); |
| 142 | + } |
| 143 | + |
| 144 | + $systemError = new SystemError($message, $this->filePathHelper->relativePath($file->getFilePath())); |
| 145 | + return new FileProcessResult([$systemError], $file->getFileDiff(), $fileHasChanged); |
| 146 | + } |
| 147 | + |
111 | 148 | private function parseFileAndDecorateNodes(File $file): ?SystemError |
112 | 149 | { |
113 | 150 | try { |
|
0 commit comments