Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Composer/InstalledPackageResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

@samsonasik samsonasik Aug 22, 2026

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 additional requirement check is needed, when the package has php version range, eg: has

"php": "^8.1 || ^8.2"

Then should follow lowest.

That will usually verify that multiple major version of requirment test eg: phpunit 10 or 11 n CI is needed.

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.

This is already handled by PHP version and the min php version interface. Different area.

}

/**
Expand Down
12 changes: 11 additions & 1 deletion src/Config/RectorConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
33 changes: 29 additions & 4 deletions tests/Composer/InstalledPackageResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'));
Expand All @@ -66,6 +66,31 @@ public function testLibraryTargetsLowestDeclaredVersionEvenWhenInstalledSatisfie
$this->assertSame('1.11.0.0', $installedPackageResolver->resolvePackageVersion('webmozart/assert'));
}

/**
* @return iterable<string, array{string}>
*/
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(
Expand Down
Loading