diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 9e8f02586..112c13174 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -95,11 +95,42 @@ class Problem } $messages = array(); + $templates = array(); + $parser = new VersionParser; + $deduplicatableRuleTypes = array(Rule::RULE_PACKAGE_REQUIRES, Rule::RULE_PACKAGE_CONFLICT); foreach ($reasons as $rule) { - $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool); + $message = $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool); + if (in_array($rule->getReason(), $deduplicatableRuleTypes, true) && preg_match('{^(?P\S+) (?P\S+) (?Prequires|conflicts)}', $message, $m)) { + $template = preg_replace('{^\S+ \S+ }', '%s%s ', $message); + $messages[] = $template; + $templates[$template][$m[1]][$parser->normalize($m[2])] = $m[2]; + } else { + $messages[] = $message; + } } - return "\n - ".implode("\n - ", array_unique($messages)); + $result = array(); + foreach (array_unique($messages) as $message) { + if (isset($templates[$message])) { + foreach ($templates[$message] as $package => $versions) { + uksort($versions, 'version_compare'); + if (!$isVerbose) { + $versions = self::condenseVersionList($versions, 1); + } + if (count($versions) > 1) { + // remove the s from requires/conflicts to correct grammar + $message = preg_replace('{^(%s%s (?:require|conflict))s}', '$1', $message); + $result[] = sprintf($message, $package, '['.implode(', ', $versions).']'); + } else { + $result[] = sprintf($message, $package, ' '.reset($versions)); + } + } + } else { + $result[] = $message; + } + } + + return "\n - ".implode("\n - ", $result); } public function isCausedByLock() @@ -299,32 +330,46 @@ class Problem if (isset($package['versions'][VersionParser::DEV_MASTER_ALIAS]) && isset($package['versions']['dev-master'])) { unset($package['versions'][VersionParser::DEV_MASTER_ALIAS]); } - if (!$isVerbose && count($package['versions']) > 4) { - uksort($package['versions'], 'version_compare'); - $filtered = array(); - $byMajor = array(); - foreach ($package['versions'] as $version => $pretty) { - $byMajor[preg_replace('{^(\d+)\..*}', '$1', $version)][] = $pretty; - } - foreach ($byMajor as $versions) { - if (count($versions) > 4) { - $filtered[] = $versions[0]; - $filtered[] = '...'; - $filtered[] = $versions[count($versions) - 1]; - } else { - $filtered = array_merge($filtered, $versions); - } - } - $package['versions'] = $filtered; + uksort($package['versions'], 'version_compare'); + + if (!$isVerbose) { + $package['versions'] = self::condenseVersionList($package['versions'], 4); } - $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']'; } return implode(', ', $prepared); } + /** + * @param string[] $versions an array of pretty versions, with normalized versions as keys + * @return list a list of pretty versions and '...' where versions were removed + */ + private static function condenseVersionList(array $versions, $max) + { + if (count($versions) <= $max) { + return $versions; + } + + $filtered = array(); + $byMajor = array(); + foreach ($versions as $version => $pretty) { + $byMajor[preg_replace('{^(\d+)\..*}', '$1', $version)][] = $pretty; + } + foreach ($byMajor as $versionsForMajor) { + if (count($versionsForMajor) > $max) { + $filtered[] = $versionsForMajor[0]; + $filtered[] = '...'; + $filtered[] = $versionsForMajor[count($versionsForMajor) - 1]; + } else { + $filtered = array_merge($filtered, $versionsForMajor); + } + } + + return $filtered; + } + private static function hasMultipleNames(array $packages) { $name = null; diff --git a/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test b/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test index ce1aea7bb..613bc4159 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test @@ -47,7 +47,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires b/b *@dev -> satisfiable by b/b[dev-master]. - a/a dev-master requires d/d 1.0.0 -> satisfiable by d/d[1.0.0]. - - You can only install one version of a package, so only one of these can be installed: d/d[2.0.0, 1.0.0]. + - You can only install one version of a package, so only one of these can be installed: d/d[1.0.0, 2.0.0]. - Conclusion: install d/d 2.0.0, learned rules: - Root composer.json requires b/b *@dev -> satisfiable by b/b[dev-master]. - b/b dev-master requires d/d 2.0.0 -> satisfiable by d/d[2.0.0]. diff --git a/tests/Composer/Test/Fixtures/installer/deduplicate-solver-problems.test b/tests/Composer/Test/Fixtures/installer/deduplicate-solver-problems.test new file mode 100644 index 000000000..bbb245914 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/deduplicate-solver-problems.test @@ -0,0 +1,48 @@ +--TEST-- +Test the error output of solver problems is deduplicated. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "package/a", "version": "2.0.0", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.0.1", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.0.2", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.0.3", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.1.0", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.2.0", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.3.1", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.3.2", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.3.3", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.3.4", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.3.5", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.4.0", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.5.0", "require": { "missing/dep": "^1.0" } }, + { "name": "package/a", "version": "2.6.0", "require": { "missing/dep": "^1.0" } }, + { "name": "missing/dep", "version": "2.0.0" } + ] + } + ], + "require": { + "package/a": "*" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - 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-- + diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test index 1c2ea0ceb..53d11ecf0 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test @@ -39,14 +39,14 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Conclusion: don't install regular/pkg 1.0.3, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - 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. - Conclusion: don't install regular/pkg 1.0.2, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - 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. - Conclusion: don't install regular/pkg 1.0.1, learned rules: - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. - - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. + - 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. + - 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].