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.constraint
1449 .matches(&Constraint::new(Op::Eq, ctx.arena[idx].version.clone()))
1450 }) {
1451 return (
1452 format!("- Root composer.json requires {name}{ctt}, "),
1453 format!(
1454 "found {} but {} with your temporary update constraint ({name}:{temp}).",
1455 package_list(ctx, &packages, c, false),
1456 if has_multiple_names(ctx, &packages) {
1457 "these conflict"
1458 } else {
1459 "it conflicts"
1460 }
1461 ),
1462 );
1463 }
1464 }
1465 }
1466 if let Some(locked) = locked_package {
1467 let fixed = Constraint::new(Op::Eq, ctx.arena[locked].version.clone());
1468 if !packages
1469 .iter()
1470 .any(|&idx| fixed.matches(&Constraint::new(Op::Eq, ctx.arena[idx].version.clone())))
1471 {
1472 return (
1473 format!("- Root composer.json requires {package_name}{ctt}, "),
1474 format!(
1475 "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.",
1476 package_list(ctx, &packages, c, false),
1477 ctx.arena[locked].pretty_version
1478 ),
1479 );
1480 }
1481 }
1482 if let Some(cst) = c {
1483 if ctx.pool.is_abandoned_removed(package_name, cst) {
1484 return (
1485 format!("- Root composer.json requires {package_name}{ctt}, "),
1486 format!(
1487 "found {} but these were not loaded, because they are abandoned and you configured \"policy.abandoned.block\" to true.",
1488 package_list(ctx, &packages, c, false)
1489 ),
1490 );
1491 }
1492 if ctx.pool.is_security_removed(package_name, cst) {
1493 let complete = matching_security_advisories(ctx, &packages, package_name);
1494 let (advisories_list, advisory_ids): (Vec<String>, Vec<String>) = match complete {
1495 Some(list) if !list.is_empty() => (
1496 list.iter()
1497 .map(|a| {
1498 let link = a.complete.as_ref().and_then(|c| c.link.clone());
1499 match link {
1500 Some(l) if !l.is_empty() => {
1501 format!("<href={}>{}</>", escape_output(&l), a.advisory_id)
1502 }
1503 _ if a.advisory_id.starts_with("PKSA-") => format!(
1504 "<href={}>{}</>",
1505 escape_output(&format!(
1506 "https://packagist.org/security-advisories/{}",
1507 a.advisory_id
1508 )),
1509 a.advisory_id
1510 ),
1511 _ => a.advisory_id.clone(),
1512 }
1513 })
1514 .collect(),
1515 list.iter().map(|a| a.advisory_id.clone()).collect(),
1516 ),
1517 _ => {
1518 let ids = ctx.pool.security_advisory_ids(package_name, cst);
1519 (
1520 ids.iter()
1521 .map(|id| {
1522 if id.starts_with("PKSA-") {
1523 format!(
1524 "<href={}>{id}</>",
1525 escape_output(&format!(
1526 "https://packagist.org/security-advisories/{id}"
1527 ))
1528 )
1529 } else {
1530 id.clone()
1531 }
1532 })
1533 .collect(),
1534 ids,
1535 )
1536 }
1537 };
1538 let has_packagist = advisory_ids.iter().all(|id| id.starts_with("PKSA-"));
1539 let details_hint = if has_packagist {
1540 " Go to https://packagist.org/security-advisories/ to find advisory details."
1541 } else {
1542 " Review the advisory details above for more information."
1543 };
1544 return (
1545 format!("- Root composer.json requires {package_name}{ctt}, "),
1546 format!(
1547 "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.",
1548 package_list(ctx, &packages, c, false),
1549 advisories_list.join("\", \"")
1550 ),
1551 );
1552 }
1553 if ctx.pool.is_filter_list_removed_version(package_name, cst) {
1554 let filters = ctx.pool.filter_list_entries_text(package_name, cst);
1555 let ignore_paths = filters
1556 .iter()
1557 .map(|(l, _)| format!("\"policy.{l}.ignore\""))
1558 .collect::<Vec<_>>()
1559 .join(" and ");
1560 let off_paths = filters
1561 .iter()
1562 .map(|(l, _)| format!("\"policy.{l}.block\""))
1563 .collect::<Vec<_>>()
1564 .join(" and ");
1565 return (
1566 format!("- Root composer.json requires {package_name}{ctt}, "),
1567 format!(
1568 "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.",
1569 package_list(ctx, &packages, c, false),
1570 filters.iter().map(|(_, t)| t.as_str()).collect::<Vec<_>>().join(", ")
1571 ),
1572 );
1573 }
1574 }
1575 if packages
1576 .iter()
1577 .all(|&idx| ctx.arena[idx].origin == Origin::Locked)
1578 {
1579 return (
1580 format!("- Root composer.json requires {package_name}{ctt}, "),
1581 format!(
1582 "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.",
1583 package_list(ctx, &packages, c, false)
1584 ),
1585 );
1586 }
1587 return (
1588 format!("- Root composer.json requires {package_name}{ctt}, "),
1589 format!(
1590 "found {} but these were not loaded, likely because {} with another require.",
1591 package_list(ctx, &packages, c, false),
1592 if has_multiple_names(ctx, &packages) {
1593 "they conflict"
1594 } else {
1595 "it conflicts"
1596 }
1597 ),
1598 );
1599 }
1600
1601 let packages = find_packages(ctx, package_name, c, ALLOW_UNACCEPTABLE_STABILITIES);
1602 if !packages.is_empty() {
1603 let all_repos = find_packages(ctx, package_name, c, ALLOW_SHADOWED_REPOSITORIES);
1604 if !all_repos.is_empty() {
1605 return check_for_lower_prio_repo(
1606 ctx,
1607 package_name,
1608 &packages,
1609 &all_repos,
1610 "minimum-stability",
1611 constraint,
1612 );
1613 }
1614 return (
1615 format!("- Root composer.json requires {package_name}{ctt}, "),
1616 format!(
1617 "found {} but {} not match your minimum-stability.",
1618 package_list(ctx, &packages, c, false),
1619 if has_multiple_names(ctx, &packages) {
1620 "these do"
1621 } else {
1622 "it does"
1623 }
1624 ),
1625 );
1626 }
1627
1628 let packages = find_packages(ctx, package_name, None, ALLOW_UNACCEPTABLE_STABILITIES);
1629 if !packages.is_empty() {
1630 let all_repos = find_packages(ctx, package_name, c, ALLOW_SHADOWED_REPOSITORIES);
1631 if !all_repos.is_empty() {
1632 return check_for_lower_prio_repo(
1633 ctx,
1634 package_name,
1635 &packages,
1636 &all_repos,
1637 "constraint",
1638 constraint,
1639 );
1640 }
1641 let mut suffix = String::new();
1642 if let Some(pc) = constraint {
1643 if let Constraint::Single { version, .. } = pc.constraint {
1644 if version == "dev-master" {
1645 for &idx in &packages {
1646 let v = &ctx.arena[idx].version;
1647 if v == "dev-default" || v == "dev-main" {
1648 suffix = format!(
1649 " Perhaps dev-master was renamed to {}?",
1650 ctx.arena[idx].pretty_version
1651 );
1652 break;
1653 }
1654 }
1655 }
1656 }
1657 }
1658 if ctx.arena[packages[0]].origin == Origin::Root {
1659 suffix =
1660 " See https://getcomposer.org/dep-on-root for details and assistance.".to_owned();
1661 }
1662 return (
1663 format!("- Root composer.json requires {package_name}{ctt}, "),
1664 format!(
1665 "found {} but {} not match the constraint.{suffix}",
1666 package_list(ctx, &packages, c, false),
1667 if has_multiple_names(ctx, &packages) {
1668 "these do"
1669 } else {
1670 "it does"
1671 }
1672 ),
1673 );
1674 }
1675
1676 let legal = |ch: char| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.' | '/' | '-');
1677 if !package_name.chars().all(legal) {
1678 let illegal: String = package_name.chars().filter(|c| !legal(*c)).collect();
1679 return (
1680 format!("- Root composer.json requires {package_name}, it "),
1681 format!(
1682 "could not be found, it looks like its name is invalid, \"{illegal}\" is not allowed in package names."
1683 ),
1684 );
1685 }
1686
1687 if let Some(providers_str) = providers_list(ctx, package_name, 15) {
1688 return (
1689 format!("- Root composer.json requires {package_name}{ctt}, it "),
1690 format!(
1691 "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."
1692 ),
1693 );
1694 }
1695
1696 (
1697 format!("- Root composer.json requires {package_name}, it "),
1698 "could not be found in any version, there may be a typo in the package name.".to_owned(),
1699 )
1700}
1701
1702fn strip_as_suffix(pretty: &str) -> String {
1704 let trimmed = pretty.trim_end();
1705 if let Some(pos) = trimmed.rfind(" as ") {
1706 let after = &trimmed[pos + 4..];
1707 let after = after.trim_start();
1708 if !after.is_empty() && !after.contains([',', ' ', '|']) {
1709 return trimmed[..pos].trim_end().to_owned();
1710 }
1711 }
1712 pretty.to_owned()
1713}
1714
1715fn escape_output(text: &str) -> String {
1717 text.replace('<', "\\<")
1718}
1719
1720fn matching_security_advisories(
1724 ctx: &mut MessageContext<'_>,
1725 packages: &[usize],
1726 package_name: &str,
1727) -> Option<Vec<Advisory>> {
1728 let map = crate::pool_filters::constraints_by_name(packages, ctx.arena);
1729 let mut unreachable = Vec::new();
1730 let all = crate::pool_filters::security_advisories_for_constraints(
1731 &ctx.set.repositories,
1732 &map,
1733 false,
1734 true,
1735 &mut unreachable,
1736 )
1737 .ok()?;
1738 all.into_iter()
1739 .find(|(n, _)| n == package_name)
1740 .map(|(_, list)| list)
1741}
1742
1743fn check_for_lower_prio_repo(
1745 ctx: &mut MessageContext<'_>,
1746 package_name: &str,
1747 higher: &[usize],
1748 all_repos: &[usize],
1749 reason: &str,
1750 constraint: Option<PrettyConstraint<'_>>,
1751) -> (String, String) {
1752 let ctt = constraint_to_text(constraint);
1753 let c = constraint.map(|pc| pc.constraint);
1754 let mut next_repo: Option<Origin> = None;
1755 let mut next_repo_packages: Vec<usize> = Vec::new();
1756 for &idx in all_repos {
1757 let origin = ctx.arena[idx].origin;
1758 match next_repo {
1759 None => {
1760 next_repo = Some(origin);
1761 next_repo_packages.push(idx);
1762 }
1763 Some(o) if o == origin => next_repo_packages.push(idx),
1764 Some(_) => break,
1765 }
1766 }
1767 let next_repo = next_repo.expect("non-empty");
1768 if let Some(&top) = higher.first() {
1769 if ctx.arena[top].origin == Origin::Root {
1770 let p = &ctx.arena[top];
1771 return (
1772 format!("- Root composer.json requires {package_name}{ctt}, it is "),
1773 format!(
1774 "satisfiable by {} from {} but {} {} is the root package and cannot be modified. See https://getcomposer.org/dep-on-root for details and assistance.",
1775 package_list(ctx, &next_repo_packages, c, false),
1776 repo_name(ctx, next_repo),
1777 p.pretty_name,
1778 p.pretty_version
1779 ),
1780 );
1781 }
1782 }
1783 if next_repo == Origin::Locked {
1784 let singular = higher.len() == 1;
1785 let mut suggestion = format!(
1786 "Make sure you either fix the {reason} or avoid updating this package to keep the one present in the lock file ({}).",
1787 package_list(ctx, &next_repo_packages, c, false)
1788 );
1789 let first = &ctx.arena[next_repo_packages[0]];
1790 if first.dist.as_ref().is_some_and(|d| d.kind == "path") {
1791 let symlink_off = first
1792 .raw
1793 .get("transport-options")
1794 .and_then(|t| t.get("symlink"))
1795 .and_then(serde_json::Value::as_bool)
1796 == Some(false);
1797 if !symlink_off {
1798 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.");
1799 }
1800 }
1801 return (
1802 format!("- Root composer.json requires {package_name}{ctt}, "),
1803 format!(
1804 "found {} but {} not match your {reason} and {} therefore not installable. {suggestion}",
1805 package_list(ctx, higher, c, false),
1806 if singular { "it does" } else { "these do" },
1807 if singular { "is" } else { "are" }
1808 ),
1809 );
1810 }
1811 let higher_repo = repo_name(ctx, ctx.arena[higher[0]].origin);
1812 (
1813 format!("- Root composer.json requires {package_name}{ctt}, it is "),
1814 format!(
1815 "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.",
1816 package_list(ctx, &next_repo_packages, c, false),
1817 repo_name(ctx, next_repo),
1818 package_list(ctx, higher, c, false)
1819 ),
1820 )
1821}
1822
1823fn missing_locked_package_reason(ctx: &MessageContext<'_>, idx: usize) -> (String, String) {
1825 let p = &ctx.arena[idx];
1826 let constraint = Constraint::new(Op::Eq, p.version.clone());
1827 let prefix = format!(
1828 "- Package {} {} (in the lock file) ",
1829 p.name, p.pretty_version
1830 );
1831 if ctx
1832 .pool
1833 .is_filter_list_removed_version(&p.name, &constraint)
1834 {
1835 let filters = ctx.pool.filter_list_entries_text(&p.name, &constraint);
1836 let ignore_paths = filters
1837 .iter()
1838 .map(|(l, _)| format!("\"policy.{l}.ignore\""))
1839 .collect::<Vec<_>>()
1840 .join(" and ");
1841 let off_paths = filters
1842 .iter()
1843 .map(|(l, _)| format!("\"policy.{l}.block\""))
1844 .collect::<Vec<_>>()
1845 .join(" and ");
1846 return (
1847 prefix,
1848 format!(
1849 "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.",
1850 filters.iter().map(|(_, t)| t.as_str()).collect::<Vec<_>>().join(", ")
1851 ),
1852 );
1853 }
1854 (
1855 prefix,
1856 "was not loaded (filter list removed locked package must have version removed from pool)."
1857 .to_owned(),
1858 )
1859}
1860
1861#[cfg(test)]
1862mod tests {
1863 use super::*;
1864
1865 #[test]
1866 fn numeric_strings_compare_as_numbers() {
1867 assert_eq!(php_string_cmp("1234", "98"), Ordering::Greater);
1868 assert_eq!(php_string_cmp("-12", "3"), Ordering::Less);
1869 assert_eq!(php_string_cmp("12-34", "98"), Ordering::Less); assert_eq!(
1871 php_string_cmp("acme/a-1.0.0.0", "acme/b-1.0.0.0"),
1872 Ordering::Less
1873 );
1874 assert_eq!(php_string_cmp("1e3", "999"), Ordering::Greater);
1875 }
1876
1877 #[test]
1878 fn output_styles_are_stripped_but_urls_kept() {
1879 assert_eq!(
1880 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</>"),
1881 "see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for x y PKSA-1"
1882 );
1883 }
1884
1885 #[test]
1886 fn version_lists_condense_per_major() {
1887 let versions: Vec<(String, String)> = [
1888 "1.0.0.0", "1.1.0.0", "1.2.0.0", "1.3.0.0", "1.4.0.0", "2.0.0.0",
1889 ]
1890 .iter()
1891 .map(|v| (v.to_string(), v.trim_end_matches(".0").to_string()))
1892 .collect();
1893 assert_eq!(
1894 condense_version_list(&versions, 4, 16),
1895 vec!["1", "...", "1.4", "2"]
1896 );
1897 assert_eq!(
1898 condense_version_list(&versions[..4], 4, 16),
1899 vec!["1", "1.1", "1.2", "1.3"]
1900 );
1901 }
1902
1903 #[test]
1904 fn exact_constraints_get_their_expansions() {
1905 let c = Constraint::new(Op::Eq, "1.2.0.0");
1906 assert_eq!(
1907 constraint_to_text(Some(PrettyConstraint {
1908 constraint: &c,
1909 pretty: "1.2"
1910 })),
1911 " 1.2 (exact version match: 1.2, 1.2.0 or 1.2.0.0)"
1912 );
1913 assert_eq!(
1914 constraint_to_text(Some(PrettyConstraint {
1915 constraint: &c,
1916 pretty: "v1.2"
1917 })),
1918 " v1.2 (exact version match)"
1919 );
1920 let caret = Constraint::new(Op::Ge, "1.0.0.0-dev");
1921 assert_eq!(
1922 constraint_to_text(Some(PrettyConstraint {
1923 constraint: &caret,
1924 pretty: "^1.0"
1925 })),
1926 " ^1.0"
1927 );
1928 }
1929}