From b922f2c7c98496564745435db5cf8d0608a52820 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 3 May 2026 23:56:14 +0900 Subject: fix(resolver): extract aliases from complex root-require constraints Mirror Composer's RootPackageLoader::extractAliases regex so root requires like `1.*||dev-feature-foo as 1.0.2||^2` and `dev-feature-foo, dev-feature-foo as 1.0.2` get every ` as ` clause stripped in place and recorded as a separate root alias entry. The previous single-atom strip left the alias inline, where the parser then took the RIGHT side per atom and never matched the actual dev-branch package. Also fix split_and so a comma-separated AND group like `dev-foo, dev-bar` splits into two atoms. The space-only operator-glue heuristic was collapsing it into a single atom because neither half starts with an operator or digit. Splitting on commas first preserves the unambiguous separator while keeping `>= 1.0.0` glued within each comma-part. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/mozart-semver/src/lib.rs | 51 ++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 23 deletions(-) (limited to 'crates/mozart-semver') diff --git a/crates/mozart-semver/src/lib.rs b/crates/mozart-semver/src/lib.rs index 0ceaf5a..d141566 100644 --- a/crates/mozart-semver/src/lib.rs +++ b/crates/mozart-semver/src/lib.rs @@ -650,34 +650,39 @@ fn parse_and_group(s: &str) -> Result { /// Split on spaces or commas (AND separator), respecting that version strings /// can contain `-` (pre-release). fn split_and(s: &str) -> Vec { - // A constraint "part" is separated by space or comma when not part of - // operator prefixes like `>=`, `<=`, `!=`, or version like `1.2.3-beta`. - // Strategy: tokenize by whitespace/comma, then re-join multi-token ranges. - let tokens: Vec<&str> = s.split([' ', ',']).filter(|t| !t.is_empty()).collect(); - + // Comma is an unambiguous AND separator (`dev-foo, dev-bar` → two atoms). + // Within a comma-part, space splits with an operator-aware heuristic so + // `>= 1.0.0` stays glued. Without the comma pre-pass, `dev-foo, dev-bar` + // collapses into a single atom because the second `dev-bar` doesn't start + // with an operator/digit and the heuristic treats it as a continuation. let mut parts: Vec = Vec::new(); - let mut current = String::new(); - - for token in tokens { - if current.is_empty() { - current = token.to_string(); - } else { - // If the token starts with an operator or a digit/^ ~/>, it's a new constraint - let starts_new = token.starts_with(|c: char| { - matches!(c, '>' | '<' | '!' | '=' | '^' | '~' | '*') || c.is_ascii_digit() - }); - if starts_new { - parts.push(current.trim().to_string()); + for segment in s.split(',') { + let tokens: Vec<&str> = segment.split_whitespace().collect(); + if tokens.is_empty() { + continue; + } + let mut current = String::new(); + for token in tokens { + if current.is_empty() { current = token.to_string(); } else { - // Continuation (e.g. part of a version string with spaces) - current.push(' '); - current.push_str(token); + // If the token starts with an operator or a digit/^ ~/>, it's a new constraint + let starts_new = token.starts_with(|c: char| { + matches!(c, '>' | '<' | '!' | '=' | '^' | '~' | '*') || c.is_ascii_digit() + }); + if starts_new { + parts.push(current.trim().to_string()); + current = token.to_string(); + } else { + // Continuation (e.g. part of a version string with spaces) + current.push(' '); + current.push_str(token); + } } } - } - if !current.is_empty() { - parts.push(current.trim().to_string()); + if !current.is_empty() { + parts.push(current.trim().to_string()); + } } parts -- cgit v1.3.1