Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Application/ApplicationFileProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function __construct(
public function run(Configuration $configuration, InputInterface $input): ProcessResult
{
// scope the cache to this run's --only / --only-suffix selection before any cache read/write
$this->changedFilesDetector->setActiveScope($configuration->getOnlyRule(), $configuration->getOnlySuffix(), $configuration->getFilters());
$this->changedFilesDetector->setActiveScope($configuration->getOnlyRules(), $configuration->getOnlySuffix(), $configuration->getFilters());

$filePaths = $this->filesFinder->findFilesInPaths($configuration->getPaths(), $configuration);

Expand Down Expand Up @@ -125,7 +125,7 @@ public function processFiles(
?callable $postFileCallback = null
): ProcessResult {
// also set here: parallel workers reach processFiles() via WorkerCommand, bypassing run()
$this->changedFilesDetector->setActiveScope($configuration->getOnlyRule(), $configuration->getOnlySuffix(), $configuration->getFilters());
$this->changedFilesDetector->setActiveScope($configuration->getOnlyRules(), $configuration->getOnlySuffix(), $configuration->getFilters());

/** @var SystemError[] $systemErrors */
$systemErrors = [];
Expand Down
7 changes: 4 additions & 3 deletions src/Caching/Detector/ChangedFilesDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ public function __construct(
}

/**
* @param string[] $onlyRules
* @param string[] $filters
*/
public function setActiveScope(?string $onlyRule, ?string $onlySuffix, array $filters = []): void
public function setActiveScope(array $onlyRules, ?string $onlySuffix, array $filters = []): void
{
// each selection gets its own cache key, so --only and full runs coexist without clearing or poisoning
$this->scopeSuffix = ($onlyRule === null && $onlySuffix === null && $filters === [])
$this->scopeSuffix = ($onlyRules === [] && $onlySuffix === null && $filters === [])
? ''
: '|only:' . ($onlyRule ?? '') . '|suffix:' . ($onlySuffix ?? '') . '|filter:' . implode(',', $filters);
: '|only:' . implode(',', $onlyRules) . '|suffix:' . ($onlySuffix ?? '') . '|filter:' . implode(',', $filters);
}

public function cacheFile(string $filePath): void
Expand Down
14 changes: 8 additions & 6 deletions src/Configuration/ConfigurationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,12 @@ public function createFromInput(InputInterface $input): Configuration
$fileExtensions = SimpleParameterProvider::provideArrayParameter(Option::FILE_EXTENSIONS);

// filter rule and path
$onlyRule = $input->getOption(Option::ONLY);
if ($onlyRule !== null) {
$onlyRule = $this->onlyRuleResolver->resolve($onlyRule);
}
/** @var string[] $onlyRuleInputs */
$onlyRuleInputs = (array) $input->getOption(Option::ONLY);
$onlyRules = array_map(
$this->onlyRuleResolver->resolve(...),
$onlyRuleInputs
);

$onlySuffix = $input->getOption(Option::ONLY_SUFFIX);
if ($onlySuffix !== null) {
Expand All @@ -84,7 +86,7 @@ public function createFromInput(InputInterface $input): Configuration

// "--only"/"--only-suffix"/"--filter" narrow the run, so skips outside the scope look falsely unused;
// mark the run as narrowed to disable unused skip reporting and avoid false positives
if ($onlyRule !== null || $onlySuffix !== null || $filters !== []) {
if ($onlyRules !== [] || $onlySuffix !== null || $filters !== []) {
SimpleParameterProvider::setParameter(Option::IS_RUN_NARROWED, true);
}

Expand Down Expand Up @@ -141,7 +143,7 @@ public function createFromInput(InputInterface $input): Configuration
$memoryLimit,
$isDebug,
$isReportingWithRealPath,
$onlyRule,
$onlyRules,
$onlySuffix,
$levelOverflows,
$showRulesSummary,
Expand Down
16 changes: 10 additions & 6 deletions src/Configuration/ConfigurationRuleFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ public function filter(array $rectors): array
return $rectors;
}

$onlyRule = $this->configuration->getOnlyRule();
if ($onlyRule !== null) {
return $this->filterOnlyRule($rectors, $onlyRule);
$onlyRules = $this->configuration->getOnlyRules();
if ($onlyRules !== []) {
return $this->filterOnlyRules($rectors, $onlyRules);
}

if ($this->configuration->isComposerBased()) {
Expand All @@ -59,14 +59,18 @@ public function filter(array $rectors): array

/**
* @param list<RectorInterface> $rectors
* @param string[] $onlyRules
* @return list<RectorInterface>
*/
public function filterOnlyRule(array $rectors, string $onlyRule): array
public function filterOnlyRules(array $rectors, array $onlyRules): array
{
$activeRectors = [];
foreach ($rectors as $rector) {
if ($rector instanceof $onlyRule) {
$activeRectors[] = $rector;
foreach ($onlyRules as $onlyRule) {
if ($rector instanceof $onlyRule) {
$activeRectors[] = $rector;
break;
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/Console/ProcessConfigureDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ public static function decorate(Command $command): void
);

// filter by rule and path
$command->addOption(Option::ONLY, null, InputOption::VALUE_REQUIRED, 'Fully qualified rule class name');
$command->addOption(
Option::ONLY,
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'Fully qualified rule class name; repeat to run several rules, e.g. --only=A --only=B'
);

$command->addOption(
Option::COMPOSER_BASED,
Expand Down
17 changes: 11 additions & 6 deletions src/Parallel/Command/WorkerCommandLineFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,6 @@ public function create(
$workerCommandArray[] = self::OPTION_DASHES . Option::COMPOSER_BASED;
}

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

return implode(' ', $workerCommandArray);
}

Expand Down Expand Up @@ -175,7 +170,7 @@ private function mirrorCommandOptions(InputInterface $input, array $mainCommandO
continue;
}

/** @var bool|string|null $optionValue */
/** @var bool|string|string[]|null $optionValue */
$optionValue = $input->getOption($mainCommandOptionName);

// skip clutter
Expand All @@ -191,6 +186,16 @@ private function mirrorCommandOptions(InputInterface $input, array $mainCommandO
continue;
}

// array options (e.g. repeated --only) are mirrored one flag per value
if (is_array($optionValue)) {
foreach ($optionValue as $singleOptionValue) {
$workerCommandOptions[] = self::OPTION_DASHES . $mainCommandOptionName;
$workerCommandOptions[] = \escapeshellarg($singleOptionValue);
}

continue;
}

if ($mainCommandOptionName === 'memory-limit') {
// symfony/console does not accept -1 as value without assign
$workerCommandOptions[] = self::OPTION_DASHES . $mainCommandOptionName . '=' . \escapeshellarg(
Expand Down
10 changes: 7 additions & 3 deletions src/ValueObject/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
/**
* @param string[] $fileExtensions
* @param string[] $paths
* @param string[] $onlyRules
* @param LevelOverflow[] $levelOverflows
* @param string[] $filters
*/
Expand All @@ -32,7 +33,7 @@ public function __construct(
private string|null $memoryLimit = null,
private bool $isDebug = false,
private bool $reportingWithRealPath = false,
private ?string $onlyRule = null,
private array $onlyRules = [],
private ?string $onlySuffix = null,
private array $levelOverflows = [],
private bool $showRulesSummary = false,
Expand Down Expand Up @@ -82,9 +83,12 @@ public function getFileExtensions(): array
return $this->fileExtensions;
}

public function getOnlyRule(): ?string
/**
* @return string[]
*/
public function getOnlyRules(): array
{
return $this->onlyRule;
return $this->onlyRules;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ public function testOnlyRuleRunCachesUnderOwnScopeWithoutPoisoningFullRun(): voi

$this->applicationFileProcessor->processFiles([$filePath], new Configuration(
isDryRun: true,
onlyRule: RemoveEmptyClassMethodRector::class
onlyRules: [RemoveEmptyClassMethodRector::class]
));

// a repeated --only run hits its own scoped cache entry
$this->changedFilesDetector->setActiveScope(RemoveEmptyClassMethodRector::class, null);
$this->changedFilesDetector->setActiveScope([RemoveEmptyClassMethodRector::class], null);
$this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath));

// a full run uses a different scope key, so it is not poisoned
$this->changedFilesDetector->setActiveScope(null, null);
$this->changedFilesDetector->setActiveScope([], null);
$this->assertTrue($this->changedFilesDetector->hasFileChanged($filePath));
}

Expand All @@ -70,10 +70,10 @@ public function testOnlySuffixRunCachesUnderOwnScopeWithoutPoisoningFullRun(): v
onlySuffix: 'Controller.php'
));

$this->changedFilesDetector->setActiveScope(null, 'Controller.php');
$this->changedFilesDetector->setActiveScope([], 'Controller.php');
$this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath));

$this->changedFilesDetector->setActiveScope(null, null);
$this->changedFilesDetector->setActiveScope([], null);
$this->assertTrue($this->changedFilesDetector->hasFileChanged($filePath));
}

Expand Down
8 changes: 4 additions & 4 deletions tests/Caching/Detector/ChangedFilesDetectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ public function testScopedRunReusesFullRunCache(): void
$filePath = __DIR__ . '/Source/file.php';

// full run caches the file as clean
$this->changedFilesDetector->setActiveScope(null, null);
$this->changedFilesDetector->setActiveScope([], null);
$this->changedFilesDetector->addCacheableFile($filePath);
$this->changedFilesDetector->cacheFile($filePath);
$this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath));

// an --only run reuses the full-run cache instead of re-analysing the file
$this->changedFilesDetector->setActiveScope('Rector\\SomeRule', null);
$this->changedFilesDetector->setActiveScope(['Rector\\SomeRule'], null);
$this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath));
}

Expand All @@ -83,13 +83,13 @@ public function testScopedCacheDoesNotLeakToFullRun(): void
$filePath = __DIR__ . '/Source/file.php';

// a scoped run only caches under its own key
$this->changedFilesDetector->setActiveScope('Rector\\SomeRule', null);
$this->changedFilesDetector->setActiveScope(['Rector\\SomeRule'], null);
$this->changedFilesDetector->addCacheableFile($filePath);
$this->changedFilesDetector->cacheFile($filePath);
$this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath));

// a full run must not treat the file as cached, as only one rule ran
$this->changedFilesDetector->setActiveScope(null, null);
$this->changedFilesDetector->setActiveScope([], null);
$this->assertTrue($this->changedFilesDetector->hasFileChanged($filePath));
}

Expand Down
26 changes: 26 additions & 0 deletions tests/Configuration/ConfigurationRuleFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Rector\Tests\Configuration;

use Rector\Configuration\ConfigurationRuleFilter;
use Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector;
use Rector\DeadCode\Rector\If_\RemoveDeadInstanceOfRector;
use Rector\Php80\Rector\Class_\StringableForToStringRector;
use Rector\Testing\PHPUnit\AbstractLazyTestCase;
Expand All @@ -22,6 +23,13 @@ protected function setUp(): void
$this->configurationRuleFilter = $this->make(ConfigurationRuleFilter::class);
}

protected function tearDown(): void
{
// the filter is a shared container service; reset it so a configured onlyRules/isPhpOnly
// does not leak into other tests in the same process
$this->configurationRuleFilter->setConfiguration(new Configuration());
}

public function testPhpOnlyKeepsMinPhpVersionRules(): void
{
$stringableForToStringRector = $this->make(StringableForToStringRector::class);
Expand Down Expand Up @@ -64,6 +72,24 @@ public function testFiltersOutDeprecatedRules(): void
$this->assertSame([$removeDeadInstanceOfRector], $filteredRectors);
}

public function testOnlyRulesKeepsEveryListedRule(): void
{
$stringableForToStringRector = $this->make(StringableForToStringRector::class);
$removeDeadInstanceOfRector = $this->make(RemoveDeadInstanceOfRector::class);
$removeEmptyClassMethodRector = $this->make(RemoveEmptyClassMethodRector::class);

$this->configurationRuleFilter->setConfiguration(new Configuration(onlyRules: [
StringableForToStringRector::class,
RemoveEmptyClassMethodRector::class,
]));

$filteredRectors = $this->configurationRuleFilter->filter(
[$stringableForToStringRector, $removeDeadInstanceOfRector, $removeEmptyClassMethodRector]
);

$this->assertSame([$stringableForToStringRector, $removeEmptyClassMethodRector], $filteredRectors);
}

private function createConfiguration(bool $isPhpOnly): Configuration
{
return new Configuration(isPhpOnly: $isPhpOnly);
Expand Down
Loading