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
52impl RootPackage {
53    /// Loads composer.json (already parsed) with the root version guessed by
54    /// vivacity-core (same rules as RootPackageLoader + VersionGuesser).
55    pub fn load(manifest: &Value, project_dir: &Path) -> Result<RootPackage, RootError> {
56        let mut config = manifest
57            .as_object()
58            .cloned()
59            .ok_or_else(|| RootError("composer.json is not an object".into()))?;
60        if !config.contains_key("name") {
61            config.insert("name".into(), Value::String("__root__".into()));
62        }
63        let mut auto_versioned = false;
64        if !config.contains_key("version") {
65            let rv = vivacity_core::root_version::detect(manifest, project_dir);
66            if rv.pretty_version == vivacity_core::root_version::DEFAULT_PRETTY_VERSION {
67                config.insert("version".into(), Value::String("1.0.0".into()));
68                auto_versioned = true;
69            } else {
70                config.insert("version".into(), Value::String(rv.pretty_version.clone()));
71                config.insert(
72                    "version_normalized".into(),
73                    Value::String(rv.version.clone()),
74                );
75                if let Some(commit) = rv.reference {
76                    let r = serde_json::json!({"type": "", "url": "", "reference": commit});
77                    config.insert("source".into(), r.clone());
78                    config.insert("dist".into(), r);
79                }
80            }
81        }
82        let value = Value::Object(config.clone());
83        let (mut package, alias) =
84            loader::load(&value, Origin::Root, false).map_err(|e| RootError(e.0))?;
85        if auto_versioned {
86            package.pretty_version = DEFAULT_PRETTY_VERSION.to_owned();
87        }
88        let minimum_stability = match config.get("minimum-stability").and_then(Value::as_str) {
89            Some(s) => normalize_stability(s)?,
90            None => "stable".to_owned(),
91        };
92        let mut aliases = Vec::new();
93        let mut stability_flags = BTreeMap::new();
94        let mut references = BTreeMap::new();
95        for links in [&package.requires, &package.dev_requires] {
96            let map: Vec<(String, String)> = links
97                .iter()
98                // `$link->getConstraint()->getPrettyString()`: for
99                // `self.version`, the root version.
100                .map(|l| {
101                    let pretty = if l.pretty_constraint == "self.version" {
102                        // Pretty version at parseLinks time (before
103                        // `setPrettyVersion('1.0.0+no-version-set')`).
104                        if auto_versioned {
105                            "1.0.0".to_owned()
106                        } else {
107                            package.pretty_version.clone()
108                        }
109                    } else {
110                        l.pretty_constraint.clone()
111                    };
112                    (l.target.clone(), pretty)
113                })
114                .collect();
115            extract_aliases(&map, &mut aliases)?;
116            extract_stability_flags(&map, &minimum_stability, &mut stability_flags);
117            extract_references(&map, &mut references);
118            if map.iter().any(|(n, _)| *n == package.name) {
119                return Err(RootError(format!(
120                    "Root package '{}' cannot require itself in its composer.json\nDid you accidentally name your root package after an external package?",
121                    package.pretty_name
122                )));
123            }
124        }
125        let prefer_stable = config
126            .get("prefer-stable")
127            .map(|v| match v {
128                Value::Bool(b) => *b,
129                Value::Number(n) => n.as_f64() != Some(0.0),
130                Value::String(s) => !(s.is_empty() || s == "0"),
131                Value::Null => false,
132                _ => true,
133            })
134            .unwrap_or(false);
135        let platform_overrides = config
136            .get("config")
137            .and_then(|c| c.get("platform"))
138            .and_then(Value::as_object)
139            .cloned()
140            .unwrap_or_default();
141        Ok(RootPackage {
142            package,
143            branch_alias: alias,
144            minimum_stability,
145            prefer_stable,
146            stability_flags,
147            aliases,
148            references,
149            platform_overrides,
150            manifest: manifest.clone(),
151        })
152    }
153
154    /// `require` + `require-dev` links (`array_merge`: dev-requires
155    /// overwrite a duplicate target, at its position).
156    pub fn all_requires(&self) -> Links {
157        let mut out = self.package.requires.clone();
158        for l in self.package.dev_requires.iter() {
159            out.insert(l.clone());
160        }
161        out
162    }
163}
164
165/// `VersionParser::normalizeStability`.
166pub fn normalize_stability(s: &str) -> Result<String, RootError> {
167    let lower = s.to_lowercase();
168    match lower.as_str() {
169        "stable" | "beta" | "alpha" | "dev" => Ok(lower),
170        "rc" => Ok("RC".to_owned()),
171        _ => Err(RootError(format!(
172            "Invalid stability string \"{s}\", expected one of stable, RC, beta, alpha or dev"
173        ))),
174    }
175}
176
177/// `RootPackageLoader::extractAliases`.
178fn extract_aliases(
179    requires: &[(String, String)],
180    aliases: &mut Vec<RootAlias>,
181) -> Result<(), RootError> {
182    static RE: OnceLock<Regex> = OnceLock::new();
183    let re = regex(
184        &RE,
185        r"(?:^|\| *|, *)([^,\s#|]+)(?:#[^ ]+)? +as +([^,\s|]+)(?:$| *\|| *,)",
186        false,
187    );
188    for (name, req) in requires {
189        if let Ok(Some(caps)) = re.captures(req.as_bytes()) {
190            let v = group(&caps, 1);
191            let a = group(&caps, 2);
192            aliases.push(RootAlias {
193                package: name.to_lowercase(),
194                version: normalize(v, Some(req)).map_err(|e: VersionError| RootError(e.0))?,
195                alias: a.to_owned(),
196                alias_normalized: normalize(a, Some(req))
197                    .map_err(|e: VersionError| RootError(e.0))?,
198            });
199        } else if req.contains(" as ") {
200            return Err(RootError(format!(
201                "Invalid alias definition in \"{name}\": \"{req}\". Aliases should be in the form \"exact-version as other-exact-version\"."
202            )));
203        }
204    }
205    Ok(())
206}
207
208/// `preg_split` of the constraints into "and" pieces across the "or"s.
209fn split_constraints(req: &str) -> Vec<String> {
210    static OR: OnceLock<Regex> = OnceLock::new();
211    static AND: OnceLock<Regex> = OnceLock::new();
212    let or = regex(&OR, r"\s*\|\|?\s*", false);
213    let and = regex(
214        &AND,
215        r"(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)",
216        false,
217    );
218    let mut out = Vec::new();
219    for part in split(or, req.trim()) {
220        out.extend(split(and, &part));
221    }
222    out
223}
224
225fn split(re: &Regex, subject: &str) -> Vec<String> {
226    let bytes = subject.as_bytes();
227    let mut out = Vec::new();
228    let mut last = 0;
229    for m in re.find_iter(bytes).flatten() {
230        if m.start() == m.end() && m.start() == last && last == bytes.len() {
231            break;
232        }
233        out.push(subject[last..m.start()].to_owned());
234        last = m.end();
235    }
236    out.push(subject[last..].to_owned());
237    out
238}
239
240/// `RootPackageLoader::extractStabilityFlags`.
241pub fn extract_stability_flags(
242    requires: &[(String, String)],
243    minimum_stability: &str,
244    flags: &mut BTreeMap<String, i32>,
245) {
246    static AT: OnceLock<Regex> = OnceLock::new();
247    static AS: OnceLock<Regex> = OnceLock::new();
248    static PLAIN: OnceLock<Regex> = OnceLock::new();
249    let at = regex(&AT, &format!("^[^@]*?@({STABILITIES_REGEX})$"), true);
250    let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
251    let plain = regex(&PLAIN, r"^[^,\s@]+$", false);
252    let minimum = stability_rank(minimum_stability);
253    for (req_name, req) in requires {
254        let constraints = split_constraints(req);
255        let mut matched = false;
256        for c in &constraints {
257            if let Ok(Some(caps)) = at.captures(c.as_bytes()) {
258                let name = req_name.to_lowercase();
259                let Ok(stab) = normalize_stability(group(&caps, 1)) else {
260                    continue;
261                };
262                let rank = stability_rank(&stab);
263                if flags.get(&name).is_some_and(|f| *f > rank) {
264                    continue;
265                }
266                flags.insert(name, rank);
267                matched = true;
268            }
269        }
270        if matched {
271            continue;
272        }
273        for c in &constraints {
274            let stripped = match as_re.captures(c.as_bytes()) {
275                Ok(Some(caps)) => group(&caps, 1).to_owned(),
276                _ => c.clone(),
277            };
278            if plain.is_match(stripped.as_bytes()).unwrap_or(false) {
279                let stability = version::parse_stability(&stripped);
280                if stability != "stable" {
281                    let name = req_name.to_lowercase();
282                    let rank = stability_rank(stability);
283                    if flags.get(&name).is_some_and(|f| *f > rank) || minimum > rank {
284                        continue;
285                    }
286                    flags.insert(name, rank);
287                }
288            }
289        }
290    }
291}
292
293/// `RootPackageLoader::extractReferences`.
294fn extract_references(requires: &[(String, String)], references: &mut BTreeMap<String, String>) {
295    static AS: OnceLock<Regex> = OnceLock::new();
296    static REF: OnceLock<Regex> = OnceLock::new();
297    let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
298    let re = regex(&REF, r"^[^,\s@]+?#([a-f0-9]+)$", false);
299    for (name, req) in requires {
300        let stripped = match as_re.captures(req.as_bytes()) {
301            Ok(Some(caps)) => group(&caps, 1).to_owned(),
302            _ => req.clone(),
303        };
304        if let Ok(Some(caps)) = re.captures(stripped.as_bytes()) {
305            if version::parse_stability(&stripped) == "dev" {
306                references.insert(name.to_lowercase(), group(&caps, 1).to_owned());
307            }
308        }
309    }
310}
311
312/// Constraint of a root require (with `as`: the source part).
313pub fn root_constraint(pretty: &str) -> Result<crate::constraint::Constraint, VersionError> {
314    Ok(parse_constraints(pretty)?.constraint)
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use serde_json::json;
321
322    #[test]
323    fn extracts_flags_aliases_references() {
324        let reqs = vec![
325            ("a/b".to_owned(), "^1.0@beta".to_owned()),
326            ("c/d".to_owned(), "dev-main as 1.0.x-dev".to_owned()),
327            ("e/f".to_owned(), "dev-main#abcdef".to_owned()),
328            ("g/h".to_owned(), "1.x-dev || ^2.0".to_owned()),
329            ("i/j".to_owned(), "^1.0".to_owned()),
330        ];
331        let mut flags = BTreeMap::new();
332        extract_stability_flags(&reqs, "stable", &mut flags);
333        assert_eq!(flags.get("a/b"), Some(&10));
334        assert_eq!(flags.get("c/d"), Some(&20));
335        assert_eq!(flags.get("e/f"), Some(&20));
336        assert_eq!(flags.get("g/h"), Some(&20));
337        assert_eq!(flags.get("i/j"), None);
338        let mut aliases = Vec::new();
339        extract_aliases(&reqs, &mut aliases).unwrap();
340        assert_eq!(aliases.len(), 1);
341        assert_eq!(aliases[0].alias_normalized, "1.0.9999999.9999999-dev");
342        let mut refs = BTreeMap::new();
343        extract_references(&reqs, &mut refs);
344        assert_eq!(refs.get("e/f").map(String::as_str), Some("abcdef"));
345    }
346
347    #[test]
348    fn loads_root_without_git() {
349        let m = json!({"name": "acme/app", "require": {"php": "^8.1", "monolog/monolog": "^3"}, "minimum-stability": "RC", "prefer-stable": true});
350        let r = RootPackage::load(&m, Path::new("/nonexistent-vivacity")).unwrap();
351        assert_eq!(r.package.pretty_version, DEFAULT_PRETTY_VERSION);
352        assert_eq!(r.package.version, "1.0.0.0");
353        assert_eq!(r.minimum_stability, "RC");
354        assert!(r.prefer_stable);
355        assert_eq!(r.package.requires.len(), 2);
356    }
357}