Skip to main content

vivacity_resolver/
root.rs

1//! The root package as `RootPackageLoader::load` builds it
2//! (docs/reference/RootPackageLoader.php): version (composer.json,
3//! COMPOSER_ROOT_VERSION, git, otherwise `1.0.0+no-version-set`), links,
4//! `minimum-stability`, `prefer-stable`, and the three extractions from the
5//! require constraints: aliases (`X as Y`), stability flags (`@dev`,
6//! branches), references (`#sha`).
7
8use crate::constraint::parse_constraints;
9use crate::loader;
10use crate::package::{Links, Origin, Package};
11use crate::version::{
12    self, group, normalize, regex, stability_rank, VersionError, STABILITIES_REGEX,
13};
14use pcre2::bytes::Regex;
15use serde_json::{Map, Value};
16use std::collections::BTreeMap;
17use std::path::Path;
18use std::sync::OnceLock;
19
20pub const DEFAULT_PRETTY_VERSION: &str = "1.0.0+no-version-set";
21
22/// `RootPackage::getAliases()`: an alias declared in a require.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct RootAlias {
25    pub package: String,
26    pub version: String,
27    pub alias: String,
28    pub alias_normalized: String,
29}
30
31#[derive(Debug, Clone)]
32pub struct RootPackage {
33    pub package: Package,
34    /// `RootAliasPackage`: (normalized alias, pretty alias) if `extra.branch-alias` applies.
35    pub branch_alias: Option<(String, String)>,
36    pub minimum_stability: String,
37    pub prefer_stable: bool,
38    /// name -> stability rank (`BasePackage::STABILITIES`).
39    pub stability_flags: BTreeMap<String, i32>,
40    pub aliases: Vec<RootAlias>,
41    /// name -> git reference.
42    pub references: BTreeMap<String, String>,
43    /// `config.platform`.
44    pub platform_overrides: Map<String, Value>,
45    pub manifest: Value,
46}
47
48#[derive(Debug, thiserror::Error)]
49#[error("{0}")]
50pub struct RootError(pub String);
51
52/// What a dry run changes on the root package in memory instead of in
53/// composer.json.
54#[derive(Debug, Clone, Default)]
55pub struct RootPatch {
56    /// `(name, constraint)` to add (`require` or, with `dev`, `require-dev`).
57    pub requirements: Vec<(String, String)>,
58    pub dev: bool,
59    /// `(from require-dev?, name as typed)` to remove.
60    pub removals: Vec<(bool, String)>,
61}
62
63impl RootPackage {
64    /// Loads composer.json (already parsed) with the root version guessed by
65    /// vivacity-core (same rules as RootPackageLoader + VersionGuesser).
66    pub fn load(manifest: &Value, project_dir: &Path) -> Result<RootPackage, RootError> {
67        let mut config = manifest
68            .as_object()
69            .cloned()
70            .ok_or_else(|| RootError("composer.json is not an object".into()))?;
71        if !config.contains_key("name") {
72            config.insert("name".into(), Value::String("__root__".into()));
73        }
74        let mut auto_versioned = false;
75        if !config.contains_key("version") {
76            let rv = vivacity_core::root_version::detect(manifest, project_dir);
77            if rv.pretty_version == vivacity_core::root_version::DEFAULT_PRETTY_VERSION {
78                config.insert("version".into(), Value::String("1.0.0".into()));
79                auto_versioned = true;
80            } else {
81                config.insert("version".into(), Value::String(rv.pretty_version.clone()));
82                config.insert(
83                    "version_normalized".into(),
84                    Value::String(rv.version.clone()),
85                );
86                if let Some(commit) = rv.reference {
87                    let r = serde_json::json!({"type": "", "url": "", "reference": commit});
88                    config.insert("source".into(), r.clone());
89                    config.insert("dist".into(), r);
90                }
91            }
92        }
93        let value = Value::Object(config.clone());
94        let (mut package, alias) =
95            loader::load(&value, Origin::Root, false).map_err(|e| RootError(e.0))?;
96        if auto_versioned {
97            package.pretty_version = DEFAULT_PRETTY_VERSION.to_owned();
98        }
99        let minimum_stability = match config.get("minimum-stability").and_then(Value::as_str) {
100            Some(s) => normalize_stability(s)?,
101            None => "stable".to_owned(),
102        };
103        let mut aliases = Vec::new();
104        let mut stability_flags = BTreeMap::new();
105        let mut references = BTreeMap::new();
106        for links in [&package.requires, &package.dev_requires] {
107            let map: Vec<(String, String)> = links
108                .iter()
109                // `$link->getConstraint()->getPrettyString()`: for
110                // `self.version`, the root version.
111                .map(|l| {
112                    let pretty = if l.pretty_constraint == "self.version" {
113                        // Pretty version at parseLinks time (before
114                        // `setPrettyVersion('1.0.0+no-version-set')`).
115                        if auto_versioned {
116                            "1.0.0".to_owned()
117                        } else {
118                            package.pretty_version.clone()
119                        }
120                    } else {
121                        l.pretty_constraint.clone()
122                    };
123                    (l.target.clone(), pretty)
124                })
125                .collect();
126            extract_aliases(&map, &mut aliases)?;
127            extract_stability_flags(&map, &minimum_stability, &mut stability_flags);
128            extract_references(&map, &mut references);
129            if map.iter().any(|(n, _)| *n == package.name) {
130                return Err(RootError(format!(
131                    "Root package '{}' cannot require itself in its composer.json\nDid you accidentally name your root package after an external package?",
132                    package.pretty_name
133                )));
134            }
135        }
136        let prefer_stable = config
137            .get("prefer-stable")
138            .map(|v| match v {
139                Value::Bool(b) => *b,
140                Value::Number(n) => n.as_f64() != Some(0.0),
141                Value::String(s) => !(s.is_empty() || s == "0"),
142                Value::Null => false,
143                _ => true,
144            })
145            .unwrap_or(false);
146        let platform_overrides = config
147            .get("config")
148            .and_then(|c| c.get("platform"))
149            .and_then(Value::as_object)
150            .cloned()
151            .unwrap_or_default();
152        Ok(RootPackage {
153            package,
154            branch_alias: alias,
155            minimum_stability,
156            prefer_stable,
157            stability_flags,
158            aliases,
159            references,
160            platform_overrides,
161            manifest: manifest.clone(),
162        })
163    }
164
165    /// `require` + `require-dev` links (`array_merge`: dev-requires
166    /// overwrite a duplicate target, at its position).
167    /// The in-memory root patch of a dry run (`RequireCommand`,
168    /// `RemoveCommand`): the new links are `array_merge`d onto the key's
169    /// links (a duplicate key keeps its position), removed from the other
170    /// key by their name, and the references / stability flags are
171    /// extended; aliases are not re-extracted (the reference does not).
172    pub fn apply_patch(&mut self, patch: &RootPatch) -> Result<(), RootError> {
173        if !patch.requirements.is_empty() {
174            let map: Map<String, Value> = patch
175                .requirements
176                .iter()
177                .map(|(n, c)| (n.clone(), Value::String(c.clone())))
178                .collect();
179            let kind = if patch.dev {
180                crate::package::LinkType::DevRequire
181            } else {
182                crate::package::LinkType::Require
183            };
184            let new_links = loader::parse_links(
185                &self.package.name,
186                &self.package.pretty_version,
187                kind,
188                Some(&Value::Object(map)),
189                false,
190            )
191            .map_err(|e| RootError(e.0))?;
192            let (own_links, other_links) = if kind == crate::package::LinkType::DevRequire {
193                (&mut self.package.dev_requires, &mut self.package.requires)
194            } else {
195                (&mut self.package.requires, &mut self.package.dev_requires)
196            };
197            for l in new_links.iter() {
198                own_links.insert(l.clone());
199            }
200            for (name, _) in &patch.requirements {
201                other_links.remove(name);
202            }
203            let pairs: Vec<(String, String)> = patch.requirements.clone();
204            extract_references(&pairs, &mut self.references);
205            extract_stability_flags(&pairs, &self.minimum_stability, &mut self.stability_flags);
206        }
207        // `RemoveCommand`: `unset($links[$type][$name])` with the name as
208        // typed against lowercase keys.
209        for (dev, name) in &patch.removals {
210            if *dev {
211                self.package.dev_requires.remove(name);
212            } else {
213                self.package.requires.remove(name);
214            }
215        }
216        Ok(())
217    }
218
219    pub fn all_requires(&self) -> Links {
220        let mut out = self.package.requires.clone();
221        for l in self.package.dev_requires.iter() {
222            out.insert(l.clone());
223        }
224        out
225    }
226}
227
228/// `VersionParser::normalizeStability`.
229pub fn normalize_stability(s: &str) -> Result<String, RootError> {
230    let lower = s.to_lowercase();
231    match lower.as_str() {
232        "stable" | "beta" | "alpha" | "dev" => Ok(lower),
233        "rc" => Ok("RC".to_owned()),
234        _ => Err(RootError(format!(
235            "Invalid stability string \"{s}\", expected one of stable, RC, beta, alpha or dev"
236        ))),
237    }
238}
239
240/// `RootPackageLoader::extractAliases`.
241fn extract_aliases(
242    requires: &[(String, String)],
243    aliases: &mut Vec<RootAlias>,
244) -> Result<(), RootError> {
245    static RE: OnceLock<Regex> = OnceLock::new();
246    let re = regex(
247        &RE,
248        r"(?:^|\| *|, *)([^,\s#|]+)(?:#[^ ]+)? +as +([^,\s|]+)(?:$| *\|| *,)",
249        false,
250    );
251    for (name, req) in requires {
252        if let Ok(Some(caps)) = re.captures(req.as_bytes()) {
253            let v = group(&caps, 1);
254            let a = group(&caps, 2);
255            aliases.push(RootAlias {
256                package: name.to_lowercase(),
257                version: normalize(v, Some(req)).map_err(|e: VersionError| RootError(e.0))?,
258                alias: a.to_owned(),
259                alias_normalized: normalize(a, Some(req))
260                    .map_err(|e: VersionError| RootError(e.0))?,
261            });
262        } else if req.contains(" as ") {
263            return Err(RootError(format!(
264                "Invalid alias definition in \"{name}\": \"{req}\". Aliases should be in the form \"exact-version as other-exact-version\"."
265            )));
266        }
267    }
268    Ok(())
269}
270
271/// `preg_split` of the constraints into "and" pieces across the "or"s.
272fn split_constraints(req: &str) -> Vec<String> {
273    static OR: OnceLock<Regex> = OnceLock::new();
274    static AND: OnceLock<Regex> = OnceLock::new();
275    let or = regex(&OR, r"\s*\|\|?\s*", false);
276    let and = regex(
277        &AND,
278        r"(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)",
279        false,
280    );
281    let mut out = Vec::new();
282    for part in split(or, req.trim()) {
283        out.extend(split(and, &part));
284    }
285    out
286}
287
288fn split(re: &Regex, subject: &str) -> Vec<String> {
289    let bytes = subject.as_bytes();
290    let mut out = Vec::new();
291    let mut last = 0;
292    for m in re.find_iter(bytes).flatten() {
293        if m.start() == m.end() && m.start() == last && last == bytes.len() {
294            break;
295        }
296        out.push(subject[last..m.start()].to_owned());
297        last = m.end();
298    }
299    out.push(subject[last..].to_owned());
300    out
301}
302
303/// composer-merge-plugin's `ExtraPackage::mergeStabilityFlags` for one
304/// merged file: `StabilityFlags::extractAll` over the file's own
305/// requirements (an explicit `@flag`, the most unstable across the
306/// constraint's parts, else the parsed stability when it is dev and not
307/// more stable than the minimum), each raised to at least the root's
308/// current flag, then `array_merge`d over the root's flags.
309pub fn merge_plugin_stability_flags(
310    flags: &mut BTreeMap<String, i32>,
311    minimum_stability: &str,
312    requires: &[(String, String)],
313) {
314    static AT: OnceLock<Regex> = OnceLock::new();
315    static AS: OnceLock<Regex> = OnceLock::new();
316    let at = regex(&AT, &format!("^[^@]*?@({STABILITIES_REGEX})$"), true);
317    let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
318    let minimum = stability_rank(minimum_stability);
319    for (req_name, pretty) in requires {
320        let name = req_name.to_lowercase();
321        let mut explicit: Option<i32> = None;
322        for c in split_constraints(pretty) {
323            if let Ok(Some(caps)) = at.captures(c.as_bytes()) {
324                let Ok(stab) = normalize_stability(group(&caps, 1)) else {
325                    continue;
326                };
327                let rank = stability_rank(&stab);
328                explicit = Some(explicit.map_or(rank, |e| e.max(rank)));
329            }
330        }
331        let stability = match explicit {
332            Some(e) => Some(e),
333            None => {
334                // Drop aliasing if used.
335                let v = match as_re.captures(pretty.as_bytes()) {
336                    Ok(Some(caps)) => group(&caps, 1).to_owned(),
337                    _ => pretty.clone(),
338                };
339                let rank = stability_rank(version::parse_stability(&v));
340                if rank == stability_rank("stable") || minimum > rank {
341                    None
342                } else {
343                    Some(rank)
344                }
345            }
346        };
347        if let Some(st) = stability {
348            let current = flags.get(&name).copied();
349            flags.insert(name, current.map_or(st, |c| c.max(st)));
350        }
351    }
352}
353
354/// `RootPackageLoader::extractStabilityFlags`.
355pub fn extract_stability_flags(
356    requires: &[(String, String)],
357    minimum_stability: &str,
358    flags: &mut BTreeMap<String, i32>,
359) {
360    static AT: OnceLock<Regex> = OnceLock::new();
361    static AS: OnceLock<Regex> = OnceLock::new();
362    static PLAIN: OnceLock<Regex> = OnceLock::new();
363    let at = regex(&AT, &format!("^[^@]*?@({STABILITIES_REGEX})$"), true);
364    let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
365    let plain = regex(&PLAIN, r"^[^,\s@]+$", false);
366    let minimum = stability_rank(minimum_stability);
367    for (req_name, req) in requires {
368        let constraints = split_constraints(req);
369        let mut matched = false;
370        for c in &constraints {
371            if let Ok(Some(caps)) = at.captures(c.as_bytes()) {
372                let name = req_name.to_lowercase();
373                let Ok(stab) = normalize_stability(group(&caps, 1)) else {
374                    continue;
375                };
376                let rank = stability_rank(&stab);
377                if flags.get(&name).is_some_and(|f| *f > rank) {
378                    continue;
379                }
380                flags.insert(name, rank);
381                matched = true;
382            }
383        }
384        if matched {
385            continue;
386        }
387        for c in &constraints {
388            let stripped = match as_re.captures(c.as_bytes()) {
389                Ok(Some(caps)) => group(&caps, 1).to_owned(),
390                _ => c.clone(),
391            };
392            if plain.is_match(stripped.as_bytes()).unwrap_or(false) {
393                let stability = version::parse_stability(&stripped);
394                if stability != "stable" {
395                    let name = req_name.to_lowercase();
396                    let rank = stability_rank(stability);
397                    if flags.get(&name).is_some_and(|f| *f > rank) || minimum > rank {
398                        continue;
399                    }
400                    flags.insert(name, rank);
401                }
402            }
403        }
404    }
405}
406
407/// `RootPackageLoader::extractReferences`.
408pub fn extract_references(
409    requires: &[(String, String)],
410    references: &mut BTreeMap<String, String>,
411) {
412    static AS: OnceLock<Regex> = OnceLock::new();
413    static REF: OnceLock<Regex> = OnceLock::new();
414    let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
415    let re = regex(&REF, r"^[^,\s@]+?#([a-f0-9]+)$", false);
416    for (name, req) in requires {
417        let stripped = match as_re.captures(req.as_bytes()) {
418            Ok(Some(caps)) => group(&caps, 1).to_owned(),
419            _ => req.clone(),
420        };
421        if let Ok(Some(caps)) = re.captures(stripped.as_bytes()) {
422            if version::parse_stability(&stripped) == "dev" {
423                references.insert(name.to_lowercase(), group(&caps, 1).to_owned());
424            }
425        }
426    }
427}
428
429/// Constraint of a root require (with `as`: the source part).
430pub fn root_constraint(pretty: &str) -> Result<crate::constraint::Constraint, VersionError> {
431    Ok(parse_constraints(pretty)?.constraint)
432}
433
434#[cfg(test)]
435mod tests {
436    #[test]
437    fn merge_plugin_flags_follow_the_plugin() {
438        use super::merge_plugin_stability_flags as flags_of;
439        use std::collections::BTreeMap;
440        let req = |pairs: &[(&str, &str)]| -> Vec<(String, String)> {
441            pairs
442                .iter()
443                .map(|(n, c)| ((*n).to_owned(), (*c).to_owned()))
444                .collect()
445        };
446        // Explicit flag: the most unstable part of the constraint wins,
447        // whatever the minimum stability; the name is lowercased.
448        let mut f = BTreeMap::new();
449        flags_of(
450            &mut f,
451            "stable",
452            &req(&[("Acme/Lib", "^1.0@beta || ^2.0@RC")]),
453        );
454        assert_eq!(f.get("acme/lib"), Some(&10));
455        // Parsed dev stability counts unless the minimum is more unstable
456        // than it (`$this->minimumStability > $stability`), and never a
457        // stable one; unlike the loader, no "single plain token" check.
458        let mut f = BTreeMap::new();
459        flags_of(
460            &mut f,
461            "stable",
462            &req(&[("a/b", "dev-main || ^1.0"), ("c/d", "^1.0")]),
463        );
464        assert_eq!(f.get("a/b"), Some(&20));
465        assert!(!f.contains_key("c/d"));
466        let mut f = BTreeMap::new();
467        flags_of(&mut f, "dev", &req(&[("a/b", "1.0.0-beta")]));
468        assert!(f.is_empty(), "beta is more stable than the dev minimum");
469        // `max($stability, current)`: an existing flag is never lowered,
470        // and a requirement without a flag leaves it untouched.
471        let mut f: BTreeMap<String, i32> = [("a/b".to_owned(), 20)].into();
472        flags_of(
473            &mut f,
474            "stable",
475            &req(&[("a/b", "^1.0@beta"), ("a/b", "^1.0")]),
476        );
477        assert_eq!(f.get("a/b"), Some(&20));
478        // The alias is dropped before parsing.
479        let mut f = BTreeMap::new();
480        flags_of(&mut f, "stable", &req(&[("a/b", "dev-main as 1.0.0")]));
481        assert_eq!(f.get("a/b"), Some(&20));
482    }
483
484    use super::*;
485    use serde_json::json;
486
487    #[test]
488    fn extracts_flags_aliases_references() {
489        let reqs = vec![
490            ("a/b".to_owned(), "^1.0@beta".to_owned()),
491            ("c/d".to_owned(), "dev-main as 1.0.x-dev".to_owned()),
492            ("e/f".to_owned(), "dev-main#abcdef".to_owned()),
493            ("g/h".to_owned(), "1.x-dev || ^2.0".to_owned()),
494            ("i/j".to_owned(), "^1.0".to_owned()),
495        ];
496        let mut flags = BTreeMap::new();
497        extract_stability_flags(&reqs, "stable", &mut flags);
498        assert_eq!(flags.get("a/b"), Some(&10));
499        assert_eq!(flags.get("c/d"), Some(&20));
500        assert_eq!(flags.get("e/f"), Some(&20));
501        assert_eq!(flags.get("g/h"), Some(&20));
502        assert_eq!(flags.get("i/j"), None);
503        let mut aliases = Vec::new();
504        extract_aliases(&reqs, &mut aliases).unwrap();
505        assert_eq!(aliases.len(), 1);
506        assert_eq!(aliases[0].alias_normalized, "1.0.9999999.9999999-dev");
507        let mut refs = BTreeMap::new();
508        extract_references(&reqs, &mut refs);
509        assert_eq!(refs.get("e/f").map(String::as_str), Some("abcdef"));
510    }
511
512    #[test]
513    fn loads_root_without_git() {
514        let m = json!({"name": "acme/app", "require": {"php": "^8.1", "monolog/monolog": "^3"}, "minimum-stability": "RC", "prefer-stable": true});
515        let r = RootPackage::load(&m, Path::new("/nonexistent-vivacity")).unwrap();
516        assert_eq!(r.package.pretty_version, DEFAULT_PRETTY_VERSION);
517        assert_eq!(r.package.version, "1.0.0.0");
518        assert_eq!(r.minimum_stability, "RC");
519        assert!(r.prefer_stable);
520        assert_eq!(r.package.requires.len(), 2);
521    }
522}