aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-semver
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 23:56:14 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 23:56:14 +0900
commitb922f2c7c98496564745435db5cf8d0608a52820 (patch)
treebf8e02c7b9ad59d0c0923366aed76a2a587cc669 /crates/mozart-semver
parent65993be1b2ecdc590f566b2bcfea803d0d08b5e6 (diff)
downloadphp-mozart-b922f2c7c98496564745435db5cf8d0608a52820.tar.gz
php-mozart-b922f2c7c98496564745435db5cf8d0608a52820.tar.zst
php-mozart-b922f2c7c98496564745435db5cf8d0608a52820.zip
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 `<X> as <Y>` 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) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-semver')
-rw-r--r--crates/mozart-semver/src/lib.rs51
1 files changed, 28 insertions, 23 deletions
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<VersionConstraint, String> {
/// Split on spaces or commas (AND separator), respecting that version strings
/// can contain `-` (pre-release).
fn split_and(s: &str) -> Vec<String> {
- // 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<String> = 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