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/// `RootPackageLoader::extractStabilityFlags`.
304pub fn extract_stability_flags(
305    requires: &[(String, String)],
306    minimum_stability: &str,
307    flags: &mut BTreeMap<String, i32>,
308) {
309    static AT: OnceLock<Regex> = OnceLock::new();
310    static AS: OnceLock<Regex> = OnceLock::new();
311    static PLAIN: OnceLock<Regex> = OnceLock::new();
312    let at = regex(&AT, &format!("^[^@]*?@({STABILITIES_REGEX})$"), true);
313    let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
314    let plain = regex(&PLAIN, r"^[^,\s@]+$", false);
315    let minimum = stability_rank(minimum_stability);
316    for (req_name, req) in requires {
317        let constraints = split_constraints(req);
318        let mut matched = false;
319        for c in &constraints {
320            if let Ok(Some(caps)) = at.captures(c.as_bytes()) {
321                let name = req_name.to_lowercase();
322                let Ok(stab) = normalize_stability(group(&caps, 1)) else {
323                    continue;
324                };
325                let rank = stability_rank(&stab);
326                if flags.get(&name).is_some_and(|f| *f > rank) {
327                    continue;
328                }
329                flags.insert(name, rank);
330                matched = true;
331            }
332        }
333        if matched {
334            continue;
335        }
336        for c in &constraints {
337            let stripped = match as_re.captures(c.as_bytes()) {
338                Ok(Some(caps)) => group(&caps, 1).to_owned(),
339                _ => c.clone(),
340            };
341            if plain.is_match(stripped.as_bytes()).unwrap_or(false) {
342                let stability = version::parse_stability(&stripped);
343                if stability != "stable" {
344                    let name = req_name.to_lowercase();
345                    let rank = stability_rank(stability);
346                    if flags.get(&name).is_some_and(|f| *f > rank) || minimum > rank {
347                        continue;
348                    }
349                    flags.insert(name, rank);
350                }
351            }
352        }
353    }
354}
355
356/// `RootPackageLoader::extractReferences`.
357fn extract_references(requires: &[(String, String)], references: &mut BTreeMap<String, String>) {
358    static AS: OnceLock<Regex> = OnceLock::new();
359    static REF: OnceLock<Regex> = OnceLock::new();
360    let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
361    let re = regex(&REF, r"^[^,\s@]+?#([a-f0-9]+)$", false);
362    for (name, req) in requires {
363        let stripped = match as_re.captures(req.as_bytes()) {
364            Ok(Some(caps)) => group(&caps, 1).to_owned(),
365            _ => req.clone(),
366        };
367        if let Ok(Some(caps)) = re.captures(stripped.as_bytes()) {
368            if version::parse_stability(&stripped) == "dev" {
369                references.insert(name.to_lowercase(), group(&caps, 1).to_owned());
370            }
371        }
372    }
373}
374
375/// Constraint of a root require (with `as`: the source part).
376pub fn root_constraint(pretty: &str) -> Result<crate::constraint::Constraint, VersionError> {
377    Ok(parse_constraints(pretty)?.constraint)
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use serde_json::json;
384
385    #[test]
386    fn extracts_flags_aliases_references() {
387        let reqs = vec![
388            ("a/b".to_owned(), "^1.0@beta".to_owned()),
389            ("c/d".to_owned(), "dev-main as 1.0.x-dev".to_owned()),
390            ("e/f".to_owned(), "dev-main#abcdef".to_owned()),
391            ("g/h".to_owned(), "1.x-dev || ^2.0".to_owned()),
392            ("i/j".to_owned(), "^1.0".to_owned()),
393        ];
394        let mut flags = BTreeMap::new();
395        extract_stability_flags(&reqs, "stable", &mut flags);
396        assert_eq!(flags.get("a/b"), Some(&10));
397        assert_eq!(flags.get("c/d"), Some(&20));
398        assert_eq!(flags.get("e/f"), Some(&20));
399        assert_eq!(flags.get("g/h"), Some(&20));
400        assert_eq!(flags.get("i/j"), None);
401        let mut aliases = Vec::new();
402        extract_aliases(&reqs, &mut aliases).unwrap();
403        assert_eq!(aliases.len(), 1);
404        assert_eq!(aliases[0].alias_normalized, "1.0.9999999.9999999-dev");
405        let mut refs = BTreeMap::new();
406        extract_references(&reqs, &mut refs);
407        assert_eq!(refs.get("e/f").map(String::as_str), Some("abcdef"));
408    }
409
410    #[test]
411    fn loads_root_without_git() {
412        let m = json!({"name": "acme/app", "require": {"php": "^8.1", "monolog/monolog": "^3"}, "minimum-stability": "RC", "prefer-stable": true});
413        let r = RootPackage::load(&m, Path::new("/nonexistent-vivacity")).unwrap();
414        assert_eq!(r.package.pretty_version, DEFAULT_PRETTY_VERSION);
415        assert_eq!(r.package.version, "1.0.0.0");
416        assert_eq!(r.minimum_stability, "RC");
417        assert!(r.prefer_stable);
418        assert_eq!(r.package.requires.len(), 2);
419    }
420}