diff --git a/src/Console/Command/ComposerBasedCommand.php b/src/Console/Command/ComposerBasedCommand.php index 31b06dce6f1..60be06c7729 100644 --- a/src/Console/Command/ComposerBasedCommand.php +++ b/src/Console/Command/ComposerBasedCommand.php @@ -103,19 +103,25 @@ private function createTableRows(): array } $composerPackageConstraint = $rector->provideComposerPackageConstraint(); - $packageName = $composerPackageConstraint->getPackageName(); - $constraint = $composerPackageConstraint->getConstraint(); - - $installedVersion = $this->installedPackageResolver->resolvePackageVersion($packageName); - $isActive = $installedVersion !== null && Semver::satisfies($installedVersion, $constraint); - - $tableRows[] = [ - $this->printShortClassName($rector::class), - $packageName, - $constraint, - $installedVersion ?? '-', - $isActive ? 'yes' : 'no', - ]; + $composerPackageConstraints = is_array($composerPackageConstraint) + ? $composerPackageConstraint + : [$composerPackageConstraint]; + + foreach ($composerPackageConstraints as $composerPackageConstraint) { + $packageName = $composerPackageConstraint->getPackageName(); + $constraint = $composerPackageConstraint->getConstraint(); + + $installedVersion = $this->installedPackageResolver->resolvePackageVersion($packageName); + $isActive = $installedVersion !== null && Semver::satisfies($installedVersion, $constraint); + + $tableRows[] = [ + $this->printShortClassName($rector::class), + $packageName, + $constraint, + $installedVersion ?? '-', + $isActive ? 'yes' : 'no', + ]; + } } // sort by package name first, then by rule class diff --git a/src/VersionBonding/ComposerPackageConstraintFilter.php b/src/VersionBonding/ComposerPackageConstraintFilter.php index 54bb124b5ab..e5a4f05947e 100644 --- a/src/VersionBonding/ComposerPackageConstraintFilter.php +++ b/src/VersionBonding/ComposerPackageConstraintFilter.php @@ -43,14 +43,25 @@ public function filter(array $rectors): array private function satisfiesComposerPackageConstraint(ComposerPackageConstraintInterface $rector): bool { $composerPackageConstraint = $rector->provideComposerPackageConstraint(); - $packageVersion = $this->installedPackageResolver->resolvePackageVersion( - $composerPackageConstraint->getPackageName(), - ); + $composerPackageConstraints = is_array($composerPackageConstraint) + ? $composerPackageConstraint + : [$composerPackageConstraint]; - if ($packageVersion === null) { - return false; + // every constraint must be satisfied, so the rule fits every required package at once + foreach ($composerPackageConstraints as $composerPackageConstraint) { + $packageVersion = $this->installedPackageResolver->resolvePackageVersion( + $composerPackageConstraint->getPackageName(), + ); + + if ($packageVersion === null) { + return false; + } + + if (! Semver::satisfies($packageVersion, $composerPackageConstraint->getConstraint())) { + return false; + } } - return Semver::satisfies($packageVersion, $composerPackageConstraint->getConstraint()); + return true; } } diff --git a/src/VersionBonding/Contract/ComposerPackageConstraintInterface.php b/src/VersionBonding/Contract/ComposerPackageConstraintInterface.php index f591813e015..887e16afcaa 100644 --- a/src/VersionBonding/Contract/ComposerPackageConstraintInterface.php +++ b/src/VersionBonding/Contract/ComposerPackageConstraintInterface.php @@ -15,5 +15,11 @@ */ interface ComposerPackageConstraintInterface { - public function provideComposerPackageConstraint(): ComposerPackageConstraint; + /** + * Return a single constraint, or a list of constraints that must all be satisfied at once, + * e.g. an attribute that only works when both a library and its framework integration are new enough. + * + * @return ComposerPackageConstraint|list + */ + public function provideComposerPackageConstraint(): ComposerPackageConstraint|array; } diff --git a/tests/VersionBonding/ComposerPackageConstraintFilterTest.php b/tests/VersionBonding/ComposerPackageConstraintFilterTest.php index c762fa64e7b..1ac3a0477be 100644 --- a/tests/VersionBonding/ComposerPackageConstraintFilterTest.php +++ b/tests/VersionBonding/ComposerPackageConstraintFilterTest.php @@ -7,8 +7,10 @@ use PHPUnit\Framework\TestCase; use Rector\Composer\InstalledPackageResolver; use Rector\Tests\VersionBonding\Fixture\ComposerPackageConstraintRector; +use Rector\Tests\VersionBonding\Fixture\ComposerPackageConstraintsRector; use Rector\Tests\VersionBonding\Fixture\NoInterfaceRector; use Rector\VersionBonding\ComposerPackageConstraintFilter; +use Rector\VersionBonding\ValueObject\ComposerPackageConstraint; final class ComposerPackageConstraintFilterTest extends TestCase { @@ -71,4 +73,38 @@ public function testRectorWithLessThanConstraintExcludesNewerVersions(): void $this->assertCount(0, $filtered); } + + public function testRectorWithAllConstraintsSatisfiedIsIncluded(): void + { + $composerPackageConstraintsRector = new ComposerPackageConstraintsRector( + new ComposerPackageConstraint('nikic/php-parser', '>=4.0.0'), + new ComposerPackageConstraint('phpstan/phpstan', '>=1.0.0'), + ); + $filtered = $this->composerPackageConstraintFilter->filter([$composerPackageConstraintsRector]); + + $this->assertCount(1, $filtered); + $this->assertSame($composerPackageConstraintsRector, $filtered[0]); + } + + public function testRectorWithOneUnsatisfiedConstraintIsExcluded(): void + { + $composerPackageConstraintsRector = new ComposerPackageConstraintsRector( + new ComposerPackageConstraint('nikic/php-parser', '>=4.0.0'), + new ComposerPackageConstraint('nikic/php-parser', '>=999.0.0'), + ); + $filtered = $this->composerPackageConstraintFilter->filter([$composerPackageConstraintsRector]); + + $this->assertCount(0, $filtered); + } + + public function testRectorWithOneMissingPackageIsExcluded(): void + { + $composerPackageConstraintsRector = new ComposerPackageConstraintsRector( + new ComposerPackageConstraint('nikic/php-parser', '>=4.0.0'), + new ComposerPackageConstraint('non-existent/package', '>=1.0.0'), + ); + $filtered = $this->composerPackageConstraintFilter->filter([$composerPackageConstraintsRector]); + + $this->assertCount(0, $filtered); + } } diff --git a/tests/VersionBonding/Fixture/ComposerPackageConstraintsRector.php b/tests/VersionBonding/Fixture/ComposerPackageConstraintsRector.php new file mode 100644 index 00000000000..c075d8c2cd6 --- /dev/null +++ b/tests/VersionBonding/Fixture/ComposerPackageConstraintsRector.php @@ -0,0 +1,47 @@ +composerPackageConstraints = $composerPackageConstraints; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Test rector with multiple composer package constraints', []); + } + + public function getNodeTypes(): array + { + return [Node\Stmt\Class_::class]; + } + + public function refactor(Node $node): ?Node + { + return null; + } + + /** + * @return list + */ + public function provideComposerPackageConstraint(): array + { + return array_values($this->composerPackageConstraints); + } +}