Skip to main content

cargo_cgp/
error_formatting.rs

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/// Node in a dependency tree showing trait requirement relationships
13#[derive(Debug, Clone)]
14struct DependencyNode {
15    /// Description of this requirement
16    description: String,
17    /// Type of trait (check, consumer, provider, getter)
18    trait_type: Option<String>,
19    /// Whether this requirement is satisfied
20    is_satisfied: Option<bool>,
21    /// Whether this node is a reference to an earlier node (shown with (*) marker)
22    /// Used in flattened dependency trees to avoid duplicating subtrees
23    is_reference: bool,
24    /// Child dependencies
25    children: Vec<DependencyNode>,
26}
27
28/// Checks if a field name contains non-basic identifier characters
29/// Basic identifier characters are: a-z, A-Z, 0-9, underscore, hyphen, and the replacement character
30fn 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
36/// Formats a field name for display, escaping it like a Rust string if it contains special characters
37fn format_field_name(field_name: &str) -> String {
38    if has_non_basic_identifier_chars(field_name) {
39        // Escape like a Rust string
40        format!("\"{}\"", field_name.escape_default())
41    } else {
42        // Display as-is
43        field_name.to_string()
44    }
45}
46
47/// Formats a diagnostic entry as an improved CGP error message
48pub fn format_error_message(entry: &DiagnosticEntry) -> Option<CgpDiagnostic> {
49    // Format based on what kind of error this is
50    if let Some(field_info) = &entry.field_info {
51        // This is a missing field error - the most common CGP error
52        format_missing_field_error(entry, field_info)
53    } else {
54        // Fallback to a generic CGP error format
55        format_generic_cgp_error(entry)
56    }
57}
58
59/// Formats a missing field error with CGP-aware messaging
60fn 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    // Build the main error message
67    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    // Build help message with clear sections
80    let mut help_sections = Vec::new();
81
82    // Get component names for context
83    // If we have multiple components, we'll list them all
84    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    // Section 1: High-level context
92    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                // Multiple components affected
101                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    // Add note about missing field or derive
129    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()); // Blank line
142
143    // Section 2: Field name warnings (if applicable)
144    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    // Section 3: Struct location (if we have source span)
152    // Use the first span if available
153    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    // Section 4: Dependency chain as tree
162    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    // Section 5: Inner provider note (for higher-order providers)
172    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    // Section 6: Available fields (optional - requires additional extraction)
195    // For now, we skip this since we'd need to parse additional diagnostics to find existing fields
196
197    // Section 7: How to fix
198    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    // Build source code and labels
232    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
243/// Formats a generic CGP error (when we don't have specific field info)
244fn format_generic_cgp_error(entry: &DiagnosticEntry) -> Option<CgpDiagnostic> {
245    let message = entry.message.clone();
246
247    // Build help with simplified notes
248    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()); // Blank line
257    }
258
259    // Check for nested consumer traits and add help message for indirect components
260    let nested_consumers = extract_nested_consumer_traits(&entry.delegation_notes);
261    if !nested_consumers.is_empty() {
262        // Get the context type from the unsatisfied provider or delegation notes
263        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 each nested consumer trait, suggest checking its component
269        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    // Build source code and labels
289    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
300/// Builds source code and labeled spans from diagnostic entry
301/// When there are multiple components, creates a label for each span
302fn 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    // Use the first span to determine the file
310    let first_span = &entry.primary_spans[0];
311
312    // Try to read the actual source file to get proper content and offsets
313    // The file_name might be absolute or relative
314    let file_result = std::fs::read_to_string(&first_span.file_name).or_else(|_| {
315        // If the path is relative, try from the workspace root
316        // Look for common workspace patterns
317        if let Ok(current_dir) = std::env::current_dir() {
318            // Try current directory first
319            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            // Try parent directory (in case we're in a subdirectory)
325            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            // Use the actual file content
341            let source_code = NamedSource::new(&first_span.file_name, file_content.clone());
342
343            // Create a labeled span for each primary span
344            let mut labels = Vec::new();
345
346            for span in &entry.primary_spans {
347                // Calculate byte offset in the actual file
348                let lines: Vec<&str> = file_content.lines().collect();
349
350                let mut byte_offset = 0;
351
352                // Add bytes for all lines before the target line (1-indexed)
353                for (line_idx, line) in lines.iter().enumerate() {
354                    if line_idx + 1 < span.line_start {
355                        byte_offset += line.len() + 1; // +1 for newline
356                    } else {
357                        break;
358                    }
359                }
360
361                // Add column offset (1-indexed, so subtract 1)
362                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            // Fallback: reconstruct from span text of the first span
383            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                // If we have no source text at all, just return nothing
392                return (None, vec![]);
393            }
394
395            let source_code = NamedSource::new(&first_span.file_name, source_text);
396
397            // For fallback, create simple labels for each span
398            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
422/// Renders a dependency tree with box-drawing characters
423fn 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    // Build the line for this node
432    if is_root {
433        // Root node has no branch character
434        let mut line = node.description.clone();
435
436        // Add trait type annotation if present
437        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        // Add trait type annotation if present
447        if let Some(ref trait_type) = node.trait_type {
448            line.push_str(&format!(" ({})", trait_type));
449        }
450
451        // Add satisfaction marker if present
452        if let Some(is_satisfied) = node.is_satisfied {
453            line.push_str(if is_satisfied { " ✓" } else { " ✗" });
454        }
455
456        // If this is a reference node, add (*) marker
457        // This indicates the full tree is shown elsewhere (cargo tree style)
458        if node.is_reference {
459            line.push_str(" (*)");
460        }
461
462        result.push(line);
463    }
464
465    // If this is a reference node, don't render children
466    // The full tree is shown at the root level
467    if node.is_reference {
468        return result;
469    }
470
471    // Render children with updated prefix
472    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    // Render all children normally
481    // All children are treated the same in the flattened structure
482    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
495/// Derives a component name from a consumer trait name
496/// E.g., "CanCalculateArea" -> "AreaCalculatorComponent"
497/// This is a heuristic that works for common CGP naming patterns:
498/// - Consumer trait: Can{Action} (e.g., CanCalculateArea)
499/// - Component: {Action}Component (e.g., AreaCalculatorComponent)
500fn derive_component_from_consumer_trait(consumer_trait: &str) -> Option<String> {
501    // Check if it starts with "Can"
502    if let Some(action_part) = consumer_trait.strip_prefix("Can") {
503        // Remove the "Can" prefix and append "Component"
504        // E.g., "CalculateArea" -> "AreaCalculatorComponent"
505        Some(format!("{}Component", action_part))
506    } else {
507        None
508    }
509}
510
511/// Finds the actual consumer trait name for a given component
512/// by looking it up in the diagnostic entry's consumer trait dependencies
513///
514/// This avoids the need to derive consumer trait names from component names,
515/// which is error-prone due to naming variations. Instead, we use the actual
516/// trait names extracted from the compiler diagnostics.
517///
518/// Returns None if no matching consumer trait is found for this component.
519fn find_consumer_trait_for_component(
520    component_name: &str,
521    entry: &DiagnosticEntry,
522) -> Option<String> {
523    // Check each consumer trait dependency to see if it matches this component
524    for dep in &entry.consumer_trait_dependencies {
525        if let Some(ref derived_component) = dep.component_name {
526            // Match by the component name derived from the consumer trait
527            if derived_component == component_name {
528                return Some(dep.trait_name.clone());
529            }
530        }
531    }
532
533    // Also check provider relationships to find the provider trait,
534    // then search for consumer traits that might correspond to it
535    for provider_rel in &entry.provider_relationships {
536        if strip_module_prefixes(&provider_rel.component) == component_name {
537            // Found the provider for this component
538            // Now look for consumer traits that might match this provider's trait
539            // This is a fallback heuristic when the component_name derivation doesn't match
540
541            // The heuristic here is fuzzy matching based on significant words
542            // For example: AreaCalculator (provider) ~ CanCalculateArea (consumer)
543            // We look for shared words between them
544
545            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                // Check if they share significant words
561                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
575/// Matches a component to its specific provider relationship from the error
576/// This is based on the IsProviderFor<Component, Context> notes in the diagnostics
577fn 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    // Try exact match first
584    for rel in provider_relationships {
585        if strip_module_prefixes(&rel.component) == component_name {
586            return Some(rel);
587        }
588    }
589
590    // If no exact match, try matching by provider trait name
591    // The provider trait should be derivable from the component name
592    if let Some(ref provider_trait) = component_info.provider_trait {
593        for rel in provider_relationships {
594            // Check if this relationship's component derives the same provider trait
595            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
606/// Builds a dependency tree from delegation notes and provider relationships
607/// When there are multiple components, shows them as siblings at the root level (flattened structure)
608/// This creates a cargo-tree-style view where shared dependencies are marked with (*)
609fn build_dependency_tree(entry: &DiagnosticEntry) -> Option<DependencyNode> {
610    // Build root node from check trait
611    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            // Try to extract from delegation notes
618            extract_context_from_notes(&entry.delegation_notes)
619        })?;
620
621    let mut root = DependencyNode {
622        // Wrap trait and type names in backticks for consistent code construct formatting
623        // Rationale: Backticks visually distinguish code elements from descriptive text
624        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    // Track which consumer traits have been rendered to avoid duplicating full trees
632    // This implements the flattened dependency view similar to cargo tree
633    let mut rendered_consumer_traits: Vec<String> = Vec::new();
634
635    // Process all components in order, showing them as siblings at the root level
636    // This is the key change for flattened dependency trees
637    for component_info in &entry.component_infos {
638        let component_name = strip_module_prefixes(&component_info.component_type);
639
640        // Try to find the actual consumer trait name for this component
641        // If found, use it directly; otherwise fall back to generic description
642        let (consumer_desc, consumer_trait_name) =
643            if let Some(trait_name) = find_consumer_trait_for_component(&component_name, entry) {
644                // Found the actual consumer trait - use it directly
645                // Wrap both trait name and context type in backticks
646                let desc = format!("`{}` for `{}`", trait_name, context_type);
647                (desc, Some(trait_name.clone()))
648            } else {
649                // Fallback to generic description
650                // Note: component_name and context_type are already wrapped in backticks
651                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        // Match this component to its specific provider relationship
667        if let Some(provider_rel) =
668            match_component_to_provider(component_info, &entry.provider_relationships)
669        {
670            // Build provider node for this specific relationship
671            // Pass the rendered_consumer_traits and current consumer trait name
672            // to avoid showing the component's own consumer trait as a nested dependency
673            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            // Fallback: build without specific provider relationship
684            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        // Track this consumer trait as rendered (if we know its name)
696        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 no component info, try building without it (fallback)
704    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
713/// Builds provider nodes for a specific component and its provider relationship
714/// If component_info is None, builds nodes based on provider relationships alone
715/// If provider_rel is provided, uses that specific relationship; otherwise uses first available
716/// The rendered_consumer_traits parameter tracks which consumer traits have already been shown
717/// at the root level, so we can mark them as references (*) instead of duplicating the full tree
718/// The current_consumer_trait parameter is the consumer trait of the current component,
719/// which should be excluded from nested consumer traits to avoid showing it as its own dependency
720fn 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    // Determine which provider relationship to use
731    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            // Check if this is a higher-order provider (has inner providers)
743            let is_higher_order = all_inner_providers
744                .iter()
745                .any(|inner| is_contained_type_parameter(inner, &rel.provider_type));
746
747            // Wrap all code constructs in backticks: provider trait, context type, and provider type
748            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            // Add nested consumer trait dependencies (transitive dependencies)
761            // These are consumer traits that this provider depends on
762            // Filter out the current component's own consumer trait to avoid showing it as its own dependency
763            let all_nested_consumers: Vec<_> =
764                extract_nested_consumer_traits(&entry.delegation_notes)
765                    .into_iter()
766                    .filter(|nested| {
767                        // Exclude the current component's consumer trait
768                        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            // Add getter requirements as children (if this provider directly requires fields)
778            // Only add getters if there's no nested consumer trait (to avoid duplication)
779            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            // Add all nested consumer dependencies
785            for nested_consumer in &all_nested_consumers {
786                // Build nodes for the nested consumer + its provider tree
787                // Pass rendered_consumer_traits to mark references as needed
788                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 this is a higher-order provider, add inner provider as info node
798            if is_higher_order {
799                if let Some(inner_provider) = all_inner_providers.first() {
800                    // Wrap inner provider description with backticks
801                    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), // Inner is OK if outer has the error
809                        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
823/// Builds getter trait nodes from delegation notes
824fn build_getter_nodes(entry: &DiagnosticEntry, context_type: &str) -> Vec<DependencyNode> {
825    let mut getter_nodes = Vec::new();
826
827    // Look for "HasXxx" patterns in delegation notes
828    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                // Wrap getter trait name and context type in backticks
832                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 we have field info, add the field requirement as a child
840            // We add it to the first getter trait we find, since that's typically the most relevant one
841            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                        // Wrap both field name and target type in backticks
846                        description: format!(
847                            "field `{}` on `{}`",
848                            formatted_field, field_info.target_type
849                        ),
850                        trait_type: None,
851                        is_satisfied: Some(false), // This is the missing field
852                        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
866/// Builds nodes for nested consumer+provider dependencies
867/// This handles cases where a provider depends on another consumer trait,
868/// which in turn requires another provider that's not satisfied.
869///
870/// For example: DensityFromMassField -> requires CanCalculateArea -> requires RectangleArea
871///
872/// If the nested consumer is in rendered_consumer_traits, marks it as a reference (*)
873/// instead of building the full tree to avoid duplication (cargo tree style).
874fn 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    // Check if this consumer trait has already been rendered at the root level
883    // If so, mark it as a reference instead of building the full tree
884    let is_reference = rendered_consumer_traits
885        .iter()
886        .any(|rendered| *rendered == nested_consumer.trait_name);
887
888    // Check if this consumer trait maps to a checked component (appears in component_infos)
889    // We match by checking if the provider trait of any component matches this consumer trait
890    // For example: CanCalculateArea consumer trait → AreaCalculator provider trait
891    let matching_component = entry.component_infos.iter().find(|comp| {
892        if let Some(ref provider_trait) = comp.provider_trait {
893            // Extract the action/noun part from consumer trait (e.g., "CanCalculateArea" → "CalculateArea")
894            // and check if it matches the provider trait "AreaCalculator"
895            // This is a fuzzy match since the names follow similar patterns but aren't exact
896            if let Some(action_part) = nested_consumer.trait_name.strip_prefix("Can") {
897                // Both should contain similar words, just potentially in different order
898                // Simple heuristic: check if provider trait contains any significant word from action part
899                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                // Check if they share significant words
910                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    // Create a node for the nested consumer trait
925    // Wrap consumer trait name and context type in backticks
926    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, // Mark if it's a reference to an earlier node
936    };
937
938    // If this is a reference, don't build children - the full tree is shown elsewhere
939    if is_reference {
940        nodes.push(consumer_node);
941        return nodes;
942    }
943
944    if is_shared_component {
945        // This is a shared component dependency that's also checked at root level
946        // Build the full provider tree for it, including getter and field requirements
947
948        if let Some(component_info) = matching_component {
949            // Match this component to its provider relationship
950            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                    // Wrap all code constructs in backticks
955                    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                    // Add getter requirements and field nodes for this provider
969                    // This ensures the full dependency tree is shown, including the missing field
970                    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        // Not a shared component - this consumer trait is not checked at root
979        // Just show that it's not satisfied, don't build a full tree
980        // Try to extract the unsatisfied provider from the main error message
981        if let Some(unsatisfied) = extract_unsatisfied_provider_from_message(&entry.message) {
982            // Create a provider node that's marked as unsatisfied
983            // Wrap all code constructs in backticks
984            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), // Mark as unsatisfied
993                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
1005/// Extracts getter trait name from a delegation note
1006fn extract_getter_trait_from_note(note: &str) -> Option<String> {
1007    // Look for "to implement `HasXxx`" pattern
1008    if let Some(trait_name) = extract_trait_from_note(note) {
1009        // Only return if it looks like a getter trait (Has*)
1010        if trait_name.starts_with("Has") {
1011            return Some(trait_name);
1012        }
1013    }
1014    None
1015}
1016
1017/// Extracts any trait name from a delegation note
1018fn 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 = &note[after_start..after_start + end];
1023            let cleaned = strip_module_prefixes(trait_name);
1024            // Further clean up IsProviderFor patterns
1025            if cleaned.starts_with("IsProviderFor<") {
1026                // Extract the component/trait from IsProviderFor<Component, Context>
1027                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                        // Just return the component part
1031                        return Some(
1032                            cleaned[after_bracket..after_bracket + comma_pos]
1033                                .trim()
1034                                .to_string(),
1035                        );
1036                    }
1037                }
1038                // If parsing fails, return None to skip this
1039                return None;
1040            }
1041            return Some(cleaned);
1042        }
1043    }
1044    None
1045}
1046
1047/// Extracts any trait name from a delegation note
1048fn extract_context_from_notes(notes: &[String]) -> Option<String> {
1049    for note in notes {
1050        // Look for "for `Type` to implement" pattern
1051        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 = &note[after_start..after_start + end];
1055                return Some(strip_module_prefixes(type_name));
1056            }
1057        }
1058    }
1059    None
1060}
1061
1062/// Information about a nested consumer trait dependency extracted from delegation notes
1063/// This represents a consumer trait that a provider depends on
1064#[derive(Debug, Clone)]
1065struct NestedConsumerTrait {
1066    /// The consumer trait name (e.g., "CanCalculateArea")
1067    trait_name: String,
1068    /// The context type (e.g., "Rectangle")
1069    context_type: String,
1070}
1071
1072/// Extracts nested consumer trait dependencies from delegation notes
1073/// These are consumer traits that providers depend on, shown in notes like:
1074/// "required for `Rectangle` to implement `CanCalculateArea`"
1075fn extract_nested_consumer_traits(notes: &[String]) -> Vec<NestedConsumerTrait> {
1076    let mut results = Vec::new();
1077
1078    for note in notes {
1079        // Look for pattern: "required for `Context` to implement `TraitName`"
1080        // This indicates that the provider depends on this consumer trait
1081        if let Some(for_pos) = note.find("required for `") {
1082            let after_for = for_pos + "required for `".len();
1083
1084            // Extract context type (between first ` and next `)
1085            if let Some(context_end) = note[after_for..].find('`') {
1086                let context_type = &note[after_for..after_for + context_end];
1087
1088                // Look for the trait name after "to implement `"
1089                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 = &note[trait_start..trait_start + trait_end];
1096
1097                        // Filter out internal CGP traits - we only want consumer traits
1098                        // Consumer traits typically start with "Can" but exclude framework traits
1099                        let cleaned_trait = strip_module_prefixes(trait_name);
1100
1101                        // Skip if it's an IsProviderFor or CanUseComponent trait (these are internal)
1102                        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/// Information about an unsatisfied provider trait extracted from the error message
1121#[derive(Debug, Clone)]
1122struct UnsatisfiedProvider {
1123    /// The provider type (e.g., "RectangleArea")
1124    provider_type: String,
1125    /// The trait that's not satisfied (e.g., "AreaCalculator")
1126    trait_name: String,
1127    /// The context type (e.g., "Rectangle")
1128    context_type: String,
1129}
1130
1131/// Extracts unsatisfied provider information from the main error message
1132/// Error messages follow the pattern:
1133/// "the trait bound `ProviderType: TraitName<Context>` is not satisfied"
1134fn extract_unsatisfied_provider_from_message(message: &str) -> Option<UnsatisfiedProvider> {
1135    // Look for pattern: "the trait bound `Provider: Trait<Context>` is not satisfied"
1136    if let Some(bound_start) = message.find("the trait bound `") {
1137        let after_bound = bound_start + "the trait bound `".len();
1138
1139        // Find the closing backtick
1140        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            // Parse "Provider: Trait<Context>"
1144            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                // Parse "Trait<Context>"
1149                if let Some(open_bracket) = trait_and_context.find('<') {
1150                    let trait_name = trait_and_context[..open_bracket].trim();
1151
1152                    // Extract context (everything between < and >)
1153                    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
1171/// Formats the delegation chain with better structure and CGP-aware terminology
1172fn format_delegation_chain(entry: &DiagnosticEntry) -> Vec<String> {
1173    // Try to build a proper dependency tree
1174    if let Some(tree) = build_dependency_tree(entry) {
1175        return render_dependency_tree(&tree, "", true, true);
1176    }
1177
1178    // Fallback to old format if tree building fails
1179    format_delegation_chain_legacy(entry)
1180}
1181
1182/// Legacy delegation chain formatting (fallback)
1183fn format_delegation_chain_legacy(entry: &DiagnosticEntry) -> Vec<String> {
1184    // Detect inner providers BEFORE deduplication
1185    let all_inner_providers: Vec<String> = detect_inner_providers(&entry.provider_relationships);
1186
1187    // First deduplicate the provider relationships to remove nested redundancies
1188    let deduped_relationships = deduplicate_provider_relationships(&entry.provider_relationships);
1189
1190    // Build a set of provider types we should keep
1191    let kept_provider_types: std::collections::HashSet<String> = deduped_relationships
1192        .iter()
1193        .map(|r| r.provider_type.clone())
1194        .collect();
1195
1196    // Deduplicate notes first
1197    let deduped_notes = deduplicate_delegation_notes(&entry.delegation_notes);
1198
1199    let mut formatted = Vec::new();
1200
1201    // If we have inner providers and field errors, add a hint about the root cause
1202    // We check this AFTER deduplication to see which outer providers remain
1203    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        // Parse provider info from the note to check if it should be kept
1223        let should_keep = if let Some(provider_info) =
1224            crate::cgp_patterns::extract_provider_relationship(&note)
1225        {
1226            // Keep this note only if its provider type is in the kept set, OR if we have no provider info
1227            kept_provider_types.is_empty()
1228                || kept_provider_types.contains(&provider_info.provider_type)
1229        } else {
1230            // Not a provider relationship note, always keep it
1231            true
1232        };
1233
1234        if !should_keep {
1235            // Skip this note as it's redundant
1236            continue;
1237        }
1238
1239        let formatted_note = format_delegation_note(&note, entry);
1240        formatted.push(format!("→ {}", formatted_note));
1241    }
1242
1243    formatted
1244}
1245
1246/// Detects inner providers in a list of provider relationships
1247/// Returns the list of inner provider types (those that appear as type parameters in other providers)
1248fn detect_inner_providers(relationships: &[ProviderRelationship]) -> Vec<String> {
1249    let mut inner_providers = Vec::new();
1250
1251    for rel in relationships {
1252        // Check if this provider appears as a type parameter in any other provider
1253        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
1267/// Checks if inner_type appears as a type parameter within outer_type
1268/// For example, "RectangleArea" is contained in "ScaledArea<RectangleArea>"
1269fn is_contained_type_parameter(inner_type: &str, outer_type: &str) -> bool {
1270    // Check various patterns where inner could appear in outer
1271    let patterns = [
1272        format!("<{}>", inner_type),
1273        format!("<{},", inner_type),
1274        format!(", {}>", inner_type),
1275        format!(", {},", inner_type),
1276        format!("< {}", inner_type), // with spaces
1277        format!("{} >", inner_type),
1278    ];
1279
1280    patterns.iter().any(|pattern| outer_type.contains(pattern))
1281}
1282
1283/// Simplifies a single delegation note
1284fn format_delegation_note(note: &str, _entry: &DiagnosticEntry) -> String {
1285    let mut result = note.to_string();
1286
1287    // Remove module prefixes
1288    result = strip_module_prefixes(&result);
1289
1290    // Replace IsProviderFor with user-friendly "provider trait" terminology
1291    result = replace_is_provider_for(&result);
1292
1293    // Replace CanUseComponent with simpler terminology
1294    result = replace_can_use_component(&result);
1295
1296    // Truncate overly long type names
1297    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
1306/// Replaces `IsProviderFor<Component, Context>` with "the provider trait `ProviderTrait`"
1307fn replace_is_provider_for(message: &str) -> String {
1308    if !message.contains("IsProviderFor") {
1309        return message.to_string();
1310    }
1311
1312    // Find the IsProviderFor pattern
1313    if let Some(start) = message.find("IsProviderFor<") {
1314        let after_start = start + "IsProviderFor<".len();
1315
1316        // Extract component name (up to the first comma)
1317        if let Some(comma_pos) = find_top_level_comma(after_start, message) {
1318            let component_name = message[after_start..comma_pos].trim();
1319
1320            // Derive provider trait name
1321            let provider_trait_name = derive_provider_trait_name(component_name)
1322                .unwrap_or_else(|| format!("the provider trait for `{}`", component_name));
1323
1324            // Find the end of IsProviderFor<...>
1325            let end_pos = find_matching_bracket(after_start, message).unwrap_or(message.len());
1326
1327            // Build replacement
1328            let before = &message[..start];
1329            let after = &message[end_pos..];
1330
1331            // Handle backticks
1332            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
1354/// Replaces `CanUseComponent<Component>` with simpler terminology
1355fn replace_can_use_component(message: &str) -> String {
1356    if !message.contains("CanUseComponent") {
1357        return message.to_string();
1358    }
1359
1360    // Find the CanUseComponent pattern
1361    if let Some(start) = message.find("CanUseComponent<") {
1362        let after_start = start + "CanUseComponent<".len();
1363
1364        // Find the end of the generic type
1365        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        // Build replacement - just explain it's checking component availability
1370        let replacement = format!("use component `{}`", component_name);
1371
1372        // Handle backticks
1373        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
1394/// Finds the position of a comma at the top level of generic nesting
1395fn 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
1410/// Finds the position of the matching closing bracket
1411fn 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
1430/// Renders a CGP diagnostic to a string using the graphical (colorful) handler
1431pub 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            // Fallback to simple display if rendering fails
1439            format!("error: {}", diagnostic.message)
1440        }
1441    }
1442}
1443
1444/// Renders a CGP diagnostic to a plain text string (no colors)
1445pub fn render_diagnostic_plain(diagnostic: &CgpDiagnostic) -> String {
1446    // Use the narratable handler which produces plain text
1447    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            // Fallback to simple display if rendering fails
1454            format!("error: {}", diagnostic.message)
1455        }
1456    }
1457}
1458
1459/// Detects if we're running in a terminal that supports colors
1460pub 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}