From 7b4cb9c370f16759c8f548d99d37215c0ddd06cc Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 21 Oct 2020 17:11:13 +0200 Subject: [PATCH 1/2] Solver: Prevent infinite recursion in analyzeUnsolvableRule In complex scenarios reasons for learned rules can themselves be learned rules caused by other learned rules which had the some of the same reasons. In this situation iterating over all problem rules requires keeping track of which rules have previously been analyzed to avoid and endless loop. Side effect is that the sorting of problems including learned rules changes slightly. --- src/Composer/DependencyResolver/Solver.php | 14 +- .../installer/github-issues-7051.test | 8 +- .../installer/github-issues-9012.test | 325 ++++++++++++++++++ .../installer/provider-conflicts3.test | 4 +- 4 files changed, 341 insertions(+), 10 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/github-issues-9012.test diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 92444eaac..35b97e6ae 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -518,15 +518,19 @@ class Solver * @param Problem $problem * @param Rule $conflictRule */ - private function analyzeUnsolvableRule(Problem $problem, Rule $conflictRule) + private function analyzeUnsolvableRule(Problem $problem, Rule $conflictRule, &$ruleSeen) { + $ruleSeen[spl_object_hash($conflictRule)] = true; + if ($conflictRule->getType() == RuleSet::TYPE_LEARNED) { $why = spl_object_hash($conflictRule); $learnedWhy = $this->learnedWhy[$why]; $problemRules = $this->learnedPool[$learnedWhy]; foreach ($problemRules as $problemRule) { - $this->analyzeUnsolvableRule($problem, $problemRule); + if (!isset($ruleSeen[spl_object_hash($problemRule)])) { + $this->analyzeUnsolvableRule($problem, $problemRule, $ruleSeen); + } } return; @@ -550,7 +554,9 @@ class Solver $problem = new Problem(); $problem->addRule($conflictRule); - $this->analyzeUnsolvableRule($problem, $conflictRule); + $ruleSeen = array(); + + $this->analyzeUnsolvableRule($problem, $conflictRule, $ruleSeen); $this->problems[] = $problem; @@ -576,7 +582,7 @@ class Solver $why = $decision[Decisions::DECISION_REASON]; $problem->addRule($why); - $this->analyzeUnsolvableRule($problem, $why); + $this->analyzeUnsolvableRule($problem, $why, $ruleSeen); $literals = $why->getLiterals(); diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-7051.test b/tests/Composer/Test/Fixtures/installer/github-issues-7051.test index 9c39c1441..7683c7482 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-7051.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-7051.test @@ -116,13 +116,13 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Conclusion: don't install friendsofphp/php-cs-fixer v2.10.5 (conflict analysis result) - - Conclusion: don't install symfony/console v3.4.29 (conflict analysis result) - - Conclusion: don't install symfony/console v2.8.8 (conflict analysis result) - - Conclusion: don't install symfony/console v3.4.28 (conflict analysis result) + - Root composer.json requires friendsofphp/php-cs-fixer * -> satisfiable by friendsofphp/php-cs-fixer[v2.10.4, v2.10.5]. - illuminate/queue v5.2.0 requires illuminate/console 5.2.* -> satisfiable by illuminate/console[v5.2.25, v5.2.26]. - illuminate/console v5.2.26 requires symfony/console 2.8.* -> satisfiable by symfony/console[v2.8.7, v2.8.8]. - Conclusion: don't install symfony/console v2.8.7 (conflict analysis result) - - Root composer.json requires friendsofphp/php-cs-fixer * -> satisfiable by friendsofphp/php-cs-fixer[v2.10.4, v2.10.5]. + - Conclusion: don't install symfony/console v3.4.28 (conflict analysis result) + - Conclusion: don't install symfony/console v2.8.8 (conflict analysis result) + - Conclusion: don't install symfony/console v3.4.29 (conflict analysis result) - Root composer.json requires illuminate/queue * -> satisfiable by illuminate/queue[v5.2.0]. - friendsofphp/php-cs-fixer v2.10.4 requires symfony/console ^3.2 || ^4.0 -> satisfiable by symfony/console[v3.2.13, ..., v3.4.29]. - 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]. diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-9012.test b/tests/Composer/Test/Fixtures/installer/github-issues-9012.test new file mode 100644 index 000000000..f118584d1 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/github-issues-9012.test @@ -0,0 +1,325 @@ +--TEST-- + +See Github issue #9012 ( https://github.com/composer/composer/issues/9012 and https://gist.github.com/Seldaek/4e2dbc2cea4b4fd7a8207bb310ec8e34). + +Recursive analysis of the same learnt rules can lead to infinite recursion in solver. + +--COMPOSER-- +{ + "require": { + "php": ">=7.1.7", + "laravel/framework": "^6.0", + "nunomaduro/collision": "^4.0" + }, + "config": { + "preferred-install": "dist", + "sort-packages": true, + "optimize-autoloader": true, + "process-timeout": 0 + }, + "repositories": { + "laravel/framework": { + "type": "package", + "package": [ + { + "name": "laravel/framework", + "version": "v6.0.0", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "require": { + "symfony/console": "^4.3.4" + } + } + ] + }, + "nunomaduro/collision": { + "type": "package", + "package": [ + { + "name": "nunomaduro/collision", + "version": "v4.2.0", + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + } + }, + "require": { + "symfony/console": "^5.0" + } + } + ] + }, + "symfony/console": { + "type": "package", + "package": [ + { + "name": "symfony/console", + "version": "v5.1.7", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.1.6", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.1.5", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.1.4", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.1.3", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.1.2", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.1.1", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.1.0", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.1.0-RC2", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.1.0-RC1", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.1.0-BETA1", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.11", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.10", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.9", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.8", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.7", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.6", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.5", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.4", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.3", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.2", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.1", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v5.0.0", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.15", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.14", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.13", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.12", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.11", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.10", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.9", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.8", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.7", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.6", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.5", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.4", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.3", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.2", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.1", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.0", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.0-RC1", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.0-BETA2", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.4.0-BETA1", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.3.11", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.3.10", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.3.9", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.3.8", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.3.7", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.3.6", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.3.5", + "type": "library" + }, + { + "name": "symfony/console", + "version": "v4.3.4", + "type": "library" + } + ] + } + } +} + +--RUN-- +update + +--EXPECT-- + +--EXPECT-OUTPUT-- + +--EXPECT-EXIT-CODE-- +2 diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test index de0744b53..213b1e1e1 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test @@ -37,9 +37,9 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - Conclusion: don't install regular/pkg 1.0.3 (conflict analysis result) - - Conclusion: don't install regular/pkg 1.0.2 (conflict analysis result) - Conclusion: don't install regular/pkg 1.0.1 (conflict analysis result) + - Conclusion: don't install regular/pkg 1.0.2 (conflict analysis result) + - Conclusion: don't install regular/pkg 1.0.3 (conflict analysis result) - Only one of these can be installed: regular/pkg[1.0.0, 1.0.1, 1.0.2, 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.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]. From 9338401be7a09c4c740b74aad6f10bdcf93a096d Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 21 Oct 2020 18:11:24 +0200 Subject: [PATCH 2/2] CS: Typehint array, remove unnecessary docblock, call spl_object_hash once --- src/Composer/DependencyResolver/Solver.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 35b97e6ae..9a9190dbe 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -514,16 +514,12 @@ class Solver return array($learnedLiterals[0], $ruleLevel, $newRule, $why); } - /** - * @param Problem $problem - * @param Rule $conflictRule - */ - private function analyzeUnsolvableRule(Problem $problem, Rule $conflictRule, &$ruleSeen) + private function analyzeUnsolvableRule(Problem $problem, Rule $conflictRule, array &$ruleSeen) { - $ruleSeen[spl_object_hash($conflictRule)] = true; + $why = spl_object_hash($conflictRule); + $ruleSeen[$why] = true; if ($conflictRule->getType() == RuleSet::TYPE_LEARNED) { - $why = spl_object_hash($conflictRule); $learnedWhy = $this->learnedWhy[$why]; $problemRules = $this->learnedPool[$learnedWhy];