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 19, 2024
1 parent 37226f1 commit 86e10d9
Show file tree
Hide file tree
Showing 23 changed files with 338 additions and 7 deletions.
1 change: 1 addition & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
- 'e2e/invalid-paths'
- 'e2e/applied-polyfill-php80'
- 'e2e/print-new-node'
- 'e2e/only-option'

name: End to end test - ${{ matrix.directory }}

Expand Down
6 changes: 6 additions & 0 deletions e2e/e2eTestRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
$e2eCommand .= ' -a ' . $argv[2];
}

$cliOptions = 'cli-options.txt';
if (file_exists($cliOptions)) {
$e2eCommand .= ' ' . trim(file_get_contents($cliOptions));
}


exec($e2eCommand, $output, $exitCode);
$output = trim(implode("\n", $output));
$output = str_replace(__DIR__, '.', $output);
Expand Down
1 change: 1 addition & 0 deletions e2e/only-option/cli-options.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--only="Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector"
7 changes: 7 additions & 0 deletions e2e/only-option/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"require": {
"php": "^8.1"
},
"minimum-stability": "dev",
"prefer-stable": true
}
22 changes: 22 additions & 0 deletions e2e/only-option/expected-output.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
1 file with changes
===================

1) src/MultiRules.php:9

---------- begin diff ----------
@@ @@
echo 'a statement';
}
}
-
- private function notUsed()
- {
- }
}
----------- end diff -----------

Applied rules:
* RemoveUnusedPrivateMethodRector


[OK] 1 file would have been changed (dry-run) by Rector
19 changes: 19 additions & 0 deletions e2e/only-option/rector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector;
use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([
__DIR__ . '/src',
]);

$rectorConfig->rules([
RemoveAlwaysElseRector::class,
RemoveUnusedPrivateMethodRector::class,
]);
};

17 changes: 17 additions & 0 deletions e2e/only-option/src/MultiRules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

final class MultiRules
{
public function doSomething()
{
if (true === false) {
return -1;
} else {
echo 'a statement';
}
}

private function notUsed()
{
}
}
13 changes: 13 additions & 0 deletions e2e/only-option/src/RemoveAlwaysElse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

class RemoveAlwaysElse
{
public function run($value)
{
if ($value) {
throw new \InvalidStateException;
} else {
return 10;
}
}
}
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
{
private ?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
18 changes: 16 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,17 @@ 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 +89,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
3 changes: 3 additions & 0 deletions src/Console/Command/ProcessCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Rector\ChangesReporting\Output\JsonOutputFormatter;
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 +43,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 +87,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
2 changes: 2 additions & 0 deletions src/Console/ProcessConfigureDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ 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
Loading

0 comments on commit 86e10d9

Please sign in to comment.