diff --git a/src/Composer/InstalledPackageResolver.php b/src/Composer/InstalledPackageResolver.php index b92eed1cf99..9933b6b0c70 100644 --- a/src/Composer/InstalledPackageResolver.php +++ b/src/Composer/InstalledPackageResolver.php @@ -108,6 +108,7 @@ public function resolvePackageVersion(string $packageName): ?string private function createInstalledPackages(array $packages): array { $packageConstraints = $this->resolvePackageConstraints(); + $isLibrary = $this->isLibrary(); $installedPackages = []; foreach ($packages as $package) { @@ -116,9 +117,15 @@ private function createInstalledPackages(array $packages): array $constraint = $packageConstraints[$name] ?? null; if (is_string($constraint)) { - // the "installed.json" can be outdated, e.g. after a branch switch; - // in such case the "composer.json" constraint has a priority - $version = $this->matchConstraintVersion($version, $constraint) ?? $version; + if ($isLibrary) { + // a library must stay compatible with the lowest version it declares, + // regardless of which one happens to be installed locally + $version = $this->resolveConstraintLowestVersion($constraint) ?? $version; + } else { + // the "installed.json" can be outdated, e.g. after a branch switch; + // in such case the "composer.json" constraint has a priority + $version = $this->matchConstraintVersion($version, $constraint) ?? $version; + } } $installedPackages[$name] = new InstalledPackage($name, $version); @@ -127,6 +134,17 @@ private function createInstalledPackages(array $packages): array return $installedPackages; } + /** + * 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. + */ + private function isLibrary(): bool + { + $projectComposerJson = $this->loadProjectComposerJson(); + + return ($projectComposerJson['type'] ?? null) === 'library'; + } + /** * There is no vendor to read the installed versions from, so the constraints themselves are the only source * diff --git a/tests/Composer/Fixture/InstalledPackageResolver/library_composer_json/composer.json b/tests/Composer/Fixture/InstalledPackageResolver/library_composer_json/composer.json new file mode 100644 index 00000000000..7117b6da35a --- /dev/null +++ b/tests/Composer/Fixture/InstalledPackageResolver/library_composer_json/composer.json @@ -0,0 +1,10 @@ +{ + "type": "library", + "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/library_composer_json/vendor/composer/installed.json b/tests/Composer/Fixture/InstalledPackageResolver/library_composer_json/vendor/composer/installed.json new file mode 100644 index 00000000000..4c74a1f0641 --- /dev/null +++ b/tests/Composer/Fixture/InstalledPackageResolver/library_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 1a0e020f120..28795aaca62 100644 --- a/tests/Composer/InstalledPackageResolverTest.php +++ b/tests/Composer/InstalledPackageResolverTest.php @@ -47,6 +47,25 @@ public function testComposerJsonHasPriorityOverOutdatedInstalledJson(): void $this->assertSame('1.11.0.0', $installedPackageResolver->resolvePackageVersion('webmozart/assert')); } + public function testLibraryTargetsLowestDeclaredVersionEvenWhenInstalledSatisfies(): void + { + $installedPackageResolver = new InstalledPackageResolver( + __DIR__ . '/Fixture/InstalledPackageResolver/library_composer_json' + ); + + // 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')); + + // installed 7.2.0 satisfies "^7.0", still lowered to the declared floor + $this->assertSame('7.0.0.0', $installedPackageResolver->resolvePackageVersion('symfony/console')); + + // require-dev is respected as well + $this->assertSame('3.2.0.0', $installedPackageResolver->resolvePackageVersion('nette/utils')); + + // not required in the "composer.json", the installed version stands + $this->assertSame('1.11.0.0', $installedPackageResolver->resolvePackageVersion('webmozart/assert')); + } + public function testStandaloneComposerJsonResolvesVersionsWithoutVendor(): void { $installedPackageResolver = new InstalledPackageResolver(