1use crate::constraint::{Constraint, Op};
15use crate::package::{Origin, Package};
16use crate::phpver::version_compare;
17use crate::platform::is_platform_package;
18use crate::pool::{Pool, Repository, RepositorySet, Request};
19use crate::repository::{is_package_acceptable, Advisory};
20use crate::rule::{Reason, Rule};
21use crate::solver::SolvedProblem;
22use std::cmp::Ordering;
23use std::collections::{BTreeMap, BTreeSet, HashSet};
24
25pub struct MessageContext<'a> {
27 pub arena: &'a mut Vec<Package>,
28 pub set: &'a RepositorySet,
29 pub pool: &'a Pool,
30 pub request: &'a Request,
31 pub is_verbose: bool,
32 pub ini_files: &'a [String],
34 pub loaded_extensions: &'a BTreeSet<String>,
36}
37
38const ALLOW_UNACCEPTABLE_STABILITIES: u8 = 4;
40const ALLOW_SHADOWED_REPOSITORIES: u8 = 8;
42
43#[derive(Clone, Copy)]
45pub struct PrettyConstraint<'a> {
46 pub constraint: &'a Constraint,
47 pub pretty: &'a str,
48}
49
50pub fn pretty_string(
56 ctx: &mut MessageContext<'_>,
57 problems: &[SolvedProblem],
58 is_dev_extraction: bool,
59) -> String {
60 let installed_map = present_map(ctx);
61 let mut missing_extensions: Vec<String> = Vec::new();
62 let mut caused_by_lock = false;
63 let mut texts: Vec<String> = Vec::new();
64 for problem in problems {
65 texts.push(format!(
66 "{}\n",
67 problem_pretty_string(ctx, problem, &installed_map)
68 ));
69 for rule in problem.sections.iter().flatten() {
72 if let Some(required) = required_package(ctx.arena, rule) {
73 if required.starts_with("ext-") && !missing_extensions.contains(&required) {
74 missing_extensions.push(required);
75 }
76 }
77 }
78 caused_by_lock = caused_by_lock
79 || problem
80 .reasons()
81 .iter()
82 .any(|rule| is_caused_by_lock(ctx, rule));
83 }
84 let mut text = String::from("\n");
85 let mut seen: Vec<&String> = Vec::new();
86 let mut i = 1;
87 for t in &texts {
88 if seen.contains(&t) {
89 continue;
90 }
91 seen.push(t);
92 text.push_str(&format!(" Problem {i}{t}"));
93 i += 1;
94 }
95 let mut hints: Vec<String> = Vec::new();
96 if !is_dev_extraction
97 && (text.contains("could not be found") || text.contains("no matching package found"))
98 {
99 hints.push("Potential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.\n - It's a private package and you forgot to add a custom repository to find it\n\nRead <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.".to_owned());
100 }
101 if !missing_extensions.is_empty() {
102 hints.push(extension_hint(ctx.ini_files, &missing_extensions));
103 }
104 if caused_by_lock
105 && !is_dev_extraction
106 && !ctx.request.update_allow_transitive_root_dependencies()
107 {
108 hints.push("Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.".to_owned());
109 }
110 if text.contains("found composer-plugin-api[2.0.0] but it does not match")
111 && text.contains("- ocramius/package-versions")
112 {
113 hints.push("<warning>ocramius/package-versions only provides support for Composer 2 in 1.8+, which requires PHP 7.4.</warning>\nIf you can not upgrade PHP you can require <info>composer/package-versions-deprecated</info> to resolve this with PHP 7.0+.".to_owned());
114 }
115 if text.contains("found composer-plugin-api[2.0.0] but it does not match") {
116 hints.push("You are using Composer 2, which some of your plugins seem to be incompatible with. Make sure you update your plugins or report a plugin-issue to ask them to support Composer 2.".to_owned());
117 }
118 if !hints.is_empty() {
119 text.push('\n');
120 text.push_str(&hints.join("\n\n"));
121 }
122 strip_output_styles(&text)
123}
124
125fn extension_hint(ini_files: &[String], missing: &[String]) -> String {
127 let mut paths: Vec<&String> = ini_files.iter().collect();
128 if paths.first().is_some_and(|p| p.is_empty()) {
129 if paths.len() == 1 {
130 return String::new();
131 }
132 paths.remove(0);
133 }
134 let args: Vec<String> = missing
135 .iter()
136 .map(|e| format!("--ignore-platform-req={e}"))
137 .collect();
138 let mut text = String::from(
139 "To enable extensions, verify that they are enabled in your .ini files:\n - ",
140 );
141 text.push_str(
142 &paths
143 .iter()
144 .map(|p| p.as_str())
145 .collect::<Vec<_>>()
146 .join("\n - "),
147 );
148 text.push_str(
149 "\nYou can also run `php --ini` in a terminal to see which files are used by PHP in CLI mode.",
150 );
151 text.push_str(&format!(
152 "\nAlternatively, you can run Composer with `{}` to temporarily ignore these required extensions.",
153 args.join(" ")
154 ));
155 text
156}
157
158fn required_package(arena: &[Package], rule: &Rule) -> Option<String> {
160 match &rule.reason {
161 Reason::RootRequire { package_name, .. } => Some(package_name.clone()),
162 Reason::PackageRequires(link) => Some(link.target.clone()),
163 Reason::Fixed { package } | Reason::LockedFilterListRemoved { package } => {
164 Some(arena[*package].name.clone())
165 }
166 _ => None,
167 }
168}
169
170fn present_map(ctx: &MessageContext<'_>) -> HashSet<usize> {
173 let mut map = HashSet::new();
174 if let Some(locked) = &ctx.request.locked_repository {
175 for &idx in locked {
176 if let Some(id) = ctx.pool.id_of(idx) {
177 map.insert(id);
178 }
179 }
180 }
181 for &idx in &ctx.request.fixed_packages {
182 if let Some(id) = ctx.pool.id_of(idx) {
183 map.insert(id);
184 }
185 }
186 map
187}
188
189pub fn strip_output_styles(text: &str) -> String {
192 let mut out = String::with_capacity(text.len());
193 let mut rest = text;
194 while let Some(start) = rest.find('<') {
195 out.push_str(&rest[..start]);
196 let after = &rest[start + 1..];
197 let Some(end) = after.find('>') else {
198 out.push_str(rest);
199 return out;
200 };
201 let tag = &after[..end];
202 let name = tag.trim_start_matches('/');
203 let known = matches!(
204 name,
205 "error" | "warning" | "info" | "comment" | "question" | "highlight" | ""
206 ) || name.starts_with("href=")
207 || name.starts_with("fg=")
208 || name.starts_with("bg=")
209 || name.starts_with("options=");
210 if known && !tag.contains('\n') {
211 rest = &after[end + 1..];
212 } else {
213 out.push('<');
214 rest = after;
215 }
216 }
217 out.push_str(rest);
218 out
219}
220
221fn problem_pretty_string(
226 ctx: &mut MessageContext<'_>,
227 problem: &SolvedProblem,
228 installed_map: &HashSet<usize>,
229) -> String {
230 let reasons: Vec<Rule> = problem.reasons().into_iter().cloned().collect();
231 if reasons.len() == 1 {
232 let rule = &reasons[0];
233 match &rule.reason {
234 Reason::RootRequire {
235 package_name,
236 constraint,
237 pretty,
238 } if ctx
239 .pool
240 .what_provides(ctx.arena, package_name, Some(constraint))
241 .is_empty() =>
242 {
243 let (a, b) = missing_package_reason(
244 ctx,
245 package_name,
246 Some(PrettyConstraint { constraint, pretty }),
247 );
248 return format!("\n {a}{b}");
249 }
250 Reason::LockedFilterListRemoved { package } => {
251 let (a, b) = missing_locked_package_reason(ctx, *package);
252 return format!("\n {a}{b}");
253 }
254 _ => {}
255 }
256 }
257 let mut sorted = reasons;
258 sorted.sort_by(|r1, r2| {
259 let p1 = rule_priority(r1);
260 let p2 = rule_priority(r2);
261 if p1 != p2 {
262 return p2.cmp(&p1);
263 }
264 php_string_cmp(&sortable_string(ctx, r1), &sortable_string(ctx, r2))
265 });
266 format_deduplicated_rules(ctx, &sorted, " ", installed_map)
267}
268
269fn rule_priority(rule: &Rule) -> i32 {
271 match &rule.reason {
272 Reason::Fixed { .. } | Reason::LockedFilterListRemoved { .. } => 3,
273 Reason::RootRequire { .. } => 2,
274 Reason::PackageConflict(_) | Reason::PackageRequires(_) => 1,
275 Reason::PackageSameName(_)
276 | Reason::Learned(_)
277 | Reason::PackageAlias { .. }
278 | Reason::PackageInverseAlias { .. } => 0,
279 }
280}
281
282fn sortable_string(ctx: &MessageContext<'_>, rule: &Rule) -> String {
284 match &rule.reason {
285 Reason::RootRequire { package_name, .. } => package_name.clone(),
286 Reason::Fixed { package } | Reason::LockedFilterListRemoved { package } => {
287 package_to_string(ctx, *package)
288 }
289 Reason::PackageConflict(link) | Reason::PackageRequires(link) => {
290 let source = source_package(ctx, rule);
291 format!(
292 "{}//{}",
293 package_to_string(ctx, source),
294 link_pretty_string(&ctx.arena[source], link)
295 )
296 }
297 Reason::PackageSameName(name) => name.clone(),
298 Reason::PackageAlias { alias } => package_to_string(ctx, *alias),
299 Reason::PackageInverseAlias { package } => package_to_string(ctx, *package),
300 Reason::Learned(_) => rule
301 .literals
302 .iter()
303 .map(|l| l.to_string())
304 .collect::<Vec<_>>()
305 .join("-"),
306 }
307}
308
309fn package_to_string(ctx: &MessageContext<'_>, idx: usize) -> String {
312 let p = &ctx.arena[idx];
313 match p.alias_of {
314 Some(base) => format!(
315 "{} ({}alias of {})",
316 p.unique_name(),
317 if p.root_package_alias { "root " } else { "" },
318 ctx.arena[base].version
319 ),
320 None => p.unique_name(),
321 }
322}
323
324pub fn php_string_cmp(a: &str, b: &str) -> Ordering {
326 match (php_numeric(a), php_numeric(b)) {
327 (Some(x), Some(y)) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
328 _ => a.as_bytes().cmp(b.as_bytes()),
329 }
330}
331
332fn php_numeric(s: &str) -> Option<f64> {
335 let t = s.trim_matches([' ', '\t', '\n', '\r', '\x0B', '\x0C']);
336 if t.is_empty() {
337 return None;
338 }
339 let body = t.strip_prefix(['+', '-']).unwrap_or(t);
340 let (mantissa, exponent) = match body.split_once(['e', 'E']) {
341 Some((m, e)) => (m, Some(e)),
342 None => (body, None),
343 };
344 let (int_part, frac) = match mantissa.split_once('.') {
345 Some((i, f)) => (i, Some(f)),
346 None => (mantissa, None),
347 };
348 let digits = |x: &str| !x.is_empty() && x.bytes().all(|b| b.is_ascii_digit());
349 let mantissa_ok = match frac {
350 Some(f) => {
351 (digits(int_part) || int_part.is_empty())
352 && (digits(f) || f.is_empty())
353 && !(int_part.is_empty() && f.is_empty())
354 }
355 None => digits(int_part),
356 };
357 if !mantissa_ok {
358 return None;
359 }
360 if let Some(e) = exponent {
361 let e = e.strip_prefix(['+', '-']).unwrap_or(e);
362 if !digits(e) {
363 return None;
364 }
365 }
366 t.parse::<f64>().ok()
367}
368
369fn source_package(ctx: &MessageContext<'_>, rule: &Rule) -> usize {
371 match &rule.reason {
372 Reason::PackageConflict(link) => {
373 let p1 = dedup_default_branch_alias(ctx, ctx.pool.literal_to_package(rule.literals[0]));
374 let p2 = dedup_default_branch_alias(ctx, ctx.pool.literal_to_package(rule.literals[1]));
375 if link.source == ctx.arena[p1].name {
376 p1
377 } else {
378 p2
379 }
380 }
381 _ => dedup_default_branch_alias(ctx, ctx.pool.literal_to_package(rule.literals[0])),
382 }
383}
384
385fn dedup_default_branch_alias(ctx: &MessageContext<'_>, idx: usize) -> usize {
387 let p = &ctx.arena[idx];
388 match p.alias_of {
389 Some(base) if p.pretty_version == "9999999-dev" => base,
390 _ => idx,
391 }
392}
393
394fn link_pretty_string(source: &Package, link: &crate::package::Link) -> String {
397 format!(
398 "{} {} {} {}",
399 source.pretty_string(),
400 link.kind.description(),
401 link.target,
402 link_constraint_pretty(source, link)
403 )
404}
405
406fn link_constraint_pretty(source: &Package, link: &crate::package::Link) -> String {
410 if link.pretty_constraint == "self.version" {
411 source.pretty_version.clone()
412 } else {
413 link.pretty_constraint.clone()
414 }
415}
416
417type TemplatePackages = Vec<(String, BTreeMap<String, String>)>;
420
421fn format_deduplicated_rules(
423 ctx: &mut MessageContext<'_>,
424 rules: &[Rule],
425 indent: &str,
426 installed_map: &HashSet<usize>,
427) -> String {
428 let mut messages: Vec<String> = Vec::new();
430 let mut templates: Vec<(String, TemplatePackages)> = Vec::new();
431 for rule in rules {
432 let message = rule_pretty_string(ctx, rule, installed_map);
433 let deduplicatable = matches!(
434 rule.reason,
435 Reason::PackageRequires(_) | Reason::PackageConflict(_)
436 );
437 let parsed = deduplicatable.then(|| parse_dedup_head(&message)).flatten();
438 if let Some((package, version, _)) = parsed {
439 let escaped = message.replace('%', "%%");
440 let head_len = escaped
441 .find(' ')
442 .and_then(|i| escaped[i + 1..].find(' ').map(|j| i + 1 + j + 1));
443 let template = match head_len {
444 Some(n) => format!("%s%s {}", &escaped[n..]),
445 None => escaped.clone(),
446 };
447 messages.push(template.clone());
448 let normalized = crate::version::normalize(&version, None).unwrap_or(version.clone());
449 let source = source_package(ctx, rule);
450 let entry = match templates.iter_mut().find(|(t, _)| *t == template) {
451 Some((_, e)) => e,
452 None => {
453 templates.push((template.clone(), Vec::new()));
454 &mut templates.last_mut().expect("just pushed").1
455 }
456 };
457 let versions = match entry.iter_mut().find(|(p, _)| *p == package) {
458 Some((_, v)) => v,
459 None => {
460 entry.push((package.clone(), BTreeMap::new()));
461 &mut entry.last_mut().expect("just pushed").1
462 }
463 };
464 versions.insert(normalized, version);
465 for (v, pretty) in ctx.pool.removed_versions_by_package(source) {
466 versions.insert(v, pretty);
467 }
468 } else if !message.is_empty() {
469 messages.push(message);
470 }
471 }
472 let mut result: Vec<String> = Vec::new();
473 let mut seen: Vec<&String> = Vec::new();
474 for message in &messages {
475 if seen.contains(&message) {
476 continue;
477 }
478 seen.push(message);
479 if let Some((_, packages)) = templates.iter().find(|(t, _)| t == message) {
480 for (package, versions) in packages {
481 let mut list: Vec<(&String, &String)> = versions.iter().collect();
482 list.sort_by(|a, b| version_compare(a.0, b.0));
483 let mut pretty: Vec<String> = list.into_iter().map(|(_, p)| p.clone()).collect();
484 if !ctx.is_verbose {
485 pretty = condense_version_list(&versions_in_order(versions), 1, 16);
486 }
487 let rest = message.replacen("%s%s ", "", 1).replace("%%", "%");
488 if pretty.len() > 1 {
489 let rest = if let Some(r) = rest.strip_prefix("requires") {
491 format!("require{r}")
492 } else if let Some(r) = rest.strip_prefix("conflicts") {
493 format!("conflict{r}")
494 } else {
495 rest
496 };
497 result.push(format!("{package}[{}] {rest}", pretty.join(", ")));
498 } else {
499 result.push(format!("{package} {} {rest}", pretty[0]));
500 }
501 }
502 } else {
503 result.push(message.clone());
504 }
505 }
506 format!("\n{indent}- {}", result.join(&format!("\n{indent}- ")))
507}
508
509fn parse_dedup_head(message: &str) -> Option<(String, String, String)> {
511 let mut parts = message.splitn(3, ' ');
512 let package = parts.next()?;
513 let version = parts.next()?;
514 let rest = parts.next()?;
515 if package.is_empty() || version.is_empty() {
516 return None;
517 }
518 let kind = if rest.starts_with("requires") {
519 "requires"
520 } else if rest.starts_with("conflicts") {
521 "conflicts"
522 } else {
523 return None;
524 };
525 Some((package.to_owned(), version.to_owned(), kind.to_owned()))
526}
527
528fn versions_in_order(versions: &BTreeMap<String, String>) -> Vec<(String, String)> {
530 let mut list: Vec<(String, String)> = versions
531 .iter()
532 .map(|(v, p)| (v.clone(), p.clone()))
533 .collect();
534 list.sort_by(|a, b| version_compare(&a.0, &b.0));
535 list
536}
537
538fn condense_version_list(versions: &[(String, String)], max: usize, max_dev: usize) -> Vec<String> {
541 if versions.len() <= max {
542 return versions.iter().map(|(_, p)| p.clone()).collect();
543 }
544 let mut by_major: Vec<(String, Vec<String>)> = Vec::new();
545 for (version, pretty) in versions {
546 let major = if version.to_lowercase().starts_with("dev-") {
547 "dev".to_owned()
548 } else {
549 let digits: String = version.chars().take_while(|c| c.is_ascii_digit()).collect();
550 if !digits.is_empty() && version[digits.len()..].starts_with('.') {
551 digits
552 } else {
553 version.clone()
554 }
555 };
556 match by_major.iter_mut().find(|(m, _)| *m == major) {
557 Some((_, list)) => list.push(pretty.clone()),
558 None => by_major.push((major, vec![pretty.clone()])),
559 }
560 }
561 let mut filtered: Vec<String> = Vec::new();
562 for (major, list) in by_major {
563 let cap = if major == "dev" { max_dev } else { max };
564 if list.len() > cap {
565 filtered.push(list[0].clone());
566 filtered.push("...".to_owned());
567 filtered.push(list[list.len() - 1].clone());
568 } else {
569 filtered.extend(list);
570 }
571 }
572 filtered
573}
574
575fn is_caused_by_lock(ctx: &MessageContext<'_>, rule: &Rule) -> bool {
577 let (target, constraint) = match &rule.reason {
578 Reason::PackageRequires(link) => (link.target.as_str(), &link.constraint),
579 Reason::RootRequire {
580 package_name,
581 constraint,
582 ..
583 } => (package_name.as_str(), constraint),
584 _ => return false,
585 };
586 if is_platform_package(target) {
587 return false;
588 }
589 let Some(locked) = &ctx.request.locked_repository else {
590 return false;
591 };
592 for &idx in locked {
593 let p = &ctx.arena[idx];
594 if p.name != target {
595 continue;
596 }
597 if ctx.pool.is_unacceptable_fixed_or_locked(idx) {
598 return true;
599 }
600 if !constraint.matches(&Constraint::new(Op::Eq, p.version.clone())) {
601 return true;
602 }
603 if matches!(rule.reason, Reason::PackageRequires(_)) && !ctx.request.is_locked_package(idx)
604 {
605 return true;
606 }
607 break;
608 }
609 false
610}
611
612fn rule_pretty_string(
617 ctx: &mut MessageContext<'_>,
618 rule: &Rule,
619 installed_map: &HashSet<usize>,
620) -> String {
621 let literals = &rule.literals;
622 match &rule.reason {
623 Reason::RootRequire {
624 package_name,
625 constraint,
626 pretty,
627 } => {
628 let packages = ctx
629 .pool
630 .what_provides(ctx.arena, package_name, Some(constraint));
631 if packages.is_empty() {
632 return format!(
633 "No package found to satisfy root composer.json require {package_name} {pretty}"
634 );
635 }
636 let non_alias: Vec<usize> = packages
637 .iter()
638 .map(|&id| ctx.pool.package_by_id(id))
639 .filter(|&idx| ctx.arena[idx].alias_of.is_none())
640 .collect();
641 if non_alias.len() == 1 && ctx.request.is_locked_package(non_alias[0]) {
642 let p = &ctx.arena[non_alias[0]];
643 return format!(
644 "{} is locked to version {} and an update of this package was not requested.",
645 p.pretty_name, p.pretty_version
646 );
647 }
648 let idxs: Vec<usize> = packages
649 .iter()
650 .map(|&id| ctx.pool.package_by_id(id))
651 .collect();
652 format!(
653 "Root composer.json requires {package_name} {pretty} -> satisfiable by {}.",
654 package_list(ctx, &idxs, Some(constraint), false)
655 )
656 }
657 Reason::Fixed { package } => {
658 let idx = dedup_default_branch_alias(ctx, *package);
659 let p = &ctx.arena[idx];
660 if ctx.request.is_locked_package(idx) {
661 return format!(
662 "{} is locked to version {} and an update of this package was not requested.",
663 p.pretty_name, p.pretty_version
664 );
665 }
666 format!(
667 "{} is present at version {} and cannot be modified by Composer",
668 p.pretty_name, p.pretty_version
669 )
670 }
671 Reason::LockedFilterListRemoved { package } => {
672 let idx = dedup_default_branch_alias(ctx, *package);
673 let p = &ctx.arena[idx];
674 format!(
675 "{} {} was removed by a dependency policy (e.g. malware) and cannot be installed.",
676 p.pretty_name, p.pretty_version
677 )
678 }
679 Reason::PackageConflict(link) => {
680 let mut package1 =
681 dedup_default_branch_alias(ctx, ctx.pool.literal_to_package(literals[0]));
682 let mut package2 =
683 dedup_default_branch_alias(ctx, ctx.pool.literal_to_package(literals[1]));
684 let mut conflict_target = ctx.arena[package1].pretty_string();
685 if link.source == ctx.arena[package1].name {
686 std::mem::swap(&mut package1, &mut package2);
687 conflict_target = format!(
688 "{} {}",
689 ctx.arena[package1].pretty_name, link.pretty_constraint
690 );
691 }
692 if link.target != ctx.arena[package1].name {
693 let p1 = &ctx.arena[package1];
694 let mut provided: Option<(&str, &str)> = None;
697 for l in p1.provides.iter() {
698 if l.target == link.target {
699 provided = Some(("provides", &l.pretty_constraint));
700 break;
701 }
702 }
703 for l in p1.replaces.iter() {
704 if l.target == link.target {
705 provided = Some(("replaces", &l.pretty_constraint));
706 break;
707 }
708 }
709 if let Some((kind, constraint)) = provided {
710 conflict_target = format!(
711 "{} {} ({} {} {} {})",
712 link.target,
713 link.pretty_constraint,
714 p1.pretty_string(),
715 kind,
716 link.target,
717 constraint
718 );
719 }
720 }
721 format!(
722 "{} conflicts with {}.",
723 ctx.arena[package2].pretty_string(),
724 conflict_target
725 )
726 }
727 Reason::PackageRequires(link) => {
728 let source = dedup_default_branch_alias(ctx, ctx.pool.literal_to_package(literals[0]));
729 let requires: Vec<usize> = literals[1..]
730 .iter()
731 .map(|&l| ctx.pool.literal_to_package(l))
732 .collect();
733 let text = link_pretty_string(&ctx.arena[source], link);
734 if !requires.is_empty() {
735 return format!(
736 "{text} -> satisfiable by {}.",
737 package_list(ctx, &requires, Some(&link.constraint), false)
738 );
739 }
740 let pretty = link_constraint_pretty(&ctx.arena[source], link);
741 let (_, reason) = missing_package_reason(
742 ctx,
743 &link.target,
744 Some(PrettyConstraint {
745 constraint: &link.constraint,
746 pretty: &pretty,
747 }),
748 );
749 format!("{text} -> {reason}")
750 }
751 Reason::PackageSameName(replaced_name) => {
752 let mut package_names: Vec<String> = Vec::new();
753 for &l in literals {
754 let name = ctx.arena[ctx.pool.literal_to_package(l)].name.clone();
755 if !package_names.contains(&name) {
756 package_names.push(name);
757 }
758 }
759 let all: Vec<usize> = literals
760 .iter()
761 .map(|&l| ctx.pool.literal_to_package(l))
762 .collect();
763 if package_names.len() > 1 {
764 let reason = if !package_names.contains(replaced_name) {
765 format!(
766 "They {} replace {replaced_name} and thus cannot coexist.",
767 if literals.len() == 2 { "both" } else { "all" }
768 )
769 } else {
770 let replacers: Vec<&String> = package_names
771 .iter()
772 .filter(|n| *n != replaced_name)
773 .collect();
774 let head = if replacers.len() == 1 {
775 format!("{} replaces ", replacers[0])
776 } else {
777 format!(
778 "[{}] replace ",
779 replacers
780 .iter()
781 .map(|s| s.as_str())
782 .collect::<Vec<_>>()
783 .join(", ")
784 )
785 };
786 format!("{head}{replaced_name} and thus cannot coexist with it.")
787 };
788 let mut installed: Vec<usize> = Vec::new();
789 let mut removable: Vec<usize> = Vec::new();
790 for &l in literals {
791 let idx = ctx.pool.literal_to_package(l);
792 if installed_map.contains(&(l.unsigned_abs() as usize)) {
793 installed.push(idx);
794 } else {
795 removable.push(idx);
796 }
797 }
798 if !installed.is_empty() && !removable.is_empty() {
799 return format!(
800 "{} cannot be installed as that would require removing {}. {reason}",
801 package_list(ctx, &removable, None, true),
802 package_list(ctx, &installed, None, true)
803 );
804 }
805 return format!(
806 "Only one of these can be installed: {}. {reason}",
807 package_list(ctx, &all, None, true)
808 );
809 }
810 format!(
811 "You can only install one version of a package, so only one of these can be installed: {}.",
812 package_list(ctx, &all, None, true)
813 )
814 }
815 Reason::Learned(_) => {
816 let rule_text = if literals.len() == 1 {
817 literal_to_pretty_string(ctx, literals[0], installed_map)
818 } else {
819 let mut groups: Vec<(&str, Vec<usize>)> = Vec::new();
820 for &l in literals {
821 let idx = ctx.pool.literal_to_package(l);
822 let id = l.unsigned_abs() as usize;
823 let group = if installed_map.contains(&id) {
824 if l > 0 {
825 "keep"
826 } else {
827 "remove"
828 }
829 } else if l > 0 {
830 "install"
831 } else {
832 "don't install"
833 };
834 let dedup = dedup_default_branch_alias(ctx, idx);
835 match groups.iter_mut().find(|(g, _)| *g == group) {
836 Some((_, list)) => list.push(dedup),
837 None => groups.push((group, vec![dedup])),
838 }
839 }
840 let mut texts: Vec<String> = Vec::new();
841 for (group, packages) in groups {
842 texts.push(format!(
843 "{group}{} {}",
844 if packages.len() > 1 { " one of" } else { "" },
845 package_list(ctx, &packages, None, false)
846 ));
847 }
848 texts.join(" | ")
849 };
850 format!("Conclusion: {rule_text} (conflict analysis result)")
851 }
852 Reason::PackageAlias { .. } => {
853 let alias = ctx.pool.literal_to_package(literals[0]);
854 if ctx.arena[alias].version == "9999999-dev" {
855 return String::new();
856 }
857 let package = dedup_default_branch_alias(ctx, ctx.pool.literal_to_package(literals[1]));
858 format!(
859 "{} is an alias of {} and thus requires it to be installed too.",
860 ctx.arena[alias].pretty_string(),
861 ctx.arena[package].pretty_string()
862 )
863 }
864 Reason::PackageInverseAlias { .. } => {
865 let alias = ctx.pool.literal_to_package(literals[1]);
866 if ctx.arena[alias].version == "9999999-dev" {
867 return String::new();
868 }
869 let package = dedup_default_branch_alias(ctx, ctx.pool.literal_to_package(literals[0]));
870 format!(
871 "{} is an alias of {} and must be installed with it.",
872 ctx.arena[alias].pretty_string(),
873 ctx.arena[package].pretty_string()
874 )
875 }
876 }
877}
878
879fn literal_to_pretty_string(
881 ctx: &MessageContext<'_>,
882 literal: i64,
883 installed_map: &HashSet<usize>,
884) -> String {
885 let idx = ctx.pool.literal_to_package(literal);
886 let prefix = if installed_map.contains(&(literal.unsigned_abs() as usize)) {
887 if literal > 0 {
888 "keep"
889 } else {
890 "remove"
891 }
892 } else if literal > 0 {
893 "install"
894 } else {
895 "don't install"
896 };
897 format!("{prefix} {}", ctx.arena[idx].pretty_string())
898}
899
900fn package_list(
905 ctx: &MessageContext<'_>,
906 packages: &[usize],
907 constraint: Option<&Constraint>,
908 use_removed_version_group: bool,
909) -> String {
910 let mut prepared: Vec<(String, String, BTreeMap<String, String>)> = Vec::new();
912 let mut has_default_branch: HashSet<String> = HashSet::new();
913 for &idx in packages {
914 let p = &ctx.arena[idx];
915 let slot = match prepared.iter_mut().position(|(n, _, _)| *n == p.name) {
916 Some(i) => i,
917 None => {
918 prepared.push((p.name.clone(), p.pretty_name.clone(), BTreeMap::new()));
919 prepared.len() - 1
920 }
921 };
922 prepared[slot].1 = p.pretty_name.clone();
923 let pretty = match p.alias_of {
924 Some(base) => format!(
925 "{} (alias of {})",
926 p.pretty_version, ctx.arena[base].pretty_version
927 ),
928 None => p.pretty_version.clone(),
929 };
930 prepared[slot].2.insert(p.version.clone(), pretty);
931 if let Some(c) = constraint {
932 for (v, pretty) in ctx.pool.removed_versions(&p.name, c) {
933 prepared[slot].2.insert(v, pretty);
934 }
935 }
936 if use_removed_version_group {
937 for (v, pretty) in ctx.pool.removed_versions_by_package(idx) {
938 prepared[slot].2.insert(v, pretty);
939 }
940 }
941 if p.is_default_branch {
942 has_default_branch.insert(p.name.clone());
943 }
944 }
945 let mut strings: Vec<String> = Vec::new();
946 for (name, pretty_name, mut versions) in prepared {
947 if has_default_branch.contains(&name) {
948 versions.remove("9999999-dev");
949 }
950 let ordered = versions_in_order(&versions);
951 let list: Vec<String> = if ctx.is_verbose {
952 ordered.into_iter().map(|(_, p)| p).collect()
953 } else {
954 condense_version_list(&ordered, 4, 16)
955 };
956 strings.push(format!("{pretty_name}[{}]", list.join(", ")));
957 }
958 strings.join(", ")
959}
960
961fn has_multiple_names(ctx: &MessageContext<'_>, packages: &[usize]) -> bool {
963 let mut name: Option<&str> = None;
964 for &idx in packages {
965 let n = ctx.arena[idx].name.as_str();
966 match name {
967 None => name = Some(n),
968 Some(current) if current == n => {}
969 Some(_) => return true,
970 }
971 }
972 false
973}
974
975fn constraint_to_text(constraint: Option<PrettyConstraint<'_>>) -> String {
977 let Some(pc) = constraint else {
978 return String::new();
979 };
980 if let Constraint::Single {
981 op: Op::Eq,
982 version,
983 } = pc.constraint
984 {
985 if !version.starts_with("dev-") {
986 let plain = !pc.pretty.is_empty()
987 && pc
988 .pretty
989 .split('.')
990 .all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit()));
991 if !plain {
992 return format!(" {} (exact version match)", pc.pretty);
993 }
994 let mut versions = vec![pc.pretty.to_owned()];
995 let dots = pc.pretty.matches('.').count();
996 let mut i = 3i64 - dots as i64;
997 while i > 0 {
998 let last = versions.last().expect("non-empty").clone();
999 versions.push(format!("{last}.0"));
1000 i -= 1;
1001 }
1002 let text = if versions.len() > 1 {
1003 format!(
1004 "{} or {}",
1005 versions[..versions.len() - 1].join(", "),
1006 versions[versions.len() - 1]
1007 )
1008 } else {
1009 versions[0].clone()
1010 };
1011 return format!(" {} (exact version match: {text})", pc.pretty);
1012 }
1013 }
1014 format!(" {}", pc.pretty)
1015}
1016
1017fn repo_name(ctx: &MessageContext<'_>, origin: Origin) -> String {
1022 match origin {
1023 Origin::Root => "root package repo".to_owned(),
1024 Origin::Platform => "platform repo".to_owned(),
1025 Origin::Locked => "lock repo".to_owned(),
1026 Origin::Repository(i) => match ctx.set.repositories.get(i) {
1027 Some(Repository::Composer(repo)) => repo.repo_name(),
1028 Some(Repository::Path(repo)) => repo.repo_name(),
1029 _ => "unknown repo".to_owned(),
1030 },
1031 Origin::Detached | Origin::Result => "unknown repo".to_owned(),
1032 }
1033}
1034
1035fn find_packages(
1038 ctx: &mut MessageContext<'_>,
1039 name: &str,
1040 constraint: Option<&Constraint>,
1041 flags: u8,
1042) -> Vec<usize> {
1043 let ignore_stability = flags & ALLOW_UNACCEPTABLE_STABILITIES != 0;
1044 let from_all_repos = flags & ALLOW_SHADOWED_REPOSITORIES != 0;
1045 let all_stabilities: BTreeMap<String, i32> = ["stable", "RC", "beta", "alpha", "dev"]
1046 .iter()
1047 .map(|s| (s.to_string(), crate::version::stability_rank(s)))
1048 .collect();
1049 let empty_flags = BTreeMap::new();
1050 let already = BTreeMap::new();
1051 let map = vec![(
1052 name.to_owned(),
1053 constraint.cloned().unwrap_or(Constraint::MatchAll),
1054 )];
1055 let mut found: Vec<usize> = Vec::new();
1056 for (i, repository) in ctx.set.repositories.iter().enumerate() {
1057 let members: Option<&Vec<usize>> = match repository {
1058 Repository::Root(m) | Repository::Platform(m) | Repository::Locked(m) => Some(m),
1059 Repository::Path(repo) => Some(&repo.members),
1060 Repository::Composer(_) => None,
1061 };
1062 if from_all_repos {
1063 match repository {
1066 Repository::Composer(repo) => {
1067 if let Ok((_, ids)) = repo.load_packages(
1068 &map,
1069 &all_stabilities,
1070 &empty_flags,
1071 &already,
1072 Origin::Repository(i),
1073 ctx.arena,
1074 ) {
1075 found.extend(ids);
1076 }
1077 }
1078 _ => {
1079 for &idx in members.expect("array repository") {
1080 let p = &ctx.arena[idx];
1081 if p.name == name
1082 && constraint.is_none_or(|c| {
1083 c.matches(&Constraint::new(Op::Eq, p.version.clone()))
1084 })
1085 {
1086 found.push(idx);
1087 }
1088 }
1089 }
1090 }
1091 continue;
1092 }
1093 let (acceptable, stability_flags) = if ignore_stability {
1094 (&all_stabilities, &empty_flags)
1095 } else {
1096 (&ctx.set.acceptable_stabilities, &ctx.set.stability_flags)
1097 };
1098 let (names_found, ids) = match repository {
1099 Repository::Composer(repo) => repo
1100 .load_packages(
1101 &map,
1102 acceptable,
1103 stability_flags,
1104 &already,
1105 Origin::Repository(i),
1106 ctx.arena,
1107 )
1108 .unwrap_or_default(),
1109 _ => crate::pool::array_repository_load_packages(
1110 members.expect("array repository"),
1111 &map,
1112 acceptable,
1113 stability_flags,
1114 &already,
1115 ctx.arena,
1116 ),
1117 };
1118 found.extend(ids);
1119 if names_found.iter().any(|n| n == name) {
1120 break;
1121 }
1122 }
1123 if ignore_stability || !from_all_repos {
1124 return found;
1125 }
1126 found
1127 .into_iter()
1128 .filter(|&idx| {
1129 let p = &ctx.arena[idx];
1130 is_package_acceptable(
1131 &ctx.set.acceptable_stabilities,
1132 &ctx.set.stability_flags,
1133 &p.names(true),
1134 p.stability,
1135 )
1136 })
1137 .collect()
1138}
1139
1140fn providers(ctx: &mut MessageContext<'_>, name: &str) -> Vec<(String, Option<String>)> {
1144 let mut result: Vec<(String, Option<String>)> = Vec::new();
1145 for (i, repository) in ctx.set.repositories.iter().enumerate() {
1146 let candidates: Vec<usize> = match repository {
1147 Repository::Root(m) | Repository::Platform(m) | Repository::Locked(m) => m.clone(),
1148 Repository::Path(repo) => repo.members.clone(),
1149 Repository::Composer(repo) => {
1150 if let Ok(Some(api)) = repo.providers_api(name) {
1153 for (n, d) in api {
1154 match result.iter_mut().find(|(existing, _)| *existing == n) {
1155 Some(slot) => slot.1 = d,
1156 None => result.push((n, d)),
1157 }
1158 }
1159 continue;
1160 }
1161 repo.provider_candidates(Origin::Repository(i), ctx.arena)
1162 .unwrap_or_default()
1163 }
1164 };
1165 let mut repo_result: Vec<(String, Option<String>)> = Vec::new();
1166 for idx in candidates {
1167 let p = &ctx.arena[idx];
1168 if repo_result.iter().any(|(n, _)| *n == p.name) {
1169 continue;
1170 }
1171 if p.provides.iter().any(|l| l.target == name) {
1172 let description = p
1173 .raw
1174 .get("description")
1175 .and_then(serde_json::Value::as_str)
1176 .map(str::to_owned);
1177 repo_result.push((p.name.clone(), description));
1178 }
1179 }
1180 for (n, d) in repo_result {
1182 match result.iter_mut().find(|(existing, _)| *existing == n) {
1183 Some(slot) => slot.1 = d,
1184 None => result.push((n, d)),
1185 }
1186 }
1187 }
1188 result
1189}
1190
1191fn providers_list(ctx: &mut MessageContext<'_>, name: &str, max: usize) -> Option<String> {
1193 let list = providers(ctx, name);
1194 if list.is_empty() {
1195 return None;
1196 }
1197 let shown: &[(String, Option<String>)] = if list.len() > max + 1 {
1198 &list[..max]
1199 } else {
1200 &list
1201 };
1202 let mut text = String::new();
1203 for (n, description) in shown {
1204 let d = match description {
1205 Some(d) if !d.is_empty() => format!(" {}", php_substr(d, 100)),
1206 _ => String::new(),
1207 };
1208 text.push_str(&format!(" - {n}{d}\n"));
1209 }
1210 if list.len() > max + 1 {
1211 text.push_str(&format!(" ... and {} more.\n", list.len() - max));
1212 }
1213 Some(text)
1214}
1215
1216fn php_substr(s: &str, n: usize) -> String {
1218 let bytes = s.as_bytes();
1219 if bytes.len() <= n {
1220 return s.to_owned();
1221 }
1222 String::from_utf8_lossy(&bytes[..n]).into_owned()
1223}
1224
1225fn platform_package_version(ctx: &MessageContext<'_>, name: &str) -> Option<String> {
1227 let available = ctx.pool.what_provides(ctx.arena, name, None);
1228 if available.is_empty() {
1229 return None;
1230 }
1231 let idxs: Vec<usize> = available
1232 .iter()
1233 .map(|&id| ctx.pool.package_by_id(id))
1234 .collect();
1235 let selected = idxs
1236 .iter()
1237 .copied()
1238 .find(|&idx| ctx.arena[idx].origin == Origin::Platform)
1239 .unwrap_or(idxs[0]);
1240 let p = &ctx.arena[selected];
1241 if p.name != name {
1242 for link in p.provides.iter().chain(p.replaces.iter()) {
1243 if link.target == name {
1244 let description = link.kind.description();
1245 return Some(format!(
1246 "{} {}d by {}",
1247 link.pretty_constraint,
1248 &description[..description.len() - 1],
1249 p.pretty_string()
1250 ));
1251 }
1252 }
1253 }
1254 let mut version = p.pretty_version.clone();
1255 let overridden = p
1256 .raw
1257 .get("extra")
1258 .and_then(|e| e.get("config.platform"))
1259 .and_then(serde_json::Value::as_bool)
1260 == Some(true);
1261 if overridden {
1262 let description = p
1263 .raw
1264 .get("description")
1265 .and_then(serde_json::Value::as_str)
1266 .unwrap_or("");
1267 version.push_str(&format!("; {}", description.replace("Package ", "")));
1268 }
1269 Some(version)
1270}
1271
1272fn missing_package_reason(
1277 ctx: &mut MessageContext<'_>,
1278 package_name: &str,
1279 constraint: Option<PrettyConstraint<'_>>,
1280) -> (String, String) {
1281 let ctt = constraint_to_text(constraint);
1282 let c = constraint.map(|pc| pc.constraint);
1283 if is_platform_package(package_name) {
1284 if package_name.to_lowercase().starts_with("php") || package_name == "hhvm" {
1285 let version = platform_package_version(ctx, package_name);
1286 let msg = format!("- Root composer.json requires {package_name}{ctt} but ");
1287 if package_name == "hhvm" {
1288 if !ctx
1289 .pool
1290 .what_provides(ctx.arena, package_name, None)
1291 .is_empty()
1292 {
1293 return (
1294 msg,
1295 "your HHVM version does not satisfy that requirement.".to_owned(),
1296 );
1297 }
1298 return (
1299 msg,
1300 "HHVM was not detected on this machine, make sure it is in your PATH."
1301 .to_owned(),
1302 );
1303 }
1304 return match version {
1305 None => (
1306 msg,
1307 format!("the {package_name} package is disabled by your platform config. Enable it again with \"composer config platform.{package_name} --unset\"."),
1308 ),
1309 Some(v) => (
1310 msg,
1311 format!("your {package_name} version ({v}) does not satisfy that requirement."),
1312 ),
1313 };
1314 }
1315 if package_name.to_lowercase().starts_with("ext-") {
1316 if package_name.contains(' ') {
1317 return (
1318 "- ".to_owned(),
1319 format!(
1320 "PHP extension {package_name} should be required as {}.",
1321 package_name.replace(' ', "-")
1322 ),
1323 );
1324 }
1325 let ext = &package_name[4..];
1326 let msg =
1327 format!("- Root composer.json requires PHP extension {package_name}{ctt} but ");
1328 let version = platform_package_version(ctx, package_name);
1329 if version.is_none() {
1330 let providers_str = providers_list(ctx, package_name, 5).map(|s| {
1331 format!("\n\n Alternatively you can require one of these packages that provide the extension (or parts of it):\n <warning>Keep in mind that the suggestions are automated and may not be valid or safe to use</warning>\n{s}")
1332 }).unwrap_or_default();
1333 if ctx.loaded_extensions.contains(&package_name.to_lowercase()) {
1334 return (
1335 msg,
1336 format!("the {package_name} package is disabled by your platform config. Enable it again with \"composer config platform.{package_name} --unset\".{providers_str}"),
1337 );
1338 }
1339 return (
1340 msg,
1341 format!("it is missing from your system. Install or enable PHP's {ext} extension.{providers_str}"),
1342 );
1343 }
1344 return (
1345 msg,
1346 format!(
1347 "it has the wrong version installed ({}).",
1348 version.expect("checked")
1349 ),
1350 );
1351 }
1352 if package_name.to_lowercase().starts_with("lib-") {
1353 if package_name.to_lowercase() == "lib-icu" {
1354 let error = if ctx.loaded_extensions.contains("ext-intl") {
1355 "it has the wrong version installed, try upgrading the intl extension."
1356 } else {
1357 "it is missing from your system, make sure the intl extension is loaded."
1358 };
1359 return (
1360 format!(
1361 "- Root composer.json requires linked library {package_name}{ctt} but "
1362 ),
1363 error.to_owned(),
1364 );
1365 }
1366 let providers_str = providers_list(ctx, package_name, 5).map(|s| {
1367 format!("\n\n Alternatively you can require one of these packages that provide the library (or parts of it):\n <warning>Keep in mind that the suggestions are automated and may not be valid or safe to use</warning>\n{s}")
1368 }).unwrap_or_default();
1369 return (
1370 format!("- Root composer.json requires linked library {package_name}{ctt} but "),
1371 format!("it has the wrong version installed or is missing from your system, make sure to load the extension providing it.{providers_str}"),
1372 );
1373 }
1374 }
1375
1376 let mut locked_package: Option<usize> = None;
1377 for idx in ctx.request.locked_packages_all() {
1378 if ctx.arena[idx].name == package_name {
1379 locked_package = Some(idx);
1380 if ctx.pool.is_unacceptable_fixed_or_locked(idx) {
1381 let p = &ctx.arena[idx];
1382 return (
1383 "- ".to_owned(),
1384 format!("{} is fixed to {} (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you list it as an argument for the update command.", p.pretty_name, p.pretty_version),
1385 );
1386 }
1387 break;
1388 }
1389 }
1390
1391 if let Some(pc) = constraint {
1392 if let Constraint::Single { op: Op::Eq, .. } = pc.constraint {
1393 if pc.pretty.starts_with("dev-") && pc.pretty.contains('#') {
1394 let new_constraint = strip_as_suffix(pc.pretty);
1395 let alt = new_constraint.replace('#', "+");
1396 let multi = Constraint::Multi {
1397 constraints: vec![
1398 Constraint::new(Op::Eq, new_constraint.clone()),
1399 Constraint::new(Op::Eq, alt),
1400 ],
1401 conjunctive: false,
1402 };
1403 let packages = find_packages(ctx, package_name, Some(&multi), 0);
1404 if !packages.is_empty() {
1405 return (
1406 format!("- Root composer.json requires {package_name}{ctt}, "),
1407 format!(
1408 "found {}. The # character in branch names is replaced by a + character. Make sure to require it as \"{}\".",
1409 package_list(ctx, &packages, c, false),
1410 pc.pretty.replace('#', "+")
1411 ),
1412 );
1413 }
1414 }
1415 }
1416 }
1417
1418 let packages = find_packages(ctx, package_name, c, 0);
1419 if !packages.is_empty() {
1420 if let Some(root_req) = ctx.set.root_requires.get(package_name) {
1421 let root_pretty = ctx
1422 .request
1423 .pretty_requires
1424 .get(package_name)
1425 .cloned()
1426 .unwrap_or_else(|| root_req.to_string());
1427 if !packages.iter().any(|&idx| {
1428 root_req.matches(&Constraint::new(Op::Eq, ctx.arena[idx].version.clone()))
1429 }) {
1430 return (
1431 format!("- Root composer.json requires {package_name}{ctt}, "),
1432 format!(
1433 "found {} but {} with your root composer.json require ({root_pretty}).",
1434 package_list(ctx, &packages, c, false),
1435 if has_multiple_names(ctx, &packages) {
1436 "these conflict"
1437 } else {
1438 "it conflicts"
1439 }
1440 ),
1441 );
1442 }
1443 }
1444 let first_names = ctx.arena[packages[0]].names(true);
1445 for name in first_names {
1446 if let Some(temp) = ctx.set.temporary_constraints.get(&name) {
1447 if !packages.iter().any(|&idx| {
1448 temp.matches(&Constraint::new(Op::Eq, ctx.arena[idx].version.clone()))
1449 }) {
1450 return (
1451 format!("- Root composer.json requires {name}{ctt}, "),
1452 format!(
1453 "found {} but {} with your temporary update constraint ({name}:{temp}).",
1454 package_list(ctx, &packages, c, false),
1455 if has_multiple_names(ctx, &packages) {
1456 "these conflict"
1457 } else {
1458 "it conflicts"
1459 }
1460 ),
1461 );
1462 }
1463 }
1464 }
1465 if let Some(locked) = locked_package {
1466 let fixed = Constraint::new(Op::Eq, ctx.arena[locked].version.clone());
1467 if !packages
1468 .iter()
1469 .any(|&idx| fixed.matches(&Constraint::new(Op::Eq, ctx.arena[idx].version.clone())))
1470 {
1471 return (
1472 format!("- Root composer.json requires {package_name}{ctt}, "),
1473 format!(
1474 "found {} but the package is fixed to {} (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.",
1475 package_list(ctx, &packages, c, false),
1476 ctx.arena[locked].pretty_version
1477 ),
1478 );
1479 }
1480 }
1481 if let Some(cst) = c {
1482 if ctx.pool.is_abandoned_removed(package_name, cst) {
1483 return (
1484 format!("- Root composer.json requires {package_name}{ctt}, "),
1485 format!(
1486 "found {} but these were not loaded, because they are abandoned and you configured \"policy.abandoned.block\" to true.",
1487 package_list(ctx, &packages, c, false)
1488 ),
1489 );
1490 }
1491 if ctx.pool.is_security_removed(package_name, cst) {
1492 let complete = matching_security_advisories(ctx, &packages, package_name);
1493 let (advisories_list, advisory_ids): (Vec<String>, Vec<String>) = match complete {
1494 Some(list) if !list.is_empty() => (
1495 list.iter()
1496 .map(|a| {
1497 let link = a.complete.as_ref().and_then(|c| c.link.clone());
1498 match link {
1499 Some(l) if !l.is_empty() => {
1500 format!("<href={}>{}</>", escape_output(&l), a.advisory_id)
1501 }
1502 _ if a.advisory_id.starts_with("PKSA-") => format!(
1503 "<href={}>{}</>",
1504 escape_output(&format!(
1505 "https://packagist.org/security-advisories/{}",
1506 a.advisory_id
1507 )),
1508 a.advisory_id
1509 ),
1510 _ => a.advisory_id.clone(),
1511 }
1512 })
1513 .collect(),
1514 list.iter().map(|a| a.advisory_id.clone()).collect(),
1515 ),
1516 _ => {
1517 let ids = ctx.pool.security_advisory_ids(package_name, cst);
1518 (
1519 ids.iter()
1520 .map(|id| {
1521 if id.starts_with("PKSA-") {
1522 format!(
1523 "<href={}>{id}</>",
1524 escape_output(&format!(
1525 "https://packagist.org/security-advisories/{id}"
1526 ))
1527 )
1528 } else {
1529 id.clone()
1530 }
1531 })
1532 .collect(),
1533 ids,
1534 )
1535 }
1536 };
1537 let has_packagist = advisory_ids.iter().all(|id| id.starts_with("PKSA-"));
1538 let details_hint = if has_packagist {
1539 " Go to https://packagist.org/security-advisories/ to find advisory details."
1540 } else {
1541 " Review the advisory details above for more information."
1542 };
1543 return (
1544 format!("- Root composer.json requires {package_name}{ctt}, "),
1545 format!(
1546 "found {} but these were not loaded, because they are affected by security advisories (\"{}\").{details_hint} To ignore the advisories, add their IDs to the \"policy.advisories.ignore-id\" config or add the package to \"policy.advisories.ignore\". To turn the feature off entirely, you can set \"policy.advisories.block\" to false.",
1547 package_list(ctx, &packages, c, false),
1548 advisories_list.join("\", \"")
1549 ),
1550 );
1551 }
1552 if ctx.pool.is_filter_list_removed_version(package_name, cst) {
1553 let filters = ctx.pool.filter_list_entries_text(package_name, cst);
1554 let ignore_paths = filters
1555 .iter()
1556 .map(|(l, _)| format!("\"policy.{l}.ignore\""))
1557 .collect::<Vec<_>>()
1558 .join(" and ");
1559 let off_paths = filters
1560 .iter()
1561 .map(|(l, _)| format!("\"policy.{l}.block\""))
1562 .collect::<Vec<_>>()
1563 .join(" and ");
1564 return (
1565 format!("- Root composer.json requires {package_name}{ctt}, "),
1566 format!(
1567 "found {} but these were not loaded, because they were {}. To ignore filters for this package, add the package to the {ignore_paths} config. To turn the feature off entirely, you can set {off_paths} to false.",
1568 package_list(ctx, &packages, c, false),
1569 filters.iter().map(|(_, t)| t.as_str()).collect::<Vec<_>>().join(", ")
1570 ),
1571 );
1572 }
1573 }
1574 if packages
1575 .iter()
1576 .all(|&idx| ctx.arena[idx].origin == Origin::Locked)
1577 {
1578 return (
1579 format!("- Root composer.json requires {package_name}{ctt}, "),
1580 format!(
1581 "found {} in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file.",
1582 package_list(ctx, &packages, c, false)
1583 ),
1584 );
1585 }
1586 return (
1587 format!("- Root composer.json requires {package_name}{ctt}, "),
1588 format!(
1589 "found {} but these were not loaded, likely because {} with another require.",
1590 package_list(ctx, &packages, c, false),
1591 if has_multiple_names(ctx, &packages) {
1592 "they conflict"
1593 } else {
1594 "it conflicts"
1595 }
1596 ),
1597 );
1598 }
1599
1600 let packages = find_packages(ctx, package_name, c, ALLOW_UNACCEPTABLE_STABILITIES);
1601 if !packages.is_empty() {
1602 let all_repos = find_packages(ctx, package_name, c, ALLOW_SHADOWED_REPOSITORIES);
1603 if !all_repos.is_empty() {
1604 return check_for_lower_prio_repo(
1605 ctx,
1606 package_name,
1607 &packages,
1608 &all_repos,
1609 "minimum-stability",
1610 constraint,
1611 );
1612 }
1613 return (
1614 format!("- Root composer.json requires {package_name}{ctt}, "),
1615 format!(
1616 "found {} but {} not match your minimum-stability.",
1617 package_list(ctx, &packages, c, false),
1618 if has_multiple_names(ctx, &packages) {
1619 "these do"
1620 } else {
1621 "it does"
1622 }
1623 ),
1624 );
1625 }
1626
1627 let packages = find_packages(ctx, package_name, None, ALLOW_UNACCEPTABLE_STABILITIES);
1628 if !packages.is_empty() {
1629 let all_repos = find_packages(ctx, package_name, c, ALLOW_SHADOWED_REPOSITORIES);
1630 if !all_repos.is_empty() {
1631 return check_for_lower_prio_repo(
1632 ctx,
1633 package_name,
1634 &packages,
1635 &all_repos,
1636 "constraint",
1637 constraint,
1638 );
1639 }
1640 let mut suffix = String::new();
1641 if let Some(pc) = constraint {
1642 if let Constraint::Single { version, .. } = pc.constraint {
1643 if version == "dev-master" {
1644 for &idx in &packages {
1645 let v = &ctx.arena[idx].version;
1646 if v == "dev-default" || v == "dev-main" {
1647 suffix = format!(
1648 " Perhaps dev-master was renamed to {}?",
1649 ctx.arena[idx].pretty_version
1650 );
1651 break;
1652 }
1653 }
1654 }
1655 }
1656 }
1657 if ctx.arena[packages[0]].origin == Origin::Root {
1658 suffix =
1659 " See https://getcomposer.org/dep-on-root for details and assistance.".to_owned();
1660 }
1661 return (
1662 format!("- Root composer.json requires {package_name}{ctt}, "),
1663 format!(
1664 "found {} but {} not match the constraint.{suffix}",
1665 package_list(ctx, &packages, c, false),
1666 if has_multiple_names(ctx, &packages) {
1667 "these do"
1668 } else {
1669 "it does"
1670 }
1671 ),
1672 );
1673 }
1674
1675 let legal = |ch: char| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.' | '/' | '-');
1676 if !package_name.chars().all(legal) {
1677 let illegal: String = package_name.chars().filter(|c| !legal(*c)).collect();
1678 return (
1679 format!("- Root composer.json requires {package_name}, it "),
1680 format!(
1681 "could not be found, it looks like its name is invalid, \"{illegal}\" is not allowed in package names."
1682 ),
1683 );
1684 }
1685
1686 if let Some(providers_str) = providers_list(ctx, package_name, 15) {
1687 return (
1688 format!("- Root composer.json requires {package_name}{ctt}, it "),
1689 format!(
1690 "could not be found in any version, but the following packages provide it:\n{providers_str} Consider requiring one of these to satisfy the {package_name} requirement."
1691 ),
1692 );
1693 }
1694
1695 (
1696 format!("- Root composer.json requires {package_name}, it "),
1697 "could not be found in any version, there may be a typo in the package name.".to_owned(),
1698 )
1699}
1700
1701fn strip_as_suffix(pretty: &str) -> String {
1703 let trimmed = pretty.trim_end();
1704 if let Some(pos) = trimmed.rfind(" as ") {
1705 let after = &trimmed[pos + 4..];
1706 let after = after.trim_start();
1707 if !after.is_empty() && !after.contains([',', ' ', '|']) {
1708 return trimmed[..pos].trim_end().to_owned();
1709 }
1710 }
1711 pretty.to_owned()
1712}
1713
1714fn escape_output(text: &str) -> String {
1716 text.replace('<', "\\<")
1717}
1718
1719fn matching_security_advisories(
1723 ctx: &mut MessageContext<'_>,
1724 packages: &[usize],
1725 package_name: &str,
1726) -> Option<Vec<Advisory>> {
1727 let map = crate::pool_filters::constraints_by_name(packages, ctx.arena);
1728 let mut unreachable = Vec::new();
1729 let all = crate::pool_filters::security_advisories_for_constraints(
1730 &ctx.set.repositories,
1731 &map,
1732 false,
1733 true,
1734 &mut unreachable,
1735 )
1736 .ok()?;
1737 all.into_iter()
1738 .find(|(n, _)| n == package_name)
1739 .map(|(_, list)| list)
1740}
1741
1742fn check_for_lower_prio_repo(
1744 ctx: &mut MessageContext<'_>,
1745 package_name: &str,
1746 higher: &[usize],
1747 all_repos: &[usize],
1748 reason: &str,
1749 constraint: Option<PrettyConstraint<'_>>,
1750) -> (String, String) {
1751 let ctt = constraint_to_text(constraint);
1752 let c = constraint.map(|pc| pc.constraint);
1753 let mut next_repo: Option<Origin> = None;
1754 let mut next_repo_packages: Vec<usize> = Vec::new();
1755 for &idx in all_repos {
1756 let origin = ctx.arena[idx].origin;
1757 match next_repo {
1758 None => {
1759 next_repo = Some(origin);
1760 next_repo_packages.push(idx);
1761 }
1762 Some(o) if o == origin => next_repo_packages.push(idx),
1763 Some(_) => break,
1764 }
1765 }
1766 let next_repo = next_repo.expect("non-empty");
1767 if let Some(&top) = higher.first() {
1768 if ctx.arena[top].origin == Origin::Root {
1769 let p = &ctx.arena[top];
1770 return (
1771 format!("- Root composer.json requires {package_name}{ctt}, it is "),
1772 format!(
1773 "satisfiable by {} from {} but {} {} is the root package and cannot be modified. See https://getcomposer.org/dep-on-root for details and assistance.",
1774 package_list(ctx, &next_repo_packages, c, false),
1775 repo_name(ctx, next_repo),
1776 p.pretty_name,
1777 p.pretty_version
1778 ),
1779 );
1780 }
1781 }
1782 if next_repo == Origin::Locked {
1783 let singular = higher.len() == 1;
1784 let mut suggestion = format!(
1785 "Make sure you either fix the {reason} or avoid updating this package to keep the one present in the lock file ({}).",
1786 package_list(ctx, &next_repo_packages, c, false)
1787 );
1788 let first = &ctx.arena[next_repo_packages[0]];
1789 if first.dist.as_ref().is_some_and(|d| d.kind == "path") {
1790 let symlink_off = first
1791 .raw
1792 .get("transport-options")
1793 .and_then(|t| t.get("symlink"))
1794 .and_then(serde_json::Value::as_bool)
1795 == Some(false);
1796 if !symlink_off {
1797 suggestion = format!("Make sure you fix the {reason} as packages installed from symlinked path repos are updated even in partial updates and the one from the lock file can thus not be used.");
1798 }
1799 }
1800 return (
1801 format!("- Root composer.json requires {package_name}{ctt}, "),
1802 format!(
1803 "found {} but {} not match your {reason} and {} therefore not installable. {suggestion}",
1804 package_list(ctx, higher, c, false),
1805 if singular { "it does" } else { "these do" },
1806 if singular { "is" } else { "are" }
1807 ),
1808 );
1809 }
1810 let higher_repo = repo_name(ctx, ctx.arena[higher[0]].origin);
1811 (
1812 format!("- Root composer.json requires {package_name}{ctt}, it is "),
1813 format!(
1814 "satisfiable by {} from {} but {} from {higher_repo} has higher repository priority. The packages from the higher priority repository do not match your {reason} and are therefore not installable. That repository is canonical so the lower priority repo's packages are not installable. See https://getcomposer.org/repoprio for details and assistance.",
1815 package_list(ctx, &next_repo_packages, c, false),
1816 repo_name(ctx, next_repo),
1817 package_list(ctx, higher, c, false)
1818 ),
1819 )
1820}
1821
1822fn missing_locked_package_reason(ctx: &MessageContext<'_>, idx: usize) -> (String, String) {
1824 let p = &ctx.arena[idx];
1825 let constraint = Constraint::new(Op::Eq, p.version.clone());
1826 let prefix = format!(
1827 "- Package {} {} (in the lock file) ",
1828 p.name, p.pretty_version
1829 );
1830 if ctx
1831 .pool
1832 .is_filter_list_removed_version(&p.name, &constraint)
1833 {
1834 let filters = ctx.pool.filter_list_entries_text(&p.name, &constraint);
1835 let ignore_paths = filters
1836 .iter()
1837 .map(|(l, _)| format!("\"policy.{l}.ignore\""))
1838 .collect::<Vec<_>>()
1839 .join(" and ");
1840 let off_paths = filters
1841 .iter()
1842 .map(|(l, _)| format!("\"policy.{l}.block\""))
1843 .collect::<Vec<_>>()
1844 .join(" and ");
1845 return (
1846 prefix,
1847 format!(
1848 "was not loaded, because it was {}. To ignore filters for this package, add the package to the {ignore_paths} config. To turn the feature off entirely, you can set {off_paths} to false.",
1849 filters.iter().map(|(_, t)| t.as_str()).collect::<Vec<_>>().join(", ")
1850 ),
1851 );
1852 }
1853 (
1854 prefix,
1855 "was not loaded (filter list removed locked package must have version removed from pool)."
1856 .to_owned(),
1857 )
1858}
1859
1860#[cfg(test)]
1861mod tests {
1862 use super::*;
1863
1864 #[test]
1865 fn numeric_strings_compare_as_numbers() {
1866 assert_eq!(php_string_cmp("1234", "98"), Ordering::Greater);
1867 assert_eq!(php_string_cmp("-12", "3"), Ordering::Less);
1868 assert_eq!(php_string_cmp("12-34", "98"), Ordering::Less); assert_eq!(
1870 php_string_cmp("acme/a-1.0.0.0", "acme/b-1.0.0.0"),
1871 Ordering::Less
1872 );
1873 assert_eq!(php_string_cmp("1e3", "999"), Ordering::Greater);
1874 }
1875
1876 #[test]
1877 fn output_styles_are_stripped_but_urls_kept() {
1878 assert_eq!(
1879 strip_output_styles("see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for <info>x</info> <warning>y</warning> <href=https://a>PKSA-1</>"),
1880 "see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for x y PKSA-1"
1881 );
1882 }
1883
1884 #[test]
1885 fn version_lists_condense_per_major() {
1886 let versions: Vec<(String, String)> = [
1887 "1.0.0.0", "1.1.0.0", "1.2.0.0", "1.3.0.0", "1.4.0.0", "2.0.0.0",
1888 ]
1889 .iter()
1890 .map(|v| (v.to_string(), v.trim_end_matches(".0").to_string()))
1891 .collect();
1892 assert_eq!(
1893 condense_version_list(&versions, 4, 16),
1894 vec!["1", "...", "1.4", "2"]
1895 );
1896 assert_eq!(
1897 condense_version_list(&versions[..4], 4, 16),
1898 vec!["1", "1.1", "1.2", "1.3"]
1899 );
1900 }
1901
1902 #[test]
1903 fn exact_constraints_get_their_expansions() {
1904 let c = Constraint::new(Op::Eq, "1.2.0.0");
1905 assert_eq!(
1906 constraint_to_text(Some(PrettyConstraint {
1907 constraint: &c,
1908 pretty: "1.2"
1909 })),
1910 " 1.2 (exact version match: 1.2, 1.2.0 or 1.2.0.0)"
1911 );
1912 assert_eq!(
1913 constraint_to_text(Some(PrettyConstraint {
1914 constraint: &c,
1915 pretty: "v1.2"
1916 })),
1917 " v1.2 (exact version match)"
1918 );
1919 let caret = Constraint::new(Op::Ge, "1.0.0.0-dev");
1920 assert_eq!(
1921 constraint_to_text(Some(PrettyConstraint {
1922 constraint: &caret,
1923 pretty: "^1.0"
1924 })),
1925 " ^1.0"
1926 );
1927 }
1928}