diff --git a/src/Configuration/ConfigurationFactory.php b/src/Configuration/ConfigurationFactory.php index 9fb6eb0672f..da8329e1433 100644 --- a/src/Configuration/ConfigurationFactory.php +++ b/src/Configuration/ConfigurationFactory.php @@ -16,7 +16,8 @@ final readonly class ConfigurationFactory { public function __construct( - private SymfonyStyle $symfonyStyle + private SymfonyStyle $symfonyStyle, + private readonly OnlyRuleResolver $onlyRuleResolver, ) { } @@ -41,7 +42,8 @@ public function createForTests(array $paths): Configuration false, null, false, - false + false, + null ); } @@ -62,6 +64,11 @@ public function createFromInput(InputInterface $input): Configuration $fileExtensions = SimpleParameterProvider::provideArrayParameter(Option::FILE_EXTENSIONS); + $onlyRule = $input->getOption(Option::ONLY); + if ($onlyRule !== null) { + $onlyRule = $this->onlyRuleResolver->resolve($onlyRule); + } + $isParallel = SimpleParameterProvider::provideBoolParameter(Option::PARALLEL); $parallelPort = (string) $input->getOption(Option::PARALLEL_PORT); $parallelIdentifier = (string) $input->getOption(Option::PARALLEL_IDENTIFIER); @@ -90,6 +97,7 @@ public function createFromInput(InputInterface $input): Configuration $memoryLimit, $isDebug, $isReportingWithRealPath, + $onlyRule, ); } diff --git a/src/Configuration/ConfigurationRuleFilter.php b/src/Configuration/ConfigurationRuleFilter.php new file mode 100644 index 00000000000..a1a926aab1d --- /dev/null +++ b/src/Configuration/ConfigurationRuleFilter.php @@ -0,0 +1,56 @@ +configuration = $configuration; + } + + /** + * @param array $rectors + * @return array + */ + public function filter(array $rectors): array + { + if ($this->configuration === null) { + return $rectors; + } + + $onlyRule = $this->configuration->getOnlyRule(); + if ($onlyRule !== null) { + $rectors = $this->filterOnlyRule($rectors, $onlyRule); + return $rectors; + } + + return $rectors; + } + + /** + * @param array $rectors + * @return array + */ + public function filterOnlyRule(array $rectors, string $onlyRule): array + { + $activeRectors = []; + foreach ($rectors as $rector) { + if (is_a($rector, $onlyRule)) { + $activeRectors[] = $rector; + } + } + + return $activeRectors; + } +} diff --git a/src/Configuration/OnlyRuleResolver.php b/src/Configuration/OnlyRuleResolver.php new file mode 100644 index 00000000000..9512ef19c3e --- /dev/null +++ b/src/Configuration/OnlyRuleResolver.php @@ -0,0 +1,48 @@ +rectors as $rector) { + if (is_a($rector, $rule, true)) { + return $rule; + } + } + + if (strpos($rule, '\\') === false) { + $message = sprintf( + 'Rule "%s" was not found.%sThe rule has no namespace - make sure to escape the backslashes correctly.', + $rule, + PHP_EOL + ); + } else { + $message = sprintf( + 'Rule "%s" was not found.%sMake sure it is registered in your config or in one of the sets', + $rule, + PHP_EOL + ); + } + throw new RectorRuleNotFoundException($message); + } +} diff --git a/src/Configuration/Option.php b/src/Configuration/Option.php index 44416da88d4..01d5c4c12b6 100644 --- a/src/Configuration/Option.php +++ b/src/Configuration/Option.php @@ -98,6 +98,11 @@ final class Option */ public const CLEAR_CACHE = 'clear-cache'; + /** + * @var string + */ + public const ONLY = 'only'; + /** * @internal Use @see \Rector\Config\RectorConfig::parallel() instead * @var string diff --git a/src/Console/Command/ListRulesCommand.php b/src/Console/Command/ListRulesCommand.php index 3863e250619..dbfb9d93a6b 100644 --- a/src/Console/Command/ListRulesCommand.php +++ b/src/Console/Command/ListRulesCommand.php @@ -6,6 +6,8 @@ use Nette\Utils\Json; use Rector\ChangesReporting\Output\ConsoleOutputFormatter; +use Rector\Configuration\ConfigurationRuleFilter; +use Rector\Configuration\OnlyRuleResolver; use Rector\Configuration\Option; use Rector\Contract\Rector\RectorInterface; use Rector\PostRector\Contract\Rector\PostRectorInterface; @@ -24,6 +26,8 @@ final class ListRulesCommand extends Command public function __construct( private readonly SymfonyStyle $symfonyStyle, private readonly SkippedClassResolver $skippedClassResolver, + private readonly OnlyRuleResolver $onlyRuleResolver, + private readonly ConfigurationRuleFilter $configurationRuleFilter, private readonly array $rectors ) { parent::__construct(); @@ -43,11 +47,22 @@ protected function configure(): void 'Select output format', ConsoleOutputFormatter::NAME ); + + $this->addOption( + Option::ONLY, + null, + InputOption::VALUE_REQUIRED, + 'Fully qualified rule class name' + ); } protected function execute(InputInterface $input, OutputInterface $output): int { - $rectorClasses = $this->resolveRectorClasses(); + $onlyRule = $input->getOption(Option::ONLY); + if ($onlyRule !== null) { + $onlyRule = $this->onlyRuleResolver->resolve($onlyRule); + } + $rectorClasses = $this->resolveRectorClasses($onlyRule); $skippedClasses = $this->getSkippedCheckers(); @@ -79,13 +94,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * @return array> */ - private function resolveRectorClasses(): array + private function resolveRectorClasses(?string $onlyRule): array { $customRectors = array_filter( $this->rectors, static fn (RectorInterface $rector): bool => ! $rector instanceof PostRectorInterface ); + if ($onlyRule !== null) { + $customRectors = $this->configurationRuleFilter->filterOnlyRule($customRectors, $onlyRule); + } + $rectorClasses = array_map(static fn (RectorInterface $rector): string => $rector::class, $customRectors); sort($rectorClasses); diff --git a/src/Console/Command/ProcessCommand.php b/src/Console/Command/ProcessCommand.php index 7857010fa0e..982cf6e417d 100644 --- a/src/Console/Command/ProcessCommand.php +++ b/src/Console/Command/ProcessCommand.php @@ -8,8 +8,10 @@ use Rector\Autoloading\AdditionalAutoloader; use Rector\Caching\Detector\ChangedFilesDetector; use Rector\ChangesReporting\Output\JsonOutputFormatter; +use Rector\Config\RectorConfig; use Rector\Configuration\ConfigInitializer; use Rector\Configuration\ConfigurationFactory; +use Rector\Configuration\ConfigurationRuleFilter; use Rector\Configuration\Option; use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Console\ExitCode; @@ -42,6 +44,7 @@ public function __construct( private readonly ConfigurationFactory $configurationFactory, private readonly DeprecatedRulesReporter $deprecatedRulesReporter, private readonly MissConfigurationReporter $missConfigurationReporter, + private ConfigurationRuleFilter $configurationRuleFilter, ) { parent::__construct(); } @@ -85,6 +88,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $configuration = $this->configurationFactory->createFromInput($input); $this->memoryLimiter->adjust($configuration); + $this->configurationRuleFilter->setConfiguration($configuration); // disable console output in case of json output formatter if ($configuration->getOutputFormat() === JsonOutputFormatter::NAME) { diff --git a/src/Console/Command/WorkerCommand.php b/src/Console/Command/WorkerCommand.php index 36ea55e0393..aa7d0df4bb0 100644 --- a/src/Console/Command/WorkerCommand.php +++ b/src/Console/Command/WorkerCommand.php @@ -11,6 +11,7 @@ use React\Socket\TcpConnector; use Rector\Application\ApplicationFileProcessor; use Rector\Configuration\ConfigurationFactory; +use Rector\Configuration\ConfigurationRuleFilter; use Rector\Console\ProcessConfigureDecorator; use Rector\Parallel\ValueObject\Bridge; use Rector\StaticReflection\DynamicSourceLocatorDecorator; @@ -44,7 +45,8 @@ public function __construct( private readonly DynamicSourceLocatorDecorator $dynamicSourceLocatorDecorator, private readonly ApplicationFileProcessor $applicationFileProcessor, private readonly MemoryLimiter $memoryLimiter, - private readonly ConfigurationFactory $configurationFactory + private readonly ConfigurationFactory $configurationFactory, + private readonly ConfigurationRuleFilter $configurationRuleFilter, ) { parent::__construct(); } @@ -63,6 +65,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $configuration = $this->configurationFactory->createFromInput($input); $this->memoryLimiter->adjust($configuration); + $this->configurationRuleFilter->setConfiguration($configuration); $streamSelectLoop = new StreamSelectLoop(); $parallelIdentifier = $configuration->getParallelIdentifier(); diff --git a/src/Console/ProcessConfigureDecorator.php b/src/Console/ProcessConfigureDecorator.php index a45f9f6e876..a4be2fd2dc1 100644 --- a/src/Console/ProcessConfigureDecorator.php +++ b/src/Console/ProcessConfigureDecorator.php @@ -56,6 +56,13 @@ public static function decorate(Command $command): void ConsoleOutputFormatter::NAME ); + $command->addOption( + Option::ONLY, + null, + InputOption::VALUE_REQUIRED, + 'Fully qualified rule class name' + ); + $command->addOption(Option::DEBUG, null, InputOption::VALUE_NONE, 'Display debug output.'); $command->addOption(Option::MEMORY_LIMIT, null, InputOption::VALUE_REQUIRED, 'Memory limit for process'); $command->addOption(Option::CLEAR_CACHE, null, InputOption::VALUE_NONE, 'Clear unchanged files cache'); diff --git a/src/DependencyInjection/LazyContainerFactory.php b/src/DependencyInjection/LazyContainerFactory.php index d3323b1fa8c..82b2c91cbd2 100644 --- a/src/DependencyInjection/LazyContainerFactory.php +++ b/src/DependencyInjection/LazyContainerFactory.php @@ -46,6 +46,8 @@ use Rector\CodingStyle\Contract\ClassNameImport\ClassNameImportSkipVoterInterface; use Rector\Config\RectorConfig; use Rector\Configuration\ConfigInitializer; +use Rector\Configuration\ConfigurationRuleFilter; +use Rector\Configuration\OnlyRuleResolver; use Rector\Configuration\RenamedClassesDataCollector; use Rector\Console\Command\CustomRuleCommand; use Rector\Console\Command\ListRulesCommand; @@ -394,6 +396,8 @@ public function create(): RectorConfig return $inflectorFactory->build(); }); + $rectorConfig->singleton(ConfigurationRuleFilter::class); + $rectorConfig->singleton(ProcessCommand::class); $rectorConfig->singleton(WorkerCommand::class); $rectorConfig->singleton(SetupCICommand::class); @@ -404,6 +408,10 @@ public function create(): RectorConfig ->needs('$rectors') ->giveTagged(RectorInterface::class); + $rectorConfig->when(OnlyRuleResolver::class) + ->needs('$rectors') + ->giveTagged(RectorInterface::class); + $rectorConfig->singleton(FileProcessor::class); $rectorConfig->singleton(PostFileProcessor::class); diff --git a/src/Exception/Configuration/RectorRuleNotFoundException.php b/src/Exception/Configuration/RectorRuleNotFoundException.php new file mode 100644 index 00000000000..0f2e17919e1 --- /dev/null +++ b/src/Exception/Configuration/RectorRuleNotFoundException.php @@ -0,0 +1,11 @@ +filePathHelper->relativePath($config)); } + if ($input->getOption(Option::ONLY) !== null) { + $workerCommandArray[] = self::OPTION_DASHES . Option::ONLY; + $workerCommandArray[] = escapeshellarg($input->getOption(Option::ONLY)); + } + return implode(' ', $workerCommandArray); } diff --git a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php index e5fb6d1b25d..c5e58c7b7bf 100644 --- a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php +++ b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor; +use Rector\Configuration\ConfigurationRuleFilter; use Rector\Contract\Rector\RectorInterface; use Rector\VersionBonding\PhpVersionedFilter; @@ -25,7 +26,8 @@ final class RectorNodeTraverser extends NodeTraverser */ public function __construct( private array $rectors, - private readonly PhpVersionedFilter $phpVersionedFilter + private readonly PhpVersionedFilter $phpVersionedFilter, + private readonly ConfigurationRuleFilter $configurationRuleFilter, ) { parent::__construct(); } @@ -93,6 +95,9 @@ private function prepareNodeVisitors(): void // filer out by version $this->visitors = $this->phpVersionedFilter->filter($this->rectors); + // filter by configuration + $this->visitors = $this->configurationRuleFilter->filter($this->visitors); + $this->areNodeVisitorsPrepared = true; } } diff --git a/src/ValueObject/Configuration.php b/src/ValueObject/Configuration.php index b99186a5894..f34315fea7d 100644 --- a/src/ValueObject/Configuration.php +++ b/src/ValueObject/Configuration.php @@ -26,7 +26,8 @@ public function __construct( private bool $isParallel = false, private string|null $memoryLimit = null, private bool $isDebug = false, - private bool $reportingWithRealPath = false + private bool $reportingWithRealPath = false, + private ?string $onlyRule = null ) { } @@ -54,6 +55,11 @@ public function getFileExtensions(): array return $this->fileExtensions; } + public function getOnlyRule(): ?string + { + return $this->onlyRule; + } + /** * @return string[] */ diff --git a/tests/Configuration/OnlyRuleResolverTest.php b/tests/Configuration/OnlyRuleResolverTest.php new file mode 100644 index 00000000000..a96c5c05ac5 --- /dev/null +++ b/tests/Configuration/OnlyRuleResolverTest.php @@ -0,0 +1,64 @@ +bootFromConfigFiles([__DIR__ . '/config/only_rule_resolver_config.php']); + $rectorConfig = self::getContainer(); + + $this->resolver = new OnlyRuleResolver( + iterator_to_array($rectorConfig->tagged(RectorInterface::class)) + ); + } + + public function testResolveOk(): void + { + $this->assertEquals( + \Rector\DeadCode\Rector\Assign\RemoveDoubleAssignRector::class, + $this->resolver->resolve('Rector\\DeadCode\\Rector\\Assign\\RemoveDoubleAssignRector') + ); + } + + public function testResolveOkLeadingBackslash(): void + { + $this->assertEquals( + \Rector\DeadCode\Rector\Assign\RemoveDoubleAssignRector::class, + $this->resolver->resolve('\\Rector\\DeadCode\\Rector\\Assign\\RemoveDoubleAssignRector') + ); + } + + public function testResolveMissingBackslash(): void + { + $this->expectExceptionMessage( + 'Rule "RectorDeadCodeRectorAssignRemoveDoubleAssignRector" was not found.' . PHP_EOL + . 'The rule has no namespace - make sure to escape the backslashes correctly.' + ); + $this->expectException(RectorRuleNotFoundException::class); + + $this->resolver->resolve('RectorDeadCodeRectorAssignRemoveDoubleAssignRector'); + } + + public function testResolveNotFound(): void + { + $this->expectExceptionMessage( + 'Rule "This\Rule\Does\Not\Exist" was not found.' . PHP_EOL + . 'Make sure it is registered in your config or in one of the sets' + ); + $this->expectException(RectorRuleNotFoundException::class); + + $this->resolver->resolve('This\\Rule\\Does\\Not\\Exist'); + } +} diff --git a/tests/Configuration/config/only_rule_resolver_config.php b/tests/Configuration/config/only_rule_resolver_config.php new file mode 100644 index 00000000000..88890f0dbd4 --- /dev/null +++ b/tests/Configuration/config/only_rule_resolver_config.php @@ -0,0 +1,10 @@ +withRules([RemoveDoubleAssignRector::class, RemoveUnusedPrivateMethodRector::class]);