Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix PoolOptimizer should consider disjunctive MultiConstraints #10579

Merged
merged 8 commits into from Mar 12, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
65 changes: 59 additions & 6 deletions src/Composer/DependencyResolver/PoolOptimizer.php
Expand Up @@ -110,21 +110,18 @@ private function prepare(Request $request, Pool $pool)

// Extract requested package requirements
foreach ($request->getRequires() as $require => $constraint) {
$constraint = Intervals::compactConstraint($constraint);
$this->requireConstraintsPerPackage[$require][(string) $constraint] = $constraint;
$this->extractRequireConstraintsPerPackage($require, $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;
$this->extractRequireConstraintsPerPackage($link->getTarget(), $link->getConstraint());
}
// Extract package conflicts
foreach ($package->getConflicts() as $link) {
$constraint = Intervals::compactConstraint($link->getConstraint());
$this->conflictConstraintsPerPackage[$link->getTarget()][(string) $constraint] = $constraint;
$this->extractConflictConstraintsPerPackage($link->getTarget(), $link->getConstraint());
}

// Keep track of alias packages for every package so if either the alias or aliased is kept
Expand Down Expand Up @@ -453,4 +450,60 @@ private function optimizeImpossiblePackagesAway(Request $request, Pool $pool)
}
}
}

/**
* Disjunctive require constraints need to be considered in their own group. E.g. "^2.14 || ^3.3" needs to generate
* two require constraint groups in order for us to keep the best matching package for "^2.14" AND "^3.3" as otherwise, we'd
* only keep either one which can cause trouble (e.g. when using --prefer-lowest).
*
* @param string $package
* @param ConstraintInterface $constraint
* @return void
*/
private function extractRequireConstraintsPerPackage($package, ConstraintInterface $constraint)
{
foreach ($this->expandDisjunctiveMultiConstraints($constraint) as $expanded) {
$this->requireConstraintsPerPackage[$package][(string) $expanded] = $expanded;
}
}

/**
* Disjunctive conflict constraints need to be considered in their own group. E.g. "^2.14 || ^3.3" needs to generate
* two conflict constraint groups in order for us to keep the best matching package for "^2.14" AND "^3.3" as otherwise, we'd
* only keep either one which can cause trouble (e.g. when using --prefer-lowest).
*
* @param string $package
* @param ConstraintInterface $constraint
* @return void
*/
private function extractConflictConstraintsPerPackage($package, ConstraintInterface $constraint)
{
foreach ($this->expandDisjunctiveMultiConstraints($constraint) as $expanded) {
$this->conflictConstraintsPerPackage[$package][(string) $expanded] = $expanded;
}
}

/**
*
* @param ConstraintInterface $constraint
* @return ConstraintInterface[]
*/
private function expandDisjunctiveMultiConstraints(ConstraintInterface $constraint)
{
$expanded = array();
$constraint = Intervals::compactConstraint($constraint);

if ($constraint instanceof MultiConstraint && $constraint->isDisjunctive()) {
foreach ($constraint->getConstraints() as $sub) {
$expanded = array_merge($expanded, $this->expandDisjunctiveMultiConstraints($sub));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the recursion here needed vs simply $expanded[] = $sub;? I don't think we ever create nested disjunctive constraints but maybe I'm overlooking something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's one of my questions ;) I don't know if it's even possible to define something like (^2.0 || ^3.0) || (^4.1 || ^2.1)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It technically is possible I think, although very uncommon. But we do currently allow multiconstraints to be arbitrarily nested.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to work on another testcase then. I want to have this covered.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brackets are not allowed in version constraints so I don't know how I could create this. I also tried in an actual project and there was no case 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It already is. I just left a comment there to maybe give our future selves an idea where to start debugging if something like this happens. Might want to remove the TODO or rephrase but I think you have to decide how you'd like to have that note :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah indeed, compact constraint will never have nested disjunction in the output. Disjunction is only used on top level to basically list all the intervals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With that knowledge in mind, we can also drop the hint (and the time-consuming test if you want 😢) 😉 Just let me know what to do :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry had missed that compact was called :( I'd remove it then and maybe just leave a comment that because of the compact that can't exist there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No prob, it helped us find out that it's not a use case so that's good :) This PR is now finished then, I guess.

}

return $expanded;
}

// Regular constraints and conjunctive MultiConstraints
$expanded[] = $constraint;

return $expanded;
}
}
@@ -0,0 +1,55 @@
--TEST--
Test keeps package "package/b" in version 2.2.0 because for prefer-lowest either one might be relevant

--REQUEST--
{
"require": {
"package/a": "^1.0"
},
"preferLowest": true
}


--POOL-BEFORE--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0 || ^2.2"
}
},
{
"name": "package/b",
"version": "1.0.0"
},
{
"name": "package/b",
"version": "1.0.1"
},
{
"name": "package/b",
"version": "2.2.0"
}
]


--POOL-AFTER--
[
{
"name": "package/a",
"version": "1.0.0",
"require": {
"package/b": "^1.0"
}
},
{
"name": "package/b",
"version": "1.0.0"
},
{
"name": "package/b",
"version": "2.2.0"
}
]