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
96 changes: 96 additions & 0 deletions src/Console/Command/ComposerBasedCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

namespace Rector\Console\Command;

use Composer\Semver\Semver;
use Rector\Composer\InstalledPackageResolver;
use Rector\Contract\Rector\RectorInterface;
use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
* @see \Rector\Tests\Console\Command\ComposerBasedCommandTest
*/
final class ComposerBasedCommand extends Command
{
/**
* @param RectorInterface[] $rectors
*/
public function __construct(
private readonly SymfonyStyle $symfonyStyle,
private readonly InstalledPackageResolver $installedPackageResolver,
private readonly array $rectors
) {
parent::__construct();
}

protected function configure(): void
{
$this->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<array{string, string, string, string, string}>
*/
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;
}
}
6 changes: 6 additions & 0 deletions src/DependencyInjection/LazyContainerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
76 changes: 76 additions & 0 deletions tests/Console/Command/ComposerBasedCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\Console\Command;

use PHPUnit\Framework\TestCase;
use Rector\Composer\InstalledPackageResolver;
use Rector\Console\Command\ComposerBasedCommand;
use Rector\Tests\Console\Command\Source\ComposerBoundRector;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Style\SymfonyStyle;

final class ComposerBasedCommandTest extends TestCase
{
private BufferedOutput $bufferedOutput;

protected function setUp(): void
{
$this->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);
}
}
41 changes: 41 additions & 0 deletions tests/Console/Command/Source/ComposerBoundRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\Console\Command\Source;

use PhpParser\Node;
use Rector\Rector\AbstractRector;
use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface;
use Rector\VersionBonding\ValueObject\ComposerPackageConstraint;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class ComposerBoundRector extends AbstractRector implements ComposerPackageConstraintInterface
{
public function __construct(
private readonly string $packageName,
private readonly string $constraint
) {
}

public function provideComposerPackageConstraint(): ComposerPackageConstraint
{
return new ComposerPackageConstraint($this->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;
}
}
Loading