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
62 changes: 59 additions & 3 deletions src/Composer/InstalledPackageResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,32 @@ final class InstalledPackageResolver

private readonly string $projectDirectory;

public function __construct(?string $projectDirectory = null)
{
/**
* @param null|string $composerJsonFilePath a standalone "composer.json" to read the versions from, instead of the
* installed packages; used to test rules bonded to a composer package
*/
public function __construct(
?string $projectDirectory = null,
private ?string $composerJsonFilePath = null
) {
// fallback to root project directory
$this->projectDirectory = $projectDirectory ?? (string) getcwd();

Assert::directory($this->projectDirectory);
}

/**
* @api used in tests to resolve the versions from a standalone "composer.json"
*/
public function changeComposerJsonFilePath(?string $composerJsonFilePath): void
{
$this->composerJsonFilePath = $composerJsonFilePath;

// the previous file is no longer the source, drop what was read from it
$this->resolvedInstalledPackages = null;
$this->projectComposerJson = null;
}

/**
* @return array<string, InstalledPackage>
*/
Expand All @@ -51,6 +69,10 @@ public function resolve(): array
return $this->resolvedInstalledPackages;
}

if ($this->composerJsonFilePath !== null) {
return $this->resolvedInstalledPackages = $this->createPackagesFromConstraints();
}

$installedPackagesFilePath = $this->resolveVendorDir() . '/composer/installed.json';
if (! file_exists($installedPackagesFilePath)) {
throw new ShouldNotHappenException(
Expand Down Expand Up @@ -105,6 +127,27 @@ private function createInstalledPackages(array $packages): array
return $installedPackages;
}

/**
* There is no vendor to read the installed versions from, so the constraints themselves are the only source
*
* @return array<string, InstalledPackage>
*/
private function createPackagesFromConstraints(): array
{
$installedPackages = [];

foreach ($this->resolvePackageConstraints() as $packageName => $constraint) {
$version = $this->resolveConstraintLowestVersion($constraint);
if ($version === null) {
continue;
}

$installedPackages[$packageName] = new InstalledPackage($packageName, $version);
}

return $installedPackages;
}

/**
* @return null|string the lowest version allowed by the constraint, if the installed version is out of it
*/
Expand All @@ -114,7 +157,20 @@ private function matchConstraintVersion(string $installedVersion, string $constr
if (Semver::satisfies($installedVersion, $constraint)) {
return null;
}
} catch (UnexpectedValueException) {
// non-comparable version or constraint, e.g. a dev one
return null;
}

return $this->resolveConstraintLowestVersion($constraint);
}

/**
* @return null|string the lowest version the constraint allows, null if there is no comparable one
*/
private function resolveConstraintLowestVersion(string $constraint): ?string
{
try {
$lowestVersion = new VersionParser()
->parseConstraints($constraint)
->getLowerBound()
Expand Down Expand Up @@ -171,7 +227,7 @@ private function loadProjectComposerJson(): array
return $this->projectComposerJson;
}

$projectComposerJsonFilePath = $this->projectDirectory . '/composer.json';
$projectComposerJsonFilePath = $this->composerJsonFilePath ?? $this->projectDirectory . '/composer.json';
if (! file_exists($projectComposerJsonFilePath)) {
return $this->projectComposerJson = [];
}
Expand Down
23 changes: 23 additions & 0 deletions src/Testing/PHPUnit/AbstractRectorTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Rector\Application\ApplicationFileProcessor;
use Rector\Autoloading\AdditionalAutoloader;
use Rector\Autoloading\BootstrapFilesIncluder;
use Rector\Composer\InstalledPackageResolver;
use Rector\Configuration\ConfigurationFactory;
use Rector\Configuration\Option;
use Rector\Configuration\Parameter\SimpleParameterProvider;
Expand Down Expand Up @@ -77,6 +78,16 @@ protected function setUp(): void
$cacheKey = sha1($configFile . static::class);

if (! isset(self::$cacheByRuleAndConfig[$cacheKey])) {
// rules bonded to a composer package are filtered out unless the package is installed.
// the composer package constraint filter keeps the very first resolver it is given, so the binding must
// stay untouched; only the file it reads changes, on every test case, so it never leaks to the next one
if (! $rectorConfig->bound(InstalledPackageResolver::class)) {
$rectorConfig->singleton(InstalledPackageResolver::class);
}

$rectorConfig->make(InstalledPackageResolver::class)
->changeComposerJsonFilePath($this->provideComposerJsonFilePath());

// reset
/** @var RewindableGenerator<int, ResettableInterface> $resettables */
$resettables = $rectorConfig->tagged(ResettableInterface::class);
Expand Down Expand Up @@ -133,6 +144,18 @@ protected static function yieldFilesFromDirectory(string $directory, string $suf
return FixtureFileFinder::yieldDirectory($directory, $suffix);
}

/**
* Override to test a rule that implements @see \Rector\VersionBonding\Contract\ComposerPackageConstraintInterface
* against a package that is not installed. The versions are read from the "require" and "require-dev" sections of
* the provided "composer.json", instead of the installed packages.
*
* @api used by extensions
*/
protected function provideComposerJsonFilePath(): ?string
{
return null;
}

protected function doTestFile(string $fixtureFilePath, bool $includeFixtureDirectoryAsSource = false): void
{
// prepare input file contents and expected file output contents
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"require": {
"symfony/framework-bundle": "^2.6",
"nette/utils": ">=3.2",
"webmozart/assert": "*"
},
"require-dev": {
"phpunit/phpunit": "^10.5"
}
}
34 changes: 34 additions & 0 deletions tests/Composer/InstalledPackageResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,38 @@ public function testComposerJsonHasPriorityOverOutdatedInstalledJson(): void
// not required in the "composer.json" at all
$this->assertSame('1.11.0.0', $installedPackageResolver->resolvePackageVersion('webmozart/assert'));
}

public function testStandaloneComposerJsonResolvesVersionsWithoutVendor(): void
{
$installedPackageResolver = new InstalledPackageResolver(
null,
__DIR__ . '/Fixture/InstalledPackageResolver/standalone_composer_json/composer.json'
);

// the package is not installed anywhere, the constraint is the only source
$this->assertSame('2.6.0.0', $installedPackageResolver->resolvePackageVersion('symfony/framework-bundle'));

// require-dev is respected as well
$this->assertSame('10.5.0.0', $installedPackageResolver->resolvePackageVersion('phpunit/phpunit'));

// an open constraint has no lowest version to fall back to
$this->assertNull($installedPackageResolver->resolvePackageVersion('webmozart/assert'));

// not required in the "composer.json" at all
$this->assertNull($installedPackageResolver->resolvePackageVersion('symfony/console'));
}

public function testChangeComposerJsonFilePathDropsPreviousVersions(): void
{
$installedPackageResolver = new InstalledPackageResolver(getcwd());
$this->assertNull($installedPackageResolver->resolvePackageVersion('symfony/framework-bundle'));

$installedPackageResolver->changeComposerJsonFilePath(
__DIR__ . '/Fixture/InstalledPackageResolver/standalone_composer_json/composer.json'
);
$this->assertSame('2.6.0.0', $installedPackageResolver->resolvePackageVersion('symfony/framework-bundle'));

$installedPackageResolver->changeComposerJsonFilePath(null);
$this->assertNull($installedPackageResolver->resolvePackageVersion('symfony/framework-bundle'));
}
}
Loading