diff --git a/src/Config/RectorConfig.php b/src/Config/RectorConfig.php index 5c2c17d52ea..249933e889f 100644 --- a/src/Config/RectorConfig.php +++ b/src/Config/RectorConfig.php @@ -40,6 +40,16 @@ final class RectorConfig extends Container */ private array $ruleConfigurations = []; + /** + * @var array, true> + */ + private array $registeredRectorClasses = []; + + /** + * @var array + */ + private array $registeredComposerBoundRuleConfigurations = []; + /** * @var string[] */ @@ -195,9 +205,6 @@ public function ruleWithConfiguration(string $rectorClass, array $configuration) $ruleConfiguration = $this->ruleConfigurations[$rectorClass]; $configurableRector->configure($ruleConfiguration); }); - - // for cache invalidation in case of sets change - SimpleParameterProvider::addParameter(Option::REGISTERED_RECTOR_RULES, $rectorClass); } /** @@ -217,16 +224,25 @@ public function ruleWithConfigurationComposerVersionBound( $packageVersion = $this->resolveInstalledPackageVersion($packageName); $isActive = $packageVersion !== null && Semver::satisfies($packageVersion, $versionConstraint); - // reported by the "composer-based" command, the inactive ones as well - SimpleParameterProvider::addParameter(Option::COMPOSER_BOUND_RULE_CONFIGURATIONS, [ - new ComposerBoundRuleConfiguration( - $rectorClass, - $packageName, - $versionConstraint, - $configuration, - $isActive - ), - ]); + // the same rule configuration can be registered by multiple sets, report it only once + $configurationKey = $rectorClass . '|' . $packageName . '|' . $versionConstraint . '|' . serialize( + $configuration + ); + + if (! isset($this->registeredComposerBoundRuleConfigurations[$configurationKey])) { + $this->registeredComposerBoundRuleConfigurations[$configurationKey] = true; + + // reported by the "composer-based" command, the inactive ones as well + SimpleParameterProvider::addParameter(Option::COMPOSER_BOUND_RULE_CONFIGURATIONS, [ + new ComposerBoundRuleConfiguration( + $rectorClass, + $packageName, + $versionConstraint, + $configuration, + $isActive + ), + ]); + } if (! $isActive) { return; @@ -244,10 +260,17 @@ public function rule(string $rectorClass): void Assert::isAOf($rectorClass, RectorInterface::class); $this->singleton($rectorClass); - $this->tag($rectorClass, RectorInterface::class); - // for cache invalidation in case of change - SimpleParameterProvider::addParameter(Option::REGISTERED_RECTOR_RULES, $rectorClass); + // the same rule can be registered by multiple sets, tag it only once, + // otherwise it is run twice on every node and listed twice in the reports + if (! isset($this->registeredRectorClasses[$rectorClass])) { + $this->registeredRectorClasses[$rectorClass] = true; + + $this->tag($rectorClass, RectorInterface::class); + + // for cache invalidation in case of change + SimpleParameterProvider::addParameter(Option::REGISTERED_RECTOR_RULES, $rectorClass); + } if (is_a($rectorClass, RelatedConfigInterface::class, true)) { $configFile = $rectorClass::getConfigFile(); @@ -442,6 +465,8 @@ public function indent(string $character, int $count): void public function resetRuleConfigurations(): void { $this->ruleConfigurations = []; + $this->registeredRectorClasses = []; + $this->registeredComposerBoundRuleConfigurations = []; } /** diff --git a/src/Configuration/ConfigurationFactory.php b/src/Configuration/ConfigurationFactory.php index 8f52a7a2111..4cb759bdcea 100644 --- a/src/Configuration/ConfigurationFactory.php +++ b/src/Configuration/ConfigurationFactory.php @@ -95,6 +95,13 @@ public function createFromInput(InputInterface $input): Configuration $showRulesSummary = (bool) $input->getOption(Option::RULES_SUMMARY); + $isComposerBased = (bool) $input->getOption(Option::COMPOSER_BASED); + + // "--composer-based" narrows the run the same way "--only" does + if ($isComposerBased) { + SimpleParameterProvider::setParameter(Option::IS_RUN_NARROWED, true); + } + return new Configuration( $isDryRun, $showProgressBar, @@ -113,6 +120,7 @@ public function createFromInput(InputInterface $input): Configuration $onlySuffix, $levelOverflows, $showRulesSummary, + $isComposerBased, ); } diff --git a/src/Configuration/ConfigurationRuleFilter.php b/src/Configuration/ConfigurationRuleFilter.php index 83e351ce575..9ec95d6f671 100644 --- a/src/Configuration/ConfigurationRuleFilter.php +++ b/src/Configuration/ConfigurationRuleFilter.php @@ -4,8 +4,11 @@ namespace Rector\Configuration; +use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Contract\Rector\RectorInterface; use Rector\ValueObject\Configuration; +use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface; +use Rector\VersionBonding\ValueObject\ComposerBoundRuleConfiguration; /** * Modify available rector rules based on the configuration options @@ -34,6 +37,10 @@ public function filter(array $rectors): array return $this->filterOnlyRule($rectors, $onlyRule); } + if ($this->configuration->isComposerBased()) { + return $this->filterComposerBased($rectors); + } + return $rectors; } @@ -52,4 +59,55 @@ public function filterOnlyRule(array $rectors, string $onlyRule): array return $activeRectors; } + + /** + * Keeps rules that declare a composer package constraint themselves, and rules whose configuration + * was registered with a composer package constraint. + * + * @param list $rectors + * @return list + */ + private function filterComposerBased(array $rectors): array + { + $composerBoundRectorClasses = $this->resolveComposerBoundRectorClasses(); + + $activeRectors = []; + foreach ($rectors as $rector) { + if ($rector instanceof ComposerPackageConstraintInterface) { + $activeRectors[] = $rector; + continue; + } + + if (in_array($rector::class, $composerBoundRectorClasses, true)) { + $activeRectors[] = $rector; + } + } + + return $activeRectors; + } + + /** + * @return string[] + */ + private function resolveComposerBoundRectorClasses(): array + { + $composerBoundRuleConfigurations = SimpleParameterProvider::provideArrayParameter( + Option::COMPOSER_BOUND_RULE_CONFIGURATIONS + ); + + $rectorClasses = []; + foreach ($composerBoundRuleConfigurations as $composerBoundRuleConfiguration) { + if (! $composerBoundRuleConfiguration instanceof ComposerBoundRuleConfiguration) { + continue; + } + + if (! $composerBoundRuleConfiguration->isActive()) { + continue; + } + + $rectorClasses[] = $composerBoundRuleConfiguration->getRectorClass(); + } + + return $rectorClasses; + } } diff --git a/src/Configuration/Option.php b/src/Configuration/Option.php index 2938cae32d4..f0fbabbdce9 100644 --- a/src/Configuration/Option.php +++ b/src/Configuration/Option.php @@ -267,6 +267,11 @@ final class Option */ public const string COMPOSER_BOUND_RULE_CONFIGURATIONS = 'composer_bound_rule_configurations'; + /** + * Run only rules bound to an installed composer package version + */ + public const string COMPOSER_BASED = 'composer-based'; + /** * @internal To filter files by specific suffix */ diff --git a/src/Console/ProcessConfigureDecorator.php b/src/Console/ProcessConfigureDecorator.php index 7b3f20f107e..5e9095dae25 100644 --- a/src/Console/ProcessConfigureDecorator.php +++ b/src/Console/ProcessConfigureDecorator.php @@ -59,6 +59,13 @@ 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::COMPOSER_BASED, + null, + InputOption::VALUE_NONE, + 'Run only rules bound to an installed composer package version' + ); + $command->addOption( Option::ONLY_SUFFIX, null, diff --git a/src/Parallel/Command/WorkerCommandLineFactory.php b/src/Parallel/Command/WorkerCommandLineFactory.php index 34dc91e5010..b8bbba0ad16 100644 --- a/src/Parallel/Command/WorkerCommandLineFactory.php +++ b/src/Parallel/Command/WorkerCommandLineFactory.php @@ -123,6 +123,10 @@ public function create( } } + if ((bool) $input->getOption(Option::COMPOSER_BASED)) { + $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)); diff --git a/src/ValueObject/Configuration.php b/src/ValueObject/Configuration.php index 0c1d516d875..2c546343a4b 100644 --- a/src/ValueObject/Configuration.php +++ b/src/ValueObject/Configuration.php @@ -35,9 +35,15 @@ public function __construct( private ?string $onlySuffix = null, private array $levelOverflows = [], private bool $showRulesSummary = false, + private bool $isComposerBased = false, ) { } + public function isComposerBased(): bool + { + return $this->isComposerBased; + } + public function isDryRun(): bool { return $this->isDryRun;