diff --git a/src/Config/RectorConfig.php b/src/Config/RectorConfig.php index fd70120a55c..5c2c17d52ea 100644 --- a/src/Config/RectorConfig.php +++ b/src/Config/RectorConfig.php @@ -25,6 +25,7 @@ use Rector\ValueObject\Configuration\LevelOverflow; use Rector\ValueObject\PhpVersion; use Rector\ValueObject\PolyfillPackage; +use Rector\VersionBonding\ValueObject\ComposerBoundRuleConfiguration; use Symfony\Component\Console\Command\Command; use Webmozart\Assert\Assert; @@ -214,11 +215,20 @@ public function ruleWithConfigurationComposerVersionBound( string $versionConstraint ): void { $packageVersion = $this->resolveInstalledPackageVersion($packageName); - if ($packageVersion === null) { - return; - } - - if (! Semver::satisfies($packageVersion, $versionConstraint)) { + $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 + ), + ]); + + if (! $isActive) { return; } diff --git a/src/Configuration/Option.php b/src/Configuration/Option.php index 158030f7c13..2938cae32d4 100644 --- a/src/Configuration/Option.php +++ b/src/Configuration/Option.php @@ -261,6 +261,12 @@ final class Option */ public const string COMPOSER_BASED_SETS = 'composer_based_sets'; + /** + * @internal To report rule configuration bound to an installed package version + * @see \Rector\Config\RectorConfig::ruleWithConfigurationComposerVersionBound() + */ + public const string COMPOSER_BOUND_RULE_CONFIGURATIONS = 'composer_bound_rule_configurations'; + /** * @internal To filter files by specific suffix */ diff --git a/src/Console/Command/ComposerBasedCommand.php b/src/Console/Command/ComposerBasedCommand.php index d6604caf27b..0418cc8030f 100644 --- a/src/Console/Command/ComposerBasedCommand.php +++ b/src/Console/Command/ComposerBasedCommand.php @@ -5,9 +5,14 @@ namespace Rector\Console\Command; use Composer\Semver\Semver; +use Nette\Utils\Strings; use Rector\Composer\InstalledPackageResolver; +use Rector\Configuration\Option; +use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Contract\Rector\RectorInterface; use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface; +use Rector\VersionBonding\ValueObject\ComposerBoundRuleConfiguration; +use ReflectionObject; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -38,20 +43,32 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $tableRows = $this->createTableRows(); + $configurationTableRows = $this->createConfigurationTableRows(); - if ($tableRows === []) { + if ($tableRows === [] && $configurationTableRows === []) { $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); + if ($tableRows !== []) { + $this->symfonyStyle->title('Composer package bound rules'); + $this->symfonyStyle->table(['Rule', 'Package', 'Requires', 'Installed', 'Active'], $tableRows); + } + + if ($configurationTableRows !== []) { + $this->symfonyStyle->title('Composer package bound rule configuration'); + $this->symfonyStyle->table( + ['Rule', 'Package', 'Requires', 'Installed', 'Active', 'Configuration'], + $configurationTableRows + ); + } - $activeCount = count(array_filter($tableRows, static fn (array $tableRow): bool => $tableRow[4] === 'yes')); + $allTableRows = [...$tableRows, ...$configurationTableRows]; + $activeCount = count(array_filter($allTableRows, static fn (array $tableRow): bool => $tableRow[4] === 'yes')); $this->symfonyStyle->note( - sprintf('%d of %d composer package bound rules are active', $activeCount, count($tableRows)) + sprintf('%d of %d composer package bound items are active', $activeCount, count($allTableRows)) ); return Command::SUCCESS; @@ -77,7 +94,7 @@ private function createTableRows(): array $isActive = $installedVersion !== null && Semver::satisfies($installedVersion, $constraint); $tableRows[] = [ - $rector::class, + $this->printShortClassName($rector::class), $packageName, $constraint, $installedVersion ?? '-', @@ -93,4 +110,86 @@ private function createTableRows(): array return $tableRows; } + + /** + * @return array + */ + private function createConfigurationTableRows(): array + { + $composerBoundRuleConfigurations = SimpleParameterProvider::provideArrayParameter( + Option::COMPOSER_BOUND_RULE_CONFIGURATIONS + ); + + $tableRows = []; + + foreach ($composerBoundRuleConfigurations as $composerBoundRuleConfiguration) { + if (! $composerBoundRuleConfiguration instanceof ComposerBoundRuleConfiguration) { + continue; + } + + $packageName = $composerBoundRuleConfiguration->getPackageName(); + $installedVersion = $this->installedPackageResolver->resolvePackageVersion($packageName); + + $tableRows[] = [ + $this->printShortClassName($composerBoundRuleConfiguration->getRectorClass()), + $packageName, + $composerBoundRuleConfiguration->getVersionConstraint(), + $installedVersion ?? '-', + $composerBoundRuleConfiguration->isActive() ? 'yes' : 'no', + $this->printConfiguration($composerBoundRuleConfiguration->getConfiguration()), + ]; + } + + return $tableRows; + } + + /** + * @param mixed[] $configuration + */ + private function printConfiguration(array $configuration): string + { + $printedItems = []; + + foreach ($configuration as $key => $value) { + $printedValue = $this->printConfigurationValue($value); + + $printedItems[] = is_string($key) ? $key . ': ' . $printedValue : $printedValue; + } + + return implode(PHP_EOL, $printedItems); + } + + private function printConfigurationValue(mixed $value): string + { + if (is_object($value)) { + $printedPropertyValues = []; + + $reflectionObject = new ReflectionObject($value); + foreach ($reflectionObject->getProperties() as $reflectionProperty) { + $printedPropertyValues[] = $this->printConfigurationValue($reflectionProperty->getValue($value)); + } + + return $this->printShortClassName($value::class) . '(' . implode(', ', $printedPropertyValues) . ')'; + } + + if (is_array($value)) { + $printedItems = array_map( + $this->printConfigurationValue(...), + $value + ); + + return '[' . implode(', ', $printedItems) . ']'; + } + + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + + return (string) $value; + } + + private function printShortClassName(string $className): string + { + return Strings::after($className, '\\', -1) ?? $className; + } } diff --git a/src/VersionBonding/ValueObject/ComposerBoundRuleConfiguration.php b/src/VersionBonding/ValueObject/ComposerBoundRuleConfiguration.php new file mode 100644 index 00000000000..44c0a1c433c --- /dev/null +++ b/src/VersionBonding/ValueObject/ComposerBoundRuleConfiguration.php @@ -0,0 +1,57 @@ + $rectorClass + * @param mixed[] $configuration + */ + public function __construct( + private string $rectorClass, + private string $packageName, + private string $versionConstraint, + private array $configuration, + private bool $isActive + ) { + } + + /** + * @return class-string + */ + public function getRectorClass(): string + { + return $this->rectorClass; + } + + public function getPackageName(): string + { + return $this->packageName; + } + + public function getVersionConstraint(): string + { + return $this->versionConstraint; + } + + /** + * @return mixed[] + */ + public function getConfiguration(): array + { + return $this->configuration; + } + + public function isActive(): bool + { + return $this->isActive; + } +} diff --git a/tests/Console/Command/ComposerBasedCommandTest.php b/tests/Console/Command/ComposerBasedCommandTest.php index fdf4c1ab33e..ec9448d7861 100644 --- a/tests/Console/Command/ComposerBasedCommandTest.php +++ b/tests/Console/Command/ComposerBasedCommandTest.php @@ -6,7 +6,11 @@ use PHPUnit\Framework\TestCase; use Rector\Composer\InstalledPackageResolver; +use Rector\Config\RectorConfig; +use Rector\Configuration\Option; +use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Console\Command\ComposerBasedCommand; +use Rector\Renaming\Rector\Name\RenameClassRector; use Rector\Tests\Console\Command\Source\ComposerBoundRector; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; @@ -19,6 +23,9 @@ final class ComposerBasedCommandTest extends TestCase protected function setUp(): void { $this->bufferedOutput = new BufferedOutput(); + + // the parameter provider is static, reset it between tests + SimpleParameterProvider::setParameter(Option::COMPOSER_BOUND_RULE_CONFIGURATIONS, []); } public function testName(): void @@ -48,7 +55,7 @@ public function testActiveRule(): void $this->assertStringContainsString('phpunit/phpunit', $output); $this->assertStringContainsString('>=9.0', $output); - $this->assertStringContainsString('1 of 1 composer package bound rules are active', $output); + $this->assertStringContainsString('1 of 1 composer package bound items are active', $output); } public function testNotInstalledPackage(): void @@ -61,7 +68,53 @@ public function testNotInstalledPackage(): void $output = $this->bufferedOutput->fetch(); $this->assertStringContainsString('not-installed/package', $output); - $this->assertStringContainsString('0 of 1 composer package bound rules are active', $output); + $this->assertStringContainsString('0 of 1 composer package bound items are active', $output); + } + + public function testRuleConfiguration(): void + { + $rectorConfig = new RectorConfig(); + + // this project requires PHPUnit + $rectorConfig->ruleWithConfigurationComposerVersionBound( + RenameClassRector::class, + [ + 'SomeOldClass' => 'SomeNewClass', + ], + 'phpunit/phpunit', + '>=9.0' + ); + + $composerBasedCommand = $this->createComposerBasedCommand([]); + $composerBasedCommand->run(new ArrayInput([]), $this->bufferedOutput); + + $output = $this->bufferedOutput->fetch(); + + $this->assertStringContainsString('Composer package bound rule configuration', $output); + $this->assertStringContainsString('SomeOldClass: SomeNewClass', $output); + $this->assertStringContainsString('>=9.0', $output); + } + + public function testInactiveRuleConfigurationIsReported(): void + { + $rectorConfig = new RectorConfig(); + + $rectorConfig->ruleWithConfigurationComposerVersionBound( + RenameClassRector::class, + [ + 'SomeOldClass' => 'SomeNewClass', + ], + 'not-installed/package', + '>=1.0' + ); + + $composerBasedCommand = $this->createComposerBasedCommand([]); + $composerBasedCommand->run(new ArrayInput([]), $this->bufferedOutput); + + $output = $this->bufferedOutput->fetch(); + + $this->assertStringContainsString('not-installed/package', $output); + $this->assertStringContainsString('0 of 1 composer package bound items are active', $output); } /**