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
20 changes: 15 additions & 5 deletions src/Config/RectorConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down
6 changes: 6 additions & 0 deletions src/Configuration/Option.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
111 changes: 105 additions & 6 deletions src/Console/Command/ComposerBasedCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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 ?? '-',
Expand All @@ -93,4 +110,86 @@ private function createTableRows(): array

return $tableRows;
}

/**
* @return array<array{string, string, string, string, string, string}>
*/
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;
}
}
57 changes: 57 additions & 0 deletions src/VersionBonding/ValueObject/ComposerBoundRuleConfiguration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Rector\VersionBonding\ValueObject;

use Rector\Contract\Rector\ConfigurableRectorInterface;

/**
* @see \Rector\Config\RectorConfig::ruleWithConfigurationComposerVersionBound()
*/
final readonly class ComposerBoundRuleConfiguration
{
/**
* @param class-string<ConfigurableRectorInterface> $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<ConfigurableRectorInterface>
*/
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;
}
}
57 changes: 55 additions & 2 deletions tests/Console/Command/ComposerBasedCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
}

/**
Expand Down
Loading