Skip to content

Commit

Permalink
Add "--only" option to process only a single rule
Browse files Browse the repository at this point in the history
The option for the "process" and "list-rules" commands applies
the single given rule only, without needing to modify
the configuration file.

The option value must be a fully classified class name:

  --only="Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector"

A hint is given when the user forgot to escape the backslashes.

----

It is impossible to modify the injected "$rectors" after the
command line configuration is parsed, so I had to introduce the
ConfigurationRuleFilter singleton.

Since both ListRulesCommand and ProcessCommand make use of the
ConfigurationRuleFilter - but list-rules does not have a Configuration -
I had to make the filterOnlyRule() method public to prevent
code duplication.

Resolves rectorphp/rector#8899
  • Loading branch information
cweiske committed Nov 15, 2024
1 parent 9cd6376 commit e5b3888
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 7 deletions.
12 changes: 10 additions & 2 deletions src/Configuration/ConfigurationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
final readonly class ConfigurationFactory
{
public function __construct(
private SymfonyStyle $symfonyStyle
private SymfonyStyle $symfonyStyle,
private readonly OnlyRuleResolver $onlyRuleResolver,
) {
}

Expand All @@ -41,7 +42,8 @@ public function createForTests(array $paths): Configuration
false,
null,
false,
false
false,
null
);
}

Expand All @@ -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);
Expand Down Expand Up @@ -90,6 +97,7 @@ public function createFromInput(InputInterface $input): Configuration
$memoryLimit,
$isDebug,
$isReportingWithRealPath,
$onlyRule,
);
}

Expand Down
56 changes: 56 additions & 0 deletions src/Configuration/ConfigurationRuleFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Rector\Configuration;

use Rector\Contract\Rector\RectorInterface;
use Rector\ValueObject\Configuration;

/**
* Modify available rector rules based on the configuration options
*/
final class ConfigurationRuleFilter
{
protected ?Configuration $configuration = null;

public function setConfiguration(Configuration $configuration): void
{
$this->configuration = $configuration;
}

/**
* @param array<RectorInterface> $rectors
* @return array<RectorInterface>
*/
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<RectorInterface> $rectors
* @return array<RectorInterface>
*/
public function filterOnlyRule(array $rectors, string $onlyRule): array
{
$activeRectors = [];
foreach ($rectors as $rector) {
if (is_a($rector, $onlyRule)) {
$activeRectors[] = $rector;
}
}

return $activeRectors;
}
}
48 changes: 48 additions & 0 deletions src/Configuration/OnlyRuleResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Rector\Configuration;

use Rector\Contract\Rector\RectorInterface;
use Rector\Exception\Configuration\RectorRuleNotFoundException;

/**
* @see \Rector\Tests\Configuration\OnlyRuleResolverTest
*/
final class OnlyRuleResolver
{
/**
* @param RectorInterface[] $rectors
*/
public function __construct(
private readonly array $rectors
) {
}

public function resolve(string $rule): string
{
$rule = ltrim($rule, '\\');

foreach ($this->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);
}
}
5 changes: 5 additions & 0 deletions src/Configuration/Option.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions src/Console/Command/ListRulesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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();

Expand Down Expand Up @@ -79,13 +94,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/**
* @return array<class-string<RectorInterface>>
*/
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);

Expand Down
4 changes: 4 additions & 0 deletions src/Console/Command/ProcessCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,6 +44,7 @@ public function __construct(
private readonly ConfigurationFactory $configurationFactory,
private readonly DeprecatedRulesReporter $deprecatedRulesReporter,
private readonly MissConfigurationReporter $missConfigurationReporter,
private ConfigurationRuleFilter $configurationRuleFilter,
) {
parent::__construct();
}
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion src/Console/Command/WorkerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand All @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions src/Console/ProcessConfigureDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
8 changes: 8 additions & 0 deletions src/DependencyInjection/LazyContainerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand Down
11 changes: 11 additions & 0 deletions src/Exception/Configuration/RectorRuleNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Rector\Exception\Configuration;

use Exception;

final class RectorRuleNotFoundException extends Exception
{
}
5 changes: 5 additions & 0 deletions src/Parallel/Command/WorkerCommandLineFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ public function create(
$workerCommandArray[] = escapeshellarg($this->filePathHelper->relativePath($config));
}

if ($input->getOption(Option::ONLY) !== null) {
$workerCommandArray[] = self::OPTION_DASHES . Option::ONLY;
$workerCommandArray[] = escapeshellarg($input->getOption(Option::ONLY));
}

return implode(' ', $workerCommandArray);
}

Expand Down
7 changes: 6 additions & 1 deletion src/PhpParser/NodeTraverser/RectorNodeTraverser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
}
Expand Down Expand Up @@ -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;
}
}
8 changes: 7 additions & 1 deletion src/ValueObject/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
}

Expand Down Expand Up @@ -54,6 +55,11 @@ public function getFileExtensions(): array
return $this->fileExtensions;
}

public function getOnlyRule(): ?string
{
return $this->onlyRule;
}

/**
* @return string[]
*/
Expand Down
Loading

0 comments on commit e5b3888

Please sign in to comment.