From 73537e77148f8b4cac9e1c159af02ea07f4827c9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 30 Oct 2025 13:52:52 +0100 Subject: [PATCH] Fix ignoring unstable releases with --prefer-lowest and --prefer-stable --- src/Flex.php | 23 ++++++---------- src/PackageFilter.php | 16 ++++++++++-- tests/PackageFilterTest.php | 52 +++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/Flex.php b/src/Flex.php index cff411a2..5bfa0cf1 100644 --- a/src/Flex.php +++ b/src/Flex.php @@ -31,7 +31,6 @@ use Composer\IO\NullIO; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; -use Composer\Package\BasePackage; use Composer\Package\Locker; use Composer\Package\Package; use Composer\Plugin\PluginEvents; @@ -77,6 +76,7 @@ class Flex implements PluginInterface, EventSubscriberInterface private $operations = []; private $lock; private $displayThanksReminder = 0; + private $ignoreUnstableReleases = false; private $reinstall; private static $activated = true; private static $aliasResolveCommands = [ @@ -126,16 +126,8 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__) Flex::$storedOperations = []; } - $symfonyRequire = preg_replace('/\.x$/', '.x-dev', getenv('SYMFONY_REQUIRE') ?: ($composer->getPackage()->getExtra()['symfony']['require'] ?? '')); - $rfs = $composer->getLoop()->getHttpDownloader(); - $this->downloader = $downloader = new Downloader($composer, $io, $rfs); - - if ($symfonyRequire) { - $this->filter = new PackageFilter($io, $symfonyRequire, $this->downloader); - } - $this->configurator = new Configurator($composer, $io, $this->options); $disable = true; @@ -190,12 +182,7 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__) } } - if ($input->hasParameterOption('--prefer-lowest', true)) { - // When prefer-lowest is set and no stable version has been released, - // we consider "dev" more stable than "alpha", "beta" or "RC". This - // allows testing lowest versions with potential fixes applied. - BasePackage::$stabilities['dev'] = 1 + BasePackage::STABILITY_STABLE; - } + $this->ignoreUnstableReleases = $input->hasParameterOption('--prefer-lowest', true) && $input->hasParameterOption('--prefer-stable', true); $addCommand = 'add'.(method_exists($app, 'addCommand') ? 'Command' : ''); $app->$addCommand(new Command\RecipesCommand($this, $this->lock, $rfs)); @@ -205,6 +192,12 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__) break; } + + $symfonyRequire = preg_replace('/\.x$/', '.x-dev', getenv('SYMFONY_REQUIRE') ?: ($composer->getPackage()->getExtra()['symfony']['require'] ?? '')); + + if ($symfonyRequire || $this->ignoreUnstableReleases) { + $this->filter = new PackageFilter($io, $symfonyRequire, $this->downloader, $this->ignoreUnstableReleases); + } } /** diff --git a/src/PackageFilter.php b/src/PackageFilter.php index 96144b68..9ad2de5c 100644 --- a/src/PackageFilter.php +++ b/src/PackageFilter.php @@ -30,14 +30,16 @@ class PackageFilter private $symfonyConstraints; private $downloader; private $io; + private $ignoreUnstableReleases; - public function __construct(IOInterface $io, string $symfonyRequire, Downloader $downloader) + public function __construct(IOInterface $io, string $symfonyRequire, Downloader $downloader, bool $ignoreUnstableReleases = false) { $this->versionParser = new VersionParser(); $this->symfonyRequire = $symfonyRequire; - $this->symfonyConstraints = $this->versionParser->parseConstraints($symfonyRequire); + $this->symfonyConstraints = '' !== $symfonyRequire ? $this->versionParser->parseConstraints($symfonyRequire) : null; $this->downloader = $downloader; $this->io = $io; + $this->ignoreUnstableReleases = $ignoreUnstableReleases; } /** @@ -48,6 +50,16 @@ public function __construct(IOInterface $io, string $symfonyRequire, Downloader */ public function removeLegacyPackages(array $data, RootPackageInterface $rootPackage, array $lockedPackages): array { + if ($this->ignoreUnstableReleases) { + $filteredPackages = []; + foreach ($data as $package) { + if (\in_array($package->getStability(), ['stable', 'dev'], true)) { + $filteredPackages[] = $package; + } + } + $data = $filteredPackages; + } + if (!$this->symfonyConstraints || !$data) { return $data; } diff --git a/tests/PackageFilterTest.php b/tests/PackageFilterTest.php index 5793c6bf..062ab337 100644 --- a/tests/PackageFilterTest.php +++ b/tests/PackageFilterTest.php @@ -17,8 +17,10 @@ use Composer\Package\Loader\ArrayLoader; use Composer\Package\PackageInterface; use Composer\Package\RootPackage; +use Composer\Package\RootPackageInterface; use Composer\Semver\Constraint\Constraint; use PHPUnit\Framework\TestCase; +use Symfony\Flex\Downloader; use Symfony\Flex\PackageFilter; /** @@ -207,4 +209,54 @@ public function provideRemoveLegacyPackages() 'symfony/bar' => ['2.8', '3.0'], ]]]; } + + public function testIgnoreUnstableReleasesFiltersPreReleases() + { + $io = new NullIO(); + $downloader = $this->getMockBuilder(Downloader::class)->disableOriginalConstructor()->getMock(); + $filter = new PackageFilter($io, '', $downloader, true); + + $stablePkg = $this->createPackageMock('pkg/stable', 'stable'); + $devPkg = $this->createPackageMock('pkg/dev', 'dev'); + $alphaPkg = $this->createPackageMock('pkg/alpha', 'alpha'); + $betaPkg = $this->createPackageMock('pkg/beta', 'beta'); + $rcPkg = $this->createPackageMock('pkg/rc', 'RC'); + + $root = $this->getMockBuilder(RootPackageInterface::class)->disableOriginalConstructor()->getMock(); + + $result = $filter->removeLegacyPackages([$stablePkg, $devPkg, $alphaPkg, $betaPkg, $rcPkg], $root, []); + + $this->assertSame([$stablePkg, $devPkg], $result); + } + + public function testWithoutIgnoreUnstableReleasesKeepsAll() + { + $io = new NullIO(); + $downloader = $this->getMockBuilder(Downloader::class)->disableOriginalConstructor()->getMock(); + $filter = new PackageFilter($io, '', $downloader, false); + + $packages = [ + $this->createPackageMock('pkg/stable', 'stable'), + $this->createPackageMock('pkg/dev', 'dev'), + $this->createPackageMock('pkg/alpha', 'alpha'), + $this->createPackageMock('pkg/beta', 'beta'), + $this->createPackageMock('pkg/rc', 'RC'), + ]; + + $root = $this->getMockBuilder(RootPackageInterface::class)->disableOriginalConstructor()->getMock(); + + $result = $filter->removeLegacyPackages($packages, $root, []); + + $this->assertSame($packages, $result); + } + + private function createPackageMock(string $name, string $stability): PackageInterface + { + $package = $this->getMockBuilder(PackageInterface::class)->getMock(); + $package->method('getName')->willReturn($name); + $package->method('getVersion')->willReturn('1.0.0'); + $package->method('getStability')->willReturn($stability); + + return $package; + } }