diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-03 11:17:02 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-03 11:17:02 +0900 |
| commit | 489d00ca3f096f69f3b05f9564b23bb70a2475c7 (patch) | |
| tree | c8caf78fa618eb9971ea2f9680be5875d7b9a996 /crates/mozart-registry/src/resolver.rs | |
| parent | d175ff0aca312eafc1e125ba12a9b4e8cf81960a (diff) | |
| download | php-mozart-489d00ca3f096f69f3b05f9564b23bb70a2475c7.tar.gz php-mozart-489d00ca3f096f69f3b05f9564b23bb70a2475c7.tar.zst php-mozart-489d00ca3f096f69f3b05f9564b23bb70a2475c7.zip | |
fix(resolver): fail when a root require has no matching providers
Mirror Composer's `Solver::checkForRootRequireProblems`: a root require
that resolves to zero pool providers produces no SAT rule, so the
solver previously succeeded with an empty plan instead of reporting
the unresolvable requirement. `RuleSetGenerator::generate` now returns
those misses alongside the rule set, and `resolve()` short-circuits
into `ResolveError::NoSolution` so install/update exit with code 2 to
match Composer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry/src/resolver.rs')
| -rw-r--r-- | crates/mozart-registry/src/resolver.rs | 29 |
1 files changed, 27 insertions, 2 deletions
diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index 9cc0751..da2f444 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -861,13 +861,38 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R let mut generator = RuleSetGenerator::new(&mut pool); generator.set_ignore_platform_reqs(ignore_set); generator.set_ignore_all_platform_reqs(request.ignore_platform_reqs); - let rules = generator.generate( + let (rules, missing_root_requires) = generator.generate( &root_requires, &fixed_ids, &request.root_provide, &request.root_replace, ); + // Mirror Composer's `Solver::checkForRootRequireProblems`: a root require + // with no providers in the pool yields no SAT rule, so the solver would + // succeed with an empty plan. Surface it as an unresolvable problem + // instead, matching Composer's exit code 2 behaviour. + if !missing_root_requires.is_empty() { + let problems: Vec<String> = missing_root_requires + .iter() + .map(|(name, constraint)| match constraint.as_deref() { + Some(c) if !c.is_empty() => format!( + " - Root composer.json requires {name} {c}, no matching package found." + ), + _ => { + format!(" - Root composer.json requires {name}, no matching package found.") + } + }) + .collect(); + let report = problems + .into_iter() + .enumerate() + .map(|(i, msg)| format!(" Problem {}\n{}", i + 1, msg)) + .collect::<Vec<_>>() + .join("\n"); + return Err(ResolveError::NoSolution(report)); + } + // Create policy and solve let policy = DefaultPolicy::new(request.prefer_stable, request.prefer_lowest); let fixed_set: HashSet<u32> = fixed_ids.into_iter().collect(); @@ -1265,7 +1290,7 @@ mod tests { requires.insert("foo/foo".to_string(), Some("^1.0".to_string())); let generator = RuleSetGenerator::new(&mut pool); - let rules = generator.generate(&requires, &[], &HashMap::new(), &HashMap::new()); + let (rules, _) = generator.generate(&requires, &[], &HashMap::new(), &HashMap::new()); let policy = DefaultPolicy::default(); let solver = Solver::new(rules, &pool, policy, HashSet::new()); |
