diff --git a/src/Application/RectorRegistry.php b/src/Application/RectorRegistry.php index c251d01d43f..cb1772af53b 100644 --- a/src/Application/RectorRegistry.php +++ b/src/Application/RectorRegistry.php @@ -6,7 +6,9 @@ use PhpParser\Node; use Rector\Configuration\ConfigurationRuleFilter; +use Rector\Contract\Rector\OrderedRectorInterface; use Rector\Contract\Rector\RectorInterface; +use Rector\Exception\ShouldNotHappenException; use Rector\Skipper\Skipper\Skipper; use Rector\ValueObject\Application\File; use Rector\VersionBonding\ComposerPackageConstraintFilter; @@ -112,9 +114,77 @@ private function prepareRectors(): void $activeRectors = $this->phpVersionedFilter->filter($this->rectors); $activeRectors = $this->composerPackageConstraintFilter->filter($activeRectors); + $activeRectors = $this->configurationRuleFilter->filter($activeRectors); - $this->activeRectors = $this->configurationRuleFilter->filter($activeRectors); + $this->activeRectors = $this->sortByPrecededRule($activeRectors); $this->areRectorsPrepared = true; } + + /** + * Rules implementing OrderedRectorInterface are moved after the rule they depend on, + * so a chain of dependent rules converges in a single file pass. + * + * @param RectorInterface[] $rectors + * @return RectorInterface[] + */ + private function sortByPrecededRule(array $rectors): array + { + $rectorByClass = []; + foreach ($rectors as $rector) { + $rectorByClass[$rector::class] = $rector; + } + + $precededClassByClass = []; + foreach ($rectors as $rector) { + if (! $rector instanceof OrderedRectorInterface) { + continue; + } + + $precededClass = $rector->precededRule(); + + // ignore a predecessor filtered out of the active set + if (isset($rectorByClass[$precededClass])) { + $precededClassByClass[$rector::class] = $precededClass; + } + } + + if ($precededClassByClass === []) { + return $rectors; + } + + $sortedRectors = []; + $emittedClasses = []; + $remainingRectors = $rectors; + + while ($remainingRectors !== []) { + $hasProgress = false; + + foreach ($remainingRectors as $key => $rector) { + $precededClass = $precededClassByClass[$rector::class] ?? null; + if ($precededClass !== null && ! isset($emittedClasses[$precededClass])) { + continue; + } + + $sortedRectors[] = $rector; + $emittedClasses[$rector::class] = true; + unset($remainingRectors[$key]); + $hasProgress = true; + } + + if (! $hasProgress) { + $remainingClasses = []; + foreach ($remainingRectors as $rector) { + $remainingClasses[] = $rector::class; + } + + throw new ShouldNotHappenException(sprintf( + 'Circular rule ordering detected among: %s', + implode(', ', $remainingClasses) + )); + } + } + + return $sortedRectors; + } } diff --git a/src/Contract/Rector/OrderedRectorInterface.php b/src/Contract/Rector/OrderedRectorInterface.php new file mode 100644 index 00000000000..dba82fb1f0e --- /dev/null +++ b/src/Contract/Rector/OrderedRectorInterface.php @@ -0,0 +1,15 @@ + + */ + public function precededRule(): string; +} diff --git a/tests/Application/RectorRegistryTest.php b/tests/Application/RectorRegistryTest.php index 294afab94c2..6c21584b750 100644 --- a/tests/Application/RectorRegistryTest.php +++ b/tests/Application/RectorRegistryTest.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt\Class_; use Rector\Application\RectorRegistry; use Rector\Testing\PHPUnit\AbstractLazyTestCase; +use Rector\Tests\PhpParser\NodeTraverser\Class_\OrderedClassRector; use Rector\Tests\PhpParser\NodeTraverser\Class_\RuleUsingClassRector; use Rector\Tests\PhpParser\NodeTraverser\ClassLike\RuleUsingClassLikeRector; use Rector\Tests\PhpParser\NodeTraverser\Function_\RuleUsingFunctionRector; @@ -72,4 +73,19 @@ public function testForNodeWhenAllRectorsMatch(): void $this->assertEquals([$this->ruleUsingClassRector, $this->ruleUsingClassLikeRector], $rectors); } + + public function testForNodeSortsByPrecededRule(): void + { + $orderedClassRector = new OrderedClassRector(); + + // registered before the rule it depends on + $this->rectorRegistry->refreshRectors([ + $orderedClassRector, + $this->ruleUsingClassRector, + ]); + + $rectors = $this->rectorRegistry->forNode(new Class_('test')); + + $this->assertEquals([$this->ruleUsingClassRector, $orderedClassRector], $rectors); + } } diff --git a/tests/PhpParser/NodeTraverser/Class_/OrderedClassRector.php b/tests/PhpParser/NodeTraverser/Class_/OrderedClassRector.php new file mode 100644 index 00000000000..b7a5fd10d12 --- /dev/null +++ b/tests/PhpParser/NodeTraverser/Class_/OrderedClassRector.php @@ -0,0 +1,44 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): Node + { + return $node; + } +}