diff --git a/src/PackageFilter.php b/src/PackageFilter.php index 2f2a10d6..e17cfc1b 100644 --- a/src/PackageFilter.php +++ b/src/PackageFilter.php @@ -31,6 +31,8 @@ class PackageFilter private $downloader; private $io; private $ignorePreleases; + private $notifiedRestriction = false; + private $warnedPackages = []; public function __construct(IOInterface $io, string $symfonyRequire, Downloader $downloader, bool $ignorePreleases = false) { @@ -91,13 +93,25 @@ public function removeLegacyPackages(array $data, RootPackageInterface $rootPack if ('symfony/symfony' !== $name && ( array_intersect($versions, $lockedVersions[$name] ?? []) || (($knownVersions ??= $this->getVersions()) && !isset($knownVersions['splits'][$name])) - || (isset($rootConstraints[$name]) && !Intervals::haveIntersections($this->symfonyConstraints, $rootConstraints[$name])) || ('symfony/psr-http-message-bridge' === $name && 6.4 > $versions[0]) )) { $filteredPackages[] = $package; continue; } + if ('symfony/symfony' !== $name && isset($rootConstraints[$name]) && !Intervals::haveIntersections($this->symfonyConstraints, $rootConstraints[$name])) { + // the root constraint wins when it has no intersection with the + // "symfony/*" one, e.g. to allow requiring a dev version; be + // loud about it as this also triggers when the root constraint + // simply misses the versions the user asked for + if (null !== $this->io && !isset($this->warnedPackages[$name])) { + $this->warnedPackages[$name] = true; + $this->io->writeError(\sprintf('Version constraint "%s" for "%s" has no intersection with "%s", keeping the package unrestricted', $rootConstraints[$name]->getPrettyString(), $name, $this->symfonyRequire)); + } + $filteredPackages[] = $package; + continue; + } + if (null !== $alias = $package->getExtra()['branch-alias'][$package->getVersion()] ?? null) { $versions[] = $this->versionParser->normalize($alias); } @@ -112,9 +126,9 @@ public function removeLegacyPackages(array $data, RootPackageInterface $rootPack if ('symfony/symfony' === $name) { $symfonyPackages[] = $package; - } elseif (null !== $this->io) { + } elseif (null !== $this->io && !$this->notifiedRestriction) { + $this->notifiedRestriction = true; $this->io->writeError(\sprintf('Restricting packages listed in "symfony/symfony" to "%s"', $this->symfonyRequire)); - $this->io = null; } } diff --git a/tests/PackageFilterTest.php b/tests/PackageFilterTest.php index 7809bc85..a437e5d1 100644 --- a/tests/PackageFilterTest.php +++ b/tests/PackageFilterTest.php @@ -11,6 +11,7 @@ namespace Symfony\Flex\Tests; +use Composer\IO\BufferIO; use Composer\IO\NullIO; use Composer\Package\CompletePackage; use Composer\Package\Link; @@ -209,6 +210,33 @@ public static function provideRemoveLegacyPackages() ]]]; } + public function testRootConstraintWithoutIntersectionIsPreservedButWarnedAbout() + { + $io = new BufferIO(); + $downloader = $this->createStub(Downloader::class); + $downloader + ->method('getVersions') + ->willReturn(['splits' => ['symfony/bar' => ['2.8', '3.0']]]); + $filter = new PackageFilter($io, '~2.8', $downloader); + + $l = new ArrayLoader(); + $packages = []; + foreach (['2.8.0', '3.0.0'] as $version) { + $packages[] = $l->load(['name' => 'symfony/bar', 'version' => $version], CompletePackage::class); + } + + $rootPackage = new RootPackage('test/test', '1.0.0.0', '1.0'); + $rootPackage->setRequires([ + 'symfony/bar' => new Link('__root__', 'symfony/bar', new Constraint('>=', '3.0.0.0'), Link::TYPE_REQUIRE, '>=3.0'), + ]); + + $this->assertSame($packages, $filter->removeLegacyPackages($packages, $rootPackage, [])); + + $output = $io->getOutput(); + $this->assertStringContainsString('Version constraint ">= 3.0.0.0" for "symfony/bar" has no intersection with "~2.8", keeping the package unrestricted', $output); + $this->assertSame(1, substr_count($output, 'no intersection'), 'the warning should be emitted once per package, not once per version'); + } + public function testIgnorePreleases() { $io = new NullIO();