1use cargo_metadata::diagnostic::{Diagnostic, DiagnosticLevel};
4
5pub 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 if cgp_patterns.iter().any(|p| diagnostic.message.contains(p)) {
20 return true;
21 }
22
23 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub struct ComponentInfo {
36 pub component_type: String,
38 pub provider_trait: Option<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct FieldInfo {
45 pub field_name: String,
47 pub is_complete: bool,
49 pub has_unknown_chars: bool,
51 pub target_type: String,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub struct ProviderRelationship {
58 pub provider_type: String,
60 pub component: String,
62 pub context: String,
64}
65
66pub 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
81pub fn extract_component_info(message: &str) -> Option<ComponentInfo> {
83 if let Some(info) = extract_component_from_can_use(message) {
85 return Some(info);
86 }
87 if let Some(start) = message.find("IsProviderFor<") {
90 let after_start = start + "IsProviderFor<".len();
91
92 if let Some(comma_pos) = message[after_start..].find(',') {
94 let component_type = message[after_start..after_start + comma_pos].trim();
95
96 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 for word in message.split_whitespace() {
110 let clean_word =
111 word.trim_matches(|c: char| !c.is_alphanumeric() && c != '<' && c != '>' && c != ',');
112
113 if clean_word.contains("IsProviderFor") {
116 continue;
117 }
118
119 if clean_word.contains("Component") {
120 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
134fn extract_component_type_name(text: &str) -> Option<String> {
136 if text.ends_with("Component") && !text.contains('<') {
138 return Some(text.to_string());
139 }
140
141 if let Some(component_pos) = text.rfind("Component") {
144 let before_component = &text[..component_pos + "Component".len()];
146
147 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
168pub fn derive_provider_trait_name(component_name: &str) -> Option<String> {
171 if let Some(stripped) = component_name.strip_suffix("Component") {
173 if !stripped.is_empty() {
174 return Some(stripped.to_string());
175 }
176 }
177
178 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
192pub 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 let field_name_result = extract_field_name_from_symbol(message)?;
202
203 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
219fn extract_field_name_from_symbol(message: &str) -> Option<(String, bool, bool)> {
222 let relevant_part = if let Some(pos) = message.find("but trait") {
224 &message[..pos]
225 } else {
226 message
227 };
228
229 let expected_length = extract_symbol_length(relevant_part)?;
231
232 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
245fn 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
253fn 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 let char_start = idx + 7;
265 if let Some(ch) = text[char_start..].chars().next() {
266 if ch != '\'' {
268 chars.push(ch);
269 }
270 }
271 } else if text[idx..].starts_with("Chars<_") {
272 chars.push('\u{FFFD}'); has_unknown = true;
275 }
276 idx += 1;
277 }
278
279 (chars, has_unknown)
280}
281
282fn 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 let simple_name = full_name.split("::").last().unwrap_or(full_name);
291 Some(simple_name.to_string())
292}
293
294pub fn extract_provider_relationship(message: &str) -> Option<ProviderRelationship> {
297 if !message.contains("IsProviderFor") {
298 return None;
299 }
300
301 let provider_type = extract_type_from_for_to_implement(message)?;
303
304 let start = message.find("IsProviderFor<")?;
306 let after_start = start + "IsProviderFor<".len();
307
308 let comma_pos = find_comma_at_depth(after_start, message)?;
310 let component = message[after_start..comma_pos].trim().to_string();
311
312 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
323fn 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 let simple_name = full_name.split("::").last().unwrap_or(full_name);
332 Some(simple_name.to_string())
333}
334
335fn 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
351fn extract_balanced_generic(text: &str, start_pos: usize) -> Option<String> {
354 let mut depth = 1; 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 Some(text[start_pos..].trim_end_matches('>').trim().to_string())
376 }
377}
378
379pub 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
388pub 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
407pub struct ConsumerTraitDependency {
408 pub trait_name: String,
410 pub context_type: String,
412 pub component_name: Option<String>,
414}
415
416pub fn extract_consumer_trait_dependency(note: &str) -> Option<ConsumerTraitDependency> {
421 if let Some(for_pos) = note.find("required for `") {
424 let after_for = for_pos + "required for `".len();
425
426 if let Some(context_end) = note[after_for..].find('`') {
428 let context_type = ¬e[after_for..after_for + context_end];
429
430 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 = ¬e[trait_start..trait_start + trait_end];
436
437 let cleaned_trait = strip_module_prefixes(trait_name);
440
441 if cleaned_trait.starts_with("Can")
443 && !cleaned_trait.contains("CanUseComponent")
444 && !cleaned_trait.starts_with("IsProviderFor")
445 {
446 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
463pub fn derive_component_from_consumer_trait(consumer_trait: &str) -> Option<String> {
469 if let Some(action_part) = consumer_trait.strip_prefix("Can") {
471 Some(format!("{}Component", action_part))
474 } else {
475 None
476 }
477}
478
479pub fn strip_module_prefixes(message: &str) -> String {
481 let mut result = message.to_string();
483
484 for _ in 0..5 {
486 result = result.replace("cgp::prelude::", "");
487 result = result.replace("cgp::", "");
488 }
489
490 if result.starts_with("IsProviderFor<") {
493 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 let text2 = "Chars<'w', Chars<'i', Chars<'d', Chars<_, Chars<'h', Nil>>>>>";
539 let (chars2, has_unknown2) = extract_chars_from_pattern(text2);
540 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 let note2 = "required for `Rectangle` to implement `CanUseComponent<Something>`";
574 assert!(extract_consumer_trait_dependency(note2).is_none());
575 }
576}