From a278f087feb037e9664fff79c99dd39e7727e467 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 31 Jul 2026 21:50:46 +0200 Subject: [PATCH] [Console] Add composer-based command to list rules bound to an installed package version --- src/Console/Command/ComposerBasedCommand.php | 96 +++++++++++++++++++ .../LazyContainerFactory.php | 6 ++ .../Command/ComposerBasedCommandTest.php | 76 +++++++++++++++ .../Command/Source/ComposerBoundRector.php | 41 ++++++++ 4 files changed, 219 insertions(+) create mode 100644 src/Console/Command/ComposerBasedCommand.php create mode 100644 tests/Console/Command/ComposerBasedCommandTest.php create mode 100644 tests/Console/Command/Source/ComposerBoundRector.php diff --git a/src/Console/Command/ComposerBasedCommand.php b/src/Console/Command/ComposerBasedCommand.php new file mode 100644 index 00000000000..d6604caf27b --- /dev/null +++ b/src/Console/Command/ComposerBasedCommand.php @@ -0,0 +1,96 @@ +setName('composer-based'); + $this->setDescription('Show loaded rules that are triggered by an installed composer package version'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $tableRows = $this->createTableRows(); + + if ($tableRows === []) { + $this->symfonyStyle->warning('No composer package bound rule is loaded'); + + return Command::SUCCESS; + } + + $this->symfonyStyle->title('Composer package bound rules'); + $this->symfonyStyle->table(['Rule', 'Package', 'Requires', 'Installed', 'Active'], $tableRows); + + $activeCount = count(array_filter($tableRows, static fn (array $tableRow): bool => $tableRow[4] === 'yes')); + + $this->symfonyStyle->note( + sprintf('%d of %d composer package bound rules are active', $activeCount, count($tableRows)) + ); + + return Command::SUCCESS; + } + + /** + * @return array + */ + private function createTableRows(): array + { + $tableRows = []; + + foreach ($this->rectors as $rector) { + if (! $rector instanceof ComposerPackageConstraintInterface) { + continue; + } + + $composerPackageConstraint = $rector->provideComposerPackageConstraint(); + $packageName = $composerPackageConstraint->getPackageName(); + $constraint = $composerPackageConstraint->getConstraint(); + + $installedVersion = $this->installedPackageResolver->resolvePackageVersion($packageName); + $isActive = $installedVersion !== null && Semver::satisfies($installedVersion, $constraint); + + $tableRows[] = [ + $rector::class, + $packageName, + $constraint, + $installedVersion ?? '-', + $isActive ? 'yes' : 'no', + ]; + } + + // sort by package name first, then by rule class + usort( + $tableRows, + static fn (array $firstTableRow, array $secondTableRow): int => [$firstTableRow[1], $firstTableRow[0]] <=> [$secondTableRow[1], $secondTableRow[0]] + ); + + return $tableRows; + } +} diff --git a/src/DependencyInjection/LazyContainerFactory.php b/src/DependencyInjection/LazyContainerFactory.php index 09aff5e6cf3..70391988268 100644 --- a/src/DependencyInjection/LazyContainerFactory.php +++ b/src/DependencyInjection/LazyContainerFactory.php @@ -58,6 +58,7 @@ use Rector\Configuration\ConfigurationRuleFilter; use Rector\Configuration\OnlyRuleResolver; use Rector\Configuration\RenamedClassesDataCollector; +use Rector\Console\Command\ComposerBasedCommand; use Rector\Console\Command\CustomRuleCommand; use Rector\Console\Command\ListRulesCommand; use Rector\Console\Command\ProcessCommand; @@ -435,11 +436,16 @@ private function registerConsole(RectorConfig $rectorConfig): void $rectorConfig->singleton(SetupCICommand::class); $rectorConfig->singleton(ListRulesCommand::class); $rectorConfig->singleton(CustomRuleCommand::class); + $rectorConfig->singleton(ComposerBasedCommand::class); $rectorConfig->when(ListRulesCommand::class) ->needs('$rectors') ->giveTagged(RectorInterface::class); + $rectorConfig->when(ComposerBasedCommand::class) + ->needs('$rectors') + ->giveTagged(RectorInterface::class); + $rectorConfig->when(OnlyRuleResolver::class) ->needs('$rectors') ->giveTagged(RectorInterface::class); diff --git a/tests/Console/Command/ComposerBasedCommandTest.php b/tests/Console/Command/ComposerBasedCommandTest.php new file mode 100644 index 00000000000..fdf4c1ab33e --- /dev/null +++ b/tests/Console/Command/ComposerBasedCommandTest.php @@ -0,0 +1,76 @@ +bufferedOutput = new BufferedOutput(); + } + + public function testName(): void + { + $composerBasedCommand = $this->createComposerBasedCommand([]); + + $this->assertSame('composer-based', $composerBasedCommand->getName()); + } + + public function testSkipWithoutComposerBoundRules(): void + { + $composerBasedCommand = $this->createComposerBasedCommand([]); + $composerBasedCommand->run(new ArrayInput([]), $this->bufferedOutput); + + $this->assertStringContainsString('No composer package bound rule is loaded', $this->bufferedOutput->fetch()); + } + + public function testActiveRule(): void + { + // this project requires PHPUnit + $composerBoundRector = new ComposerBoundRector('phpunit/phpunit', '>=9.0'); + + $composerBasedCommand = $this->createComposerBasedCommand([$composerBoundRector]); + $composerBasedCommand->run(new ArrayInput([]), $this->bufferedOutput); + + $output = $this->bufferedOutput->fetch(); + + $this->assertStringContainsString('phpunit/phpunit', $output); + $this->assertStringContainsString('>=9.0', $output); + $this->assertStringContainsString('1 of 1 composer package bound rules are active', $output); + } + + public function testNotInstalledPackage(): void + { + $composerBoundRector = new ComposerBoundRector('not-installed/package', '>=1.0'); + + $composerBasedCommand = $this->createComposerBasedCommand([$composerBoundRector]); + $composerBasedCommand->run(new ArrayInput([]), $this->bufferedOutput); + + $output = $this->bufferedOutput->fetch(); + + $this->assertStringContainsString('not-installed/package', $output); + $this->assertStringContainsString('0 of 1 composer package bound rules are active', $output); + } + + /** + * @param ComposerBoundRector[] $rectors + */ + private function createComposerBasedCommand(array $rectors): ComposerBasedCommand + { + $symfonyStyle = new SymfonyStyle(new ArrayInput([]), $this->bufferedOutput); + + return new ComposerBasedCommand($symfonyStyle, new InstalledPackageResolver(getcwd()), $rectors); + } +} diff --git a/tests/Console/Command/Source/ComposerBoundRector.php b/tests/Console/Command/Source/ComposerBoundRector.php new file mode 100644 index 00000000000..3255736cb0d --- /dev/null +++ b/tests/Console/Command/Source/ComposerBoundRector.php @@ -0,0 +1,41 @@ +packageName, $this->constraint); + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Testing rule', [new CodeSample('$before;', '$after;')]); + } + + public function getNodeTypes(): array + { + return [Node\Stmt\Class_::class]; + } + + public function refactor(Node $node): ?Node + { + return null; + } +}