Implemented PoolOptimizer

main
Yanick Witschi 4 years ago committed by Jordi Boggiano
parent 7eca450d9b
commit 34183f49f9
No known key found for this signature in database
GPG Key ID: 7BBD42C429EC80BC

@ -11,7 +11,6 @@ on:
env:
COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist"
COMPOSER_UPDATE_FLAGS: ""
COMPOSER_TESTS_ARE_RUNNING: "1"
jobs:
tests:

@ -353,3 +353,27 @@ See also https://github.com/composer/composer/issues/4180 for more information.
Composer can unpack zipballs using either a system-provided `unzip` or `7z` (7-Zip) utility, or PHP's
native `ZipArchive` class. On OSes where ZIP files can contain permissions and symlinks, we recommend
installing `unzip` or `7z` as these features are not supported by `ZipArchive`.
## Disabling the pool optimizer
In Composer, the `Pool` class contains all the packages that are relevant for the dependency
resolving process. That is what is used to generate all the rules which are then
passed on to the dependency solver.
In order to improve performance, Composer tries to optimize this `Pool` by removing useless
package information early on.
If all goes well, you should never notice any issues with it but in case you run into
an unexpected result such as an unresolvable set of dependencies or conflicts where you
think Composer is wrong, you might want to disable the optimizer by using the environment
variable `COMPOSER_POOL_OPTIMIZER` and run the update again like so:
```bash
COMPOSER_POOL_OPTIMIZER=0 php composer.phar update
```
Now double check if the result is still the same. It will take significantly longer and use
a lot more memory to run the dependency resolving process.
If the result is different, you likely hit a problem in the pool optimizer.
Please [report this issue](https://github.com/composer/composer/issues) so it can be fixed.

@ -221,6 +221,14 @@ class Pool implements \Countable
return \in_array($package, $this->unacceptableFixedOrLockedPackages, true);
}
/**
* @return BasePackage[]
*/
public function getUnacceptableFixedOrLockedPackages()
{
return $this->unacceptableFixedOrLockedPackages;
}
public function __toString()
{
$str = "Pool:\n";

@ -61,6 +61,10 @@ class PoolBuilder
* @var ?EventDispatcher
*/
private $eventDispatcher;
/**
* @var PoolOptimizer|null
*/
private $poolOptimizer;
/**
* @var IOInterface
*/
@ -128,13 +132,14 @@ class PoolBuilder
* @param string[] $rootReferences an array of package name => source reference
* @phpstan-param array<string, string> $rootReferences
*/
public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null)
public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null, PoolOptimizer $poolOptimizer = null)
{
$this->acceptableStabilities = $acceptableStabilities;
$this->stabilityFlags = $stabilityFlags;
$this->rootAliases = $rootAliases;
$this->rootReferences = $rootReferences;
$this->eventDispatcher = $eventDispatcher;
$this->poolOptimizer = $poolOptimizer;
$this->io = $io;
}
@ -259,6 +264,8 @@ class PoolBuilder
$this->skippedLoad = array();
$this->indexCounter = 0;
$pool = $this->runOptimizer($request, $pool);
Intervals::clear();
return $pool;
@ -572,4 +579,33 @@ class PoolBuilder
unset($this->aliasMap[spl_object_hash($package)]);
}
}
/**
* @return Pool
*/
private function runOptimizer(Request $request, Pool $pool)
{
if (null === $this->poolOptimizer) {
return $pool;
}
$total = \count($pool->getPackages());
$pool = $this->poolOptimizer->optimize($request, $pool);
$filtered = $total - \count($pool->getPackages());
if (0 === $filtered) {
return $pool;
}
$this->io->write(sprintf(
'<info>Found %s package versions referenced in your dependency graph. %s (%d%%) were optimized away.</info>',
number_format($total),
number_format($filtered),
round(100/$total*$filtered)
), true, IOInterface::VERY_VERBOSE);
return $pool;
}
}

@ -0,0 +1,345 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\DependencyResolver;
use Composer\Package\AliasPackage;
use Composer\Package\BasePackage;
use Composer\Semver\CompilingMatcher;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\Semver\Intervals;
/**
* Optimizes a given pool
*
* @author Yanick Witschi <yanick.witschi@terminal42.ch>
*/
class PoolOptimizer
{
/**
* @var PolicyInterface
*/
private $policy;
/**
* @var array<int, true>
*/
private $irremovablePackages = array();
/**
* @var array<string, array<string, ConstraintInterface>>
*/
private $requireConstraintsPerPackage = array();
/**
* @var array<string, array<string, ConstraintInterface>>
*/
private $conflictConstraintsPerPackage = array();
/**
* @var array<int, true>
*/
private $packagesToRemove = array();
/**
* @var array<int, BasePackage[]>
*/
private $aliasesPerPackage = array();
public function __construct(PolicyInterface $policy)
{
$this->policy = $policy;
}
/**
* @return Pool
*/
public function optimize(Request $request, Pool $pool)
{
$this->prepare($request, $pool);
$optimizedPool = $this->optimizeByIdenticalDependencies($pool);
// No need to run this recursively at the moment
// because the current optimizations cannot provide
// even more gains when ran again. Might change
// in the future with additional optimizations.
$this->irremovablePackages = array();
$this->requireConstraintsPerPackage = array();
$this->conflictConstraintsPerPackage = array();
$this->packagesToRemove = array();
$this->aliasesPerPackage = array();
return $optimizedPool;
}
/**
* @return void
*/
private function prepare(Request $request, Pool $pool)
{
$irremovablePackageConstraintGroups = array();
// Mark fixed or locked packages as irremovable
foreach ($request->getFixedOrLockedPackages() as $package) {
$irremovablePackageConstraintGroups[$package->getName()][] = new Constraint('==', $package->getVersion());
}
// Extract requested package requirements
foreach ($request->getRequires() as $require => $constraint) {
$constraint = Intervals::compactConstraint($constraint);
$this->requireConstraintsPerPackage[$require][(string) $constraint] = $constraint;
}
// First pass over all packages to extract information and mark package constraints irremovable
foreach ($pool->getPackages() as $package) {
// Extract package requirements
foreach ($package->getRequires() as $link) {
$constraint = Intervals::compactConstraint($link->getConstraint());
$this->requireConstraintsPerPackage[$link->getTarget()][(string) $constraint] = $constraint;
}
// Extract package conflicts
foreach ($package->getConflicts() as $link) {
$constraint = Intervals::compactConstraint($link->getConstraint());
$this->conflictConstraintsPerPackage[$link->getTarget()][(string) $constraint] = $constraint;
}
// Keep track of alias packages for every package so if either the alias or aliased is kept
// we keep the others as they are a unit of packages really
if ($package instanceof AliasPackage) {
$this->aliasesPerPackage[$package->getAliasOf()->id][] = $package;
}
}
$irremovablePackageConstraints = array();
foreach ($irremovablePackageConstraintGroups as $packageName => $constraints) {
$irremovablePackageConstraints[$packageName] = 1 === \count($constraints) ? $constraints[0] : new MultiConstraint($constraints, false);
}
unset($irremovablePackageConstraintGroups);
// Mark the packages as irremovable based on the constraints
foreach ($pool->getPackages() as $package) {
if (!isset($irremovablePackageConstraints[$package->getName()])) {
continue;
}
if (CompilingMatcher::match($irremovablePackageConstraints[$package->getName()], Constraint::OP_EQ, $package->getVersion())) {
$this->markPackageIrremovable($package);
}
}
}
/**
* @return void
*/
private function markPackageIrremovable(BasePackage $package)
{
$this->irremovablePackages[$package->id] = true;
if ($package instanceof AliasPackage) {
// recursing here so aliasesPerPackage for the aliasOf can be checked
// and all its aliases marked as irremovable as well
$this->markPackageIrremovable($package->getAliasOf());
}
if (isset($this->aliasesPerPackage[$package->id])) {
foreach ($this->aliasesPerPackage[$package->id] as $aliasPackage) {
$this->irremovablePackages[$aliasPackage->id] = true;
}
}
}
/**
* @return Pool Optimized pool
*/
private function applyRemovalsToPool(Pool $pool)
{
$packages = array();
foreach ($pool->getPackages() as $package) {
if (!isset($this->packagesToRemove[$package->id])) {
$packages[] = $package;
}
}
$optimizedPool = new Pool($packages, $pool->getUnacceptableFixedOrLockedPackages());
// Reset package removals
$this->packagesToRemove = array();
return $optimizedPool;
}
/**
* @return Pool
*/
private function optimizeByIdenticalDependencies(Pool $pool)
{
$identicalDefinitionPerPackage = array();
$packageIdsToRemove = array();
foreach ($pool->getPackages() as $package) {
// If that package was already marked irremovable, we can skip
// the entire process for it
if (isset($this->irremovablePackages[$package->id])) {
continue;
}
$packageIdsToRemove[$package->id] = true;
$dependencyHash = $this->calculateDependencyHash($package);
foreach ($package->getNames(false) as $packageName) {
if (!isset($this->requireConstraintsPerPackage[$packageName])) {
continue;
}
foreach ($this->requireConstraintsPerPackage[$packageName] as $requireConstraint) {
$groupHashParts = array();
if (CompilingMatcher::match($requireConstraint, Constraint::OP_EQ, $package->getVersion())) {
$groupHashParts[] = 'require:' . (string) $requireConstraint;
}
if ($package->getReplaces()) {
foreach ($package->getReplaces() as $link) {
if (CompilingMatcher::match($link->getConstraint(), Constraint::OP_EQ, $package->getVersion())) {
// Use the same hash part as the regular require hash because that's what the replacement does
$groupHashParts[] = 'require:' . (string) $link->getConstraint();
}
}
}
if (isset($this->conflictConstraintsPerPackage[$packageName])) {
foreach ($this->conflictConstraintsPerPackage[$packageName] as $conflictConstraint) {
if (CompilingMatcher::match($conflictConstraint, Constraint::OP_EQ, $package->getVersion())) {
$groupHashParts[] = 'conflict:' . (string) $conflictConstraint;
}
}
}
if (!$groupHashParts) {
continue;
}
$identicalDefinitionPerPackage[$packageName][implode('', $groupHashParts)][$dependencyHash][] = $package;
}
}
}
$keepPackage = function (BasePackage $package, $aliasesPerPackage) use (&$packageIdsToRemove, &$keepPackage) {
unset($packageIdsToRemove[$package->id]);
if ($package instanceof AliasPackage) {
// recursing here so aliasesPerPackage for the aliasOf can be checked
// and all its aliases marked to be kept as well
$keepPackage($package->getAliasOf(), $aliasesPerPackage);
}
if (isset($aliasesPerPackage[$package->id])) {
foreach ($aliasesPerPackage[$package->id] as $aliasPackage) {
unset($packageIdsToRemove[$aliasPackage->id]);
}
}
};
foreach ($identicalDefinitionPerPackage as $package => $constraintGroups) {
foreach ($constraintGroups as $constraintGroup) {
foreach ($constraintGroup as $hash => $packages) {
// Only one package in this constraint group has the same requirements, we're not allowed to remove that package
if (1 === \count($packages)) {
$keepPackage($packages[0], $this->aliasesPerPackage);
continue;
}
// Otherwise we find out which one is the preferred package in this constraint group which is
// then not allowed to be removed either
$literals = array();
foreach ($packages as $package) {
$literals[] = $package->id;
}
foreach ($this->policy->selectPreferredPackages($pool, $literals) as $preferredLiteral) {
$keepPackage($pool->literalToPackage($preferredLiteral), $this->aliasesPerPackage);
}
}
}
}
foreach ($packageIdsToRemove as $id => $dummy) {
$this->markPackageForRemoval($id);
}
// Apply removals
return $this->applyRemovalsToPool($pool);
}
/**
* @return string
*/
private function calculateDependencyHash(BasePackage $package)
{
$hash = '';
$hashRelevantLinks = array(
'requires' => $package->getRequires(),
'conflicts' => $package->getConflicts(),
'replaces' => $package->getReplaces(),
'provides' => $package->getProvides()
);
foreach ($hashRelevantLinks as $key => $links) {
if (0 === \count($links)) {
continue;
}
// start new hash section
$hash .= $key . ':';
$subhash = array();
foreach ($links as $link) {
// To get the best dependency hash matches we should use Intervals::compactConstraint() here.
// However, the majority of projects are going to specify their constraints already pretty
// much in the best variant possible. In other words, we'd be wasting time here and it would actually hurt
// performance more than the additional few packages that could be filtered out would benefit the process.
$subhash[$link->getTarget()] = (string) $link->getConstraint();
}
// Sort for best result
ksort($subhash);
foreach ($subhash as $target => $constraint) {
$hash .= $target . '@' . $constraint;
}
}
return $hash;
}
/**
* @param int $id
* @return void
*/
private function markPackageForRemoval($id)
{
// We are not allowed to remove packages if they have been marked as irremovable
if (isset($this->irremovablePackages[$id])) {
throw new \LogicException('Attempted removing a package which was previously marked irremovable');
}
$this->packagesToRemove[$id] = true;
}
}

@ -20,6 +20,7 @@ use Composer\DependencyResolver\LockTransaction;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\PoolOptimizer;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\Request;
use Composer\DependencyResolver\Solver;
@ -430,7 +431,7 @@ class Installer
$request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies);
}
$pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher);
$pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, $this->createPoolOptimizer($policy));
$this->io->writeError('<info>Updating dependencies</info>');
@ -999,6 +1000,23 @@ class Installer
);
}
/**
* @return PoolOptimizer|null
*/
private function createPoolOptimizer(PolicyInterface $policy)
{
// Not the best architectural decision here, would need to be able
// to configure from the outside of Installer but this is only
// a debugging tool and should never be required in any other use case
if ('0' === Platform::getEnv('COMPOSER_POOL_OPTIMIZER')) {
$this->io->write('Pool Optimizer was disabled for debugging purposes.', true, IOInterface::DEBUG);
return null;
}
return new PoolOptimizer($policy);
}
/**
* Create Installer
*

@ -12,6 +12,8 @@
namespace Composer\Repository;
use Composer\DependencyResolver\PoolOptimizer;
use Composer\DependencyResolver\PolicyInterface;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\PoolBuilder;
use Composer\DependencyResolver\Request;
@ -244,9 +246,9 @@ class RepositorySet
*
* @return Pool
*/
public function createPool(Request $request, IOInterface $io, EventDispatcher $eventDispatcher = null)
public function createPool(Request $request, IOInterface $io, EventDispatcher $eventDispatcher = null, PoolOptimizer $poolOptimizer = null)
{
$poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher);
$poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher, $poolOptimizer);
foreach ($this->repositories as $repo) {
if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) {

@ -105,3 +105,13 @@ Check that replacers from additional repositories are loaded when doing a partia
"shared/dep-1.0.0.0",
"shared/dep-1.2.0.0"
]
--EXPECT-OPTIMIZED--
[
"indirect/replacer-1.2.0.0",
"indirect/replacer-1.0.0.0",
"replacer/package-1.2.0.0",
"replacer/package-1.0.0.0",
"base/package-1.0.0.0",
"shared/dep-1.2.0.0"
]

@ -96,3 +96,13 @@ Check that replacers from additional repositories are loaded
"replacer/package-1.0.0.0",
"shared/dep-1.0.0.0"
]
--EXPECT-OPTIMIZED--
[
"base/package-1.0.0.0",
"indirect/replacer-1.2.0.0",
"indirect/replacer-1.0.0.0",
"shared/dep-1.2.0.0",
"replacer/package-1.2.0.0",
"replacer/package-1.0.0.0"
]

@ -48,3 +48,13 @@ locked packages still need to be taking into account for loading all necessary v
"dep/pkg1-1.0.1.0",
"dep/pkg1-2.0.0.0"
]
--EXPECT-OPTIMIZED--
[
"root/req1-1.0.0.0 (locked)",
"root/req2-1.0.0.0 (locked)",
"dep/pkg2-1.0.0.0",
"dep/pkg2-1.2.0.0",
"dep/pkg1-1.0.1.0",
"dep/pkg1-2.0.0.0"
]

@ -50,3 +50,12 @@ Fixed packages and replacers get unfixed correctly (refs https://github.com/comp
"replaced/pkg-1.2.3.0",
"replaced/pkg-1.2.4.0"
]
--EXPECT-OPTIMIZED--
[
"root/req3-1.0.0.0 (locked)",
"dep/dep-2.3.5.0 (locked)",
"root/req1-1.1.0.0",
"replacer/pkg-1.1.0.0",
"replaced/pkg-1.2.4.0"
]

@ -46,3 +46,10 @@ Stability flags apply
6,
"default/pkg-1.2.0.0 (alias of 6)"
]
--EXPECT-OPTIMIZED--
[
1,
6,
"default/pkg-1.2.0.0 (alias of 6)"
]

@ -0,0 +1,99 @@
--TEST--
Test aliased and aliasees remain untouched if either is required, but are still optimized away otherwise.
--REQUEST--
{
"require": {
"package/a": "^1.0",
"package/required-aliasof-and-alias": "dev-main-both",
"package/required-aliasof": "dev-main-direct",
"package/required-alias": "1.*"
}
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/required-aliasof-and-alias": "^1.0"
}
},
{
"name": "package/required-aliasof-and-alias",
"version": "dev-main-both",
"extra": {
"branch-alias": {
"dev-main-both": "1.x-dev"
}
}
},
{
"name": "package/required-aliasof",
"version": "dev-main-direct",
"extra": {
"branch-alias": {
"dev-main-direct": "1.x-dev"
}
}
},
{
"name": "package/required-alias",
"version": "dev-main-alias",
"extra": {
"branch-alias": {
"dev-main-alias": "1.x-dev"
}
}
},
{
"name": "package/not-referenced",
"version": "dev-lonesome-pkg",
"extra": {
"branch-alias": {
"dev-lonesome-pkg": "1.x-dev"
}
}
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/required-aliasof-and-alias",
"version": "dev-main-both",
"extra": {
"branch-alias": {
"dev-main-both": "1.x-dev"
}
}
},
{
"name": "package/required-aliasof",
"version": "dev-main-direct",
"extra": {
"branch-alias": {
"dev-main-direct": "1.x-dev"
}
}
},
{
"name": "package/required-alias",
"version": "dev-main-alias",
"extra": {
"branch-alias": {
"dev-main-alias": "1.x-dev"
}
}
}
]

@ -0,0 +1,46 @@
--TEST--
Test filters irrelevant package "package/b" in version 1.0.0
--REQUEST--
{
"require": {
"package/a": "^1.0"
}
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.0"
},
{
"name": "package/b",
"version": "1.0.1"
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.1"
}
]

@ -0,0 +1,47 @@
--TEST--
Test filters irrelevant package "package/b" in version 1.0.1 because prefer-lowest
--REQUEST--
{
"require": {
"package/a": "^1.0"
},
"preferLowest": true
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.0"
},
{
"name": "package/b",
"version": "1.0.1"
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.0"
}
]

@ -0,0 +1,107 @@
--TEST--
We have to make sure, conflicts are considered in the grouping so we do not remove packages
from the pool which might end up being part of the solution.
--REQUEST--
{
"require": {
"nesty/nest": "^1.0"
}
}
--POOL-BEFORE--
[
{
"name": "nesty/nest",
"version": "1.0.0",
"require": {
"conflicter/pkg": "^1.0",
"victim/pkg": "^1 <1.2"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.1",
"conflict": {
"victim/pkg": "1.1.0 || 1.1.1"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.2",
"conflict": {
"victim/pkg": "1.1.1 || 1.1.2"
}
},
{
"name": "victim/pkg",
"version": "1.0.0"
},
{
"name": "victim/pkg",
"version": "1.0.1"
},
{
"name": "victim/pkg",
"version": "1.0.2"
},
{
"name": "victim/pkg",
"version": "1.1.0"
},
{
"name": "victim/pkg",
"version": "1.1.1"
},
{
"name": "victim/pkg",
"version": "1.1.2"
},
{
"name": "victim/pkg",
"version": "1.2.0"
}
]
--POOL-AFTER--
[
{
"name": "nesty/nest",
"version": "1.0.0",
"require": {
"conflicter/pkg": "^1.0",
"victim/pkg": "^1 <1.2"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.1",
"conflict": {
"victim/pkg": "1.1.0 || 1.1.1"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.2",
"conflict": {
"victim/pkg": "1.1.2"
}
},
{
"name": "victim/pkg",
"version": "1.0.2"
},
{
"name": "victim/pkg",
"version": "1.1.0"
},
{
"name": "victim/pkg",
"version": "1.1.1"
},
{
"name": "victim/pkg",
"version": "1.1.2"
}
]

@ -0,0 +1,103 @@
--TEST--
We have to make sure, conflicts are considered in the grouping so we do not remove packages
from the pool which might end up being part of the solution.
--REQUEST--
{
"require": {
"nesty/nest": "^1.0"
}
}
--POOL-BEFORE--
[
{
"name": "nesty/nest",
"version": "1.0.0",
"require": {
"conflicter/pkg": "^1.0",
"victim/pkg": "^1 <1.2"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.1",
"conflict": {
"victim/pkg": "1.1.0"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.2",
"conflict": {
"victim/pkg": "1.1.2"
}
},
{
"name": "victim/pkg",
"version": "1.0.0"
},
{
"name": "victim/pkg",
"version": "1.0.1"
},
{
"name": "victim/pkg",
"version": "1.0.2"
},
{
"name": "victim/pkg",
"version": "1.1.0"
},
{
"name": "victim/pkg",
"version": "1.1.1"
},
{
"name": "victim/pkg",
"version": "1.1.2"
},
{
"name": "victim/pkg",
"version": "1.2.0"
}
]
--POOL-AFTER--
[
{
"name": "nesty/nest",
"version": "1.0.0",
"require": {
"conflicter/pkg": "^1.0",
"victim/pkg": "^1 <1.2"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.1",
"conflict": {
"victim/pkg": "1.1.0 || 1.1.1"
}
},
{
"name": "conflicter/pkg",
"version": "1.0.2",
"conflict": {
"victim/pkg": "1.1.2"
}
},
{
"name": "victim/pkg",
"version": "1.1.0"
},
{
"name": "victim/pkg",
"version": "1.1.1"
},
{
"name": "victim/pkg",
"version": "1.1.2"
}
]

@ -0,0 +1,99 @@
--TEST--
We are not allowed to group packages only by their dependency definition. It's also relevant what other
packages require (package/b@1.0.1 must not be dropped although it has the very same definition as 2.0.0 and both are
allowed by the request). However, package/b@1.0.0 can be removed.
--REQUEST--
{
"require": {
"package/a": "^1.0"
}
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0 || ^2.0"
}
},
{
"name": "package/b",
"version": "1.0.0",
"require": {
"package/c": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.1",
"require": {
"package/c": "^1.0"
}
},
{
"name": "package/b",
"version": "2.0.0",
"require": {
"package/c": "^1.0"
}
},
{
"name": "package/c",
"version": "1.0.0",
"require": {
"package/d": "^1.0"
}
},
{
"name": "package/d",
"version": "1.0.0",
"require": {
"package/b": ">=1.0 <1.1"
}
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0 || ^2.0"
}
},
{
"name": "package/b",
"version": "1.0.1",
"require": {
"package/c": "^1.0"
}
},
{
"name": "package/b",
"version": "2.0.0",
"require": {
"package/c": "^1.0"
}
},
{
"name": "package/c",
"version": "1.0.0",
"require": {
"package/d": "^1.0"
}
},
{
"name": "package/d",
"version": "1.0.0",
"require": {
"package/b": ">=1.0 <1.1"
}
}
]

@ -0,0 +1,46 @@
--TEST--
Test locked and fixed packages remain untouched.
--REQUEST--
{
"require": {
},
"locked": [
{
"name": "package/a",
"version": "1.0.0"
}
],
"fixed": [
{
"name": "package/c",
"version": "2.0.0"
}
]
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0"
},
{
"name": "package/c",
"version": "2.0.0"
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0"
},
{
"name": "package/c",
"version": "2.0.0"
}
]

@ -0,0 +1,59 @@
--TEST--
Test replaced packages are correctly removed.
--REQUEST--
{
"require": {
"package/a": "^1.0"
}
}
--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.0",
"replace": {
"package/c": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.1",
"replace": {
"package/c": "^1.0"
}
},
{
"name": "package/c",
"version": "1.0.0"
}
]
--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.1",
"replace": {
"package/c": "^1.0"
}
}
]

@ -12,6 +12,9 @@
namespace Composer\Test\DependencyResolver;
use Composer\DependencyResolver\DefaultPolicy;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\PoolOptimizer;
use Composer\IO\NullIO;
use Composer\Repository\ArrayRepository;
use Composer\Repository\FilterRepository;
@ -31,13 +34,14 @@ class PoolBuilderTest extends TestCase
* @dataProvider getIntegrationTests
* @param string $file
* @param string $message
* @param mixed[] $expect
* @param string[] $expect
* @param string[] $expectOptimized
* @param mixed[] $root
* @param mixed[] $requestData
* @param mixed[] $packageRepos
* @param mixed[] $fixed
*/
public function testPoolBuilder($file, $message, $expect, $root, $requestData, $packageRepos, $fixed)
public function testPoolBuilder($file, $message, $expect, $expectOptimized, $root, $requestData, $packageRepos, $fixed)
{
$rootAliases = !empty($root['aliases']) ? $root['aliases'] : array();
$minimumStability = !empty($root['minimum-stability']) ? $root['minimum-stability'] : 'stable';
@ -56,6 +60,8 @@ class PoolBuilderTest extends TestCase
$loader = new ArrayLoader();
$packageIds = array();
$loadPackage = function ($data) use ($loader, &$packageIds) {
/** @var ?int $id */
$id = null;
if (!empty($data['id'])) {
$id = $data['id'];
unset($data['id']);
@ -115,12 +121,28 @@ class PoolBuilderTest extends TestCase
}
$pool = $repositorySet->createPool($request, new NullIO());
$result = $this->getPackageResultSet($pool, $packageIds);
$this->assertSame($expect, $result, 'Unoptimized pool does not match expected package set');
$optimizer = new PoolOptimizer(new DefaultPolicy());
$result = $this->getPackageResultSet($optimizer->optimize($request, $pool), $packageIds);
$this->assertSame($expectOptimized, $result, 'Optimized pool does not match expected package set');
}
/**
* @param array<int, BasePackage> $packageIds
* @return string[]
*/
private function getPackageResultSet(Pool $pool, $packageIds)
{
$result = array();
for ($i = 1, $count = count($pool); $i <= $count; $i++) {
$result[] = $pool->packageById($i);
}
$result = array_map(function ($package) use ($packageIds) {
return array_map(function (BasePackage $package) use ($packageIds) {
if ($id = array_search($package, $packageIds, true)) {
return $id;
}
@ -143,8 +165,6 @@ class PoolBuilderTest extends TestCase
return (string) $package->getName().'-'.$package->getVersion() . $suffix;
}, $result);
$this->assertSame($expect, $result);
}
/**
@ -173,11 +193,12 @@ class PoolBuilderTest extends TestCase
$fixed = JsonFile::parseJson($testData['FIXED']);
}
$expect = JsonFile::parseJson($testData['EXPECT']);
$expectOptimized = !empty($testData['EXPECT-OPTIMIZED']) ? JsonFile::parseJson($testData['EXPECT-OPTIMIZED']) : $expect;
} catch (\Exception $e) {
die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file)));
}
$tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $expect, $root, $request, $packageRepos, $fixed);
$tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $expect, $expectOptimized, $root, $request, $packageRepos, $fixed);
}
return $tests;
@ -199,6 +220,7 @@ class PoolBuilderTest extends TestCase
'FIXED' => false,
'PACKAGE-REPOS' => true,
'EXPECT' => true,
'EXPECT-OPTIMIZED' => false,
);
$section = null;

@ -0,0 +1,197 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\DependencyResolver;
use Composer\DependencyResolver\DefaultPolicy;
use Composer\DependencyResolver\Pool;
use Composer\DependencyResolver\PoolOptimizer;
use Composer\DependencyResolver\Request;
use Composer\Json\JsonFile;
use Composer\Package\AliasPackage;
use Composer\Package\BasePackage;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\Version\VersionParser;
use Composer\Repository\LockArrayRepository;
use Composer\Test\TestCase;
class PoolOptimizerTest extends TestCase
{
/**
* @dataProvider provideIntegrationTests
* @param mixed[] $requestData
* @param BasePackage[] $packagesBefore
* @param BasePackage[] $expectedPackages
* @param string $message
*/
public function testPoolOptimizer(array $requestData, array $packagesBefore, array $expectedPackages, $message)
{
$lockedRepo = new LockArrayRepository();
$request = new Request($lockedRepo);
$parser = new VersionParser();
if (isset($requestData['locked'])) {
foreach ($requestData['locked'] as $package) {
$request->lockPackage($this->loadPackage($package));
}
}
if (isset($requestData['fixed'])) {
foreach ($requestData['fixed'] as $package) {
$request->fixPackage($this->loadPackage($package));
}
}
foreach ($requestData['require'] as $package => $constraint) {
$request->requireName($package, $parser->parseConstraints($constraint));
}
$preferStable = isset($requestData['preferStable']) ? $requestData['preferStable'] : false;
$preferLowest = isset($requestData['preferLowest']) ? $requestData['preferLowest'] : false;
$pool = new Pool($packagesBefore);
$poolOptimizer = new PoolOptimizer(new DefaultPolicy($preferStable, $preferLowest));
$pool = $poolOptimizer->optimize($request, $pool);
$this->assertSame(
$this->reducePackagesInfoForComparison($expectedPackages),
$this->reducePackagesInfoForComparison($pool->getPackages()),
$message
);
}
public function provideIntegrationTests()
{
$fixturesDir = realpath(__DIR__.'/Fixtures/pooloptimizer/');
$tests = array();
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
if (!preg_match('/\.test$/', $file)) {
continue;
}
try {
$testData = $this->readTestFile($file, $fixturesDir);
$message = $testData['TEST'];
$requestData = JsonFile::parseJson($testData['REQUEST']);
$packagesBefore = $this->loadPackages(JsonFile::parseJson($testData['POOL-BEFORE']));
$expectedPackages = $this->loadPackages(JsonFile::parseJson($testData['POOL-AFTER']));
} catch (\Exception $e) {
die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file)));
}
$tests[basename($file)] = array($requestData, $packagesBefore, $expectedPackages, $message);
}
return $tests;
}
/**
* @param string $fixturesDir
* @return mixed[]
*/
protected function readTestFile(\SplFileInfo $file, $fixturesDir)
{
$tokens = preg_split('#(?:^|\n*)--([A-Z-]+)--\n#', file_get_contents($file->getRealPath()), -1, PREG_SPLIT_DELIM_CAPTURE);
/** @var array<string, bool> $sectionInfo */
$sectionInfo = array(
'TEST' => true,
'REQUEST' => true,
'POOL-BEFORE' => true,
'POOL-AFTER' => true,
);
$section = null;
$data = array();
foreach ($tokens as $i => $token) {
if (null === $section && empty($token)) {
continue; // skip leading blank
}
if (null === $section) {
if (!isset($sectionInfo[$token])) {
throw new \RuntimeException(sprintf(
'The test file "%s" must not contain a section named "%s".',
str_replace($fixturesDir.'/', '', $file),
$token
));
}
$section = $token;
continue;
}
$sectionData = $token;
$data[$section] = $sectionData;
$section = $sectionData = null;
}
foreach ($sectionInfo as $section => $required) {
if ($required && !isset($data[$section])) {
throw new \RuntimeException(sprintf(
'The test file "%s" must have a section named "%s".',
str_replace($fixturesDir.'/', '', $file),
$section
));
}
}
return $data;
}
/**
* @param BasePackage[] $packages
* @return string[]
*/
private function reducePackagesInfoForComparison(array $packages)
{
$packagesInfo = array();
foreach ($packages as $package) {
$packagesInfo[] = $package->getName() . '@' . $package->getVersion() . ($package instanceof AliasPackage ? ' (alias of '.$package->getAliasOf()->getVersion().')' : '');
}
sort($packagesInfo);
return $packagesInfo;
}
/**
* @param mixed[][] $packagesData
* @return BasePackage[]
*/
private function loadPackages(array $packagesData)
{
$packages = array();
foreach ($packagesData as $packageData) {
$packages[] = $package = $this->loadPackage($packageData);
if ($package instanceof AliasPackage) {
$packages[] = $package->getAliasOf();
}
}
return $packages;
}
/**
* @param mixed[] $packageData
* @return BasePackage
*/
private function loadPackage(array $packageData)
{
$loader = new ArrayLoader();
return $loader->load($packageData);
}
}

@ -0,0 +1,39 @@
--TEST--
Test that a package which has a conflict does not get installed and has to be downgraded
--COMPOSER--
{
"repositories": [
{
"type": "package",
"package": [
{ "name": "nesty/nest", "version": "1.0.0", "require": {
"conflicter/pkg": "^1.0",
"victim/pkg": "^1 <1.2"
} },
{ "name": "conflicter/pkg", "version": "1.0.1", "conflict": { "victim/pkg": "1.1.0"} },
{ "name": "victim/pkg", "version": "1.0.0" },
{ "name": "victim/pkg", "version": "1.0.1" },
{ "name": "victim/pkg", "version": "1.0.2" },
{ "name": "victim/pkg", "version": "1.1.0" },
{ "name": "victim/pkg", "version": "1.2.0" }
]
}
],
"require": {
"nesty/nest": "*"
}
}
--RUN--
update
--EXPECT-EXIT-CODE--
0
--EXPECT--
Installing victim/pkg (1.0.2)
Installing conflicter/pkg (1.0.1)
Installing nesty/nest (1.0.0)

@ -0,0 +1,35 @@
--TEST--
Test that a package which has a conflict does not get installed and has to be downgraded
--COMPOSER--
{
"repositories": [
{
"type": "package",
"package": [
{ "name": "conflicter/pkg", "version": "1.0.1", "conflict": { "victim/pkg": "1.1.0"} },
{ "name": "victim/pkg", "version": "1.0.0" },
{ "name": "victim/pkg", "version": "1.0.1" },
{ "name": "victim/pkg", "version": "1.0.2" },
{ "name": "victim/pkg", "version": "1.1.0" },
{ "name": "victim/pkg", "version": "1.2.0" }
]
}
],
"require": {
"conflicter/pkg": "^1.0",
"victim/pkg": "^1 <1.2"
}
}
--RUN--
update
--EXPECT-EXIT-CODE--
0
--EXPECT--
Installing conflicter/pkg (1.0.1)
Installing victim/pkg (1.0.2)

@ -44,5 +44,14 @@ Your requirements could not be resolved to an installable set of packages.
- package/a[2.0.0, ..., 2.6.0] require missing/dep ^1.0 -> found missing/dep[2.0.0] but it does not match the constraint.
- Root composer.json requires package/a * -> satisfiable by package/a[2.0.0, ..., 2.6.0].
--EXPECT-OUTPUT-OPTIMIZED--
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires package/a * -> satisfiable by package/a[2.6.0].
- package/a 2.6.0 requires missing/dep ^1.0 -> found missing/dep[2.0.0] but it does not match the constraint.
--EXPECT--

@ -128,6 +128,21 @@ Your requirements could not be resolved to an installable set of packages.
- You can only install one version of a package, so only one of these can be installed: symfony/console[v2.8.7, v2.8.8, v3.1.9, ..., v3.4.29].
- illuminate/console v5.2.25 requires symfony/console 3.1.* -> satisfiable by symfony/console[v3.1.9, v3.1.10].
- Conclusion: don't install symfony/console v3.1.10 (conflict analysis result)
--EXPECT-OUTPUT-OPTIMIZED--
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires illuminate/queue * -> satisfiable by illuminate/queue[v5.2.0].
- illuminate/queue v5.2.0 requires illuminate/console 5.2.* -> satisfiable by illuminate/console[v5.2.25, v5.2.26].
- illuminate/console v5.2.25 requires symfony/console 3.1.* -> satisfiable by symfony/console[v3.1.10].
- illuminate/console v5.2.26 requires symfony/console 2.8.* -> satisfiable by symfony/console[v2.8.8].
- You can only install one version of a package, so only one of these can be installed: symfony/console[v2.8.8, v3.1.10, v3.4.29].
- friendsofphp/php-cs-fixer v2.10.5 requires symfony/console ^3.2 || ^4.0 -> satisfiable by symfony/console[v3.4.29].
- Root composer.json requires friendsofphp/php-cs-fixer * -> satisfiable by friendsofphp/php-cs-fixer[v2.10.5].
--EXPECT--
--EXPECT-EXIT-CODE--

@ -44,5 +44,15 @@ Your requirements could not be resolved to an installable set of packages.
- Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3].
- Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3].
--EXPECT-OUTPUT-OPTIMIZED--
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Only one of these can be installed: regular/pkg[1.0.3], replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. replacer/pkg replaces regular/pkg and thus cannot coexist with it.
- Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.3].
- Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3].
--EXPECT--

@ -36,6 +36,7 @@ use Composer\Package\Locker;
use Composer\Test\Mock\FactoryMock;
use Composer\Test\Mock\InstalledFilesystemRepositoryMock;
use Composer\Test\Mock\InstallationManagerMock;
use Composer\Util\Platform;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Output\OutputInterface;
@ -56,6 +57,8 @@ class InstallerTest extends TestCase
public function tearDown()
{
Platform::clearEnv('COMPOSER_POOL_OPTIMIZER');
chdir($this->prevCwd);
if (isset($this->tempComposerHome) && is_dir($this->tempComposerHome)) {
$fs = new Filesystem;
@ -228,16 +231,64 @@ class InstallerTest extends TestCase
* @param mixed[]|false $expectLock
* @param ?mixed[] $expectInstalled
* @param ?string $expectOutput
* @param ?string $expectOutputOptimized
* @param string $expect
* @param int|string $expectResult
*/
public function testSlowIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult)
public function testSlowIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expectOutputOptimized, $expect, $expectResult)
{
return $this->testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult);
Platform::putEnv('COMPOSER_POOL_OPTIMIZER', '0');
$this->doTestIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult);
}
/**
* @dataProvider provideIntegrationTests
* @param string $file
* @param string $message
* @param ?string $condition
* @param Config $composerConfig
* @param ?mixed[] $lock
* @param ?mixed[] $installed
* @param string $run
* @param mixed[]|false $expectLock
* @param ?mixed[] $expectInstalled
* @param ?string $expectOutput
* @param ?string $expectOutputOptimized
* @param string $expect
* @param int|string $expectResult
*/
public function testIntegrationWithPoolOptimizer($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expectOutputOptimized, $expect, $expectResult)
{
Platform::putEnv('COMPOSER_POOL_OPTIMIZER', '1');
$this->doTestIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutputOptimized ?: $expectOutput, $expect, $expectResult);
}
/**
* @dataProvider provideIntegrationTests
* @param string $file
* @param string $message
* @param ?string $condition
* @param Config $composerConfig
* @param ?mixed[] $lock
* @param ?mixed[] $installed
* @param string $run
* @param mixed[]|false $expectLock
* @param ?mixed[] $expectInstalled
* @param ?string $expectOutput
* @param ?string $expectOutputOptimized
* @param string $expect
* @param int|string $expectResult
*/
public function testIntegrationWithRawPool($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expectOutputOptimized, $expect, $expectResult)
{
Platform::putEnv('COMPOSER_POOL_OPTIMIZER', '0');
$this->doTestIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult);
}
/**
* @param string $file
* @param string $message
* @param ?string $condition
@ -250,8 +301,9 @@ class InstallerTest extends TestCase
* @param ?string $expectOutput
* @param string $expect
* @param int|string $expectResult
* @return void
*/
public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult)
private function doTestIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult)
{
if ($condition) {
eval('$res = '.$condition.';');
@ -518,6 +570,7 @@ class InstallerTest extends TestCase
$expectInstalled = JsonFile::parseJson($testData['EXPECT-INSTALLED']);
}
$expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null;
$expectOutputOptimized = isset($testData['EXPECT-OUTPUT-OPTIMIZED']) ? $testData['EXPECT-OUTPUT-OPTIMIZED'] : null;
$expect = $testData['EXPECT'];
if (!empty($testData['EXPECT-EXCEPTION'])) {
$expectResult = $testData['EXPECT-EXCEPTION'];
@ -533,7 +586,7 @@ class InstallerTest extends TestCase
die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file)));
}
$tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expect, $expectResult);
$tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectInstalled, $expectOutput, $expectOutputOptimized, $expect, $expectResult);
}
return $tests;
@ -557,6 +610,7 @@ class InstallerTest extends TestCase
'EXPECT-LOCK' => false,
'EXPECT-INSTALLED' => false,
'EXPECT-OUTPUT' => false,
'EXPECT-OUTPUT-OPTIMIZED' => false,
'EXPECT-EXIT-CODE' => false,
'EXPECT-EXCEPTION' => false,
'EXPECT' => true,

@ -10,6 +10,8 @@
* file that was distributed with this source code.
*/
use Composer\Util\Platform;
error_reporting(E_ALL);
if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) {
@ -19,3 +21,5 @@ if (function_exists('date_default_timezone_set') && function_exists('date_defaul
require __DIR__.'/../src/bootstrap.php';
require __DIR__.'/../src/Composer/InstalledVersions.php';
require __DIR__.'/Composer/Test/TestCase.php';
Platform::putEnv('COMPOSER_TESTS_ARE_RUNNING', '1');

@ -75,13 +75,18 @@
"count": 1
},
{
"location": "Composer\\Test\\InstallerTest::testIntegration",
"location": "Composer\\Test\\InstallerTest::testIntegrationWithRawPool",
"message": "preg_match(): Passing null to parameter #4 ($flags) of type int is deprecated",
"count": 1640
"count": 1728
},
{
"location": "Composer\\Test\\InstallerTest::testIntegrationWithPoolOptimizer",
"message": "preg_match(): Passing null to parameter #4 ($flags) of type int is deprecated",
"count": 1728
},
{
"location": "Composer\\Test\\Package\\Archiver\\ArchivableFilesFinderTest::testManualExcludes",
"message": "Return type of Symfony\\Component\\Finder\\Iterator\\CustomFilterIterator::accept() should either be compatible with FilterIterator::accept(): bool, or the #[\\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice",
"count": 1
}
]
]

Loading…
Cancel
Save