Skip to main content

cargo_cgp/
cgp_patterns.rs

1/// Module for detecting and extracting CGP-specific patterns from compiler diagnostics
2/// This module only patterns match on CGP library constructs, never on user code
3use cargo_metadata::diagnostic::{Diagnostic, DiagnosticLevel};
4
5/// Checks if a diagnostic is related to CGP constructs
6pub fn is_cgp_diagnostic(diagnostic: &Diagnostic) -> bool {
7    let cgp_patterns = [
8        "CanUseComponent",
9        "IsProviderFor",
10        "HasField",
11        "cgp_impl",
12        "cgp_component",
13        "cgp_auto_getter",
14        "delegate_components",
15        "check_components",
16    ];
17
18    // Check main message
19    if cgp_patterns.iter().any(|p| diagnostic.message.contains(p)) {
20        return true;
21    }
22
23    // Check children messages
24    for child in &diagnostic.children {
25        if cgp_patterns.iter().any(|p| child.message.contains(p)) {
26            return true;
27        }
28    }
29
30    false
31}
32
33/// Information about a component extracted from CGP patterns
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub struct ComponentInfo {
36    /// Full component type name (e.g., "AreaCalculatorComponent", "ScaledArea<RectangleArea>")
37    pub component_type: String,
38    /// Provider trait name derived from component (e.g., "AreaCalculator" from "AreaCalculatorComponent")
39    pub provider_trait: Option<String>,
40}
41
42/// Information about a field extracted from HasField patterns
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct FieldInfo {
45    /// The field name extracted from Symbol pattern
46    pub field_name: String,
47    /// Whether the field name was fully extracted (false if truncated)
48    pub is_complete: bool,
49    /// Whether the field name contains unknown characters (shown as �)
50    pub has_unknown_chars: bool,
51    /// The struct/type that is missing the field
52    pub target_type: String,
53}
54
55/// Information about provider trait relationships from IsProviderFor patterns
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub struct ProviderRelationship {
58    /// The provider implementation type
59    pub provider_type: String,
60    /// The component being provided
61    pub component: String,
62    /// The context type
63    pub context: String,
64}
65
66/// Extracts component information from CanUseComponent patterns
67/// Pattern: `CanUseComponent<ComponentType>`
68pub fn extract_component_from_can_use(message: &str) -> Option<ComponentInfo> {
69    let start = message.find("CanUseComponent<")?;
70    let after_start = start + "CanUseComponent<".len();
71
72    let component_type = extract_balanced_generic(message, after_start)?;
73    let provider_trait = derive_provider_trait_name(&component_type);
74
75    Some(ComponentInfo {
76        component_type,
77        provider_trait,
78    })
79}
80
81/// Extracts component information from various patterns in a message
82pub fn extract_component_info(message: &str) -> Option<ComponentInfo> {
83    // Try CanUseComponent pattern first
84    if let Some(info) = extract_component_from_can_use(message) {
85        return Some(info);
86    }
87    // Try IsProviderFor pattern - extract component from inside the generic
88    // Pattern: `IsProviderFor<ComponentName, Context>`
89    if let Some(start) = message.find("IsProviderFor<") {
90        let after_start = start + "IsProviderFor<".len();
91
92        // Find the first comma (component name ends before it)
93        if let Some(comma_pos) = message[after_start..].find(',') {
94            let component_type = message[after_start..after_start + comma_pos].trim();
95
96            // Verify it looks like a component (ends with "Component" or contains it)
97            if component_type.contains("Component") {
98                let provider_trait = derive_provider_trait_name(component_type);
99                return Some(ComponentInfo {
100                    component_type: component_type.to_string(),
101                    provider_trait,
102                });
103            }
104        }
105    }
106
107    // Try to find component names by the "*Component" suffix pattern
108    // This is a general CGP naming convention
109    for word in message.split_whitespace() {
110        let clean_word =
111            word.trim_matches(|c: char| !c.is_alphanumeric() && c != '<' && c != '>' && c != ',');
112
113        // Skip if this is part of an IsProviderFor pattern
114        // We handle that separately above
115        if clean_word.contains("IsProviderFor") {
116            continue;
117        }
118
119        if clean_word.contains("Component") {
120            // Extract the component type, handling generics
121            if let Some(component_type) = extract_component_type_name(clean_word) {
122                let provider_trait = derive_provider_trait_name(&component_type);
123                return Some(ComponentInfo {
124                    component_type,
125                    provider_trait,
126                });
127            }
128        }
129    }
130
131    None
132}
133
134/// Extracts a component type name from a string that may contain it
135fn extract_component_type_name(text: &str) -> Option<String> {
136    // Handle simple case: just "XyzComponent"
137    if text.ends_with("Component") && !text.contains('<') {
138        return Some(text.to_string());
139    }
140
141    // Handle generic case: "Xyz<A, B>Component" or more complex patterns
142    // Find all text that forms a valid component reference
143    if let Some(component_pos) = text.rfind("Component") {
144        // Find the start of this component reference
145        let before_component = &text[..component_pos + "Component".len()];
146
147        // Walk backward to find the start, handling generics
148        let mut depth = 0;
149        let mut start_idx = 0;
150
151        for (i, ch) in before_component.char_indices().rev() {
152            if ch == '>' {
153                depth += 1;
154            } else if ch == '<' {
155                depth -= 1;
156            } else if depth == 0 && !ch.is_alphanumeric() && ch != '_' {
157                start_idx = i + 1;
158                break;
159            }
160        }
161
162        return Some(before_component[start_idx..].to_string());
163    }
164
165    None
166}
167
168/// Derives provider trait name from component name by removing "Component" suffix
169/// Example: "AreaCalculatorComponent" -> Some("AreaCalculator")
170pub fn derive_provider_trait_name(component_name: &str) -> Option<String> {
171    // Handle simple case: "XyzComponent" -> "Xyz"
172    if let Some(stripped) = component_name.strip_suffix("Component") {
173        if !stripped.is_empty() {
174            return Some(stripped.to_string());
175        }
176    }
177
178    // Handle complex generic cases like "Wrapper<Inner>Component"
179    // This shouldn't normally happen in CGP, but handle it gracefully
180    if component_name.contains("Component") {
181        if let Some(pos) = component_name.rfind("Component") {
182            let before = &component_name[..pos];
183            if !before.is_empty() {
184                return Some(before.to_string());
185            }
186        }
187    }
188
189    None
190}
191
192/// Extracts field information from HasField diagnostic patterns
193/// Pattern: `HasField<Symbol<N, Chars<'c1', Chars<'c2', ...>>>>` is not implemented for `Type`
194pub fn extract_field_info(diagnostic: &Diagnostic) -> Option<FieldInfo> {
195    for child in &diagnostic.children {
196        if matches!(child.level, DiagnosticLevel::Help) {
197            let message = &child.message;
198
199            if message.contains("HasField") && message.contains("is not implemented for") {
200                // Extract the field name from Symbol pattern
201                let field_name_result = extract_field_name_from_symbol(message)?;
202
203                // Extract the target type
204                let target_type = extract_type_from_not_implemented(message)?;
205
206                return Some(FieldInfo {
207                    field_name: field_name_result.0,
208                    is_complete: field_name_result.1,
209                    has_unknown_chars: field_name_result.2,
210                    target_type,
211                });
212            }
213        }
214    }
215
216    None
217}
218
219/// Extracts field name from Symbol<N, Chars<'x', Chars<'y', ...>>> pattern
220/// Returns (field_name, is_complete, has_unknown_chars)
221fn extract_field_name_from_symbol(message: &str) -> Option<(String, bool, bool)> {
222    // Get the part before "but trait" if it exists (to focus on the unsatisfied trait)
223    let relevant_part = if let Some(pos) = message.find("but trait") {
224        &message[..pos]
225    } else {
226        message
227    };
228
229    // Extract expected length from Symbol<N, ...>
230    let expected_length = extract_symbol_length(relevant_part)?;
231
232    // Extract visible characters from Chars<'x', Chars<'y', ...>> chain
233    let (chars, has_unknown) = extract_chars_from_pattern(relevant_part);
234
235    if chars.is_empty() {
236        return None;
237    }
238
239    let field_name: String = chars.iter().collect();
240    let is_complete = field_name.len() == expected_length;
241
242    Some((field_name, is_complete, has_unknown))
243}
244
245/// Extracts the expected length from Symbol<N, ...> pattern
246fn extract_symbol_length(text: &str) -> Option<usize> {
247    let start = text.find("Symbol<")?;
248    let after_symbol = &text[start + 7..];
249    let comma_pos = after_symbol.find(',')?;
250    after_symbol[..comma_pos].trim().parse::<usize>().ok()
251}
252
253/// Extracts all characters from Chars<'x', Chars<'y', ...>> pattern
254/// Returns (chars, has_unknown_chars)
255/// Unknown characters (represented as '_' in the original) are replaced with '�' (U+FFFD)
256fn extract_chars_from_pattern(text: &str) -> (Vec<char>, bool) {
257    let mut chars = Vec::new();
258    let mut has_unknown = false;
259    let mut idx = 0;
260
261    while idx < text.len() {
262        if text[idx..].starts_with("Chars<'") {
263            // Look for the character after the quote
264            let char_start = idx + 7;
265            if let Some(ch) = text[char_start..].chars().next() {
266                // Extract the character if it's not the closing quote
267                if ch != '\'' {
268                    chars.push(ch);
269                }
270            }
271        } else if text[idx..].starts_with("Chars<_") {
272            // This is an unknown/hidden character (no quotes around it)
273            chars.push('\u{FFFD}'); // Unicode replacement character
274            has_unknown = true;
275        }
276        idx += 1;
277    }
278
279    (chars, has_unknown)
280}
281
282/// Extracts type name from "is not implemented for `Type`" pattern
283fn extract_type_from_not_implemented(message: &str) -> Option<String> {
284    let start = message.find("is not implemented for `")?;
285    let after_start = start + "is not implemented for `".len();
286    let end = message[after_start..].find('`')?;
287    let full_name = &message[after_start..after_start + end];
288
289    // Remove module prefix (e.g., "module::Type" -> "Type")
290    let simple_name = full_name.split("::").last().unwrap_or(full_name);
291    Some(simple_name.to_string())
292}
293
294/// Extracts provider relationship from IsProviderFor patterns
295/// Pattern: `for `Provider` to implement `IsProviderFor<Component, Context>`
296pub fn extract_provider_relationship(message: &str) -> Option<ProviderRelationship> {
297    if !message.contains("IsProviderFor") {
298        return None;
299    }
300
301    // Extract provider type: "for `Provider` to implement"
302    let provider_type = extract_type_from_for_to_implement(message)?;
303
304    // Extract component and context from IsProviderFor<Component, Context>
305    let start = message.find("IsProviderFor<")?;
306    let after_start = start + "IsProviderFor<".len();
307
308    // Find the comma separating component and context
309    let comma_pos = find_comma_at_depth(after_start, message)?;
310    let component = message[after_start..comma_pos].trim().to_string();
311
312    // Extract context (from comma to closing >)
313    let after_comma = comma_pos + 1;
314    let context = extract_balanced_generic(message, after_comma)?;
315
316    Some(ProviderRelationship {
317        provider_type,
318        component,
319        context,
320    })
321}
322
323/// Extracts type from "for `Type` to implement" pattern
324fn extract_type_from_for_to_implement(message: &str) -> Option<String> {
325    let start = message.find("for `")?;
326    let after_start = start + 5;
327    let end = message[after_start..].find("` to")?;
328    let full_name = &message[after_start..after_start + end];
329
330    // Remove module prefix
331    let simple_name = full_name.split("::").last().unwrap_or(full_name);
332    Some(simple_name.to_string())
333}
334
335/// Finds the position of a comma at the top level of generic nesting
336fn find_comma_at_depth(start_pos: usize, text: &str) -> Option<usize> {
337    let mut depth = 0;
338
339    for (i, ch) in text[start_pos..].char_indices() {
340        match ch {
341            '<' => depth += 1,
342            '>' => depth -= 1,
343            ',' if depth == 0 => return Some(start_pos + i),
344            _ => {}
345        }
346    }
347
348    None
349}
350
351/// Extracts a balanced generic type from text starting at position
352/// Example: extract "Foo<Bar, Baz>" from position after opening `<`
353fn extract_balanced_generic(text: &str, start_pos: usize) -> Option<String> {
354    let mut depth = 1; // We've already seen one opening bracket
355    let mut end_pos = start_pos;
356
357    for (i, ch) in text[start_pos..].char_indices() {
358        match ch {
359            '<' => depth += 1,
360            '>' => {
361                depth -= 1;
362                if depth == 0 {
363                    end_pos = start_pos + i;
364                    break;
365                }
366            }
367            _ => {}
368        }
369    }
370
371    if depth == 0 {
372        Some(text[start_pos..end_pos].trim().to_string())
373    } else {
374        // Not balanced, return what we have
375        Some(text[start_pos..].trim_end_matches('>').trim().to_string())
376    }
377}
378
379/// Extracts check trait name from "required by a bound in `TraitName`" pattern
380/// Note: This extracts the check trait (e.g., CanUseRectangle), NOT the consumer trait
381pub fn extract_check_trait(message: &str) -> Option<String> {
382    let start = message.find("required by a bound in `")?;
383    let after_start = start + "required by a bound in `".len();
384    let end = message[after_start..].find('`')?;
385    Some(message[after_start..after_start + end].to_string())
386}
387
388/// Checks if a diagnostic has help messages indicating other HasField implementations exist
389pub fn has_other_hasfield_implementations(diagnostic: &Diagnostic) -> bool {
390    for child in &diagnostic.children {
391        if matches!(child.level, DiagnosticLevel::Help) {
392            if child.message.contains("but trait `HasField")
393                || child
394                    .message
395                    .contains("the following other types implement trait")
396            {
397                return true;
398            }
399        }
400    }
401    false
402}
403
404/// Information about a consumer trait dependency extracted from delegation notes
405/// This represents a consumer trait that a provider depends on
406#[derive(Debug, Clone, PartialEq, Eq, Hash)]
407pub struct ConsumerTraitDependency {
408    /// The consumer trait name (e.g., "CanCalculateArea")
409    pub trait_name: String,
410    /// The context type (e.g., "Rectangle")
411    pub context_type: String,
412    /// The component that provides this consumer trait (if we can derive it)
413    pub component_name: Option<String>,
414}
415
416/// Extracts consumer trait dependencies from delegation notes
417/// These are consumer traits that providers depend on, shown in notes like:
418/// "required for `Rectangle` to implement `CanCalculateArea`"
419/// Returns None if the trait is not a consumer trait (e.g., internal CGP traits)
420pub fn extract_consumer_trait_dependency(note: &str) -> Option<ConsumerTraitDependency> {
421    // Look for pattern: "required for `Context` to implement `TraitName`"
422    // This indicates that the provider depends on this consumer trait
423    if let Some(for_pos) = note.find("required for `") {
424        let after_for = for_pos + "required for `".len();
425
426        // Extract context type (between first ` and next `)
427        if let Some(context_end) = note[after_for..].find('`') {
428            let context_type = &note[after_for..after_for + context_end];
429
430            // Look for the trait name after "to implement `"
431            if let Some(implement_pos) = note[after_for + context_end..].find("to implement `") {
432                let trait_start = after_for + context_end + implement_pos + "to implement `".len();
433
434                if let Some(trait_end) = note[trait_start..].find('`') {
435                    let trait_name = &note[trait_start..trait_start + trait_end];
436
437                    // Filter out internal CGP traits - we only want consumer traits
438                    // Consumer traits typically start with "Can" but exclude framework traits
439                    let cleaned_trait = strip_module_prefixes(trait_name);
440
441                    // Skip if it's an IsProviderFor or CanUseComponent trait (these are internal)
442                    if cleaned_trait.starts_with("Can")
443                        && !cleaned_trait.contains("CanUseComponent")
444                        && !cleaned_trait.starts_with("IsProviderFor")
445                    {
446                        // Try to derive the component name from the consumer trait
447                        let component_name = derive_component_from_consumer_trait(&cleaned_trait);
448
449                        return Some(ConsumerTraitDependency {
450                            trait_name: cleaned_trait,
451                            context_type: strip_module_prefixes(context_type),
452                            component_name,
453                        });
454                    }
455                }
456            }
457        }
458    }
459
460    None
461}
462
463/// Derives a component name from a consumer trait name
464/// E.g., "CanCalculateArea" -> "AreaCalculatorComponent"
465/// This is a heuristic that works for common CGP naming patterns:
466/// - Consumer trait: Can{Action} (e.g., CanCalculateArea)
467/// - Component: {Action}Component (e.g., AreaCalculatorComponent)
468pub fn derive_component_from_consumer_trait(consumer_trait: &str) -> Option<String> {
469    // Check if it starts with "Can"
470    if let Some(action_part) = consumer_trait.strip_prefix("Can") {
471        // Remove the "Can" prefix and append "Component"
472        // E.g., "CalculateArea" -> "AreaCalculatorComponent"
473        Some(format!("{}Component", action_part))
474    } else {
475        None
476    }
477}
478
479/// Removes all module prefixes from a message (e.g., "foo::bar::Baz" -> "Baz")
480pub fn strip_module_prefixes(message: &str) -> String {
481    // This is a generic transformation - we don't hardcode specific module names
482    let mut result = message.to_string();
483
484    // Remove cgp library prefixes - do this multiple times to handle nested cases
485    for _ in 0..5 {
486        result = result.replace("cgp::prelude::", "");
487        result = result.replace("cgp::", "");
488    }
489
490    // Remove IsProviderFor wrapper that sometimes appears
491    // This handles cases like "IsProviderFor<AreaCalculator<..."
492    if result.starts_with("IsProviderFor<") {
493        // Extract what's inside and clean it up
494        if let Some(start) = result.find('<') {
495            let after_start = start + 1;
496            result = result[after_start..].to_string();
497        }
498    }
499
500    result
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    #[test]
508    fn test_derive_provider_trait_name() {
509        assert_eq!(
510            derive_provider_trait_name("AreaCalculatorComponent"),
511            Some("AreaCalculator".to_string())
512        );
513        assert_eq!(
514            derive_provider_trait_name("FooComponent"),
515            Some("Foo".to_string())
516        );
517        assert_eq!(derive_provider_trait_name("Component"), None);
518        assert_eq!(derive_provider_trait_name("NoSuffix"), None);
519    }
520
521    #[test]
522    fn test_extract_symbol_length() {
523        let text = "Symbol<6, Chars<'h', Chars<'e', ...>>>";
524        assert_eq!(extract_symbol_length(text), Some(6));
525
526        let text2 = "Symbol<5, Chars<'w', ...>>";
527        assert_eq!(extract_symbol_length(text2), Some(5));
528    }
529
530    #[test]
531    fn test_extract_chars_from_pattern() {
532        let text = "Chars<'h', Chars<'e', Chars<'i', Chars<'g', Chars<'h', Chars<'t', Nil>>>>>>";
533        let (chars, has_unknown) = extract_chars_from_pattern(text);
534        assert_eq!(chars, vec!['h', 'e', 'i', 'g', 'h', 't']);
535        assert!(!has_unknown);
536
537        // When a character is hidden by the compiler, it appears as Chars<_, (without quotes)
538        let text2 = "Chars<'w', Chars<'i', Chars<'d', Chars<_, Chars<'h', Nil>>>>>";
539        let (chars2, has_unknown2) = extract_chars_from_pattern(text2);
540        // Hidden characters are shown as �
541        assert_eq!(chars2, vec!['w', 'i', 'd', '\u{FFFD}', 'h']);
542        assert!(has_unknown2);
543    }
544
545    #[test]
546    fn test_derive_component_from_consumer_trait() {
547        assert_eq!(
548            derive_component_from_consumer_trait("CanCalculateArea"),
549            Some("CalculateAreaComponent".to_string())
550        );
551        assert_eq!(
552            derive_component_from_consumer_trait("CanFoo"),
553            Some("FooComponent".to_string())
554        );
555        assert_eq!(
556            derive_component_from_consumer_trait("NotAConsumerTrait"),
557            None
558        );
559    }
560
561    #[test]
562    fn test_extract_consumer_trait_dependency() {
563        let note = "required for `Rectangle` to implement `CanCalculateArea`";
564        let dep = extract_consumer_trait_dependency(note).unwrap();
565        assert_eq!(dep.trait_name, "CanCalculateArea");
566        assert_eq!(dep.context_type, "Rectangle");
567        assert_eq!(
568            dep.component_name,
569            Some("CalculateAreaComponent".to_string())
570        );
571
572        // Should filter out internal traits
573        let note2 = "required for `Rectangle` to implement `CanUseComponent<Something>`";
574        assert!(extract_consumer_trait_dependency(note2).is_none());
575    }
576}