Skip to content

[Composer] Target lowest declared version for libraries in composer-based sets - #8359

Open
TomasVotruba wants to merge 1 commit into
mainfrom
feature-composer-library-lowest-version
Open

[Composer] Target lowest declared version for libraries in composer-based sets#8359
TomasVotruba wants to merge 1 commit into
mainfrom
feature-composer-library-lowest-version

Conversation

@TomasVotruba

Copy link
Copy Markdown
Member

Fixes deterministic version targeting for libraries in composer-based sets. Refs rectorphp/rector#9858.

Problem

withComposerBased() rules gate on the locally installed package version (vendor/composer/installed.json). That is right for applications, wrong for libraries.

A library declares a compatibility range in composer.json:

{
    "type": "library",
    "require": {
        "phpunit/phpunit": "^10.5 || ^11.0 || ^12.0"
    }
}

With PHPUnit 12 installed locally, Rector emitted 12-only code and silently broke the declared support for 10.5. Output changed between composer install, composer update, and composer update --prefer-lowest for identical source.

Fix

When composer.json has "type": "library", derive the target version from the lowest declared constraint instead of the installed version:

 $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;
+    }
 }

Applications (no type, or type other than library) keep the current installed-version behavior. The detection is explicit: only "type": "library" opts in.

Effect

composer.json installed before after
app, phpunit ^10.5 || ^11 || ^12 12.1 12.1 12.1
library, phpunit ^10.5 || ^11 || ^12 12.1 12.1 10.5
library, symfony/console ^7.0 7.2 7.2 7.0

Output is now deterministic for libraries across install / update / --prefer-lowest.

…ased sets

A library declares a compatibility range in composer.json (e.g. "^10.5 || ^11.0 || ^12.0"). Composer-based rule filtering used the locally installed version, so an installed PHPUnit 12 made Rector emit 12-only code and silently break the declared support for 10.5.

When composer.json has "type": "library", derive the target version from the lowest declared constraint instead of the installed one, making the output deterministic across composer install/update and --prefer-lowest.

Refs rectorphp/rector#9858
private function createInstalledPackages(array $packages): array
{
$packageConstraints = $this->resolvePackageConstraints();
$isLibrary = $this->isLibrary();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think library check is not needed, even on project, eg on "framework skeleton", phpunit range may exists to give user ability to use phpunit version based on specific php version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants