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`.
229/// The refusals `RootPackageLoader::load` raises from the manifest alone, in
230/// its order: the root package's own name, then — after the links are parsed,
231/// which changes nothing for a raw manifest — a package that requires itself,
232/// then the name of every link of the five `BasePackage::$supportedLinkTypes`.
233///
234/// Composer refuses these for EVERY command, because `Factory::createComposer`
235/// loads the root package; vivacity only built a `RootPackage` for a
236/// resolution, so `install` and `dump-autoload` used to accept them. They call
237/// this directly — the checks that need a parsed manifest (a malformed alias,
238/// an unknown `minimum-stability`, an unparsable constraint) stay inside
239/// `load`, since reaching them means guessing the root version, i.e. running
240/// git, on the install hot path.
241///
242/// The message is returned as Composer words it, prefixes included. vivacity
243/// prints it without Symfony Console's box, as it already does for path
244/// repositories: the box is wrapped to the terminal width, followed by the
245/// command synopsis, and sometimes chained.
246///
247/// These rules are reported even when Composer would have answered with
248/// ANOTHER message — its JSON schema runs first and catches an uppercase or
249/// malformed `name`, and `parent::load` catches an unparsable constraint before
250/// the link-name loop. Reporting ours then differs in words; staying silent
251/// would differ in OUTCOME, letting `dump-autoload` write an autoloader where
252/// Composer writes nothing. Words are the cheaper divergence, and the project
253/// already takes one for the box.
254pub fn manifest_error(manifest: &Value) -> Option<String> {
255    use crate::lockfile::package_naming_error;
256    let config = manifest.as_object()?;
257    // `if (!isset($config['name'])) { '__root__' } elseif ($err = …)`.
258    //
259    // The schema runs first, and it holds the same shape pattern for `name`
260    // but CASE SENSITIVELY (`^[a-z0-9]([_.-]?[a-z0-9]+)*/…$`, measured in
261    // `res/composer-schema.json`). So a root name with an uppercase letter or a
262    // bad shape is refused with the schema's message, which vivacity does not
263    // reproduce — only what the schema lets through is ours to report, i.e. the
264    // `.json` suffix and the reserved names.
265    let name = config.get("name").and_then(Value::as_str);
266    if let Some(name) = name {
267        if let Some(err) = package_naming_error(name, false) {
268            return Some(format!("Your package name {err}"));
269        }
270    }
271    // `if (isset($links[$config['name']]))`, for require then require-dev. The
272    // targets are lowercased by `parseLinks`; the name passed the check above,
273    // so it is already lowercase ASCII here.
274    if let Some(name) = name {
275        for link_type in ["require", "require-dev"] {
276            let Some(links) = config.get(link_type).and_then(Value::as_object) else {
277                continue;
278            };
279            if links.keys().any(|k| k.to_ascii_lowercase() == name) {
280                return Some(format!(
281                    "Root package '{name}' cannot require itself in its composer.json\nDid you accidentally name your root package after an external package?"
282                ));
283            }
284        }
285    }
286    // `foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType)`,
287    // in that order, first error wins.
288    for link_type in LINK_TYPES {
289        let Some(links) = config.get(*link_type).and_then(Value::as_object) else {
290            continue;
291        };
292        for target in links.keys() {
293            if let Some(err) = package_naming_error(target, true) {
294                return Some(format!("{link_type}.{err}"));
295            }
296        }
297    }
298    None
299}
300
301/// `array_keys(BasePackage::$supportedLinkTypes)`, in its order.
302const LINK_TYPES: &[&str] = &["require", "conflict", "provide", "replace", "require-dev"];
303
304pub fn normalize_stability(s: &str) -> Result<String, RootError> {
305    let lower = s.to_lowercase();
306    match lower.as_str() {
307        "stable" | "beta" | "alpha" | "dev" => Ok(lower),
308        "rc" => Ok("RC".to_owned()),
309        _ => Err(RootError(format!(
310            "Invalid stability string \"{s}\", expected one of stable, RC, beta, alpha or dev"
311        ))),
312    }
313}
314
315/// `RootPackageLoader::extractAliases`.
316fn extract_aliases(
317    requires: &[(String, String)],
318    aliases: &mut Vec<RootAlias>,
319) -> Result<(), RootError> {
320    static RE: OnceLock<Regex> = OnceLock::new();
321    let re = regex(
322        &RE,
323        r"(?:^|\| *|, *)([^,\s#|]+)(?:#[^ ]+)? +as +([^,\s|]+)(?:$| *\|| *,)",
324        false,
325    );
326    for (name, req) in requires {
327        if let Ok(Some(caps)) = re.captures(req.as_bytes()) {
328            let v = group(&caps, 1);
329            let a = group(&caps, 2);
330            aliases.push(RootAlias {
331                package: name.to_lowercase(),
332                version: normalize(v, Some(req)).map_err(|e: VersionError| RootError(e.0))?,
333                alias: a.to_owned(),
334                alias_normalized: normalize(a, Some(req))
335                    .map_err(|e: VersionError| RootError(e.0))?,
336            });
337        } else if req.contains(" as ") {
338            return Err(RootError(format!(
339                "Invalid alias definition in \"{name}\": \"{req}\". Aliases should be in the form \"exact-version as other-exact-version\"."
340            )));
341        }
342    }
343    Ok(())
344}
345
346/// `preg_split` of the constraints into "and" pieces across the "or"s.
347fn split_constraints(req: &str) -> Vec<String> {
348    static OR: OnceLock<Regex> = OnceLock::new();
349    static AND: OnceLock<Regex> = OnceLock::new();
350    let or = regex(&OR, r"\s*\|\|?\s*", false);
351    let and = regex(
352        &AND,
353        r"(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)",
354        false,
355    );
356    let mut out = Vec::new();
357    for part in split(or, req.trim()) {
358        out.extend(split(and, &part));
359    }
360    out
361}
362
363fn split(re: &Regex, subject: &str) -> Vec<String> {
364    let bytes = subject.as_bytes();
365    let mut out = Vec::new();
366    let mut last = 0;
367    for m in re.find_iter(bytes).flatten() {
368        if m.start() == m.end() && m.start() == last && last == bytes.len() {
369            break;
370        }
371        out.push(subject[last..m.start()].to_owned());
372        last = m.end();
373    }
374    out.push(subject[last..].to_owned());
375    out
376}
377
378/// composer-merge-plugin's `ExtraPackage::mergeStabilityFlags` for one
379/// merged file: `StabilityFlags::extractAll` over the file's own
380/// requirements (an explicit `@flag`, the most unstable across the
381/// constraint's parts, else the parsed stability when it is dev and not
382/// more stable than the minimum), each raised to at least the root's
383/// current flag, then `array_merge`d over the root's flags.
384pub fn merge_plugin_stability_flags(
385    flags: &mut BTreeMap<String, i32>,
386    minimum_stability: &str,
387    requires: &[(String, String)],
388) {
389    static AT: OnceLock<Regex> = OnceLock::new();
390    static AS: OnceLock<Regex> = OnceLock::new();
391    let at = regex(&AT, &format!("^[^@]*?@({STABILITIES_REGEX})$"), true);
392    let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
393    let minimum = stability_rank(minimum_stability);
394    for (req_name, pretty) in requires {
395        let name = req_name.to_lowercase();
396        let mut explicit: Option<i32> = None;
397        for c in split_constraints(pretty) {
398            if let Ok(Some(caps)) = at.captures(c.as_bytes()) {
399                let Ok(stab) = normalize_stability(group(&caps, 1)) else {
400                    continue;
401                };
402                let rank = stability_rank(&stab);
403                explicit = Some(explicit.map_or(rank, |e| e.max(rank)));
404            }
405        }
406        let stability = match explicit {
407            Some(e) => Some(e),
408            None => {
409                // Drop aliasing if used.
410                let v = match as_re.captures(pretty.as_bytes()) {
411                    Ok(Some(caps)) => group(&caps, 1).to_owned(),
412                    _ => pretty.clone(),
413                };
414                let rank = stability_rank(version::parse_stability(&v));
415                if rank == stability_rank("stable") || minimum > rank {
416                    None
417                } else {
418                    Some(rank)
419                }
420            }
421        };
422        if let Some(st) = stability {
423            let current = flags.get(&name).copied();
424            flags.insert(name, current.map_or(st, |c| c.max(st)));
425        }
426    }
427}
428
429/// `RootPackageLoader::extractStabilityFlags`.
430pub fn extract_stability_flags(
431    requires: &[(String, String)],
432    minimum_stability: &str,
433    flags: &mut BTreeMap<String, i32>,
434) {
435    static AT: OnceLock<Regex> = OnceLock::new();
436    static AS: OnceLock<Regex> = OnceLock::new();
437    static PLAIN: OnceLock<Regex> = OnceLock::new();
438    let at = regex(&AT, &format!("^[^@]*?@({STABILITIES_REGEX})$"), true);
439    let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
440    let plain = regex(&PLAIN, r"^[^,\s@]+$", false);
441    let minimum = stability_rank(minimum_stability);
442    for (req_name, req) in requires {
443        let constraints = split_constraints(req);
444        let mut matched = false;
445        for c in &constraints {
446            if let Ok(Some(caps)) = at.captures(c.as_bytes()) {
447                let name = req_name.to_lowercase();
448                let Ok(stab) = normalize_stability(group(&caps, 1)) else {
449                    continue;
450                };
451                let rank = stability_rank(&stab);
452                if flags.get(&name).is_some_and(|f| *f > rank) {
453                    continue;
454                }
455                flags.insert(name, rank);
456                matched = true;
457            }
458        }
459        if matched {
460            continue;
461        }
462        for c in &constraints {
463            let stripped = match as_re.captures(c.as_bytes()) {
464                Ok(Some(caps)) => group(&caps, 1).to_owned(),
465                _ => c.clone(),
466            };
467            if plain.is_match(stripped.as_bytes()).unwrap_or(false) {
468                let stability = version::parse_stability(&stripped);
469                if stability != "stable" {
470                    let name = req_name.to_lowercase();
471                    let rank = stability_rank(stability);
472                    if flags.get(&name).is_some_and(|f| *f > rank) || minimum > rank {
473                        continue;
474                    }
475                    flags.insert(name, rank);
476                }
477            }
478        }
479    }
480}
481
482/// `RootPackageLoader::extractReferences`.
483pub fn extract_references(
484    requires: &[(String, String)],
485    references: &mut BTreeMap<String, String>,
486) {
487    static AS: OnceLock<Regex> = OnceLock::new();
488    static REF: OnceLock<Regex> = OnceLock::new();
489    let as_re = regex(&AS, r"^([^,\s@]+) as .+$", false);
490    let re = regex(&REF, r"^[^,\s@]+?#([a-f0-9]+)$", false);
491    for (name, req) in requires {
492        let stripped = match as_re.captures(req.as_bytes()) {
493            Ok(Some(caps)) => group(&caps, 1).to_owned(),
494            _ => req.clone(),
495        };
496        if let Ok(Some(caps)) = re.captures(stripped.as_bytes()) {
497            if version::parse_stability(&stripped) == "dev" {
498                references.insert(name.to_lowercase(), group(&caps, 1).to_owned());
499            }
500        }
501    }
502}
503
504/// Constraint of a root require (with `as`: the source part).
505pub fn root_constraint(pretty: &str) -> Result<crate::constraint::Constraint, VersionError> {
506    Ok(parse_constraints(pretty)?.constraint)
507}
508
509#[cfg(test)]
510mod tests {
511    #[test]
512    fn merge_plugin_flags_follow_the_plugin() {
513        use super::merge_plugin_stability_flags as flags_of;
514        use std::collections::BTreeMap;
515        let req = |pairs: &[(&str, &str)]| -> Vec<(String, String)> {
516            pairs
517                .iter()
518                .map(|(n, c)| ((*n).to_owned(), (*c).to_owned()))
519                .collect()
520        };
521        // Explicit flag: the most unstable part of the constraint wins,
522        // whatever the minimum stability; the name is lowercased.
523        let mut f = BTreeMap::new();
524        flags_of(
525            &mut f,
526            "stable",
527            &req(&[("Acme/Lib", "^1.0@beta || ^2.0@RC")]),
528        );
529        assert_eq!(f.get("acme/lib"), Some(&10));
530        // Parsed dev stability counts unless the minimum is more unstable
531        // than it (`$this->minimumStability > $stability`), and never a
532        // stable one; unlike the loader, no "single plain token" check.
533        let mut f = BTreeMap::new();
534        flags_of(
535            &mut f,
536            "stable",
537            &req(&[("a/b", "dev-main || ^1.0"), ("c/d", "^1.0")]),
538        );
539        assert_eq!(f.get("a/b"), Some(&20));
540        assert!(!f.contains_key("c/d"));
541        let mut f = BTreeMap::new();
542        flags_of(&mut f, "dev", &req(&[("a/b", "1.0.0-beta")]));
543        assert!(f.is_empty(), "beta is more stable than the dev minimum");
544        // `max($stability, current)`: an existing flag is never lowered,
545        // and a requirement without a flag leaves it untouched.
546        let mut f: BTreeMap<String, i32> = [("a/b".to_owned(), 20)].into();
547        flags_of(
548            &mut f,
549            "stable",
550            &req(&[("a/b", "^1.0@beta"), ("a/b", "^1.0")]),
551        );
552        assert_eq!(f.get("a/b"), Some(&20));
553        // The alias is dropped before parsing.
554        let mut f = BTreeMap::new();
555        flags_of(&mut f, "stable", &req(&[("a/b", "dev-main as 1.0.0")]));
556        assert_eq!(f.get("a/b"), Some(&20));
557    }
558
559    use super::*;
560    use serde_json::json;
561
562    #[test]
563    fn extracts_flags_aliases_references() {
564        let reqs = vec![
565            ("a/b".to_owned(), "^1.0@beta".to_owned()),
566            ("c/d".to_owned(), "dev-main as 1.0.x-dev".to_owned()),
567            ("e/f".to_owned(), "dev-main#abcdef".to_owned()),
568            ("g/h".to_owned(), "1.x-dev || ^2.0".to_owned()),
569            ("i/j".to_owned(), "^1.0".to_owned()),
570        ];
571        let mut flags = BTreeMap::new();
572        extract_stability_flags(&reqs, "stable", &mut flags);
573        assert_eq!(flags.get("a/b"), Some(&10));
574        assert_eq!(flags.get("c/d"), Some(&20));
575        assert_eq!(flags.get("e/f"), Some(&20));
576        assert_eq!(flags.get("g/h"), Some(&20));
577        assert_eq!(flags.get("i/j"), None);
578        let mut aliases = Vec::new();
579        extract_aliases(&reqs, &mut aliases).unwrap();
580        assert_eq!(aliases.len(), 1);
581        assert_eq!(aliases[0].alias_normalized, "1.0.9999999.9999999-dev");
582        let mut refs = BTreeMap::new();
583        extract_references(&reqs, &mut refs);
584        assert_eq!(refs.get("e/f").map(String::as_str), Some("abcdef"));
585    }
586
587    #[test]
588    fn loads_root_without_git() {
589        let m = json!({"name": "acme/app", "require": {"php": "^8.1", "monolog/monolog": "^3"}, "minimum-stability": "RC", "prefer-stable": true});
590        let r = RootPackage::load(&m, Path::new("/nonexistent-vivacity")).unwrap();
591        assert_eq!(r.package.pretty_version, DEFAULT_PRETTY_VERSION);
592        assert_eq!(r.package.version, "1.0.0.0");
593        assert_eq!(r.minimum_stability, "RC");
594        assert!(r.prefer_stable);
595        assert_eq!(r.package.requires.len(), 2);
596    }
597
598    #[test]
599    fn manifest_error_follows_the_loader() {
600        use serde_json::json;
601        // A real manifest is accepted, platform packages included.
602        assert_eq!(
603            manifest_error(&json!({
604                "name": "acme/app",
605                "require": {"php": ">=8.1", "ext-json": "*", "psr/log": "^3", "a-b/c--d": "*"},
606                "require-dev": {"phpunit/phpunit": "^11"},
607            })),
608            None
609        );
610        // The root name: only what the schema lets through is ours to report.
611        assert_eq!(
612            manifest_error(&json!({"name": "acme/thing.json"})).as_deref(),
613            Some("Your package name acme/thing.json is invalid, package names can not end in .json, consider renaming it or perhaps using a -json suffix instead.")
614        );
615        assert!(manifest_error(&json!({"name": "con/thing"})).is_some());
616        // An uppercase or malformed root name is refused by Composer's JSON
617        // schema first, so its message is the schema's and ours is the
618        // loader's. Both refuse and both write nothing, which is the part that
619        // matters; staying silent here would let `dump-autoload` write.
620        assert_eq!(
621            manifest_error(&json!({"name": "acme/ThingBaz"})).as_deref(),
622            Some("Your package name acme/ThingBaz is invalid, it should not contain uppercase characters. We suggest using acme/thing-baz instead.")
623        );
624        assert!(manifest_error(&json!({"name": "acme"}))
625            .is_some_and(|e| e
626                .starts_with("Your package name acme is invalid, it should have a vendor name")));
627        // A trailing newline: the schema's `$` tolerates it, the loader's `D`
628        // does not — so the loader is what answers, and so do we.
629        assert!(manifest_error(&json!({"name": "acme/app\n"})).is_some());
630        // No name at all is `__root__`, never an error.
631        assert_eq!(manifest_error(&json!({"require": {"a/b": "*"}})), None);
632        // A package that requires itself, from either section.
633        for section in ["require", "require-dev"] {
634            let m = json!({"name": "acme/app", section: {"acme/app": "*"}});
635            assert!(
636                manifest_error(&m).is_some_and(|e| e
637                    .starts_with("Root package 'acme/app' cannot require itself")
638                    && e.contains("\nDid you accidentally")),
639                "{section}"
640            );
641        }
642        // The five link types, each prefixed with its own name.
643        for section in ["require", "conflict", "provide", "replace", "require-dev"] {
644            let m = json!({"name": "acme/app", section: {"Acme/Thing": "^1"}});
645            assert_eq!(
646                manifest_error(&m).as_deref(),
647                Some(&*format!("{section}.Acme/Thing is invalid, it should not contain uppercase characters. Please use acme/thing instead.")),
648                "{section}"
649            );
650        }
651        // Composer's order: the root name first, then self-require, then the
652        // link names in the order of `BasePackage::$supportedLinkTypes`.
653        let all = json!({
654            "name": "acme/thing.json",
655            "require": {"acme/thing.json": "*", "Bad/Name": "*"},
656            "conflict": {"Other/Name": "*"},
657        });
658        assert!(manifest_error(&all).is_some_and(|e| e.starts_with("Your package name")));
659        let links = json!({"require": {"Bad/Name": "*"}, "conflict": {"Other/Name": "*"}});
660        assert!(manifest_error(&links).is_some_and(|e| e.starts_with("require.Bad/Name")));
661    }
662}