From f92b4ecb8ddbaf3d7c05fa37e0f902e1a5df90f1 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 11 Oct 2026 22:47:01 +0200 Subject: [PATCH] [Console] Add --all option to list-rules to list all existing rules in core and rector-* packages --- src/Console/Command/ListRulesCommand.php | 13 +- src/Console/Rule/AllRectorRulesProvider.php | 111 ++++++++++++++++++ .../LazyContainerFactory.php | 2 + .../Rule/AllRectorRulesProviderTest.php | 47 ++++++++ 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/Console/Rule/AllRectorRulesProvider.php create mode 100644 tests/Console/Rule/AllRectorRulesProviderTest.php diff --git a/src/Console/Command/ListRulesCommand.php b/src/Console/Command/ListRulesCommand.php index 931f88592a2..182660b20ca 100644 --- a/src/Console/Command/ListRulesCommand.php +++ b/src/Console/Command/ListRulesCommand.php @@ -7,6 +7,7 @@ use Nette\Utils\Json; use Rector\ChangesReporting\Output\ConsoleOutputFormatter; use Rector\Configuration\Option; +use Rector\Console\Rule\AllRectorRulesProvider; use Rector\Contract\Rector\RectorInterface; use Rector\PostRector\Contract\Rector\PostRectorInterface; use Rector\Skipper\Skipper\Skipper; @@ -24,6 +25,7 @@ final class ListRulesCommand extends Command public function __construct( private readonly SymfonyStyle $symfonyStyle, private readonly Skipper $skipper, + private readonly AllRectorRulesProvider $allRectorRulesProvider, private readonly array $rectors ) { parent::__construct(); @@ -45,11 +47,20 @@ protected function configure(): void ); $this->addOption(Option::ONLY, null, InputOption::VALUE_REQUIRED, 'Fully qualified rule class name'); + + $this->addOption( + 'all', + null, + InputOption::VALUE_NONE, + 'List all existing rules in core and rector-* packages, not only the loaded ones' + ); } protected function execute(InputInterface $input, OutputInterface $output): int { - $rectorClasses = $this->resolveRectorClasses(); + $rectorClasses = $input->getOption('all') + ? $this->allRectorRulesProvider->provide() + : $this->resolveRectorClasses(); $skippedClasses = $this->getSkippedRectorClasses(); diff --git a/src/Console/Rule/AllRectorRulesProvider.php b/src/Console/Rule/AllRectorRulesProvider.php new file mode 100644 index 00000000000..d2b2c46362a --- /dev/null +++ b/src/Console/Rule/AllRectorRulesProvider.php @@ -0,0 +1,111 @@ +> + */ + public function provide(): array + { + $rectorClasses = []; + + foreach ($this->resolvePsr4Prefixes() as $namespacePrefix => $directories) { + if (! str_starts_with($namespacePrefix, 'Rector\\')) { + continue; + } + + foreach ($directories as $directory) { + if (! is_dir($directory)) { + continue; + } + + foreach ($this->findRectorFiles($directory) as $fileInfo) { + $className = $this->resolveClassName($namespacePrefix, $directory, $fileInfo); + if ($this->isConcreteRectorRule($className)) { + $rectorClasses[] = $className; + } + } + } + } + + $rectorClasses = array_unique($rectorClasses); + sort($rectorClasses); + + return $rectorClasses; + } + + /** + * @return array + */ + private function resolvePsr4Prefixes(): array + { + $psr4Prefixes = []; + + // there can be multiple registered class loaders; merge their prefixes + foreach (spl_autoload_functions() as $autoloadFunction) { + if (! is_array($autoloadFunction) || ! $autoloadFunction[0] instanceof ClassLoader) { + continue; + } + + foreach ($autoloadFunction[0]->getPrefixesPsr4() as $namespacePrefix => $directories) { + $psr4Prefixes[$namespacePrefix] = array_merge($psr4Prefixes[$namespacePrefix] ?? [], $directories); + } + } + + return $psr4Prefixes; + } + + /** + * @return iterable + */ + private function findRectorFiles(string $directory): iterable + { + return Finder::create() + ->files() + ->name('*Rector.php') + ->in($directory); + } + + private function resolveClassName(string $namespacePrefix, string $directory, SplFileInfo $fileInfo): string + { + $relativePath = substr($fileInfo->getPathname(), strlen($directory) + 1, -strlen('.php')); + + return $namespacePrefix . str_replace('/', '\\', $relativePath); + } + + /** + * @phpstan-assert-if-true class-string $className + */ + private function isConcreteRectorRule(string $className): bool + { + // is_a() autoloads the class and returns false when it does not exist + if (! is_a($className, RectorInterface::class, true)) { + return false; + } + + if (is_a($className, PostRectorInterface::class, true)) { + return false; + } + + $reflectionClass = new ReflectionClass($className); + + return ! $reflectionClass->isAbstract(); + } +} diff --git a/src/DependencyInjection/LazyContainerFactory.php b/src/DependencyInjection/LazyContainerFactory.php index f7dce52357f..7838c5b367c 100644 --- a/src/DependencyInjection/LazyContainerFactory.php +++ b/src/DependencyInjection/LazyContainerFactory.php @@ -40,6 +40,7 @@ use Rector\Console\Command\ValidateConfigCommand; use Rector\Console\Command\WorkerCommand; use Rector\Console\ConsoleApplication; +use Rector\Console\Rule\AllRectorRulesProvider; use Rector\Console\Style\SymfonyStyleFactory; use Rector\Contract\PhpParser\DecoratingNodeVisitorInterface; use Rector\NodeNameResolver\NodeNameResolver; @@ -158,6 +159,7 @@ private function registerConsole(RectorConfig $rectorConfig): void $rectorConfig->singleton(WorkerCommand::class); $rectorConfig->singleton(ValidateConfigCommand::class); $rectorConfig->singleton(SetupCICommand::class); + $rectorConfig->singleton(AllRectorRulesProvider::class); $rectorConfig->singleton(ListRulesCommand::class); $rectorConfig->singleton(CustomRuleCommand::class); $rectorConfig->singleton(ComposerBasedCommand::class); diff --git a/tests/Console/Rule/AllRectorRulesProviderTest.php b/tests/Console/Rule/AllRectorRulesProviderTest.php new file mode 100644 index 00000000000..e0b354b6514 --- /dev/null +++ b/tests/Console/Rule/AllRectorRulesProviderTest.php @@ -0,0 +1,47 @@ +allRectorRulesProvider = new AllRectorRulesProvider(); + } + + public function testProvidesCoreRule(): void + { + $rectorClasses = $this->allRectorRulesProvider->provide(); + + $this->assertContains(RemoveDoubleAssignRector::class, $rectorClasses); + } + + public function testSkipsPostRectors(): void + { + $rectorClasses = $this->allRectorRulesProvider->provide(); + + $this->assertNotContains(UnusedImportRemovingPostRector::class, $rectorClasses); + } + + public function testListIsSortedAndUnique(): void + { + $rectorClasses = $this->allRectorRulesProvider->provide(); + + $sortedClasses = $rectorClasses; + sort($sortedClasses); + + $this->assertSame($sortedClasses, $rectorClasses); + $this->assertSame(array_unique($rectorClasses), $rectorClasses); + } +}