1use miette::{
2 GraphicalReportHandler, GraphicalTheme, LabeledSpan, NamedSource, SourceOffset, SourceSpan,
3};
4
5use crate::cgp_diagnostic::CgpDiagnostic;
6use crate::cgp_patterns::{
7 ComponentInfo, ProviderRelationship, derive_provider_trait_name, strip_module_prefixes,
8};
9use crate::diagnostic_db::DiagnosticEntry;
10use crate::root_cause::{deduplicate_delegation_notes, deduplicate_provider_relationships};
11
12#[derive(Debug, Clone)]
14struct DependencyNode {
15 description: String,
17 trait_type: Option<String>,
19 is_satisfied: Option<bool>,
21 is_reference: bool,
24 children: Vec<DependencyNode>,
26}
27
28fn has_non_basic_identifier_chars(field_name: &str) -> bool {
31 field_name
32 .chars()
33 .any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '\u{FFFD}')
34}
35
36fn format_field_name(field_name: &str) -> String {
38 if has_non_basic_identifier_chars(field_name) {
39 format!("\"{}\"", field_name.escape_default())
41 } else {
42 field_name.to_string()
44 }
45}
46
47pub fn format_error_message(entry: &DiagnosticEntry) -> Option<CgpDiagnostic> {
49 if let Some(field_info) = &entry.field_info {
51 format_missing_field_error(entry, field_info)
53 } else {
54 format_generic_cgp_error(entry)
56 }
57}
58
59fn format_missing_field_error(
61 entry: &DiagnosticEntry,
62 field_info: &crate::cgp_patterns::FieldInfo,
63) -> Option<CgpDiagnostic> {
64 let formatted_field_name = format_field_name(&field_info.field_name);
65
66 let message = if entry.has_other_hasfield_impls {
68 format!(
69 "missing field `{}` in the context `{}`.",
70 formatted_field_name, field_info.target_type
71 )
72 } else {
73 format!(
74 "missing field `{}` or `#[derive(HasField)]` in the context `{}`.",
75 formatted_field_name, field_info.target_type
76 )
77 };
78
79 let mut help_sections = Vec::new();
81
82 let component_names: Vec<String> = entry
85 .component_infos
86 .iter()
87 .map(|c| strip_module_prefixes(&c.component_type))
88 .filter(|name| !name.contains("IsProviderFor<") && !name.contains("CanUseComponent<"))
89 .collect();
90
91 if entry.field_info.is_some() {
93 if !component_names.is_empty() {
94 if component_names.len() == 1 {
95 help_sections.push(format!(
96 "Context `{}` is missing a required field to use `{}`.",
97 field_info.target_type, component_names[0]
98 ));
99 } else {
100 let components_list = component_names.join("`, `");
102 help_sections.push(format!(
103 "Context `{}` is missing a required field to use multiple components: `{}`.",
104 field_info.target_type, components_list
105 ));
106 }
107 } else {
108 help_sections.push(format!(
109 "Context `{}` is missing a required field.",
110 field_info.target_type
111 ));
112 }
113 } else if !component_names.is_empty() {
114 if component_names.len() == 1 {
115 help_sections.push(format!(
116 "Context `{}` is missing a required field to use `{}`.",
117 field_info.target_type, component_names[0]
118 ));
119 } else {
120 let components_list = component_names.join("`, `");
121 help_sections.push(format!(
122 "Context `{}` is missing a required field to use multiple components: `{}`.",
123 field_info.target_type, components_list
124 ));
125 }
126 }
127
128 if entry.has_other_hasfield_impls {
130 help_sections.push(format!(
131 " note: Missing field: `{}`",
132 formatted_field_name
133 ));
134 } else {
135 help_sections.push(format!(
136 " note: Missing field: `{}` or struct needs `#[derive(HasField)]`",
137 formatted_field_name
138 ));
139 }
140
141 help_sections.push(String::new()); if field_info.has_unknown_chars {
145 help_sections.push(format!(
146 "note: some characters in the field name are hidden by the compiler and shown as '\u{FFFD}'"
147 ));
148 help_sections.push(String::new());
149 }
150
151 if let Some(span) = entry.primary_spans.first() {
154 help_sections.push(format!(
155 "The struct `{}` is defined at `{}:{}` but does not have the required field `{}`.",
156 field_info.target_type, span.file_name, span.line_start, formatted_field_name
157 ));
158 help_sections.push(String::new());
159 }
160
161 if !entry.delegation_notes.is_empty() {
163 help_sections.push("Dependency chain:".to_string());
164 let tree_lines = format_delegation_chain(entry);
165 for line in tree_lines {
166 help_sections.push(format!(" {}", line));
167 }
168 help_sections.push(String::new());
169 }
170
171 let all_inner_providers = detect_inner_providers(&entry.provider_relationships);
173 let deduped_relationships = deduplicate_provider_relationships(&entry.provider_relationships);
174
175 if !all_inner_providers.is_empty() {
176 let outer_providers: Vec<_> = deduped_relationships
177 .iter()
178 .filter(|r| {
179 !all_inner_providers
180 .iter()
181 .any(|inner| inner == &r.provider_type)
182 })
183 .collect();
184
185 if !outer_providers.is_empty() {
186 help_sections.push(format!(
187 "The error in the higher-order provider `{}` might be caused by its inner provider `{}`.",
188 outer_providers[0].provider_type, all_inner_providers[0]
189 ));
190 help_sections.push(String::new());
191 }
192 }
193
194 help_sections.push("To fix this error:".to_string());
199 if entry.has_other_hasfield_impls {
200 if let Some(span) = entry.primary_spans.first() {
201 help_sections.push(format!(
202 " • Add a field `{}` to the `{}` struct at {}:{}",
203 field_info.field_name, field_info.target_type, span.file_name, span.line_start
204 ));
205 } else {
206 help_sections.push(format!(
207 " • Add a field `{}` to the `{}` struct",
208 field_info.field_name, field_info.target_type
209 ));
210 }
211 } else {
212 if let Some(span) = entry.primary_spans.first() {
213 help_sections.push(format!(
214 " • If the struct has the field `{}`, add `#[derive(HasField)]` to the struct definition at `{}:{}`",
215 field_info.field_name, span.file_name, span.line_start
216 ));
217 } else {
218 help_sections.push(format!(
219 " • If the struct has the field `{}`, add `#[derive(HasField)]` to the struct definition",
220 field_info.field_name
221 ));
222 }
223 help_sections.push(format!(
224 " • If the field is missing, add a `{}` field to the struct",
225 field_info.field_name
226 ));
227 }
228
229 let help = Some(help_sections.join("\n"));
230
231 let (source_code, labels) = build_source_and_labels(entry);
233
234 Some(CgpDiagnostic {
235 message,
236 code: entry.error_code.clone(),
237 help,
238 source_code,
239 labels,
240 })
241}
242
243fn format_generic_cgp_error(entry: &DiagnosticEntry) -> Option<CgpDiagnostic> {
245 let message = entry.message.clone();
246
247 let mut help_sections = Vec::new();
249
250 if !entry.delegation_notes.is_empty() {
251 help_sections.push("Dependency chain:".to_string());
252 let delegation_lines = format_delegation_chain(entry);
253 for line in delegation_lines {
254 help_sections.push(format!(" {}", line));
255 }
256 help_sections.push(String::new()); }
258
259 let nested_consumers = extract_nested_consumer_traits(&entry.delegation_notes);
261 if !nested_consumers.is_empty() {
262 let context_type = extract_unsatisfied_provider_from_message(&entry.message)
264 .map(|u| u.context_type)
265 .or_else(|| extract_context_from_notes(&entry.delegation_notes))
266 .unwrap_or_else(|| "the context".to_string());
267
268 for nested_consumer in &nested_consumers {
270 if let Some(component_name) =
271 derive_component_from_consumer_trait(&nested_consumer.trait_name)
272 {
273 help_sections.push(format!(
274 "Add a check that `{}` can use `{}` using `check_components!` to get further details on the missing dependencies.",
275 context_type,
276 component_name
277 ));
278 }
279 }
280 }
281
282 let help = if help_sections.is_empty() {
283 None
284 } else {
285 Some(help_sections.join("\n"))
286 };
287
288 let (source_code, labels) = build_source_and_labels(entry);
290
291 Some(CgpDiagnostic {
292 message,
293 code: entry.error_code.clone(),
294 help,
295 source_code,
296 labels,
297 })
298}
299
300fn build_source_and_labels(
303 entry: &DiagnosticEntry,
304) -> (Option<NamedSource<String>>, Vec<LabeledSpan>) {
305 if entry.primary_spans.is_empty() {
306 return (None, vec![]);
307 }
308
309 let first_span = &entry.primary_spans[0];
311
312 let file_result = std::fs::read_to_string(&first_span.file_name).or_else(|_| {
315 if let Ok(current_dir) = std::env::current_dir() {
318 let candidate1 = current_dir.join(&first_span.file_name);
320 if let Ok(content) = std::fs::read_to_string(&candidate1) {
321 return Ok(content);
322 }
323
324 if let Some(parent) = current_dir.parent() {
326 let candidate2 = parent.join(&first_span.file_name);
327 if let Ok(content) = std::fs::read_to_string(&candidate2) {
328 return Ok(content);
329 }
330 }
331 }
332 Err(std::io::Error::new(
333 std::io::ErrorKind::NotFound,
334 "Could not find source file",
335 ))
336 });
337
338 match file_result {
339 Ok(file_content) => {
340 let source_code = NamedSource::new(&first_span.file_name, file_content.clone());
342
343 let mut labels = Vec::new();
345
346 for span in &entry.primary_spans {
347 let lines: Vec<&str> = file_content.lines().collect();
349
350 let mut byte_offset = 0;
351
352 for (line_idx, line) in lines.iter().enumerate() {
354 if line_idx + 1 < span.line_start {
355 byte_offset += line.len() + 1; } else {
357 break;
358 }
359 }
360
361 byte_offset += span.column_start.saturating_sub(1);
363
364 let span_length = span.column_end.saturating_sub(span.column_start).max(1);
365
366 let label_text = span
367 .label
368 .clone()
369 .unwrap_or_else(|| "unsatisfied trait bound".to_string());
370
371 let labeled_span = LabeledSpan::new_with_span(
372 Some(label_text),
373 SourceSpan::new(SourceOffset::from(byte_offset), span_length),
374 );
375
376 labels.push(labeled_span);
377 }
378
379 (Some(source_code), labels)
380 }
381 Err(_) => {
382 let source_text = first_span
384 .text
385 .iter()
386 .map(|line| line.text.as_str())
387 .collect::<Vec<_>>()
388 .join("\n");
389
390 if source_text.is_empty() {
391 return (None, vec![]);
393 }
394
395 let source_code = NamedSource::new(&first_span.file_name, source_text);
396
397 let mut labels = Vec::new();
399
400 for span in &entry.primary_spans {
401 let byte_offset = span.column_start.saturating_sub(1);
402 let span_length = span.column_end.saturating_sub(span.column_start).max(1);
403
404 let label_text = span
405 .label
406 .clone()
407 .unwrap_or_else(|| "unsatisfied trait bound".to_string());
408
409 let labeled_span = LabeledSpan::new_with_span(
410 Some(label_text),
411 SourceSpan::new(SourceOffset::from(byte_offset), span_length),
412 );
413
414 labels.push(labeled_span);
415 }
416
417 (Some(source_code), labels)
418 }
419 }
420}
421
422fn render_dependency_tree(
424 node: &DependencyNode,
425 prefix: &str,
426 is_last: bool,
427 is_root: bool,
428) -> Vec<String> {
429 let mut result = Vec::new();
430
431 if is_root {
433 let mut line = node.description.clone();
435
436 if let Some(ref trait_type) = node.trait_type {
438 line.push_str(&format!(" ({})", trait_type));
439 }
440
441 result.push(line);
442 } else {
443 let branch = if is_last { "└─" } else { "├─" };
444 let mut line = format!("{}{} {}", prefix, branch, node.description);
445
446 if let Some(ref trait_type) = node.trait_type {
448 line.push_str(&format!(" ({})", trait_type));
449 }
450
451 if let Some(is_satisfied) = node.is_satisfied {
453 line.push_str(if is_satisfied { " ✓" } else { " ✗" });
454 }
455
456 if node.is_reference {
459 line.push_str(" (*)");
460 }
461
462 result.push(line);
463 }
464
465 if node.is_reference {
468 return result;
469 }
470
471 let child_prefix = if is_root {
473 prefix.to_string()
474 } else if is_last {
475 format!("{} ", prefix)
476 } else {
477 format!("{}│ ", prefix)
478 };
479
480 for (i, child) in node.children.iter().enumerate() {
483 let child_is_last = i == node.children.len() - 1;
484 result.extend(render_dependency_tree(
485 child,
486 &child_prefix,
487 child_is_last,
488 false,
489 ));
490 }
491
492 result
493}
494
495fn derive_component_from_consumer_trait(consumer_trait: &str) -> Option<String> {
501 if let Some(action_part) = consumer_trait.strip_prefix("Can") {
503 Some(format!("{}Component", action_part))
506 } else {
507 None
508 }
509}
510
511fn find_consumer_trait_for_component(
520 component_name: &str,
521 entry: &DiagnosticEntry,
522) -> Option<String> {
523 for dep in &entry.consumer_trait_dependencies {
525 if let Some(ref derived_component) = dep.component_name {
526 if derived_component == component_name {
528 return Some(dep.trait_name.clone());
529 }
530 }
531 }
532
533 for provider_rel in &entry.provider_relationships {
536 if strip_module_prefixes(&provider_rel.component) == component_name {
537 let provider_trait = derive_provider_trait_name(component_name)?;
546 let provider_words: Vec<&str> = provider_trait
547 .split(|c: char| c.is_uppercase())
548 .filter(|s| !s.is_empty() && s.len() > 2)
549 .collect();
550
551 for dep in &entry.consumer_trait_dependencies {
552 let consumer_words: Vec<&str> = dep
553 .trait_name
554 .strip_prefix("Can")
555 .unwrap_or(&dep.trait_name)
556 .split(|c: char| c.is_uppercase())
557 .filter(|s| !s.is_empty() && s.len() > 2)
558 .collect();
559
560 for provider_word in &provider_words {
562 for consumer_word in &consumer_words {
563 if provider_word.eq_ignore_ascii_case(consumer_word) {
564 return Some(dep.trait_name.clone());
565 }
566 }
567 }
568 }
569 }
570 }
571
572 None
573}
574
575fn match_component_to_provider<'a>(
578 component_info: &ComponentInfo,
579 provider_relationships: &'a [ProviderRelationship],
580) -> Option<&'a ProviderRelationship> {
581 let component_name = strip_module_prefixes(&component_info.component_type);
582
583 for rel in provider_relationships {
585 if strip_module_prefixes(&rel.component) == component_name {
586 return Some(rel);
587 }
588 }
589
590 if let Some(ref provider_trait) = component_info.provider_trait {
593 for rel in provider_relationships {
594 if let Some(rel_provider_trait) = derive_provider_trait_name(&rel.component) {
596 if rel_provider_trait == *provider_trait {
597 return Some(rel);
598 }
599 }
600 }
601 }
602
603 None
604}
605
606fn build_dependency_tree(entry: &DiagnosticEntry) -> Option<DependencyNode> {
610 let check_trait = entry.check_trait.as_ref()?;
612 let context_type = entry
613 .field_info
614 .as_ref()
615 .map(|f| f.target_type.clone())
616 .or_else(|| {
617 extract_context_from_notes(&entry.delegation_notes)
619 })?;
620
621 let mut root = DependencyNode {
622 description: format!("`{}` for `{}`", check_trait, context_type),
625 trait_type: Some("check trait".to_string()),
626 is_satisfied: None,
627 is_reference: false,
628 children: Vec::new(),
629 };
630
631 let mut rendered_consumer_traits: Vec<String> = Vec::new();
634
635 for component_info in &entry.component_infos {
638 let component_name = strip_module_prefixes(&component_info.component_type);
639
640 let (consumer_desc, consumer_trait_name) =
643 if let Some(trait_name) = find_consumer_trait_for_component(&component_name, entry) {
644 let desc = format!("`{}` for `{}`", trait_name, context_type);
647 (desc, Some(trait_name.clone()))
648 } else {
649 let desc = format!(
652 "consumer trait of `{}` for `{}`",
653 component_name, context_type
654 );
655 (desc, None)
656 };
657
658 let mut consumer_node = DependencyNode {
659 description: consumer_desc,
660 trait_type: Some("consumer trait".to_string()),
661 is_satisfied: None,
662 is_reference: false,
663 children: Vec::new(),
664 };
665
666 if let Some(provider_rel) =
668 match_component_to_provider(component_info, &entry.provider_relationships)
669 {
670 let provider_nodes = build_provider_nodes_for_component(
674 entry,
675 &context_type,
676 Some(component_info),
677 Some(provider_rel),
678 &rendered_consumer_traits,
679 consumer_trait_name.as_deref(),
680 );
681 consumer_node.children = provider_nodes;
682 } else {
683 let provider_nodes = build_provider_nodes_for_component(
685 entry,
686 &context_type,
687 Some(component_info),
688 None,
689 &rendered_consumer_traits,
690 consumer_trait_name.as_deref(),
691 );
692 consumer_node.children = provider_nodes;
693 }
694
695 if let Some(trait_name) = consumer_trait_name {
697 rendered_consumer_traits.push(trait_name);
698 }
699
700 root.children.push(consumer_node);
701 }
702
703 if entry.component_infos.is_empty() && !entry.provider_relationships.is_empty() {
705 let provider_nodes =
706 build_provider_nodes_for_component(entry, &context_type, None, None, &Vec::new(), None);
707 root.children.extend(provider_nodes);
708 }
709
710 Some(root)
711}
712
713fn build_provider_nodes_for_component(
721 entry: &DiagnosticEntry,
722 context_type: &str,
723 component_info: Option<&ComponentInfo>,
724 provider_rel: Option<&ProviderRelationship>,
725 rendered_consumer_traits: &[String],
726 current_consumer_trait: Option<&str>,
727) -> Vec<DependencyNode> {
728 let mut provider_nodes = Vec::new();
729
730 let all_inner_providers = detect_inner_providers(&entry.provider_relationships);
732 let deduped_relationships = deduplicate_provider_relationships(&entry.provider_relationships);
733
734 let rel_to_use = if let Some(rel) = provider_rel {
735 Some(rel)
736 } else {
737 deduped_relationships.first()
738 };
739
740 if let Some(rel) = rel_to_use {
741 if let Some(provider_trait) = component_info.and_then(|c| c.provider_trait.clone()) {
742 let is_higher_order = all_inner_providers
744 .iter()
745 .any(|inner| is_contained_type_parameter(inner, &rel.provider_type));
746
747 let description = format!(
749 "`{}<{}>` for provider `{}`",
750 provider_trait, context_type, rel.provider_type
751 );
752 let mut provider_node = DependencyNode {
753 description: strip_module_prefixes(&description),
754 trait_type: Some("provider trait".to_string()),
755 is_satisfied: None,
756 is_reference: false,
757 children: Vec::new(),
758 };
759
760 let all_nested_consumers: Vec<_> =
764 extract_nested_consumer_traits(&entry.delegation_notes)
765 .into_iter()
766 .filter(|nested| {
767 if let Some(current_trait) = current_consumer_trait {
769 nested.trait_name != current_trait
770 } else {
771 true
772 }
773 })
774 .collect();
775 let has_nested_consumer_deps = !all_nested_consumers.is_empty();
776
777 if !has_nested_consumer_deps {
780 let getter_children = build_getter_nodes(entry, context_type);
781 provider_node.children.extend(getter_children);
782 }
783
784 for nested_consumer in &all_nested_consumers {
786 let nested_nodes = build_nested_consumer_provider_nodes(
789 entry,
790 nested_consumer,
791 context_type,
792 rendered_consumer_traits,
793 );
794 provider_node.children.extend(nested_nodes);
795 }
796
797 if is_higher_order {
799 if let Some(inner_provider) = all_inner_providers.first() {
800 let inner_desc = format!(
802 "`{}<{}>` for inner provider `{}`",
803 provider_trait, context_type, inner_provider
804 );
805 let inner_node = DependencyNode {
806 description: strip_module_prefixes(&inner_desc),
807 trait_type: Some("provider trait".to_string()),
808 is_satisfied: Some(true), is_reference: false,
810 children: Vec::new(),
811 };
812 provider_node.children.push(inner_node);
813 }
814 }
815
816 provider_nodes.push(provider_node);
817 }
818 }
819
820 provider_nodes
821}
822
823fn build_getter_nodes(entry: &DiagnosticEntry, context_type: &str) -> Vec<DependencyNode> {
825 let mut getter_nodes = Vec::new();
826
827 for note in &entry.delegation_notes {
829 if let Some(getter_trait) = extract_getter_trait_from_note(note) {
830 let mut getter_node = DependencyNode {
831 description: format!("`{}` for `{}`", getter_trait, context_type),
833 trait_type: Some("getter trait".to_string()),
834 is_satisfied: None,
835 is_reference: false,
836 children: Vec::new(),
837 };
838
839 if getter_nodes.is_empty() {
842 if let Some(field_info) = &entry.field_info {
843 let formatted_field = format_field_name(&field_info.field_name);
844 let field_node = DependencyNode {
845 description: format!(
847 "field `{}` on `{}`",
848 formatted_field, field_info.target_type
849 ),
850 trait_type: None,
851 is_satisfied: Some(false), is_reference: false,
853 children: Vec::new(),
854 };
855 getter_node.children.push(field_node);
856 }
857 }
858
859 getter_nodes.push(getter_node);
860 }
861 }
862
863 getter_nodes
864}
865
866fn build_nested_consumer_provider_nodes(
875 entry: &DiagnosticEntry,
876 nested_consumer: &NestedConsumerTrait,
877 _parent_context_type: &str,
878 rendered_consumer_traits: &[String],
879) -> Vec<DependencyNode> {
880 let mut nodes = Vec::new();
881
882 let is_reference = rendered_consumer_traits
885 .iter()
886 .any(|rendered| *rendered == nested_consumer.trait_name);
887
888 let matching_component = entry.component_infos.iter().find(|comp| {
892 if let Some(ref provider_trait) = comp.provider_trait {
893 if let Some(action_part) = nested_consumer.trait_name.strip_prefix("Can") {
897 let action_words: Vec<&str> = action_part
900 .split(|c: char| c.is_uppercase())
901 .filter(|s| !s.is_empty() && s.len() > 2)
902 .collect();
903
904 let provider_words: Vec<&str> = provider_trait
905 .split(|c: char| c.is_uppercase())
906 .filter(|s| !s.is_empty() && s.len() > 2)
907 .collect();
908
909 for action_word in &action_words {
911 for provider_word in &provider_words {
912 if action_word.eq_ignore_ascii_case(provider_word) {
913 return true;
914 }
915 }
916 }
917 }
918 }
919 false
920 });
921
922 let is_shared_component = matching_component.is_some();
923
924 let consumer_desc = format!(
927 "`{}` for `{}`",
928 nested_consumer.trait_name, nested_consumer.context_type
929 );
930 let mut consumer_node = DependencyNode {
931 description: consumer_desc,
932 trait_type: Some("consumer trait".to_string()),
933 is_satisfied: None,
934 children: Vec::new(),
935 is_reference, };
937
938 if is_reference {
940 nodes.push(consumer_node);
941 return nodes;
942 }
943
944 if is_shared_component {
945 if let Some(component_info) = matching_component {
949 if let Some(provider_rel) =
951 match_component_to_provider(component_info, &entry.provider_relationships)
952 {
953 if let Some(provider_trait) = component_info.provider_trait.clone() {
954 let provider_desc = format!(
956 "`{}<{}>` for provider `{}`",
957 provider_trait, nested_consumer.context_type, provider_rel.provider_type
958 );
959
960 let mut provider_node = DependencyNode {
961 description: strip_module_prefixes(&provider_desc),
962 trait_type: Some("provider trait".to_string()),
963 is_satisfied: None,
964 children: Vec::new(),
965 is_reference: false,
966 };
967
968 let getter_children = build_getter_nodes(entry, &nested_consumer.context_type);
971 provider_node.children.extend(getter_children);
972
973 consumer_node.children.push(provider_node);
974 }
975 }
976 }
977 } else {
978 if let Some(unsatisfied) = extract_unsatisfied_provider_from_message(&entry.message) {
982 let provider_desc = format!(
985 "`{}<{}>` for provider `{}`",
986 unsatisfied.trait_name, unsatisfied.context_type, unsatisfied.provider_type
987 );
988
989 let provider_node = DependencyNode {
990 description: strip_module_prefixes(&provider_desc),
991 trait_type: Some("provider trait".to_string()),
992 is_satisfied: Some(false), children: Vec::new(),
994 is_reference: false,
995 };
996
997 consumer_node.children.push(provider_node);
998 }
999 }
1000
1001 nodes.push(consumer_node);
1002 nodes
1003}
1004
1005fn extract_getter_trait_from_note(note: &str) -> Option<String> {
1007 if let Some(trait_name) = extract_trait_from_note(note) {
1009 if trait_name.starts_with("Has") {
1011 return Some(trait_name);
1012 }
1013 }
1014 None
1015}
1016
1017fn extract_trait_from_note(note: &str) -> Option<String> {
1019 if let Some(start) = note.find("to implement `") {
1020 let after_start = start + "to implement `".len();
1021 if let Some(end) = note[after_start..].find('`') {
1022 let trait_name = ¬e[after_start..after_start + end];
1023 let cleaned = strip_module_prefixes(trait_name);
1024 if cleaned.starts_with("IsProviderFor<") {
1026 if let Some(inner_start) = cleaned.find('<') {
1028 let after_bracket = inner_start + 1;
1029 if let Some(comma_pos) = cleaned[after_bracket..].find(',') {
1030 return Some(
1032 cleaned[after_bracket..after_bracket + comma_pos]
1033 .trim()
1034 .to_string(),
1035 );
1036 }
1037 }
1038 return None;
1040 }
1041 return Some(cleaned);
1042 }
1043 }
1044 None
1045}
1046
1047fn extract_context_from_notes(notes: &[String]) -> Option<String> {
1049 for note in notes {
1050 if let Some(start) = note.find("for `") {
1052 let after_start = start + 5;
1053 if let Some(end) = note[after_start..].find("` to") {
1054 let type_name = ¬e[after_start..after_start + end];
1055 return Some(strip_module_prefixes(type_name));
1056 }
1057 }
1058 }
1059 None
1060}
1061
1062#[derive(Debug, Clone)]
1065struct NestedConsumerTrait {
1066 trait_name: String,
1068 context_type: String,
1070}
1071
1072fn extract_nested_consumer_traits(notes: &[String]) -> Vec<NestedConsumerTrait> {
1076 let mut results = Vec::new();
1077
1078 for note in notes {
1079 if let Some(for_pos) = note.find("required for `") {
1082 let after_for = for_pos + "required for `".len();
1083
1084 if let Some(context_end) = note[after_for..].find('`') {
1086 let context_type = ¬e[after_for..after_for + context_end];
1087
1088 if let Some(implement_pos) = note[after_for + context_end..].find("to implement `")
1090 {
1091 let trait_start =
1092 after_for + context_end + implement_pos + "to implement `".len();
1093
1094 if let Some(trait_end) = note[trait_start..].find('`') {
1095 let trait_name = ¬e[trait_start..trait_start + trait_end];
1096
1097 let cleaned_trait = strip_module_prefixes(trait_name);
1100
1101 if cleaned_trait.starts_with("Can")
1103 && !cleaned_trait.contains("CanUseComponent")
1104 && !cleaned_trait.starts_with("IsProviderFor")
1105 {
1106 results.push(NestedConsumerTrait {
1107 trait_name: cleaned_trait,
1108 context_type: strip_module_prefixes(context_type),
1109 });
1110 }
1111 }
1112 }
1113 }
1114 }
1115 }
1116
1117 results
1118}
1119
1120#[derive(Debug, Clone)]
1122struct UnsatisfiedProvider {
1123 provider_type: String,
1125 trait_name: String,
1127 context_type: String,
1129}
1130
1131fn extract_unsatisfied_provider_from_message(message: &str) -> Option<UnsatisfiedProvider> {
1135 if let Some(bound_start) = message.find("the trait bound `") {
1137 let after_bound = bound_start + "the trait bound `".len();
1138
1139 if let Some(bound_end) = message[after_bound..].find("` is not satisfied") {
1141 let bound_str = &message[after_bound..after_bound + bound_end];
1142
1143 if let Some(colon_pos) = bound_str.find(": ") {
1145 let provider_type = bound_str[..colon_pos].trim();
1146 let trait_and_context = bound_str[colon_pos + 2..].trim();
1147
1148 if let Some(open_bracket) = trait_and_context.find('<') {
1150 let trait_name = trait_and_context[..open_bracket].trim();
1151
1152 if let Some(close_bracket) = trait_and_context.find('>') {
1154 let context_type =
1155 trait_and_context[open_bracket + 1..close_bracket].trim();
1156
1157 return Some(UnsatisfiedProvider {
1158 provider_type: strip_module_prefixes(provider_type),
1159 trait_name: strip_module_prefixes(trait_name),
1160 context_type: strip_module_prefixes(context_type),
1161 });
1162 }
1163 }
1164 }
1165 }
1166 }
1167
1168 None
1169}
1170
1171fn format_delegation_chain(entry: &DiagnosticEntry) -> Vec<String> {
1173 if let Some(tree) = build_dependency_tree(entry) {
1175 return render_dependency_tree(&tree, "", true, true);
1176 }
1177
1178 format_delegation_chain_legacy(entry)
1180}
1181
1182fn format_delegation_chain_legacy(entry: &DiagnosticEntry) -> Vec<String> {
1184 let all_inner_providers: Vec<String> = detect_inner_providers(&entry.provider_relationships);
1186
1187 let deduped_relationships = deduplicate_provider_relationships(&entry.provider_relationships);
1189
1190 let kept_provider_types: std::collections::HashSet<String> = deduped_relationships
1192 .iter()
1193 .map(|r| r.provider_type.clone())
1194 .collect();
1195
1196 let deduped_notes = deduplicate_delegation_notes(&entry.delegation_notes);
1198
1199 let mut formatted = Vec::new();
1200
1201 if !all_inner_providers.is_empty() && entry.field_info.is_some() {
1204 let outer_providers: Vec<_> = deduped_relationships
1205 .iter()
1206 .filter(|r| {
1207 !all_inner_providers
1208 .iter()
1209 .any(|inner| inner == &r.provider_type)
1210 })
1211 .collect();
1212
1213 if !outer_providers.is_empty() && !all_inner_providers.is_empty() {
1214 formatted.push(format!(
1215 "→ The error in `{}` is caused by the inner provider `{}`",
1216 outer_providers[0].provider_type, all_inner_providers[0]
1217 ));
1218 }
1219 }
1220
1221 for note in deduped_notes {
1222 let should_keep = if let Some(provider_info) =
1224 crate::cgp_patterns::extract_provider_relationship(¬e)
1225 {
1226 kept_provider_types.is_empty()
1228 || kept_provider_types.contains(&provider_info.provider_type)
1229 } else {
1230 true
1232 };
1233
1234 if !should_keep {
1235 continue;
1237 }
1238
1239 let formatted_note = format_delegation_note(¬e, entry);
1240 formatted.push(format!("→ {}", formatted_note));
1241 }
1242
1243 formatted
1244}
1245
1246fn detect_inner_providers(relationships: &[ProviderRelationship]) -> Vec<String> {
1249 let mut inner_providers = Vec::new();
1250
1251 for rel in relationships {
1252 for other in relationships {
1254 if rel.provider_type != other.provider_type {
1255 if is_contained_type_parameter(&rel.provider_type, &other.provider_type) {
1256 if !inner_providers.contains(&rel.provider_type) {
1257 inner_providers.push(rel.provider_type.clone());
1258 }
1259 }
1260 }
1261 }
1262 }
1263
1264 inner_providers
1265}
1266
1267fn is_contained_type_parameter(inner_type: &str, outer_type: &str) -> bool {
1270 let patterns = [
1272 format!("<{}>", inner_type),
1273 format!("<{},", inner_type),
1274 format!(", {}>", inner_type),
1275 format!(", {},", inner_type),
1276 format!("< {}", inner_type), format!("{} >", inner_type),
1278 ];
1279
1280 patterns.iter().any(|pattern| outer_type.contains(pattern))
1281}
1282
1283fn format_delegation_note(note: &str, _entry: &DiagnosticEntry) -> String {
1285 let mut result = note.to_string();
1286
1287 result = strip_module_prefixes(&result);
1289
1290 result = replace_is_provider_for(&result);
1292
1293 result = replace_can_use_component(&result);
1295
1296 if result.len() > 150 {
1298 if let Some(ellipsis_pos) = result.find(", ...>") {
1299 result = format!("{}...", &result[..ellipsis_pos]);
1300 }
1301 }
1302
1303 result
1304}
1305
1306fn replace_is_provider_for(message: &str) -> String {
1308 if !message.contains("IsProviderFor") {
1309 return message.to_string();
1310 }
1311
1312 if let Some(start) = message.find("IsProviderFor<") {
1314 let after_start = start + "IsProviderFor<".len();
1315
1316 if let Some(comma_pos) = find_top_level_comma(after_start, message) {
1318 let component_name = message[after_start..comma_pos].trim();
1319
1320 let provider_trait_name = derive_provider_trait_name(component_name)
1322 .unwrap_or_else(|| format!("the provider trait for `{}`", component_name));
1323
1324 let end_pos = find_matching_bracket(after_start, message).unwrap_or(message.len());
1326
1327 let before = &message[..start];
1329 let after = &message[end_pos..];
1330
1331 let has_opening_backtick = before.ends_with('`');
1333 let has_closing_backtick = after.starts_with('`');
1334
1335 if has_opening_backtick && has_closing_backtick {
1336 return format!(
1337 "{}the provider trait `{}`{}",
1338 &before[..before.len() - 1],
1339 provider_trait_name,
1340 &after[1..]
1341 );
1342 } else {
1343 return format!(
1344 "{}the provider trait `{}`{}",
1345 before, provider_trait_name, after
1346 );
1347 }
1348 }
1349 }
1350
1351 message.to_string()
1352}
1353
1354fn replace_can_use_component(message: &str) -> String {
1356 if !message.contains("CanUseComponent") {
1357 return message.to_string();
1358 }
1359
1360 if let Some(start) = message.find("CanUseComponent<") {
1362 let after_start = start + "CanUseComponent<".len();
1363
1364 let end_pos = find_matching_bracket(after_start, message).unwrap_or(message.len());
1366
1367 let component_name = message[after_start..end_pos].trim();
1368
1369 let replacement = format!("use component `{}`", component_name);
1371
1372 let before = &message[..start];
1374 let after = &message[end_pos + 1..];
1375
1376 let has_opening_backtick = before.ends_with('`');
1377 let has_closing_backtick = after.starts_with('`');
1378
1379 if has_opening_backtick && has_closing_backtick {
1380 return format!(
1381 "{}{}{}",
1382 &before[..before.len() - 1],
1383 replacement,
1384 &after[1..]
1385 );
1386 } else {
1387 return format!("{}{}{}", before, replacement, after);
1388 }
1389 }
1390
1391 message.to_string()
1392}
1393
1394fn find_top_level_comma(start_pos: usize, text: &str) -> Option<usize> {
1396 let mut depth = 0;
1397
1398 for (i, ch) in text[start_pos..].char_indices() {
1399 match ch {
1400 '<' => depth += 1,
1401 '>' => depth -= 1,
1402 ',' if depth == 0 => return Some(start_pos + i),
1403 _ => {}
1404 }
1405 }
1406
1407 None
1408}
1409
1410fn find_matching_bracket(start_pos: usize, text: &str) -> Option<usize> {
1412 let mut depth = 1;
1413
1414 for (i, ch) in text[start_pos..].char_indices() {
1415 match ch {
1416 '<' => depth += 1,
1417 '>' => {
1418 depth -= 1;
1419 if depth == 0 {
1420 return Some(start_pos + i + 1);
1421 }
1422 }
1423 _ => {}
1424 }
1425 }
1426
1427 None
1428}
1429
1430pub fn render_diagnostic_graphical(diagnostic: &CgpDiagnostic) -> String {
1432 let handler = GraphicalReportHandler::new();
1433 let mut output = String::new();
1434
1435 match handler.render_report(&mut output, diagnostic) {
1436 Ok(_) => output,
1437 Err(_) => {
1438 format!("error: {}", diagnostic.message)
1440 }
1441 }
1442}
1443
1444pub fn render_diagnostic_plain(diagnostic: &CgpDiagnostic) -> String {
1446 let handler = GraphicalReportHandler::new_themed(GraphicalTheme::none());
1448 let mut output = String::new();
1449
1450 match handler.render_report(&mut output, diagnostic) {
1451 Ok(_) => output,
1452 Err(_) => {
1453 format!("error: {}", diagnostic.message)
1455 }
1456 }
1457}
1458
1459pub fn is_terminal() -> bool {
1461 use std::io::IsTerminal;
1462 std::io::stdout().is_terminal()
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467 use super::*;
1468
1469 #[test]
1470 fn test_replace_is_provider_for() {
1471 let input =
1472 "required for `Foo` to implement `IsProviderFor<AreaCalculatorComponent, Context>`";
1473 let output = replace_is_provider_for(input);
1474 assert!(output.contains("provider trait `AreaCalculator`"));
1475 assert!(!output.contains("IsProviderFor"));
1476 }
1477
1478 #[test]
1479 fn test_find_top_level_comma() {
1480 let text = "IsProviderFor<Foo<A, B>, Bar>";
1481 let start = "IsProviderFor<".len();
1482 if let Some(pos) = find_top_level_comma(start, text) {
1483 assert_eq!(&text[start..pos], "Foo<A, B>");
1484 } else {
1485 panic!("Should find comma");
1486 }
1487 }
1488}