aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/mozart-registry/src/resolver.rs79
-rw-r--r--crates/mozart-semver/src/lib.rs51
-rw-r--r--crates/mozart/tests/installer.rs2
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);