aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/dependency_resolver/problem.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-core/src/dependency_resolver/problem.rs')
-rw-r--r--crates/mozart-core/src/dependency_resolver/problem.rs499
1 files changed, 499 insertions, 0 deletions
diff --git a/crates/mozart-core/src/dependency_resolver/problem.rs b/crates/mozart-core/src/dependency_resolver/problem.rs
new file mode 100644
index 0000000..e9a1464
--- /dev/null
+++ b/crates/mozart-core/src/dependency_resolver/problem.rs
@@ -0,0 +1,499 @@
+use super::pool::{Literal, Pool, literal_to_package_id};
+use super::rule::{ReasonData, Rule, RuleReason};
+use super::rule_set::{RuleId, RuleSet};
+
+/// Represents a conflict found during resolution.
+/// Collects the rules involved in the problem.
+///
+/// Port of Composer's Problem.php.
+#[derive(Debug, Clone)]
+pub struct Problem {
+ /// Sections of rules that form this problem.
+ /// Each section is a group of related rules.
+ sections: Vec<Vec<RuleId>>,
+}
+
+impl Problem {
+ pub fn new() -> Self {
+ Problem {
+ sections: vec![vec![]],
+ }
+ }
+
+ /// Add a rule to the current section.
+ pub fn add_rule(&mut self, rule_id: RuleId) {
+ if let Some(section) = self.sections.last_mut()
+ && !section.contains(&rule_id)
+ {
+ section.push(rule_id);
+ }
+ }
+
+ /// Start a new section.
+ pub fn next_section(&mut self) {
+ if self.sections.last().is_some_and(|s| !s.is_empty()) {
+ self.sections.push(vec![]);
+ }
+ }
+
+ /// Get all rule IDs in this problem.
+ pub fn rule_ids(&self) -> Vec<RuleId> {
+ self.sections.iter().flatten().copied().collect()
+ }
+
+ /// Format the problem as a human-readable string using Pool data.
+ ///
+ /// Port of Composer's Problem::getPrettyString().
+ pub fn pretty_string(&self, pool: &Pool, rules: &RuleSet) -> String {
+ // Flatten all sections (reversed) like Composer does
+ let mut all_rules: Vec<RuleId> = self.sections.iter().rev().flatten().copied().collect();
+
+ if all_rules.is_empty() {
+ return "Unknown problem".to_string();
+ }
+
+ // Sort by priority, then by sortable string
+ all_rules.sort_by(|&a, &b| {
+ let rule_a = rules.rule_by_id(a);
+ let rule_b = rules.rule_by_id(b);
+ let prio_a = rule_priority(rule_a);
+ let prio_b = rule_priority(rule_b);
+ if prio_a != prio_b {
+ return prio_b.cmp(&prio_a);
+ }
+ sortable_string(pool, rule_a).cmp(&sortable_string(pool, rule_b))
+ });
+
+ // Format each rule
+ let mut messages: Vec<String> = Vec::new();
+ for &rule_id in &all_rules {
+ let rule = rules.rule_by_id(rule_id);
+ let msg = rule_pretty_string(pool, rule);
+ if !msg.is_empty() {
+ messages.push(msg);
+ }
+ }
+
+ // Deduplicate
+ let mut seen = indexmap::IndexSet::new();
+ let mut unique = Vec::new();
+ for msg in messages {
+ if seen.insert(msg.clone()) {
+ unique.push(msg);
+ }
+ }
+
+ if unique.is_empty() {
+ return "Unknown problem".to_string();
+ }
+
+ unique
+ .iter()
+ .map(|m| format!(" - {m}"))
+ .collect::<Vec<_>>()
+ .join("\n")
+ }
+
+ /// Basic format for backward compatibility (uses rule Display).
+ pub fn format(&self, rules: &RuleSet) -> String {
+ let mut parts = Vec::new();
+ for section in &self.sections {
+ for &rule_id in section {
+ let rule = rules.rule_by_id(rule_id);
+ parts.push(format!(" - {rule}"));
+ }
+ }
+ if parts.is_empty() {
+ "Unknown problem".to_string()
+ } else {
+ parts.join("\n")
+ }
+ }
+}
+
+impl Default for Problem {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// Get the sort priority for a rule (higher = more important).
+/// Port of Problem::getRulePriority().
+fn rule_priority(rule: &Rule) -> u8 {
+ match rule.reason {
+ RuleReason::Fixed => 3,
+ RuleReason::RootRequire => 2,
+ RuleReason::PackageConflict | RuleReason::PackageRequires => 1,
+ RuleReason::PackageSameName
+ | RuleReason::Learned
+ | RuleReason::PackageAlias
+ | RuleReason::PackageInverseAlias => 0,
+ }
+}
+
+/// Get a sortable string for a rule.
+/// Port of Problem::getSortableString().
+fn sortable_string(pool: &Pool, rule: &Rule) -> String {
+ match (&rule.reason, &rule.reason_data) {
+ (RuleReason::RootRequire, ReasonData::RootRequire { package_name, .. }) => {
+ package_name.clone()
+ }
+ (RuleReason::Fixed, ReasonData::Fixed { package_id }) => {
+ pool.package_by_id(*package_id).to_string()
+ }
+ (RuleReason::PackageConflict | RuleReason::PackageRequires, ReasonData::Link(link)) => {
+ if let Some(source_lit) = rule.literals().first() {
+ let source_pkg = pool.literal_to_package(*source_lit);
+ format!("{}//{}", source_pkg, link)
+ } else {
+ link.to_string()
+ }
+ }
+ (RuleReason::PackageSameName, ReasonData::PackageName(name)) => name.clone(),
+ (RuleReason::Learned, _) => rule
+ .literals()
+ .iter()
+ .map(|l: &Literal| l.to_string())
+ .collect::<Vec<_>>()
+ .join("-"),
+ _ => String::new(),
+ }
+}
+
+/// Format a rule as a human-readable string.
+/// Port of Composer's Rule::getPrettyString().
+fn rule_pretty_string(pool: &Pool, rule: &Rule) -> String {
+ match (&rule.reason, &rule.reason_data) {
+ (
+ RuleReason::RootRequire,
+ ReasonData::RootRequire {
+ package_name,
+ constraint,
+ },
+ ) => {
+ let providers = format_providers(pool, rule.literals());
+ if providers.is_empty() {
+ format!(
+ "No package found to satisfy root composer.json require {package_name} {constraint}"
+ )
+ } else {
+ format!(
+ "Root composer.json requires {package_name} {constraint} -> satisfiable by {providers}."
+ )
+ }
+ }
+
+ (RuleReason::Fixed, ReasonData::Fixed { package_id }) => {
+ let pkg = pool.package_by_id(*package_id);
+ if pkg.is_fixed {
+ format!(
+ "{} {} is locked to version {} and an update of this package was not requested.",
+ pkg.name, pkg.pretty_version, pkg.pretty_version
+ )
+ } else {
+ format!(
+ "{} {} is present at version {} and cannot be modified by Mozart",
+ pkg.name, pkg.pretty_version, pkg.pretty_version
+ )
+ }
+ }
+
+ (RuleReason::PackageConflict, ReasonData::Link(link)) => {
+ let literals = rule.literals();
+ if literals.len() >= 2 {
+ let pkg1 = pool.literal_to_package(literals[0]);
+ let pkg2 = pool.literal_to_package(literals[1]);
+ // Determine which is the source of the conflict
+ if link.source == pkg1.name {
+ format!("{pkg2} conflicts with {pkg1}.")
+ } else {
+ format!("{pkg1} conflicts with {pkg2}.")
+ }
+ } else {
+ format!("Conflict: {link}")
+ }
+ }
+
+ (RuleReason::PackageRequires, ReasonData::Link(link)) => {
+ let literals = rule.literals();
+ if literals.is_empty() {
+ return format!("Requirement: {link}");
+ }
+
+ let source_pkg = pool.literal_to_package(literals[0]);
+ let base_text = format!(
+ "{} {} requires {} {}",
+ source_pkg.name, source_pkg.pretty_version, link.target, link.constraint
+ );
+
+ // Remaining literals are the satisfying packages
+ let provider_lits: Vec<Literal> = literals[1..].to_vec();
+ if provider_lits.is_empty() {
+ format!("{base_text} -> no matching package found.")
+ } else {
+ let providers = format_providers(pool, &provider_lits);
+ format!("{base_text} -> satisfiable by {providers}.")
+ }
+ }
+
+ (RuleReason::PackageSameName, ReasonData::PackageName(name)) => {
+ let literals = rule.literals();
+ // Collect unique package names in this rule
+ let mut pkg_names: Vec<String> = Vec::new();
+ for &lit in literals {
+ let pkg = pool.literal_to_package(lit);
+ if !pkg_names.contains(&pkg.name) {
+ pkg_names.push(pkg.name.clone());
+ }
+ }
+
+ if pkg_names.len() > 1 {
+ // Different packages that replace/provide the same name
+ let replacers: Vec<&str> = pkg_names
+ .iter()
+ .filter(|n| n.as_str() != name)
+ .map(|n| n.as_str())
+ .collect();
+
+ let reason = if replacers.is_empty() {
+ format!("They all replace {name} and thus cannot coexist.")
+ } else if !pkg_names.contains(name) {
+ format!(
+ "They {} replace {name} and thus cannot coexist.",
+ if literals.len() == 2 { "both" } else { "all" }
+ )
+ } else if replacers.len() == 1 {
+ format!(
+ "{} replaces {name} and thus cannot coexist with it.",
+ replacers[0]
+ )
+ } else {
+ format!(
+ "[{}] replace {name} and thus cannot coexist with it.",
+ replacers.join(", ")
+ )
+ };
+
+ let pkgs_str = format_providers(pool, literals);
+ format!("Only one of these can be installed: {pkgs_str}. {reason}")
+ } else {
+ // Same package, different versions
+ let pkgs_str = format_providers(pool, literals);
+ format!(
+ "You can only install one version of a package, so only one of these can be installed: {pkgs_str}."
+ )
+ }
+ }
+
+ (RuleReason::Learned, _) => {
+ let literals = rule.literals();
+ if literals.len() == 1 {
+ let pretty = pool.literal_to_pretty_string(literals[0]);
+ format!("Conclusion: {pretty} (conflict analysis result)")
+ } else {
+ // Group literals by install/don't install
+ let mut install = Vec::new();
+ let mut dont_install = Vec::new();
+ for &lit in literals {
+ if lit > 0 {
+ install.push(lit);
+ } else {
+ dont_install.push(lit);
+ }
+ }
+
+ let mut parts = Vec::new();
+ if !install.is_empty() {
+ let pkgs = format_providers(pool, &install);
+ if install.len() > 1 {
+ parts.push(format!("install one of {pkgs}"));
+ } else {
+ parts.push(format!("install {pkgs}"));
+ }
+ }
+ if !dont_install.is_empty() {
+ let pkgs = format_providers_abs(pool, &dont_install);
+ if dont_install.len() > 1 {
+ parts.push(format!("don't install one of {pkgs}"));
+ } else {
+ parts.push(format!("don't install {pkgs}"));
+ }
+ }
+
+ format!(
+ "Conclusion: {} (conflict analysis result)",
+ parts.join(" | ")
+ )
+ }
+ }
+
+ (RuleReason::PackageAlias, _) => {
+ let literals = rule.literals();
+ if literals.len() >= 2 {
+ let alias_pkg = pool.literal_to_package(literals[0]);
+ let target_pkg = pool.literal_to_package(literals[1]);
+ format!(
+ "{alias_pkg} is an alias of {target_pkg} and thus requires it to be installed too."
+ )
+ } else {
+ String::new()
+ }
+ }
+
+ (RuleReason::PackageInverseAlias, _) => {
+ let literals = rule.literals();
+ if literals.len() >= 2 {
+ let target_pkg = pool.literal_to_package(literals[0]);
+ let alias_pkg = pool.literal_to_package(literals[1]);
+ format!("{alias_pkg} is an alias of {target_pkg} and must be installed with it.")
+ } else {
+ String::new()
+ }
+ }
+
+ _ => {
+ // Fallback: display raw literals
+ let literal_strs: Vec<String> = rule
+ .literals()
+ .iter()
+ .map(|&l| pool.literal_to_pretty_string(l))
+ .collect();
+ literal_strs.join(" | ")
+ }
+ }
+}
+
+/// Format a list of literals as a list of package names grouped by name.
+/// Similar to Composer's formatPackagesUnique.
+fn format_providers(pool: &Pool, literals: &[Literal]) -> String {
+ // Group by package name
+ let mut groups: indexmap::IndexMap<&str, Vec<&str>> = indexmap::IndexMap::new();
+ for &lit in literals {
+ let pkg = pool.literal_to_package(lit);
+ groups
+ .entry(&pkg.name)
+ .or_default()
+ .push(&pkg.pretty_version);
+ }
+
+ let mut parts: Vec<String> = Vec::new();
+ for (name, versions) in &groups {
+ if versions.len() == 1 {
+ parts.push(format!("{name} {}", versions[0]));
+ } else {
+ let v_str = versions.join(", ");
+ parts.push(format!("{name}[{v_str}]"));
+ }
+ }
+
+ parts.sort();
+ parts.join(", ")
+}
+
+/// Same as format_providers but uses absolute value of literals.
+fn format_providers_abs(pool: &Pool, literals: &[Literal]) -> String {
+ let abs_lits: Vec<Literal> = literals
+ .iter()
+ .map(|&l| literal_to_package_id(l) as Literal)
+ .collect();
+ format_providers(pool, &abs_lits)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::dependency_resolver::pool::PoolPackageInput;
+ use crate::dependency_resolver::rule::{ReasonData, Rule, RuleReason, RuleType};
+
+ fn make_input(name: &str, version: &str, pretty: &str) -> PoolPackageInput {
+ PoolPackageInput {
+ name: name.to_string(),
+ version: version.to_string(),
+ pretty_version: pretty.to_string(),
+ requires: vec![],
+ replaces: vec![],
+ provides: vec![],
+ conflicts: vec![],
+ is_fixed: false,
+ is_alias_of: None,
+ }
+ }
+
+ #[test]
+ fn test_root_require_pretty_string() {
+ let pool = Pool::new(vec![make_input("foo/bar", "1.0.0.0", "1.0.0")], vec![]);
+
+ let mut rule_set = RuleSet::new();
+ let rule = Rule::new(
+ vec![1],
+ RuleReason::RootRequire,
+ ReasonData::RootRequire {
+ package_name: "foo/bar".to_string(),
+ constraint: "^1.0".to_string(),
+ },
+ );
+ rule_set.add(rule, RuleType::Request);
+
+ let mut problem = Problem::new();
+ problem.add_rule(0);
+
+ let output = problem.pretty_string(&pool, &rule_set);
+ assert!(output.contains("Root composer.json requires foo/bar ^1.0"));
+ assert!(output.contains("satisfiable by foo/bar 1.0.0"));
+ }
+
+ #[test]
+ fn test_same_name_pretty_string() {
+ let pool = Pool::new(
+ vec![
+ make_input("foo/bar", "1.0.0.0", "1.0.0"),
+ make_input("foo/bar", "2.0.0.0", "2.0.0"),
+ ],
+ vec![],
+ );
+
+ let mut rule_set = RuleSet::new();
+ let rule = Rule::new(
+ vec![-1, -2],
+ RuleReason::PackageSameName,
+ ReasonData::PackageName("foo/bar".to_string()),
+ );
+ rule_set.add(rule, RuleType::Package);
+
+ let mut problem = Problem::new();
+ problem.add_rule(0);
+
+ let output = problem.pretty_string(&pool, &rule_set);
+ assert!(output.contains("You can only install one version"));
+ }
+
+ #[test]
+ fn test_package_requires_pretty_string() {
+ let pool = Pool::new(
+ vec![
+ make_input("foo/bar", "1.0.0.0", "1.0.0"),
+ make_input("baz/qux", "2.0.0.0", "2.0.0"),
+ ],
+ vec![],
+ );
+
+ let mut rule_set = RuleSet::new();
+ let rule = Rule::new(
+ vec![-1, 2],
+ RuleReason::PackageRequires,
+ ReasonData::Link(super::super::pool::PoolLink {
+ source: "foo/bar".to_string(),
+ target: "baz/qux".to_string(),
+ constraint: "^2.0".to_string(),
+ }),
+ );
+ rule_set.add(rule, RuleType::Package);
+
+ let mut problem = Problem::new();
+ problem.add_rule(0);
+
+ let output = problem.pretty_string(&pool, &rule_set);
+ assert!(output.contains("foo/bar 1.0.0 requires baz/qux ^2.0"));
+ assert!(output.contains("satisfiable by baz/qux 2.0.0"));
+ }
+}