use miette::{
GraphicalReportHandler, GraphicalTheme, LabeledSpan, NamedSource, SourceOffset, SourceSpan,
};
use crate::cgp_diagnostic::CgpDiagnostic;
use crate::cgp_patterns::{
ComponentInfo, ProviderRelationship, derive_provider_trait_name, strip_module_prefixes,
};
use crate::diagnostic_db::DiagnosticEntry;
use crate::root_cause::{deduplicate_delegation_notes, deduplicate_provider_relationships};
#[derive(Debug, Clone)]
struct DependencyNode {
description: String,
trait_type: Option<String>,
is_satisfied: Option<bool>,
is_reference: bool,
children: Vec<DependencyNode>,
}
fn has_non_basic_identifier_chars(field_name: &str) -> bool {
field_name
.chars()
.any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '\u{FFFD}')
}
fn format_field_name(field_name: &str) -> String {
if has_non_basic_identifier_chars(field_name) {
format!("\"{}\"", field_name.escape_default())
} else {
field_name.to_string()
}
}
pub fn format_error_message(entry: &DiagnosticEntry) -> Option<CgpDiagnostic> {
if let Some(field_info) = &entry.field_info {
format_missing_field_error(entry, field_info)
} else {
format_generic_cgp_error(entry)
}
}
fn format_missing_field_error(
entry: &DiagnosticEntry,
field_info: &crate::cgp_patterns::FieldInfo,
) -> Option<CgpDiagnostic> {
let formatted_field_name = format_field_name(&field_info.field_name);
let message = if entry.has_other_hasfield_impls {
format!(
"missing field `{}` in the context `{}`.",
formatted_field_name, field_info.target_type
)
} else {
format!(
"missing field `{}` or `#[derive(HasField)]` in the context `{}`.",
formatted_field_name, field_info.target_type
)
};
let mut help_sections = Vec::new();
let component_names: Vec<String> = entry
.component_infos
.iter()
.map(|c| strip_module_prefixes(&c.component_type))
.filter(|name| !name.contains("IsProviderFor<") && !name.contains("CanUseComponent<"))
.collect();
if entry.field_info.is_some() {
if !component_names.is_empty() {
if component_names.len() == 1 {
help_sections.push(format!(
"Context `{}` is missing a required field to use `{}`.",
field_info.target_type, component_names[0]
));
} else {
let components_list = component_names.join("`, `");
help_sections.push(format!(
"Context `{}` is missing a required field to use multiple components: `{}`.",
field_info.target_type, components_list
));
}
} else {
help_sections.push(format!(
"Context `{}` is missing a required field.",
field_info.target_type
));
}
} else if !component_names.is_empty() {
if component_names.len() == 1 {
help_sections.push(format!(
"Context `{}` is missing a required field to use `{}`.",
field_info.target_type, component_names[0]
));
} else {
let components_list = component_names.join("`, `");
help_sections.push(format!(
"Context `{}` is missing a required field to use multiple components: `{}`.",
field_info.target_type, components_list
));
}
}
if entry.has_other_hasfield_impls {
help_sections.push(format!(
" note: Missing field: `{}`",
formatted_field_name
));
} else {
help_sections.push(format!(
" note: Missing field: `{}` or struct needs `#[derive(HasField)]`",
formatted_field_name
));
}
help_sections.push(String::new());
if field_info.has_unknown_chars {
help_sections.push(format!(
"note: some characters in the field name are hidden by the compiler and shown as '\u{FFFD}'"
));
help_sections.push(String::new());
}
if let Some(span) = entry.primary_spans.first() {
help_sections.push(format!(
"The struct `{}` is defined at `{}:{}` but does not have the required field `{}`.",
field_info.target_type, span.file_name, span.line_start, formatted_field_name
));
help_sections.push(String::new());
}
if !entry.delegation_notes.is_empty() {
help_sections.push("Dependency chain:".to_string());
let tree_lines = format_delegation_chain(entry);
for line in tree_lines {
help_sections.push(format!(" {}", line));
}
help_sections.push(String::new());
}
let all_inner_providers = detect_inner_providers(&entry.provider_relationships);
let deduped_relationships = deduplicate_provider_relationships(&entry.provider_relationships);
if !all_inner_providers.is_empty() {
let outer_providers: Vec<_> = deduped_relationships
.iter()
.filter(|r| {
!all_inner_providers
.iter()
.any(|inner| inner == &r.provider_type)
})
.collect();
if !outer_providers.is_empty() {
help_sections.push(format!(
"The error in the higher-order provider `{}` might be caused by its inner provider `{}`.",
outer_providers[0].provider_type, all_inner_providers[0]
));
help_sections.push(String::new());
}
}
help_sections.push("To fix this error:".to_string());
if entry.has_other_hasfield_impls {
if let Some(span) = entry.primary_spans.first() {
help_sections.push(format!(
" • Add a field `{}` to the `{}` struct at {}:{}",
field_info.field_name, field_info.target_type, span.file_name, span.line_start
));
} else {
help_sections.push(format!(
" • Add a field `{}` to the `{}` struct",
field_info.field_name, field_info.target_type
));
}
} else {
if let Some(span) = entry.primary_spans.first() {
help_sections.push(format!(
" • If the struct has the field `{}`, add `#[derive(HasField)]` to the struct definition at `{}:{}`",
field_info.field_name, span.file_name, span.line_start
));
} else {
help_sections.push(format!(
" • If the struct has the field `{}`, add `#[derive(HasField)]` to the struct definition",
field_info.field_name
));
}
help_sections.push(format!(
" • If the field is missing, add a `{}` field to the struct",
field_info.field_name
));
}
let help = Some(help_sections.join("\n"));
let (source_code, labels) = build_source_and_labels(entry);
Some(CgpDiagnostic {
message,
code: entry.error_code.clone(),
help,
source_code,
labels,
})
}
fn format_generic_cgp_error(entry: &DiagnosticEntry) -> Option<CgpDiagnostic> {
let message = entry.message.clone();
let mut help_sections = Vec::new();
if !entry.delegation_notes.is_empty() {
help_sections.push("Dependency chain:".to_string());
let delegation_lines = format_delegation_chain(entry);
for line in delegation_lines {
help_sections.push(format!(" {}", line));
}
help_sections.push(String::new()); }
let nested_consumers = extract_nested_consumer_traits(&entry.delegation_notes);
if !nested_consumers.is_empty() {
let context_type = extract_unsatisfied_provider_from_message(&entry.message)
.map(|u| u.context_type)
.or_else(|| extract_context_from_notes(&entry.delegation_notes))
.unwrap_or_else(|| "the context".to_string());
for nested_consumer in &nested_consumers {
if let Some(component_name) =
derive_component_from_consumer_trait(&nested_consumer.trait_name)
{
help_sections.push(format!(
"Add a check that `{}` can use `{}` using `check_components!` to get further details on the missing dependencies.",
context_type,
component_name
));
}
}
}
let help = if help_sections.is_empty() {
None
} else {
Some(help_sections.join("\n"))
};
let (source_code, labels) = build_source_and_labels(entry);
Some(CgpDiagnostic {
message,
code: entry.error_code.clone(),
help,
source_code,
labels,
})
}
fn build_source_and_labels(
entry: &DiagnosticEntry,
) -> (Option<NamedSource<String>>, Vec<LabeledSpan>) {
if entry.primary_spans.is_empty() {
return (None, vec![]);
}
let first_span = &entry.primary_spans[0];
let file_result = std::fs::read_to_string(&first_span.file_name).or_else(|_| {
if let Ok(current_dir) = std::env::current_dir() {
let candidate1 = current_dir.join(&first_span.file_name);
if let Ok(content) = std::fs::read_to_string(&candidate1) {
return Ok(content);
}
if let Some(parent) = current_dir.parent() {
let candidate2 = parent.join(&first_span.file_name);
if let Ok(content) = std::fs::read_to_string(&candidate2) {
return Ok(content);
}
}
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not find source file",
))
});
match file_result {
Ok(file_content) => {
let source_code = NamedSource::new(&first_span.file_name, file_content.clone());
let mut labels = Vec::new();
for span in &entry.primary_spans {
let lines: Vec<&str> = file_content.lines().collect();
let mut byte_offset = 0;
for (line_idx, line) in lines.iter().enumerate() {
if line_idx + 1 < span.line_start {
byte_offset += line.len() + 1; } else {
break;
}
}
byte_offset += span.column_start.saturating_sub(1);
let span_length = span.column_end.saturating_sub(span.column_start).max(1);
let label_text = span
.label
.clone()
.unwrap_or_else(|| "unsatisfied trait bound".to_string());
let labeled_span = LabeledSpan::new_with_span(
Some(label_text),
SourceSpan::new(SourceOffset::from(byte_offset), span_length),
);
labels.push(labeled_span);
}
(Some(source_code), labels)
}
Err(_) => {
let source_text = first_span
.text
.iter()
.map(|line| line.text.as_str())
.collect::<Vec<_>>()
.join("\n");
if source_text.is_empty() {
return (None, vec![]);
}
let source_code = NamedSource::new(&first_span.file_name, source_text);
let mut labels = Vec::new();
for span in &entry.primary_spans {
let byte_offset = span.column_start.saturating_sub(1);
let span_length = span.column_end.saturating_sub(span.column_start).max(1);
let label_text = span
.label
.clone()
.unwrap_or_else(|| "unsatisfied trait bound".to_string());
let labeled_span = LabeledSpan::new_with_span(
Some(label_text),
SourceSpan::new(SourceOffset::from(byte_offset), span_length),
);
labels.push(labeled_span);
}
(Some(source_code), labels)
}
}
}
fn render_dependency_tree(
node: &DependencyNode,
prefix: &str,
is_last: bool,
is_root: bool,
) -> Vec<String> {
let mut result = Vec::new();
if is_root {
let mut line = node.description.clone();
if let Some(ref trait_type) = node.trait_type {
line.push_str(&format!(" ({})", trait_type));
}
result.push(line);
} else {
let branch = if is_last { "└─" } else { "├─" };
let mut line = format!("{}{} {}", prefix, branch, node.description);
if let Some(ref trait_type) = node.trait_type {
line.push_str(&format!(" ({})", trait_type));
}
if let Some(is_satisfied) = node.is_satisfied {
line.push_str(if is_satisfied { " ✓" } else { " ✗" });
}
if node.is_reference {
line.push_str(" (*)");
}
result.push(line);
}
if node.is_reference {
return result;
}
let child_prefix = if is_root {
prefix.to_string()
} else if is_last {
format!("{} ", prefix)
} else {
format!("{}│ ", prefix)
};
for (i, child) in node.children.iter().enumerate() {
let child_is_last = i == node.children.len() - 1;
result.extend(render_dependency_tree(
child,
&child_prefix,
child_is_last,
false,
));
}
result
}
fn derive_component_from_consumer_trait(consumer_trait: &str) -> Option<String> {
if let Some(action_part) = consumer_trait.strip_prefix("Can") {
Some(format!("{}Component", action_part))
} else {
None
}
}
fn find_consumer_trait_for_component(
component_name: &str,
entry: &DiagnosticEntry,
) -> Option<String> {
for dep in &entry.consumer_trait_dependencies {
if let Some(ref derived_component) = dep.component_name {
if derived_component == component_name {
return Some(dep.trait_name.clone());
}
}
}
for provider_rel in &entry.provider_relationships {
if strip_module_prefixes(&provider_rel.component) == component_name {
let provider_trait = derive_provider_trait_name(component_name)?;
let provider_words: Vec<&str> = provider_trait
.split(|c: char| c.is_uppercase())
.filter(|s| !s.is_empty() && s.len() > 2)
.collect();
for dep in &entry.consumer_trait_dependencies {
let consumer_words: Vec<&str> = dep
.trait_name
.strip_prefix("Can")
.unwrap_or(&dep.trait_name)
.split(|c: char| c.is_uppercase())
.filter(|s| !s.is_empty() && s.len() > 2)
.collect();
for provider_word in &provider_words {
for consumer_word in &consumer_words {
if provider_word.eq_ignore_ascii_case(consumer_word) {
return Some(dep.trait_name.clone());
}
}
}
}
}
}
None
}
fn match_component_to_provider<'a>(
component_info: &ComponentInfo,
provider_relationships: &'a [ProviderRelationship],
) -> Option<&'a ProviderRelationship> {
let component_name = strip_module_prefixes(&component_info.component_type);
for rel in provider_relationships {
if strip_module_prefixes(&rel.component) == component_name {
return Some(rel);
}
}
if let Some(ref provider_trait) = component_info.provider_trait {
for rel in provider_relationships {
if let Some(rel_provider_trait) = derive_provider_trait_name(&rel.component) {
if rel_provider_trait == *provider_trait {
return Some(rel);
}
}
}
}
None
}
fn build_dependency_tree(entry: &DiagnosticEntry) -> Option<DependencyNode> {
let check_trait = entry.check_trait.as_ref()?;
let context_type = entry
.field_info
.as_ref()
.map(|f| f.target_type.clone())
.or_else(|| {
extract_context_from_notes(&entry.delegation_notes)
})?;
let mut root = DependencyNode {
description: format!("`{}` for `{}`", check_trait, context_type),
trait_type: Some("check trait".to_string()),
is_satisfied: None,
is_reference: false,
children: Vec::new(),
};
let mut rendered_consumer_traits: Vec<String> = Vec::new();
for component_info in &entry.component_infos {
let component_name = strip_module_prefixes(&component_info.component_type);
let (consumer_desc, consumer_trait_name) =
if let Some(trait_name) = find_consumer_trait_for_component(&component_name, entry) {
let desc = format!("`{}` for `{}`", trait_name, context_type);
(desc, Some(trait_name.clone()))
} else {
let desc = format!(
"consumer trait of `{}` for `{}`",
component_name, context_type
);
(desc, None)
};
let mut consumer_node = DependencyNode {
description: consumer_desc,
trait_type: Some("consumer trait".to_string()),
is_satisfied: None,
is_reference: false,
children: Vec::new(),
};
if let Some(provider_rel) =
match_component_to_provider(component_info, &entry.provider_relationships)
{
let provider_nodes = build_provider_nodes_for_component(
entry,
&context_type,
Some(component_info),
Some(provider_rel),
&rendered_consumer_traits,
consumer_trait_name.as_deref(),
);
consumer_node.children = provider_nodes;
} else {
let provider_nodes = build_provider_nodes_for_component(
entry,
&context_type,
Some(component_info),
None,
&rendered_consumer_traits,
consumer_trait_name.as_deref(),
);
consumer_node.children = provider_nodes;
}
if let Some(trait_name) = consumer_trait_name {
rendered_consumer_traits.push(trait_name);
}
root.children.push(consumer_node);
}
if entry.component_infos.is_empty() && !entry.provider_relationships.is_empty() {
let provider_nodes =
build_provider_nodes_for_component(entry, &context_type, None, None, &Vec::new(), None);
root.children.extend(provider_nodes);
}
Some(root)
}
fn build_provider_nodes_for_component(
entry: &DiagnosticEntry,
context_type: &str,
component_info: Option<&ComponentInfo>,
provider_rel: Option<&ProviderRelationship>,
rendered_consumer_traits: &[String],
current_consumer_trait: Option<&str>,
) -> Vec<DependencyNode> {
let mut provider_nodes = Vec::new();
let all_inner_providers = detect_inner_providers(&entry.provider_relationships);
let deduped_relationships = deduplicate_provider_relationships(&entry.provider_relationships);
let rel_to_use = if let Some(rel) = provider_rel {
Some(rel)
} else {
deduped_relationships.first()
};
if let Some(rel) = rel_to_use {
if let Some(provider_trait) = component_info.and_then(|c| c.provider_trait.clone()) {
let is_higher_order = all_inner_providers
.iter()
.any(|inner| is_contained_type_parameter(inner, &rel.provider_type));
let description = format!(
"`{}<{}>` for provider `{}`",
provider_trait, context_type, rel.provider_type
);
let mut provider_node = DependencyNode {
description: strip_module_prefixes(&description),
trait_type: Some("provider trait".to_string()),
is_satisfied: None,
is_reference: false,
children: Vec::new(),
};
let all_nested_consumers: Vec<_> =
extract_nested_consumer_traits(&entry.delegation_notes)
.into_iter()
.filter(|nested| {
if let Some(current_trait) = current_consumer_trait {
nested.trait_name != current_trait
} else {
true
}
})
.collect();
let has_nested_consumer_deps = !all_nested_consumers.is_empty();
if !has_nested_consumer_deps {
let getter_children = build_getter_nodes(entry, context_type);
provider_node.children.extend(getter_children);
}
for nested_consumer in &all_nested_consumers {
let nested_nodes = build_nested_consumer_provider_nodes(
entry,
nested_consumer,
context_type,
rendered_consumer_traits,
);
provider_node.children.extend(nested_nodes);
}
if is_higher_order {
if let Some(inner_provider) = all_inner_providers.first() {
let inner_desc = format!(
"`{}<{}>` for inner provider `{}`",
provider_trait, context_type, inner_provider
);
let inner_node = DependencyNode {
description: strip_module_prefixes(&inner_desc),
trait_type: Some("provider trait".to_string()),
is_satisfied: Some(true), is_reference: false,
children: Vec::new(),
};
provider_node.children.push(inner_node);
}
}
provider_nodes.push(provider_node);
}
}
provider_nodes
}
fn build_getter_nodes(entry: &DiagnosticEntry, context_type: &str) -> Vec<DependencyNode> {
let mut getter_nodes = Vec::new();
for note in &entry.delegation_notes {
if let Some(getter_trait) = extract_getter_trait_from_note(note) {
let mut getter_node = DependencyNode {
description: format!("`{}` for `{}`", getter_trait, context_type),
trait_type: Some("getter trait".to_string()),
is_satisfied: None,
is_reference: false,
children: Vec::new(),
};
if getter_nodes.is_empty() {
if let Some(field_info) = &entry.field_info {
let formatted_field = format_field_name(&field_info.field_name);
let field_node = DependencyNode {
description: format!(
"field `{}` on `{}`",
formatted_field, field_info.target_type
),
trait_type: None,
is_satisfied: Some(false), is_reference: false,
children: Vec::new(),
};
getter_node.children.push(field_node);
}
}
getter_nodes.push(getter_node);
}
}
getter_nodes
}
fn build_nested_consumer_provider_nodes(
entry: &DiagnosticEntry,
nested_consumer: &NestedConsumerTrait,
_parent_context_type: &str,
rendered_consumer_traits: &[String],
) -> Vec<DependencyNode> {
let mut nodes = Vec::new();
let is_reference = rendered_consumer_traits
.iter()
.any(|rendered| *rendered == nested_consumer.trait_name);
let matching_component = entry.component_infos.iter().find(|comp| {
if let Some(ref provider_trait) = comp.provider_trait {
if let Some(action_part) = nested_consumer.trait_name.strip_prefix("Can") {
let action_words: Vec<&str> = action_part
.split(|c: char| c.is_uppercase())
.filter(|s| !s.is_empty() && s.len() > 2)
.collect();
let provider_words: Vec<&str> = provider_trait
.split(|c: char| c.is_uppercase())
.filter(|s| !s.is_empty() && s.len() > 2)
.collect();
for action_word in &action_words {
for provider_word in &provider_words {
if action_word.eq_ignore_ascii_case(provider_word) {
return true;
}
}
}
}
}
false
});
let is_shared_component = matching_component.is_some();
let consumer_desc = format!(
"`{}` for `{}`",
nested_consumer.trait_name, nested_consumer.context_type
);
let mut consumer_node = DependencyNode {
description: consumer_desc,
trait_type: Some("consumer trait".to_string()),
is_satisfied: None,
children: Vec::new(),
is_reference, };
if is_reference {
nodes.push(consumer_node);
return nodes;
}
if is_shared_component {
if let Some(component_info) = matching_component {
if let Some(provider_rel) =
match_component_to_provider(component_info, &entry.provider_relationships)
{
if let Some(provider_trait) = component_info.provider_trait.clone() {
let provider_desc = format!(
"`{}<{}>` for provider `{}`",
provider_trait, nested_consumer.context_type, provider_rel.provider_type
);
let mut provider_node = DependencyNode {
description: strip_module_prefixes(&provider_desc),
trait_type: Some("provider trait".to_string()),
is_satisfied: None,
children: Vec::new(),
is_reference: false,
};
let getter_children = build_getter_nodes(entry, &nested_consumer.context_type);
provider_node.children.extend(getter_children);
consumer_node.children.push(provider_node);
}
}
}
} else {
if let Some(unsatisfied) = extract_unsatisfied_provider_from_message(&entry.message) {
let provider_desc = format!(
"`{}<{}>` for provider `{}`",
unsatisfied.trait_name, unsatisfied.context_type, unsatisfied.provider_type
);
let provider_node = DependencyNode {
description: strip_module_prefixes(&provider_desc),
trait_type: Some("provider trait".to_string()),
is_satisfied: Some(false), children: Vec::new(),
is_reference: false,
};
consumer_node.children.push(provider_node);
}
}
nodes.push(consumer_node);
nodes
}
fn extract_getter_trait_from_note(note: &str) -> Option<String> {
if let Some(trait_name) = extract_trait_from_note(note) {
if trait_name.starts_with("Has") {
return Some(trait_name);
}
}
None
}
fn extract_trait_from_note(note: &str) -> Option<String> {
if let Some(start) = note.find("to implement `") {
let after_start = start + "to implement `".len();
if let Some(end) = note[after_start..].find('`') {
let trait_name = ¬e[after_start..after_start + end];
let cleaned = strip_module_prefixes(trait_name);
if cleaned.starts_with("IsProviderFor<") {
if let Some(inner_start) = cleaned.find('<') {
let after_bracket = inner_start + 1;
if let Some(comma_pos) = cleaned[after_bracket..].find(',') {
return Some(
cleaned[after_bracket..after_bracket + comma_pos]
.trim()
.to_string(),
);
}
}
return None;
}
return Some(cleaned);
}
}
None
}
fn extract_context_from_notes(notes: &[String]) -> Option<String> {
for note in notes {
if let Some(start) = note.find("for `") {
let after_start = start + 5;
if let Some(end) = note[after_start..].find("` to") {
let type_name = ¬e[after_start..after_start + end];
return Some(strip_module_prefixes(type_name));
}
}
}
None
}
#[derive(Debug, Clone)]
struct NestedConsumerTrait {
trait_name: String,
context_type: String,
}
fn extract_nested_consumer_traits(notes: &[String]) -> Vec<NestedConsumerTrait> {
let mut results = Vec::new();
for note in notes {
if let Some(for_pos) = note.find("required for `") {
let after_for = for_pos + "required for `".len();
if let Some(context_end) = note[after_for..].find('`') {
let context_type = ¬e[after_for..after_for + context_end];
if let Some(implement_pos) = note[after_for + context_end..].find("to implement `")
{
let trait_start =
after_for + context_end + implement_pos + "to implement `".len();
if let Some(trait_end) = note[trait_start..].find('`') {
let trait_name = ¬e[trait_start..trait_start + trait_end];
let cleaned_trait = strip_module_prefixes(trait_name);
if cleaned_trait.starts_with("Can")
&& !cleaned_trait.contains("CanUseComponent")
&& !cleaned_trait.starts_with("IsProviderFor")
{
results.push(NestedConsumerTrait {
trait_name: cleaned_trait,
context_type: strip_module_prefixes(context_type),
});
}
}
}
}
}
}
results
}
#[derive(Debug, Clone)]
struct UnsatisfiedProvider {
provider_type: String,
trait_name: String,
context_type: String,
}
fn extract_unsatisfied_provider_from_message(message: &str) -> Option<UnsatisfiedProvider> {
if let Some(bound_start) = message.find("the trait bound `") {
let after_bound = bound_start + "the trait bound `".len();
if let Some(bound_end) = message[after_bound..].find("` is not satisfied") {
let bound_str = &message[after_bound..after_bound + bound_end];
if let Some(colon_pos) = bound_str.find(": ") {
let provider_type = bound_str[..colon_pos].trim();
let trait_and_context = bound_str[colon_pos + 2..].trim();
if let Some(open_bracket) = trait_and_context.find('<') {
let trait_name = trait_and_context[..open_bracket].trim();
if let Some(close_bracket) = trait_and_context.find('>') {
let context_type =
trait_and_context[open_bracket + 1..close_bracket].trim();
return Some(UnsatisfiedProvider {
provider_type: strip_module_prefixes(provider_type),
trait_name: strip_module_prefixes(trait_name),
context_type: strip_module_prefixes(context_type),
});
}
}
}
}
}
None
}
fn format_delegation_chain(entry: &DiagnosticEntry) -> Vec<String> {
if let Some(tree) = build_dependency_tree(entry) {
return render_dependency_tree(&tree, "", true, true);
}
format_delegation_chain_legacy(entry)
}
fn format_delegation_chain_legacy(entry: &DiagnosticEntry) -> Vec<String> {
let all_inner_providers: Vec<String> = detect_inner_providers(&entry.provider_relationships);
let deduped_relationships = deduplicate_provider_relationships(&entry.provider_relationships);
let kept_provider_types: std::collections::HashSet<String> = deduped_relationships
.iter()
.map(|r| r.provider_type.clone())
.collect();
let deduped_notes = deduplicate_delegation_notes(&entry.delegation_notes);
let mut formatted = Vec::new();
if !all_inner_providers.is_empty() && entry.field_info.is_some() {
let outer_providers: Vec<_> = deduped_relationships
.iter()
.filter(|r| {
!all_inner_providers
.iter()
.any(|inner| inner == &r.provider_type)
})
.collect();
if !outer_providers.is_empty() && !all_inner_providers.is_empty() {
formatted.push(format!(
"→ The error in `{}` is caused by the inner provider `{}`",
outer_providers[0].provider_type, all_inner_providers[0]
));
}
}
for note in deduped_notes {
let should_keep = if let Some(provider_info) =
crate::cgp_patterns::extract_provider_relationship(¬e)
{
kept_provider_types.is_empty()
|| kept_provider_types.contains(&provider_info.provider_type)
} else {
true
};
if !should_keep {
continue;
}
let formatted_note = format_delegation_note(¬e, entry);
formatted.push(format!("→ {}", formatted_note));
}
formatted
}
fn detect_inner_providers(relationships: &[ProviderRelationship]) -> Vec<String> {
let mut inner_providers = Vec::new();
for rel in relationships {
for other in relationships {
if rel.provider_type != other.provider_type {
if is_contained_type_parameter(&rel.provider_type, &other.provider_type) {
if !inner_providers.contains(&rel.provider_type) {
inner_providers.push(rel.provider_type.clone());
}
}
}
}
}
inner_providers
}
fn is_contained_type_parameter(inner_type: &str, outer_type: &str) -> bool {
let patterns = [
format!("<{}>", inner_type),
format!("<{},", inner_type),
format!(", {}>", inner_type),
format!(", {},", inner_type),
format!("< {}", inner_type), format!("{} >", inner_type),
];
patterns.iter().any(|pattern| outer_type.contains(pattern))
}
fn format_delegation_note(note: &str, _entry: &DiagnosticEntry) -> String {
let mut result = note.to_string();
result = strip_module_prefixes(&result);
result = replace_is_provider_for(&result);
result = replace_can_use_component(&result);
if result.len() > 150 {
if let Some(ellipsis_pos) = result.find(", ...>") {
result = format!("{}...", &result[..ellipsis_pos]);
}
}
result
}
fn replace_is_provider_for(message: &str) -> String {
if !message.contains("IsProviderFor") {
return message.to_string();
}
if let Some(start) = message.find("IsProviderFor<") {
let after_start = start + "IsProviderFor<".len();
if let Some(comma_pos) = find_top_level_comma(after_start, message) {
let component_name = message[after_start..comma_pos].trim();
let provider_trait_name = derive_provider_trait_name(component_name)
.unwrap_or_else(|| format!("the provider trait for `{}`", component_name));
let end_pos = find_matching_bracket(after_start, message).unwrap_or(message.len());
let before = &message[..start];
let after = &message[end_pos..];
let has_opening_backtick = before.ends_with('`');
let has_closing_backtick = after.starts_with('`');
if has_opening_backtick && has_closing_backtick {
return format!(
"{}the provider trait `{}`{}",
&before[..before.len() - 1],
provider_trait_name,
&after[1..]
);
} else {
return format!(
"{}the provider trait `{}`{}",
before, provider_trait_name, after
);
}
}
}
message.to_string()
}
fn replace_can_use_component(message: &str) -> String {
if !message.contains("CanUseComponent") {
return message.to_string();
}
if let Some(start) = message.find("CanUseComponent<") {
let after_start = start + "CanUseComponent<".len();
let end_pos = find_matching_bracket(after_start, message).unwrap_or(message.len());
let component_name = message[after_start..end_pos].trim();
let replacement = format!("use component `{}`", component_name);
let before = &message[..start];
let after = &message[end_pos + 1..];
let has_opening_backtick = before.ends_with('`');
let has_closing_backtick = after.starts_with('`');
if has_opening_backtick && has_closing_backtick {
return format!(
"{}{}{}",
&before[..before.len() - 1],
replacement,
&after[1..]
);
} else {
return format!("{}{}{}", before, replacement, after);
}
}
message.to_string()
}
fn find_top_level_comma(start_pos: usize, text: &str) -> Option<usize> {
let mut depth = 0;
for (i, ch) in text[start_pos..].char_indices() {
match ch {
'<' => depth += 1,
'>' => depth -= 1,
',' if depth == 0 => return Some(start_pos + i),
_ => {}
}
}
None
}
fn find_matching_bracket(start_pos: usize, text: &str) -> Option<usize> {
let mut depth = 1;
for (i, ch) in text[start_pos..].char_indices() {
match ch {
'<' => depth += 1,
'>' => {
depth -= 1;
if depth == 0 {
return Some(start_pos + i + 1);
}
}
_ => {}
}
}
None
}
pub fn render_diagnostic_graphical(diagnostic: &CgpDiagnostic) -> String {
let handler = GraphicalReportHandler::new();
let mut output = String::new();
match handler.render_report(&mut output, diagnostic) {
Ok(_) => output,
Err(_) => {
format!("error: {}", diagnostic.message)
}
}
}
pub fn render_diagnostic_plain(diagnostic: &CgpDiagnostic) -> String {
let handler = GraphicalReportHandler::new_themed(GraphicalTheme::none());
let mut output = String::new();
match handler.render_report(&mut output, diagnostic) {
Ok(_) => output,
Err(_) => {
format!("error: {}", diagnostic.message)
}
}
}
pub fn is_terminal() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replace_is_provider_for() {
let input =
"required for `Foo` to implement `IsProviderFor<AreaCalculatorComponent, Context>`";
let output = replace_is_provider_for(input);
assert!(output.contains("provider trait `AreaCalculator`"));
assert!(!output.contains("IsProviderFor"));
}
#[test]
fn test_find_top_level_comma() {
let text = "IsProviderFor<Foo<A, B>, Bar>";
let start = "IsProviderFor<".len();
if let Some(pos) = find_top_level_comma(start, text) {
assert_eq!(&text[start..pos], "Foo<A, B>");
} else {
panic!("Should find comma");
}
}
}