diff --git a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php index 79436baea39..f27610da739 100644 --- a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php +++ b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php @@ -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; @@ -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, ) { } @@ -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); diff --git a/src/VersionBonding/ComposerPackageConstraintFilter.php b/src/VersionBonding/ComposerPackageConstraintFilter.php new file mode 100644 index 00000000000..892b4e8b1ee --- /dev/null +++ b/src/VersionBonding/ComposerPackageConstraintFilter.php @@ -0,0 +1,53 @@ + $rectors + * @return list + */ + 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()); + } +} diff --git a/src/VersionBonding/Contract/ComposerPackageConstraintInterface.php b/src/VersionBonding/Contract/ComposerPackageConstraintInterface.php new file mode 100644 index 00000000000..f591813e015 --- /dev/null +++ b/src/VersionBonding/Contract/ComposerPackageConstraintInterface.php @@ -0,0 +1,19 @@ +packageName; + } + + public function getConstraint(): string + { + return $this->constraint; + } +} diff --git a/tests/VersionBonding/ComposerPackageConstraintFilterTest.php b/tests/VersionBonding/ComposerPackageConstraintFilterTest.php new file mode 100644 index 00000000000..c9cf6685523 --- /dev/null +++ b/tests/VersionBonding/ComposerPackageConstraintFilterTest.php @@ -0,0 +1,74 @@ +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); + } +} diff --git a/tests/VersionBonding/Fixture/ComposerPackageConstraintRector.php b/tests/VersionBonding/Fixture/ComposerPackageConstraintRector.php new file mode 100644 index 00000000000..5dfe01c2425 --- /dev/null +++ b/tests/VersionBonding/Fixture/ComposerPackageConstraintRector.php @@ -0,0 +1,40 @@ +packageName, $this->constraint); + } +} diff --git a/tests/VersionBonding/Fixture/NoInterfaceRector.php b/tests/VersionBonding/Fixture/NoInterfaceRector.php new file mode 100644 index 00000000000..18dd4921276 --- /dev/null +++ b/tests/VersionBonding/Fixture/NoInterfaceRector.php @@ -0,0 +1,27 @@ +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]); + } +}