Composer-based sets currently select version-specific rules primarily from the versions in vendor/composer/installed.json. This works well for applications, where the locked and installed dependency versions represent the actual deployment target. It does not work reliably for libraries, where the supported dependency range declared in composer.json is the compatibility contract.
Consider a library with:
{
"name": "acme/library",
"type": "library",
"require-dev": {
"phpunit/phpunit": "^10.5 || ^11.0 || ^12.0 || ^13.0",
"rector/rector": "^2.6"
}
}
and:
return RectorConfig::configure()
->withComposerBased(phpunit: true);
A normal composer update may install PHPUnit 13. Composer will then activate Rector rules targeting PHPUnit 11, 12, and 13, potentially producing code that no longer works with PHPUnit 10.5 even though the library still declares that version as supported.
Running the same library through:
composer update --prefer-lowest
installs PHPUnit 10.5 and causes Rector to activate a different set of rules. Rector’s output therefore depends on how dependencies happened to be installed, despite an identical composer.json and identical source code.
For a library, automated refactoring must preserve compatibility with every declared dependency version. At minimum, it must target the lowest supported version rather than the newest locally installed one. Otherwise, running Rector locally can silently generate code that later fails the library’s --prefer-lowest CI job.
Instead, Composer-based sets should derive their effective target versions from the constraints in the root composer.json. For rules with monotonically increasing package requirements, this means using the lowest version permitted by the declared constraint.
In the example above, PHPUnit-specific rules should consistently target PHPUnit 10.5 regardless of whether the local installation currently contains PHPUnit 10.5 or PHPUnit 13.
This would make Rector output deterministic across:
composer update
composer update --prefer-lowest
composer install
and would preserve the compatibility contract declared by libraries.
If applications need installed-version-based behavior, that could remain available as an explicit mode. For example:
$rectorConfig->withComposerBased(
phpunit: true,
versionSource: ComposerVersionSource::Installed,
);
while constraint-based resolution would use the declared compatibility target:
$rectorConfig->withComposerBased(
phpunit: true,
versionSource: ComposerVersionSource::LowestDeclared,
);
The important part is that CI tooling must be able to select composer-based rules from the declared constraints without modifying installed.json, synthesizing a separate installation, or relying on Rector internals.
What do you think`
Composer-based sets currently select version-specific rules primarily from the versions in
vendor/composer/installed.json. This works well for applications, where the locked and installed dependency versions represent the actual deployment target. It does not work reliably for libraries, where the supported dependency range declared in composer.json is the compatibility contract.Consider a library with:
{ "name": "acme/library", "type": "library", "require-dev": { "phpunit/phpunit": "^10.5 || ^11.0 || ^12.0 || ^13.0", "rector/rector": "^2.6" } }and:
A normal
composer updatemay install PHPUnit 13. Composer will then activate Rector rules targeting PHPUnit 11, 12, and 13, potentially producing code that no longer works with PHPUnit 10.5 even though the library still declares that version as supported.Running the same library through:
installs PHPUnit 10.5 and causes Rector to activate a different set of rules. Rector’s output therefore depends on how dependencies happened to be installed, despite an identical
composer.jsonand identical source code.For a library, automated refactoring must preserve compatibility with every declared dependency version. At minimum, it must target the lowest supported version rather than the newest locally installed one. Otherwise, running Rector locally can silently generate code that later fails the library’s
--prefer-lowestCI job.Instead, Composer-based sets should derive their effective target versions from the constraints in the root
composer.json. For rules with monotonically increasing package requirements, this means using the lowest version permitted by the declared constraint.In the example above, PHPUnit-specific rules should consistently target PHPUnit 10.5 regardless of whether the local installation currently contains PHPUnit 10.5 or PHPUnit 13.
This would make Rector output deterministic across:
and would preserve the compatibility contract declared by libraries.
If applications need installed-version-based behavior, that could remain available as an explicit mode. For example:
while constraint-based resolution would use the declared compatibility target:
The important part is that CI tooling must be able to select composer-based rules from the declared constraints without modifying
installed.json, synthesizing a separate installation, or relying on Rector internals.What do you think`