diff --git a/src/Composer/InstalledPackageResolver.php b/src/Composer/InstalledPackageResolver.php index 9933b6b0c70..c401866de8e 100644 --- a/src/Composer/InstalledPackageResolver.php +++ b/src/Composer/InstalledPackageResolver.php @@ -137,12 +137,16 @@ private function createInstalledPackages(array $packages): array /** * A library declares a compatibility range in its "composer.json"; the version-specific rules must target the * lowest declared version, not the one that happens to be installed locally. + * + * Anything but an application counts as a library: a missing "type" or an explicit "project" is an application, + * every other declared type ("library", "symfony-bundle", ...) is a distributed package. */ private function isLibrary(): bool { $projectComposerJson = $this->loadProjectComposerJson(); + $type = $projectComposerJson['type'] ?? null; - return ($projectComposerJson['type'] ?? null) === 'library'; + return is_string($type) && $type !== 'project'; } /** diff --git a/src/Config/RectorConfig.php b/src/Config/RectorConfig.php index 8ff649c093f..5fdeb2c7251 100644 --- a/src/Config/RectorConfig.php +++ b/src/Config/RectorConfig.php @@ -55,6 +55,9 @@ final class RectorConfig extends Container */ private array $autotagInterfaces = [Command::class, ResettableInterface::class]; + /** + * Optional override, e.g. injected by a test to read the versions from a standalone "composer.json" + */ private ?InstalledPackageResolver $installedPackageResolver = null; private static ?bool $recreated = null; @@ -570,7 +573,14 @@ public function setOverflowLevels(array $levelOverflows): void private function resolveInstalledPackageVersion(string $packageName): ?string { - $this->installedPackageResolver ??= new InstalledPackageResolver(); + // an explicitly injected resolver wins, e.g. a test pointing at a standalone "composer.json" + if (! $this->installedPackageResolver instanceof InstalledPackageResolver) { + // otherwise reuse the container-bound resolver, so a test-provided "composer.json" (via + // AbstractRectorTestCase::provideComposerJsonFilePath()) drives the version, not the project root + $this->installedPackageResolver = $this->bound(InstalledPackageResolver::class) + ? $this->make(InstalledPackageResolver::class) + : new InstalledPackageResolver(); + } return $this->installedPackageResolver->resolvePackageVersion($packageName); } diff --git a/tests/Composer/Fixture/InstalledPackageResolver/project_composer_json/composer.json b/tests/Composer/Fixture/InstalledPackageResolver/project_composer_json/composer.json new file mode 100644 index 00000000000..da33ee8f151 --- /dev/null +++ b/tests/Composer/Fixture/InstalledPackageResolver/project_composer_json/composer.json @@ -0,0 +1,10 @@ +{ + "type": "project", + "require": { + "phpunit/phpunit": "^10.5 || ^11.0 || ^12.0", + "symfony/console": "^7.0" + }, + "require-dev": { + "nette/utils": "^3.2" + } +} diff --git a/tests/Composer/Fixture/InstalledPackageResolver/project_composer_json/vendor/composer/installed.json b/tests/Composer/Fixture/InstalledPackageResolver/project_composer_json/vendor/composer/installed.json new file mode 100644 index 00000000000..4c74a1f0641 --- /dev/null +++ b/tests/Composer/Fixture/InstalledPackageResolver/project_composer_json/vendor/composer/installed.json @@ -0,0 +1,24 @@ +{ + "packages": [ + { + "name": "phpunit/phpunit", + "version": "12.1.0", + "version_normalized": "12.1.0.0" + }, + { + "name": "symfony/console", + "version": "v7.2.0", + "version_normalized": "7.2.0.0" + }, + { + "name": "nette/utils", + "version": "v3.2.0", + "version_normalized": "3.2.0.0" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "version_normalized": "1.11.0.0" + } + ] +} diff --git a/tests/Composer/Fixture/InstalledPackageResolver/symfony_bundle_composer_json/composer.json b/tests/Composer/Fixture/InstalledPackageResolver/symfony_bundle_composer_json/composer.json new file mode 100644 index 00000000000..7685a442ef4 --- /dev/null +++ b/tests/Composer/Fixture/InstalledPackageResolver/symfony_bundle_composer_json/composer.json @@ -0,0 +1,10 @@ +{ + "type": "symfony-bundle", + "require": { + "phpunit/phpunit": "^10.5 || ^11.0 || ^12.0", + "symfony/console": "^7.0" + }, + "require-dev": { + "nette/utils": "^3.2" + } +} diff --git a/tests/Composer/Fixture/InstalledPackageResolver/symfony_bundle_composer_json/vendor/composer/installed.json b/tests/Composer/Fixture/InstalledPackageResolver/symfony_bundle_composer_json/vendor/composer/installed.json new file mode 100644 index 00000000000..4c74a1f0641 --- /dev/null +++ b/tests/Composer/Fixture/InstalledPackageResolver/symfony_bundle_composer_json/vendor/composer/installed.json @@ -0,0 +1,24 @@ +{ + "packages": [ + { + "name": "phpunit/phpunit", + "version": "12.1.0", + "version_normalized": "12.1.0.0" + }, + { + "name": "symfony/console", + "version": "v7.2.0", + "version_normalized": "7.2.0.0" + }, + { + "name": "nette/utils", + "version": "v3.2.0", + "version_normalized": "3.2.0.0" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "version_normalized": "1.11.0.0" + } + ] +} diff --git a/tests/Composer/InstalledPackageResolverTest.php b/tests/Composer/InstalledPackageResolverTest.php index 28795aaca62..6ce3f23441a 100644 --- a/tests/Composer/InstalledPackageResolverTest.php +++ b/tests/Composer/InstalledPackageResolverTest.php @@ -4,6 +4,7 @@ namespace Rector\Tests\Composer; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Rector\Composer\InstalledPackageResolver; use Rector\Composer\ValueObject\InstalledPackage; @@ -47,11 +48,10 @@ public function testComposerJsonHasPriorityOverOutdatedInstalledJson(): void $this->assertSame('1.11.0.0', $installedPackageResolver->resolvePackageVersion('webmozart/assert')); } - public function testLibraryTargetsLowestDeclaredVersionEvenWhenInstalledSatisfies(): void + #[DataProvider('provideLibraryTypeFixtureDirectory')] + public function testLibraryTargetsLowestDeclaredVersionEvenWhenInstalledSatisfies(string $fixtureDirectory): void { - $installedPackageResolver = new InstalledPackageResolver( - __DIR__ . '/Fixture/InstalledPackageResolver/library_composer_json' - ); + $installedPackageResolver = new InstalledPackageResolver($fixtureDirectory); // installed 12.1.0 satisfies "^10.5 || ^11.0 || ^12.0", yet a library targets the lowest declared version $this->assertSame('10.5.0.0', $installedPackageResolver->resolvePackageVersion('phpunit/phpunit')); @@ -66,6 +66,31 @@ public function testLibraryTargetsLowestDeclaredVersionEvenWhenInstalledSatisfie $this->assertSame('1.11.0.0', $installedPackageResolver->resolvePackageVersion('webmozart/assert')); } + /** + * @return iterable + */ + public static function provideLibraryTypeFixtureDirectory(): iterable + { + // "library" is the default distributed type + yield 'library' => [__DIR__ . '/Fixture/InstalledPackageResolver/library_composer_json']; + + // any other non-"project" distributed type behaves the same way + yield 'symfony-bundle' => [__DIR__ . '/Fixture/InstalledPackageResolver/symfony_bundle_composer_json']; + } + + public function testProjectTypeKeepsInstalledVersionOverDeclaredRange(): void + { + $installedPackageResolver = new InstalledPackageResolver( + __DIR__ . '/Fixture/InstalledPackageResolver/project_composer_json' + ); + + // an explicit "project" is an application, so the installed version stands even within a wider range + $this->assertSame('12.1.0.0', $installedPackageResolver->resolvePackageVersion('phpunit/phpunit')); + + // installed 7.2.0 satisfies "^7.0", kept as installed + $this->assertSame('7.2.0.0', $installedPackageResolver->resolvePackageVersion('symfony/console')); + } + public function testStandaloneComposerJsonResolvesVersionsWithoutVendor(): void { $installedPackageResolver = new InstalledPackageResolver(