diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
| commit | 8cc1ba8a02c0318b65658f1634de378c780392b9 (patch) | |
| tree | fdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-sat-resolver/src/problem.rs | |
| parent | 72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff) | |
| download | php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.gz php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.zst php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.zip | |
refactor(workspace): consolidate crates into mozart-core
Merged mozart-archiver, mozart-autoload, mozart-registry,
mozart-sat-resolver, and mozart-vcs into mozart-core to align
the source layout with Composer's structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-sat-resolver/src/problem.rs')
| -rw-r--r-- | crates/mozart-sat-resolver/src/problem.rs | 499 |
1 files changed, 0 insertions, 499 deletions
diff --git a/crates/mozart-sat-resolver/src/problem.rs b/crates/mozart-sat-resolver/src/problem.rs deleted file mode 100644 index a1692fd..0000000 --- a/crates/mozart-sat-resolver/src/problem.rs +++ /dev/null @@ -1,499 +0,0 @@ -use crate::pool::{Literal, Pool, literal_to_package_id}; -use crate::rule::{ReasonData, Rule, RuleReason}; -use crate::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::pool::PoolPackageInput; - use crate::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(crate::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")); - } -} |
