Skip to content

Commit 6bc2c5b

Browse files
committed
feat: add ComposerPackageConstraintInterface
This adds the ability to filter out rectors by composer package constraint.
1 parent a134f6a commit 6bc2c5b

File tree

8 files changed

+279
-1
lines changed

8 files changed

+279
-1
lines changed

src/PhpParser/NodeTraverser/RectorNodeTraverser.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Rector\Exception\ShouldNotHappenException;
1616
use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace;
1717
use Rector\PhpParser\Node\FileNode;
18+
use Rector\VersionBonding\ComposerPackageConstraintFilter;
1819
use Rector\VersionBonding\PhpVersionedFilter;
1920
use Webmozart\Assert\Assert;
2021

@@ -51,6 +52,7 @@ final class RectorNodeTraverser implements NodeTraverserInterface
5152
public function __construct(
5253
private array $rectors,
5354
private readonly PhpVersionedFilter $phpVersionedFilter,
55+
private readonly ComposerPackageConstraintFilter $composerPackageConstraintFilter,
5456
private readonly ConfigurationRuleFilter $configurationRuleFilter,
5557
) {
5658
}
@@ -311,9 +313,12 @@ private function prepareNodeVisitors(): void
311313
return;
312314
}
313315

314-
// filer out by version
316+
// filter out by PHP version
315317
$this->visitors = $this->phpVersionedFilter->filter($this->rectors);
316318

319+
// filter out by composer package constraint
320+
$this->visitors = $this->composerPackageConstraintFilter->filter($this->visitors);
321+
317322
// filter by configuration
318323
$this->visitors = $this->configurationRuleFilter->filter($this->visitors);
319324

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\VersionBonding;
6+
7+
use Composer\Semver\Semver;
8+
use Rector\Composer\InstalledPackageResolver;
9+
use Rector\Contract\Rector\RectorInterface;
10+
use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface;
11+
12+
final readonly class ComposerPackageConstraintFilter
13+
{
14+
public function __construct(
15+
private InstalledPackageResolver $installedPackageResolver,
16+
) {
17+
}
18+
19+
/**
20+
* @param list<RectorInterface> $rectors
21+
* @return list<RectorInterface>
22+
*/
23+
public function filter(array $rectors): array
24+
{
25+
$activeRectors = [];
26+
foreach ($rectors as $rector) {
27+
if (! $rector instanceof ComposerPackageConstraintInterface) {
28+
$activeRectors[] = $rector;
29+
continue;
30+
}
31+
32+
if ($this->satisfiesComposerPackageConstraint($rector)) {
33+
$activeRectors[] = $rector;
34+
}
35+
}
36+
37+
return $activeRectors;
38+
}
39+
40+
private function satisfiesComposerPackageConstraint(ComposerPackageConstraintInterface $rector): bool
41+
{
42+
$composerPackageConstraint = $rector->provideComposerPackageConstraint();
43+
$packageVersion = $this->installedPackageResolver->resolvePackageVersion(
44+
$composerPackageConstraint->getPackageName(),
45+
);
46+
47+
if ($packageVersion === null) {
48+
return false;
49+
}
50+
51+
return Semver::satisfies($packageVersion, $composerPackageConstraint->getConstraint());
52+
}
53+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\VersionBonding\Contract;
6+
7+
use Rector\VersionBonding\ValueObject\ComposerPackageConstraint;
8+
9+
/**
10+
* Can be implemented by @see \Rector\Contract\Rector\RectorInterface
11+
*
12+
* Rules that do not meet this composer package constraint will be skipped.
13+
*
14+
* @api used by extensions
15+
*/
16+
interface ComposerPackageConstraintInterface
17+
{
18+
public function provideComposerPackageConstraint(): ComposerPackageConstraint;
19+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\VersionBonding\ValueObject;
6+
7+
/**
8+
* @api used by extensions
9+
*/
10+
final readonly class ComposerPackageConstraint
11+
{
12+
public function __construct(
13+
private string $packageName,
14+
private string $constraint,
15+
) {
16+
}
17+
18+
public function getPackageName(): string
19+
{
20+
return $this->packageName;
21+
}
22+
23+
public function getConstraint(): string
24+
{
25+
return $this->constraint;
26+
}
27+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\VersionBonding;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Rector\Composer\InstalledPackageResolver;
9+
use Rector\Tests\VersionBonding\Fixture\ComposerPackageConstraintRector;
10+
use Rector\Tests\VersionBonding\Fixture\NoInterfaceRector;
11+
use Rector\VersionBonding\ComposerPackageConstraintFilter;
12+
13+
final class ComposerPackageConstraintFilterTest extends TestCase
14+
{
15+
private ComposerPackageConstraintFilter $composerPackageConstraintFilter;
16+
17+
protected function setUp(): void
18+
{
19+
$installedPackageResolver = new InstalledPackageResolver(getcwd());
20+
21+
$this->composerPackageConstraintFilter = new ComposerPackageConstraintFilter($installedPackageResolver);
22+
}
23+
24+
public function testRectorWithoutInterfaceIsIncluded(): void
25+
{
26+
$rector = new NoInterfaceRector();
27+
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);
28+
29+
$this->assertCount(1, $filtered);
30+
$this->assertSame($rector, $filtered[0]);
31+
}
32+
33+
public function testRectorWithSatisfiedConstraintIsIncluded(): void
34+
{
35+
$rector = new ComposerPackageConstraintRector('nikic/php-parser', '>=4.0.0');
36+
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);
37+
38+
$this->assertCount(1, $filtered);
39+
$this->assertSame($rector, $filtered[0]);
40+
}
41+
42+
public function testRectorWithUnsatisfiedConstraintIsExcluded(): void
43+
{
44+
$rector = new ComposerPackageConstraintRector('nikic/php-parser', '>=999.0.0');
45+
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);
46+
47+
$this->assertCount(0, $filtered);
48+
}
49+
50+
public function testRectorWithMissingPackageIsExcluded(): void
51+
{
52+
$rector = new ComposerPackageConstraintRector('non-existent/package', '>=1.0.0');
53+
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);
54+
55+
$this->assertCount(0, $filtered);
56+
}
57+
58+
public function testRectorWithCaretConstraint(): void
59+
{
60+
$rector = new ComposerPackageConstraintRector('nikic/php-parser', '^5.0');
61+
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);
62+
63+
$this->assertCount(1, $filtered);
64+
$this->assertSame($rector, $filtered[0]);
65+
}
66+
67+
public function testRectorWithLessThanConstraintExcludesNewerVersions(): void
68+
{
69+
$rector = new ComposerPackageConstraintRector('nikic/php-parser', '<1.0.0');
70+
$filtered = $this->composerPackageConstraintFilter->filter([$rector]);
71+
72+
$this->assertCount(0, $filtered);
73+
}
74+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\VersionBonding\Fixture;
6+
7+
use PhpParser\Node;
8+
use Rector\Rector\AbstractRector;
9+
use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface;
10+
use Rector\VersionBonding\ValueObject\ComposerPackageConstraint;
11+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
12+
13+
final class ComposerPackageConstraintRector extends AbstractRector implements ComposerPackageConstraintInterface
14+
{
15+
public function __construct(
16+
private readonly string $packageName,
17+
private readonly string $constraint,
18+
) {
19+
}
20+
21+
public function getRuleDefinition(): RuleDefinition
22+
{
23+
return new RuleDefinition('Test rector with composer package constraint', []);
24+
}
25+
26+
public function getNodeTypes(): array
27+
{
28+
return [Node\Stmt\Class_::class];
29+
}
30+
31+
public function refactor(Node $node): ?Node
32+
{
33+
return null;
34+
}
35+
36+
public function provideComposerPackageConstraint(): ComposerPackageConstraint
37+
{
38+
return new ComposerPackageConstraint($this->packageName, $this->constraint);
39+
}
40+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\VersionBonding\Fixture;
6+
7+
use PhpParser\Node;
8+
use Rector\Rector\AbstractRector;
9+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
10+
11+
final class NoInterfaceRector extends AbstractRector
12+
{
13+
public function getRuleDefinition(): RuleDefinition
14+
{
15+
return new RuleDefinition('Test rector without interface', []);
16+
}
17+
18+
public function getNodeTypes(): array
19+
{
20+
return [Node\Stmt\Class_::class];
21+
}
22+
23+
public function refactor(Node $node): ?Node
24+
{
25+
return null;
26+
}
27+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\VersionBonding;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Rector\Php\PhpVersionProvider;
9+
use Rector\Php\PolyfillPackagesProvider;
10+
use Rector\Tests\VersionBonding\Fixture\NoInterfaceRector;
11+
use Rector\VersionBonding\PhpVersionedFilter;
12+
13+
final class PhpVersionedFilterTest extends TestCase
14+
{
15+
private PhpVersionedFilter $phpVersionedFilter;
16+
17+
protected function setUp(): void
18+
{
19+
$phpVersionProvider = new PhpVersionProvider();
20+
$polyfillPackagesProvider = new PolyfillPackagesProvider();
21+
22+
$this->phpVersionedFilter = new PhpVersionedFilter($phpVersionProvider, $polyfillPackagesProvider);
23+
}
24+
25+
public function testRectorWithoutInterfaceIsIncluded(): void
26+
{
27+
$rector = new NoInterfaceRector();
28+
$filtered = $this->phpVersionedFilter->filter([$rector]);
29+
30+
$this->assertCount(1, $filtered);
31+
$this->assertSame($rector, $filtered[0]);
32+
}
33+
}

0 commit comments

Comments
 (0)