Skip to content
Open
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
7 changes: 6 additions & 1 deletion src/PhpParser/NodeTraverser/RectorNodeTraverser.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Rector\Exception\ShouldNotHappenException;
use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace;
use Rector\PhpParser\Node\FileNode;
use Rector\VersionBonding\ComposerPackageConstraintFilter;
use Rector\VersionBonding\PhpVersionedFilter;
use Webmozart\Assert\Assert;

Expand Down Expand Up @@ -51,6 +52,7 @@ final class RectorNodeTraverser implements NodeTraverserInterface
public function __construct(
private array $rectors,
private readonly PhpVersionedFilter $phpVersionedFilter,
private readonly ComposerPackageConstraintFilter $composerPackageConstraintFilter,
private readonly ConfigurationRuleFilter $configurationRuleFilter,
) {
}
Expand Down Expand Up @@ -311,9 +313,12 @@ private function prepareNodeVisitors(): void
return;
}

// filer out by version
// filter out by PHP version
$this->visitors = $this->phpVersionedFilter->filter($this->rectors);

// filter out by composer package constraint
$this->visitors = $this->composerPackageConstraintFilter->filter($this->visitors);

// filter by configuration
$this->visitors = $this->configurationRuleFilter->filter($this->visitors);

Expand Down
53 changes: 53 additions & 0 deletions src/VersionBonding/ComposerPackageConstraintFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Rector\VersionBonding;

use Composer\Semver\Semver;
use Rector\Composer\InstalledPackageResolver;
use Rector\Contract\Rector\RectorInterface;
use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface;

final readonly class ComposerPackageConstraintFilter
{
public function __construct(
private InstalledPackageResolver $installedPackageResolver,
) {
}

/**
* @param list<RectorInterface> $rectors
* @return list<RectorInterface>
*/
public function filter(array $rectors): array
{
$activeRectors = [];
foreach ($rectors as $rector) {
if (! $rector instanceof ComposerPackageConstraintInterface) {
$activeRectors[] = $rector;
continue;
}

if ($this->satisfiesComposerPackageConstraint($rector)) {
$activeRectors[] = $rector;
}
}

return $activeRectors;
}

private function satisfiesComposerPackageConstraint(ComposerPackageConstraintInterface $rector): bool
{
$composerPackageConstraint = $rector->provideComposerPackageConstraint();
$packageVersion = $this->installedPackageResolver->resolvePackageVersion(
$composerPackageConstraint->getPackageName(),
);

if ($packageVersion === null) {
return false;
}

return Semver::satisfies($packageVersion, $composerPackageConstraint->getConstraint());
}
}
19 changes: 19 additions & 0 deletions src/VersionBonding/Contract/ComposerPackageConstraintInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Rector\VersionBonding\Contract;

use Rector\VersionBonding\ValueObject\ComposerPackageConstraint;

/**
* Can be implemented by @see \Rector\Contract\Rector\RectorInterface
*
* Rules that do not meet this composer package constraint will be skipped.
*
* @api used by extensions
*/
interface ComposerPackageConstraintInterface
{
public function provideComposerPackageConstraint(): ComposerPackageConstraint;
}
27 changes: 27 additions & 0 deletions src/VersionBonding/ValueObject/ComposerPackageConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Rector\VersionBonding\ValueObject;

/**
* @api used by extensions
*/
final readonly class ComposerPackageConstraint
{
public function __construct(
private string $packageName,
private string $constraint,
) {
}

public function getPackageName(): string
{
return $this->packageName;
}

public function getConstraint(): string
{
return $this->constraint;
}
}
74 changes: 74 additions & 0 deletions tests/VersionBonding/ComposerPackageConstraintFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\VersionBonding;

use PHPUnit\Framework\TestCase;
use Rector\Composer\InstalledPackageResolver;
use Rector\Tests\VersionBonding\Fixture\ComposerPackageConstraintRector;
use Rector\Tests\VersionBonding\Fixture\NoInterfaceRector;
use Rector\VersionBonding\ComposerPackageConstraintFilter;

final class ComposerPackageConstraintFilterTest extends TestCase
{
private ComposerPackageConstraintFilter $composerPackageConstraintFilter;

protected function setUp(): void
{
$installedPackageResolver = new InstalledPackageResolver(getcwd());

$this->composerPackageConstraintFilter = new ComposerPackageConstraintFilter($installedPackageResolver);
}

public function testRectorWithoutInterfaceIsIncluded(): void
{
$rector = new NoInterfaceRector();
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);

$this->assertCount(1, $filtered);
$this->assertSame($rector, $filtered[0]);
}

public function testRectorWithSatisfiedConstraintIsIncluded(): void
{
$rector = new ComposerPackageConstraintRector('nikic/php-parser', '>=4.0.0');
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);

$this->assertCount(1, $filtered);
$this->assertSame($rector, $filtered[0]);
}

public function testRectorWithUnsatisfiedConstraintIsExcluded(): void
{
$rector = new ComposerPackageConstraintRector('nikic/php-parser', '>=999.0.0');
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);

$this->assertCount(0, $filtered);
}

public function testRectorWithMissingPackageIsExcluded(): void
{
$rector = new ComposerPackageConstraintRector('non-existent/package', '>=1.0.0');
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);

$this->assertCount(0, $filtered);
}

public function testRectorWithCaretConstraint(): void
{
$rector = new ComposerPackageConstraintRector('nikic/php-parser', '^5.0');
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);

$this->assertCount(1, $filtered);
$this->assertSame($rector, $filtered[0]);
}

public function testRectorWithLessThanConstraintExcludesNewerVersions(): void
{
$rector = new ComposerPackageConstraintRector('nikic/php-parser', '<1.0.0');
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);

$this->assertCount(0, $filtered);
}
}
40 changes: 40 additions & 0 deletions tests/VersionBonding/Fixture/ComposerPackageConstraintRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\VersionBonding\Fixture;

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

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

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Test rector with composer package constraint', []);
}

public function getNodeTypes(): array
{
return [Node\Stmt\Class_::class];
}

public function refactor(Node $node): ?Node
{
return null;
}

public function provideComposerPackageConstraint(): ComposerPackageConstraint
{
return new ComposerPackageConstraint($this->packageName, $this->constraint);
}
}
27 changes: 27 additions & 0 deletions tests/VersionBonding/Fixture/NoInterfaceRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\VersionBonding\Fixture;

use PhpParser\Node;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class NoInterfaceRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Test rector without interface', []);
}

public function getNodeTypes(): array
{
return [Node\Stmt\Class_::class];
}

public function refactor(Node $node): ?Node
{
return null;
}
}
33 changes: 33 additions & 0 deletions tests/VersionBonding/PhpVersionedFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\VersionBonding;

use PHPUnit\Framework\TestCase;
use Rector\Php\PhpVersionProvider;
use Rector\Php\PolyfillPackagesProvider;
use Rector\Tests\VersionBonding\Fixture\NoInterfaceRector;
use Rector\VersionBonding\PhpVersionedFilter;

final class PhpVersionedFilterTest extends TestCase
{
private PhpVersionedFilter $phpVersionedFilter;

protected function setUp(): void
{
$phpVersionProvider = new PhpVersionProvider();
$polyfillPackagesProvider = new PolyfillPackagesProvider();

$this->phpVersionedFilter = new PhpVersionedFilter($phpVersionProvider, $polyfillPackagesProvider);
}

public function testRectorWithoutInterfaceIsIncluded(): void
{
$rector = new NoInterfaceRector();
$filtered = $this->phpVersionedFilter->filter([$rector]);

$this->assertCount(1, $filtered);
$this->assertSame($rector, $filtered[0]);
}
}
Loading