Skip to content

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

Merged
TomasVotruba merged 1 commit into
mainfrom
feature-composer-library-lowest-version
Aug 22, 2026
Merged

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

Conversation

@TomasVotruba

@TomasVotruba TomasVotruba commented Aug 21, 2026

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.

…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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The goal is to give projects real latest versions (those in installed.json),
and to give packagest the lowest safest ones. As ^7.3 can be 7.3.0 on a library, but 7.3.50 on final real project

@TomasVotruba

Copy link
Copy Markdown
Member Author

Lets give it a go 👍

@TomasVotruba
TomasVotruba merged commit 60ee400 into main Aug 22, 2026
51 checks passed
@TomasVotruba
TomasVotruba deleted the feature-composer-library-lowest-version branch August 22, 2026 10:38
{
$projectComposerJson = $this->loadProjectComposerJson();

return ($projectComposerJson['type'] ?? null) === 'library';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is wrong. It should be inverted to isProject() and compared to project. Otherwise you exclude symfony-bundle etc. pp. :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

"type" may be missing, most likely in projects, so we have to go with a default here.
Could you elaborate a bit more?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think we should consider something a project if type equals project. Everything else is not a project and thus should be considered a "library".

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Or in other words: the current solution does not fix the original issue. I have a type symfony-bundle and thus I still get the wrong rules applied.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I get your point, but if we do that, all projects will be upgraded only when someone fill the keyword explicitly in composer.json.

If a keyword is an issue, then other non-project keywords should be included as well.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Personally, I'm convinced there should be no check at all and the composer.json should always win. I always want to have to rules applied to "what is compatible", never ever to "what is installed".

@TomasVotruba TomasVotruba Aug 22, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's why composer.lock/installed.json is present. When project uses ^7.4, they rarely want to use only featuers in 7.4.0. Which is version mostly skipped for being to risky to use.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

That's exactly the point. If they specify ^7.4, the code must be compatible with 7.4.0. So the composer.json decides, no matter if your installed version is 7.4, 8.0 or 8.5.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I agree with @Toflar here. composer.json should always be leading, as that signals which what versions the project is compatible with. Installed versions can always be downgraded in a later stage (if another dependency adds an additional constraint for example), but will never be downgraded beyond the given range.

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.

4 participants