diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/mozart-registry/src/resolver.rs | 79 | ||||
| -rw-r--r-- | crates/mozart-semver/src/lib.rs | 51 | ||||
| -rw-r--r-- | crates/mozart/tests/installer.rs | 2 |
3 files changed, 79 insertions, 53 deletions
diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index ac8b89f..a59d330 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -5,8 +5,10 @@ //! a compatible set of packages to install. use indexmap::{IndexMap, IndexSet}; +use regex::{Captures, Regex}; use std::fmt; use std::sync::Arc; +use std::sync::LazyLock; use crate::packagist; use crate::repository::{PackageQuery, RepositorySet}; @@ -289,30 +291,46 @@ struct RootAlias { alias_normalized: String, } -/// Strip a single-atom `<X> as <Y>` clause from a constraint string. Returns -/// the cleaned constraint plus the `(left, right)` pieces when an alias is -/// present. Mirrors Composer's `VersionParser::parseConstraint` `as`-strip: -/// the constraint passed to the resolver is the LEFT side, and a separate -/// alias entry is recorded for the RIGHT side. A trailing `#hex` reference -/// (`dev-main#abcd`) is also stripped — Composer's `extractAliases` regex -/// `([^,\s#|]+)(?:#[^ ]+)?` excludes it from the captured constraint, and -/// `RootPackageLoader::extractReferences` records the hash separately for -/// the post-resolve `setSourceDistReferences` pass. -fn strip_root_alias_clause(constraint: &str) -> (String, Option<(String, String)>) { +/// Composer's `RootPackageLoader::extractAliases` regex. Finds every +/// `<left> as <right>` clause inside a constraint string, including those +/// nested in OR / AND expressions (e.g. `1.*||dev-feature-foo as 1.0.2||^2` +/// or `dev-feature-foo, dev-feature-foo as 1.0.2`). The optional `#hex` +/// suffix on the LEFT atom is captured but excluded from the alias target, +/// matching `RootPackageLoader::extractReferences` which records refs out +/// of band. +static ALIAS_CLAUSE_RE: LazyLock<Regex> = LazyLock::new(|| { + Regex::new( + r"(?P<sep>^|\| *|, *)(?P<left>[^,\s#|]+)(?:#[^ ]+)? +as +(?P<right>[^,\s|]+)(?P<after>$| *\|| *,)", + ) + .expect("alias clause regex compiles") +}); + +/// Strip every `<X> as <Y>` clause from a constraint string. Returns the +/// cleaned constraint plus an entry per alias. Mirrors Composer's +/// `VersionParser::parseConstraint` `as`-strip combined with +/// `RootPackageLoader::extractAliases`: the constraint passed to the +/// resolver is the LEFT side of each atom, and a separate alias entry is +/// recorded for each RIGHT side so `RootAliasPackage`-style virtual +/// packages can be materialized later. A trailing `#hex` reference +/// (`dev-main#abcd`) on the LEFT atom is also stripped from the cleaned +/// constraint — `RootPackageLoader::extractReferences` records the hash +/// out of band for the post-resolve `setSourceDistReferences` pass. +fn strip_root_alias_clause(constraint: &str) -> (String, Vec<(String, String)>) { let trimmed = constraint.trim(); - if let Some(idx) = trimmed.find(" as ") { - let before = trimmed[..idx].trim(); - let after = trimmed[idx + 4..].trim(); - if !before.is_empty() - && !after.is_empty() - && !before.contains([' ', '\t', ',', '|']) - && !after.contains([' ', '\t', ',', '|']) - { - let cleaned = strip_inline_reference(before); - return (cleaned.clone(), Some((cleaned, after.to_string()))); - } + let mut aliases: Vec<(String, String)> = Vec::new(); + let cleaned = ALIAS_CLAUSE_RE.replace_all(trimmed, |caps: &Captures<'_>| { + let sep = caps.name("sep").map_or("", |m| m.as_str()); + let left = caps.name("left").map_or("", |m| m.as_str()); + let right = caps.name("right").map_or("", |m| m.as_str()); + let after = caps.name("after").map_or("", |m| m.as_str()); + let cleaned_left = strip_inline_reference(left); + aliases.push((cleaned_left.clone(), right.to_string())); + format!("{sep}{cleaned_left}{after}") + }); + if aliases.is_empty() { + return (strip_inline_reference(trimmed), aliases); } - (strip_inline_reference(trimmed), None) + (cleaned.into_owned(), aliases) } /// Drop a trailing `#hex` reference from a single-atom `dev-*` / `*-dev` @@ -825,17 +843,20 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R let minimum_stability = request.minimum_stability; let mut insert_root_require = |name: &str, constraint: &str| { - // Strip any `<X> as <Y>` clause first (mirrors Composer's + // Strip every `<X> as <Y>` clause first (mirrors Composer's // `parseConstraint` strip + `extractAliases` capture). The cleaned - // constraint feeds the resolver; the alias is recorded for a second - // pool-population pass once real packages are in. + // constraint feeds the resolver; each alias is recorded for a second + // pool-population pass once real packages are in. Complex constraints + // (`1.*||dev-feature-foo as 1.0.2||^2`) yield one alias entry plus a + // constraint with the ` as <Y>` segment removed in place. let (constraint_no_as, alias_pieces) = strip_root_alias_clause(constraint); - if let Some((target_atom, alias_atom)) = alias_pieces - && let (Some(target_normalized), Some(alias_normalized)) = ( + for (target_atom, alias_atom) in alias_pieces { + let (Some(target_normalized), Some(alias_normalized)) = ( normalize_root_alias_atom(&target_atom), normalize_root_alias_atom(&alias_atom), - ) - { + ) else { + continue; + }; root_aliases.push(RootAlias { package: name.to_lowercase(), version_normalized: target_normalized, 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 diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index 00964d7..728b820 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -244,7 +244,7 @@ macro_rules! installer_fixture { } installer_fixture!(abandoned_listed); -installer_fixture!(alias_in_complex_constraints, ignore); +installer_fixture!(alias_in_complex_constraints); installer_fixture!(alias_in_lock); installer_fixture!(alias_in_lock2); installer_fixture!(alias_on_unloadable_package); |
