1use 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#[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 pub branch_alias: Option<(String, String)>,
36 pub minimum_stability: String,
37 pub prefer_stable: bool,
38 pub stability_flags: BTreeMap<String, i32>,
40 pub aliases: Vec<RootAlias>,
41 pub references: BTreeMap<String, String>,
43 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#[derive(Debug, Clone, Default)]
55pub struct RootPatch {
56 pub requirements: Vec<(String, String)>,
58 pub dev: bool,
59 pub removals: Vec<(bool, String)>,
61}
62
63impl RootPackage {
64 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 .map(|l| {
112 let pretty = if l.pretty_constraint == "self.version" {
113 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 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 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
228pub fn manifest_error(manifest: &Value) -> Option<String> {
255 use crate::lockfile::package_naming_error;
256 let config = manifest.as_object()?;
257 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 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 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
301const 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
315fn 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
346fn 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
378pub 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 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
429pub 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
482pub 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
504pub 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 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 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 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 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 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 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 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 assert!(manifest_error(&json!({"name": "acme/app\n"})).is_some());
630 assert_eq!(manifest_error(&json!({"require": {"a/b": "*"}})), None);
632 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 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 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}