Skip to main content

ryo_analysis/check/
checker.rs

1//! GraphChecker - Lightweight graph-based validation.
2
3use super::result::{CheckError, CheckResult};
4use super::traits::LightCheck;
5use crate::query::{CodeGraphV2, StdImplCache, TypeFlowGraphV2};
6use crate::symbol::{SymbolPath, SymbolRegistry};
7
8/// Lightweight graph-based checker for pre-mutation validation.
9///
10/// Uses the existing `CodeGraphV2` and `SymbolRegistry` to quickly validate
11/// that mutations are likely to succeed, without running the full compiler.
12///
13/// # Performance Target
14///
15/// | Operation | Target | vs cargo check |
16/// |-----------|--------|----------------|
17/// | Symbol exists | < 1ms | N/A |
18/// | Trait impl check | < 5ms | N/A |
19/// | Derive possible | < 50ms | 100x faster |
20/// | Full pre-check | < 100ms | 50-100x faster |
21pub struct GraphChecker<'a> {
22    graph: &'a CodeGraphV2,
23    typeflow: &'a TypeFlowGraphV2,
24    registry: &'a SymbolRegistry,
25    std_impls: StdImplCache,
26}
27
28impl<'a> GraphChecker<'a> {
29    /// Create a new GraphChecker.
30    pub fn new(
31        graph: &'a CodeGraphV2,
32        typeflow: &'a TypeFlowGraphV2,
33        registry: &'a SymbolRegistry,
34    ) -> Self {
35        Self {
36            graph,
37            typeflow,
38            registry,
39            std_impls: StdImplCache::default(),
40        }
41    }
42
43    /// Create a checker with a custom std impls cache.
44    pub fn with_std_impls(
45        graph: &'a CodeGraphV2,
46        typeflow: &'a TypeFlowGraphV2,
47        registry: &'a SymbolRegistry,
48        std_impls: StdImplCache,
49    ) -> Self {
50        Self {
51            graph,
52            typeflow,
53            registry,
54            std_impls,
55        }
56    }
57
58    // === Symbol Existence Checks ===
59
60    /// Check if a symbol exists in the registry.
61    ///
62    /// Searches both by full path and by short name.
63    ///
64    /// # Examples
65    /// ```rust,ignore
66    /// let checker = GraphChecker::new(&graph, &registry);
67    ///
68    /// // Full path lookup
69    /// assert!(checker.check_symbol_exists("my_crate::MyStruct"));
70    ///
71    /// // Short name lookup (finds my_crate::MyStruct)
72    /// assert!(checker.check_symbol_exists("MyStruct"));
73    /// ```
74    pub fn check_symbol_exists(&self, name: &str) -> bool {
75        // 1. Try as full path
76        if let Ok(path) = SymbolPath::parse(name) {
77            if self.registry.lookup(&path).is_some() {
78                return true;
79            }
80        }
81
82        // 2. Try as short name
83        !self.registry.find_by_name(name).is_empty()
84    }
85
86    /// Check if a symbol exists and return suggestions if not.
87    pub fn check_symbol_with_suggestions(&self, name: &str) -> Result<(), CheckError> {
88        if self.check_symbol_exists(name) {
89            Ok(())
90        } else {
91            // Try to find similar names for suggestions
92            let suggestions = self.find_similar_symbols(name);
93            Err(CheckError::UnresolvedRef {
94                name: name.to_string(),
95                location: None,
96                suggestions,
97            })
98        }
99    }
100
101    /// Check if a symbol is unique (exactly one match).
102    ///
103    /// Returns the SymbolId if exactly one symbol matches, error otherwise.
104    /// - If no symbols found: UnresolvedRef error
105    /// - If multiple symbols found: AmbiguousTarget error
106    ///
107    /// # Examples
108    /// ```rust,ignore
109    /// let checker = GraphChecker::new(&graph, &registry);
110    ///
111    /// // Unique symbol - returns SymbolId
112    /// let id = checker.check_symbol_unique("Config")?;
113    ///
114    /// // Multiple Config in different modules - AmbiguousTarget error
115    /// // e.g., my_crate::config::Config and my_crate::settings::Config
116    /// ```
117    pub fn check_symbol_unique(&self, name: &str) -> Result<crate::SymbolId, CheckError> {
118        // 1. Try as full path first (always unique)
119        if let Ok(path) = SymbolPath::parse(name) {
120            if let Some(id) = self.registry.lookup(&path) {
121                return Ok(id);
122            }
123        }
124
125        // 2. Search by short name
126        let matches = self.registry.find_by_name(name);
127
128        match matches.len() {
129            0 => {
130                let suggestions = self.find_similar_symbols(name);
131                Err(CheckError::UnresolvedRef {
132                    name: name.to_string(),
133                    location: None,
134                    suggestions,
135                })
136            }
137            1 => Ok(matches[0]),
138            _ => {
139                // Multiple matches - ambiguous
140                let candidates: Vec<String> = matches
141                    .iter()
142                    .filter_map(|id| self.registry.resolve(*id).map(|p| p.to_string()))
143                    .collect();
144                Err(CheckError::ambiguous_target(name, candidates))
145            }
146        }
147    }
148
149    /// Find symbols with similar names (for error suggestions).
150    fn find_similar_symbols(&self, name: &str) -> Vec<String> {
151        let name_lower = name.to_lowercase();
152        let mut suggestions = Vec::new();
153
154        for (_, path) in self.registry.iter() {
155            let sym_name = path.name();
156            // Simple similarity: starts with same chars or contains the name
157            if sym_name
158                .to_lowercase()
159                .starts_with(&name_lower[..name_lower.len().min(3)])
160                || sym_name.to_lowercase().contains(&name_lower)
161            {
162                suggestions.push(path.to_string());
163                if suggestions.len() >= 3 {
164                    break;
165                }
166            }
167        }
168
169        suggestions
170    }
171
172    // === Trait Implementation Checks ===
173
174    /// Check if a type implements a trait.
175    ///
176    /// First checks the std impls cache, then searches the CodeGraph
177    /// for Implements edges.
178    pub fn check_trait_impl(&self, type_name: &str, trait_name: &str) -> bool {
179        // 1. Check std impls cache first (primitives, common std types)
180        if self.std_impls.has_impl(type_name, trait_name) {
181            return true;
182        }
183
184        // 2. Check CodeGraphV2 for Implements edges
185        let type_id = self.registry.lookup_by_name(type_name);
186        let trait_id = self.registry.lookup_by_name(trait_name);
187
188        match (type_id, trait_id) {
189            (Some(tid), Some(trid)) => self.graph.implementors_of(trid).any(|id| id == tid),
190            _ => {
191                // If we can't find the symbols, check if type is a std type
192                // that might be parameterized (Vec<T>, Option<T>, etc.)
193                self.check_generic_std_impl(type_name, trait_name)
194            }
195        }
196    }
197
198    /// Check generic std type implementations.
199    ///
200    /// For types like `Vec<T>`, `Option<T>`, we check if the base type
201    /// implements the trait (conditional on T implementing it).
202    fn check_generic_std_impl(&self, type_name: &str, trait_name: &str) -> bool {
203        // Extract base type from generics: "Vec<Foo>" -> "Vec"
204        let base_type = type_name.split('<').next().unwrap_or(type_name).trim();
205
206        if self.std_impls.is_std_container(base_type) {
207            // For std containers, check if the base type has the impl
208            // (actual validity depends on T, which we can't check here)
209            self.std_impls.has_impl(base_type, trait_name)
210        } else {
211            false
212        }
213    }
214
215    // === Derive Possibility Checks ===
216
217    /// Check if a derive macro can be applied to a type.
218    ///
219    /// Verifies that all fields of the type implement the required trait.
220    ///
221    /// # Returns
222    /// - `CheckResult::Ok` if derive is possible
223    /// - `CheckResult::Error` with missing implementations
224    pub fn check_derive_possible(&self, target: &str, trait_name: &str) -> CheckResult {
225        // 1. Find the target type
226        let target_id = match self.registry.lookup_by_name(target) {
227            Some(id) => id,
228            None => {
229                return CheckResult::Error(vec![CheckError::type_not_found(target)]);
230            }
231        };
232
233        // 2. Get fields (children with Contains edge)
234        let children: Vec<_> = self.graph.children_of(target_id).collect();
235
236        if children.is_empty() {
237            // No fields - derive is always possible
238            return CheckResult::Ok;
239        }
240
241        // 3. Check each field's type
242        let mut missing = Vec::new();
243
244        for child_id in children.into_iter() {
245            // Get the field's type
246            if let Some(field_type) = self.get_field_type(child_id) {
247                if !self.check_trait_impl(&field_type, trait_name) {
248                    // Check if it's a type we know about
249                    if !self.std_impls.is_primitive(&field_type)
250                        && !self.std_impls.is_std_container(&field_type)
251                        && self.registry.lookup_by_name(&field_type).is_none()
252                    {
253                        // Unknown type - might be OK, add as warning candidate
254                        // For now, we're conservative and allow it
255                        continue;
256                    }
257                    missing.push(field_type);
258                }
259            }
260        }
261
262        if missing.is_empty() {
263            CheckResult::Ok
264        } else {
265            CheckResult::Error(vec![CheckError::derive_failed(target, trait_name, missing)])
266        }
267    }
268
269    /// Get the type of a field.
270    ///
271    /// This is a simplified version - in reality we'd need to look at
272    /// the AST or type information stored in DetailStore.
273    fn get_field_type(&self, field_id: crate::SymbolId) -> Option<String> {
274        // Get types used by this field (via TypeFlow)
275        for use_id in self.typeflow.types_used_by(field_id) {
276            if let Some(path) = self.registry.resolve(use_id) {
277                return Some(path.name().to_string());
278            }
279        }
280
281        None
282    }
283
284    // === Member Access Checks ===
285
286    /// Check if a struct/type has a specific field.
287    ///
288    /// Returns Ok if the field exists, Error with available fields if not.
289    ///
290    /// # Examples
291    /// ```rust,ignore
292    /// let checker = GraphChecker::new(&graph, &registry);
293    ///
294    /// // Check if Payment has a 'created_at' field
295    /// match checker.check_field_exists("Payment", "created_at") {
296    ///     Ok(()) => { /* field exists */ }
297    ///     Err(e) => { /* field not found, e contains suggestions */ }
298    /// }
299    /// ```
300    pub fn check_field_exists(&self, type_name: &str, field_name: &str) -> Result<(), CheckError> {
301        let type_id = match self.registry.lookup_by_name(type_name) {
302            Some(id) => id,
303            None => {
304                return Err(CheckError::type_not_found(type_name));
305            }
306        };
307
308        // Get all children (fields) of this type
309        let children: Vec<_> = self.graph.children_of(type_id).collect();
310        let mut available_fields = Vec::new();
311
312        for child_id in &children {
313            if let Some(path) = self.registry.resolve(*child_id) {
314                let name = path.name();
315                if name == field_name {
316                    return Ok(());
317                }
318                available_fields.push(name.to_string());
319            }
320        }
321
322        Err(CheckError::field_not_found(
323            type_name,
324            field_name,
325            available_fields,
326        ))
327    }
328
329    /// Check if a type has a specific method.
330    ///
331    /// Searches for methods by looking up `TypeName::method_name` pattern
332    /// and checking direct children of the type.
333    pub fn check_method_exists(
334        &self,
335        type_name: &str,
336        method_name: &str,
337    ) -> Result<(), CheckError> {
338        // First verify the type exists
339        let type_id = match self.registry.lookup_by_name(type_name) {
340            Some(id) => id,
341            None => {
342                return Err(CheckError::type_not_found(type_name));
343            }
344        };
345
346        // Try direct lookup: TypeName::method_name
347        let qualified_name = format!("{}::{}", type_name, method_name);
348        if self.registry.lookup_by_name(&qualified_name).is_some() {
349            return Ok(());
350        }
351
352        // Search for methods as children of the type
353        let mut available_methods = Vec::new();
354        for child_id in self.graph.children_of(type_id) {
355            if let Some(path) = self.registry.resolve(child_id) {
356                let name = path.name();
357                // Check if it's a function-like symbol
358                if let Some(kind) = self.registry.kind(child_id) {
359                    if matches!(
360                        kind,
361                        crate::SymbolKind::Function | crate::SymbolKind::Method
362                    ) {
363                        if name == method_name {
364                            return Ok(());
365                        }
366                        available_methods.push(name.to_string());
367                    }
368                }
369            }
370        }
371
372        // Search registry for symbols containing the type name and method name
373        for (_, path) in self.registry.iter() {
374            let path_str = path.to_string();
375            // Look for patterns like:
376            // - "crate::module::TypeName::method_name"
377            // - "crate::<impl TypeName>::method_name"
378            // Note: The actual format is `<impl TypeName>`, not `impl<TypeName>`
379            let impl_pattern_correct = format!("<impl {}>", type_name);
380            if (path_str.contains(&format!("{}::{}", type_name, method_name))
381                || path_str.contains(&impl_pattern_correct))
382                && path.name() == method_name
383            {
384                return Ok(());
385            }
386            // Collect available methods for suggestions
387            if path_str.contains(type_name) {
388                if let Some(kind) = self
389                    .registry
390                    .lookup(path)
391                    .and_then(|id| self.registry.kind(id))
392                {
393                    if matches!(
394                        kind,
395                        crate::SymbolKind::Function | crate::SymbolKind::Method
396                    ) && !available_methods.contains(&path.name().to_string())
397                    {
398                        available_methods.push(path.name().to_string());
399                    }
400                }
401            }
402        }
403
404        Err(CheckError::method_not_found(
405            type_name,
406            method_name,
407            available_methods,
408        ))
409    }
410
411    /// Check if an enum has a specific variant.
412    pub fn check_enum_variant_exists(
413        &self,
414        enum_name: &str,
415        variant_name: &str,
416    ) -> Result<(), CheckError> {
417        let enum_id = match self.registry.lookup_by_name(enum_name) {
418            Some(id) => id,
419            None => {
420                return Err(CheckError::type_not_found(enum_name));
421            }
422        };
423
424        // Get all children (variants) of this enum
425        let children: Vec<_> = self.graph.children_of(enum_id).collect();
426        let mut available_variants = Vec::new();
427
428        for child_id in &children {
429            if let Some(path) = self.registry.resolve(*child_id) {
430                let name = path.name();
431                if name == variant_name {
432                    return Ok(());
433                }
434                available_variants.push(name.to_string());
435            }
436        }
437
438        Err(CheckError::variant_not_found(
439            enum_name,
440            variant_name,
441            available_variants,
442        ))
443    }
444
445    /// Check if all required fields are provided for a struct literal.
446    ///
447    /// Returns Ok if all fields are covered, Error with missing fields if not.
448    pub fn check_struct_fields_complete(
449        &self,
450        struct_name: &str,
451        provided_fields: &[&str],
452    ) -> Result<(), CheckError> {
453        let struct_id = match self.registry.lookup_by_name(struct_name) {
454            Some(id) => id,
455            None => {
456                return Err(CheckError::type_not_found(struct_name));
457            }
458        };
459
460        // Get all fields of this struct
461        let children: Vec<_> = self.graph.children_of(struct_id).collect();
462        let mut missing_fields = Vec::new();
463
464        for child_id in &children {
465            if let Some(path) = self.registry.resolve(*child_id) {
466                let field_name = path.name();
467                // Check if it's a field (not a method)
468                if let Some(kind) = self.registry.kind(*child_id) {
469                    if matches!(kind, crate::SymbolKind::Field)
470                        && !provided_fields.contains(&field_name)
471                    {
472                        missing_fields.push(field_name.to_string());
473                    }
474                }
475            }
476        }
477
478        if missing_fields.is_empty() {
479            Ok(())
480        } else {
481            Err(CheckError::missing_fields(struct_name, missing_fields))
482        }
483    }
484
485    /// Get the expected argument count for a function.
486    ///
487    /// Returns None if the function is not found or count cannot be determined.
488    pub fn get_function_arg_count(&self, fn_name: &str) -> Option<usize> {
489        let fn_id = self.registry.lookup_by_name(fn_name)?;
490
491        // Count parameter children
492        let param_count = self
493            .graph
494            .children_of(fn_id)
495            .filter(|child_id| {
496                self.registry
497                    .kind(*child_id)
498                    .map(|k| matches!(k, crate::SymbolKind::Parameter))
499                    .unwrap_or(false)
500            })
501            .count();
502
503        Some(param_count)
504    }
505
506    /// Check if the argument count matches for a function call.
507    pub fn check_function_arg_count(
508        &self,
509        fn_name: &str,
510        actual_count: usize,
511    ) -> Result<(), CheckError> {
512        match self.get_function_arg_count(fn_name) {
513            Some(expected) if expected != actual_count => Err(CheckError::arg_count_mismatch(
514                fn_name,
515                expected,
516                actual_count,
517            )),
518            Some(_) => Ok(()),
519            None => {
520                // Function not found or count unknown - allow it (conservative)
521                Ok(())
522            }
523        }
524    }
525
526    // === Batch Checks ===
527
528    /// Check multiple symbols exist.
529    pub fn check_symbols_exist(&self, names: &[&str]) -> CheckResult {
530        let mut errors = Vec::new();
531
532        for name in names {
533            if !self.check_symbol_exists(name) {
534                errors.push(CheckError::unresolved(*name));
535            }
536        }
537
538        if errors.is_empty() {
539            CheckResult::Ok
540        } else {
541            CheckResult::Error(errors)
542        }
543    }
544
545    /// Check multiple trait implementations.
546    pub fn check_trait_impls(&self, checks: &[(&str, &str)]) -> CheckResult {
547        let mut errors = Vec::new();
548
549        for (type_name, trait_name) in checks {
550            if !self.check_trait_impl(type_name, trait_name) {
551                errors.push(CheckError::trait_not_impl(*type_name, *trait_name));
552            }
553        }
554
555        if errors.is_empty() {
556            CheckResult::Ok
557        } else {
558            CheckResult::Error(errors)
559        }
560    }
561
562    // === Accessors ===
563
564    /// Get a reference to the CodeGraphV2.
565    pub fn graph(&self) -> &CodeGraphV2 {
566        self.graph
567    }
568
569    /// Get a reference to the SymbolRegistry.
570    pub fn registry(&self) -> &SymbolRegistry {
571        self.registry
572    }
573
574    /// Get a reference to the StdImplCache.
575    pub fn std_impls(&self) -> &StdImplCache {
576        &self.std_impls
577    }
578}
579
580impl LightCheck for GraphChecker<'_> {
581    fn check_symbol_exists(&self, name: &str) -> bool {
582        GraphChecker::check_symbol_exists(self, name)
583    }
584
585    fn check_trait_impl(&self, type_name: &str, trait_name: &str) -> bool {
586        GraphChecker::check_trait_impl(self, type_name, trait_name)
587    }
588
589    fn check_derive_possible(&self, target: &str, trait_name: &str) -> CheckResult {
590        GraphChecker::check_derive_possible(self, target, trait_name)
591    }
592
593    fn pre_check(&self, mutation_type: &str, target: &str) -> CheckResult {
594        match mutation_type {
595            "AddDerive" => {
596                // For AddDerive, check if the target type exists
597                if !self.check_symbol_exists(target) {
598                    return CheckResult::Error(vec![CheckError::type_not_found(target)]);
599                }
600                CheckResult::Ok
601            }
602            "RenameSymbol" => {
603                // For rename, check if source exists
604                if !self.check_symbol_exists(target) {
605                    return CheckResult::Error(vec![CheckError::unresolved(target)]);
606                }
607                CheckResult::Ok
608            }
609            _ => CheckResult::Ok,
610        }
611    }
612
613    fn post_check(&self, mutation_type: &str, target: &str) -> CheckResult {
614        match mutation_type {
615            "RenameSymbol" => {
616                // After rename, the new symbol should exist
617                if !self.check_symbol_exists(target) {
618                    return CheckResult::Error(vec![CheckError::unresolved(target)]);
619                }
620                CheckResult::Ok
621            }
622            _ => CheckResult::Ok,
623        }
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use crate::query::GraphBuilderV2;
631    use crate::SymbolKind;
632
633    fn setup_test_env() -> (CodeGraphV2, TypeFlowGraphV2, SymbolRegistry) {
634        let mut registry = SymbolRegistry::new();
635        let mut builder = GraphBuilderV2::new(&mut registry);
636
637        // Register some test symbols
638        let my_struct = builder
639            .add_symbol(
640                SymbolPath::parse("test::MyStruct").unwrap(),
641                SymbolKind::Struct,
642            )
643            .unwrap();
644        let field = builder
645            .add_symbol(
646                SymbolPath::parse("test::MyStruct::field").unwrap(),
647                SymbolKind::Field,
648            )
649            .unwrap();
650        let i32_type = builder
651            .add_symbol(
652                SymbolPath::parse("std::i32").unwrap(),
653                SymbolKind::TypeAlias,
654            )
655            .unwrap();
656
657        builder.add_contains(my_struct, field);
658
659        let graph = builder.build();
660        let mut typeflow = TypeFlowGraphV2::new();
661        // Register field -> i32 type usage in TypeFlow
662        typeflow.add_usage(
663            crate::query::UsageContext::FieldType,
664            crate::query::RefKind::Owned,
665            Some(i32_type),
666            Some(field),
667        );
668        (graph, typeflow, registry)
669    }
670
671    #[test]
672    fn test_check_symbol_exists() {
673        let (graph, typeflow, registry) = setup_test_env();
674        let checker = GraphChecker::new(&graph, &typeflow, &registry);
675
676        // Full path lookup
677        assert!(checker.check_symbol_exists("test::MyStruct"));
678
679        // Short name lookup
680        assert!(checker.check_symbol_exists("MyStruct"));
681
682        // Non-existent
683        assert!(!checker.check_symbol_exists("NonExistent"));
684    }
685
686    #[test]
687    fn test_check_trait_impl_primitives() {
688        let (graph, typeflow, registry) = setup_test_env();
689        let checker = GraphChecker::new(&graph, &typeflow, &registry);
690
691        // Primitives from std cache
692        assert!(checker.check_trait_impl("i32", "Clone"));
693        assert!(checker.check_trait_impl("i32", "Default"));
694        assert!(checker.check_trait_impl("bool", "Eq"));
695
696        // f64 doesn't have Eq
697        assert!(!checker.check_trait_impl("f64", "Eq"));
698    }
699
700    #[test]
701    fn test_check_trait_impl_std_types() {
702        let (graph, typeflow, registry) = setup_test_env();
703        let checker = GraphChecker::new(&graph, &typeflow, &registry);
704
705        assert!(checker.check_trait_impl("String", "Clone"));
706        assert!(checker.check_trait_impl("Vec", "Default"));
707        assert!(checker.check_trait_impl("HashMap", "Default"));
708    }
709
710    #[test]
711    fn test_batch_symbol_check() {
712        let (graph, typeflow, registry) = setup_test_env();
713        let checker = GraphChecker::new(&graph, &typeflow, &registry);
714
715        // All exist
716        let result = checker.check_symbols_exist(&["MyStruct", "field"]);
717        assert!(result.is_ok());
718
719        // One missing
720        let result = checker.check_symbols_exist(&["MyStruct", "NonExistent"]);
721        assert!(result.is_err());
722        assert_eq!(result.errors().len(), 1);
723    }
724
725    // === Member Access Tests ===
726
727    fn setup_extended_test_env() -> (CodeGraphV2, TypeFlowGraphV2, SymbolRegistry) {
728        let mut registry = SymbolRegistry::new();
729        let mut builder = GraphBuilderV2::new(&mut registry);
730
731        // Struct with fields
732        let payment = builder
733            .add_symbol(
734                SymbolPath::parse("test::Payment").unwrap(),
735                SymbolKind::Struct,
736            )
737            .unwrap();
738        let amount_field = builder
739            .add_symbol(
740                SymbolPath::parse("test::Payment::amount").unwrap(),
741                SymbolKind::Field,
742            )
743            .unwrap();
744        let processed_at_field = builder
745            .add_symbol(
746                SymbolPath::parse("test::Payment::processed_at").unwrap(),
747                SymbolKind::Field,
748            )
749            .unwrap();
750        builder.add_contains(payment, amount_field);
751        builder.add_contains(payment, processed_at_field);
752
753        // Struct with method
754        let order_id = builder
755            .add_symbol(
756                SymbolPath::parse("test::OrderId").unwrap(),
757                SymbolKind::Struct,
758            )
759            .unwrap();
760        let new_method = builder
761            .add_symbol(
762                SymbolPath::parse("test::OrderId::new").unwrap(),
763                SymbolKind::Function,
764            )
765            .unwrap();
766        builder.add_contains(order_id, new_method);
767
768        // Enum with variants
769        let status = builder
770            .add_symbol(
771                SymbolPath::parse("test::ProductStatus").unwrap(),
772                SymbolKind::Enum,
773            )
774            .unwrap();
775        let active = builder
776            .add_symbol(
777                SymbolPath::parse("test::ProductStatus::Active").unwrap(),
778                SymbolKind::Variant,
779            )
780            .unwrap();
781        let discontinued = builder
782            .add_symbol(
783                SymbolPath::parse("test::ProductStatus::Discontinued").unwrap(),
784                SymbolKind::Variant,
785            )
786            .unwrap();
787        builder.add_contains(status, active);
788        builder.add_contains(status, discontinued);
789
790        let graph = builder.build();
791        let typeflow = TypeFlowGraphV2::new();
792        (graph, typeflow, registry)
793    }
794
795    #[test]
796    fn test_check_field_exists() {
797        let (graph, typeflow, registry) = setup_extended_test_env();
798        let checker = GraphChecker::new(&graph, &typeflow, &registry);
799
800        // Existing fields
801        assert!(checker.check_field_exists("Payment", "amount").is_ok());
802        assert!(checker
803            .check_field_exists("Payment", "processed_at")
804            .is_ok());
805
806        // Non-existent field (with suggestions)
807        let result = checker.check_field_exists("Payment", "created_at");
808        assert!(result.is_err());
809        if let Err(CheckError::FieldNotFound {
810            type_name,
811            field_name,
812            available_fields,
813        }) = result
814        {
815            assert_eq!(type_name, "Payment");
816            assert_eq!(field_name, "created_at");
817            assert!(available_fields.contains(&"amount".to_string()));
818            assert!(available_fields.contains(&"processed_at".to_string()));
819        } else {
820            panic!("Expected FieldNotFound error");
821        }
822
823        // Non-existent type
824        assert!(matches!(
825            checker.check_field_exists("NonExistent", "field"),
826            Err(CheckError::TypeNotFound { .. })
827        ));
828    }
829
830    #[test]
831    fn test_check_method_exists() {
832        let (graph, typeflow, registry) = setup_extended_test_env();
833        let checker = GraphChecker::new(&graph, &typeflow, &registry);
834
835        // Existing method
836        assert!(checker.check_method_exists("OrderId", "new").is_ok());
837
838        // Non-existent method
839        let result = checker.check_method_exists("OrderId", "as_u64");
840        assert!(result.is_err());
841        if let Err(CheckError::MethodNotFound {
842            type_name,
843            method_name,
844            ..
845        }) = result
846        {
847            assert_eq!(type_name, "OrderId");
848            assert_eq!(method_name, "as_u64");
849        } else {
850            panic!("Expected MethodNotFound error");
851        }
852    }
853
854    #[test]
855    fn test_check_enum_variant_exists() {
856        let (graph, typeflow, registry) = setup_extended_test_env();
857        let checker = GraphChecker::new(&graph, &typeflow, &registry);
858
859        // Existing variants
860        assert!(checker
861            .check_enum_variant_exists("ProductStatus", "Active")
862            .is_ok());
863        assert!(checker
864            .check_enum_variant_exists("ProductStatus", "Discontinued")
865            .is_ok());
866
867        // Non-existent variant (with suggestions)
868        let result = checker.check_enum_variant_exists("ProductStatus", "Inactive");
869        assert!(result.is_err());
870        if let Err(CheckError::EnumVariantNotFound {
871            enum_name,
872            variant_name,
873            available_variants,
874        }) = result
875        {
876            assert_eq!(enum_name, "ProductStatus");
877            assert_eq!(variant_name, "Inactive");
878            assert!(available_variants.contains(&"Active".to_string()));
879            assert!(available_variants.contains(&"Discontinued".to_string()));
880        } else {
881            panic!("Expected EnumVariantNotFound error");
882        }
883    }
884
885    #[test]
886    fn test_check_struct_fields_complete() {
887        let (graph, typeflow, registry) = setup_extended_test_env();
888        let checker = GraphChecker::new(&graph, &typeflow, &registry);
889
890        // All fields provided
891        let result = checker.check_struct_fields_complete("Payment", &["amount", "processed_at"]);
892        assert!(result.is_ok());
893
894        // Missing field
895        let result = checker.check_struct_fields_complete("Payment", &["amount"]);
896        assert!(result.is_err());
897        if let Err(CheckError::MissingRequiredField {
898            struct_name,
899            missing_fields,
900        }) = result
901        {
902            assert_eq!(struct_name, "Payment");
903            assert!(missing_fields.contains(&"processed_at".to_string()));
904        } else {
905            panic!("Expected MissingRequiredField error");
906        }
907    }
908
909    #[test]
910    fn test_check_error_display() {
911        // Test new error display formats
912        let err = CheckError::field_not_found("Payment", "created_at", vec!["amount".into()]);
913        let msg = format!("{}", err);
914        assert!(msg.contains("field `created_at` not found"));
915        assert!(msg.contains("Payment"));
916        assert!(msg.contains("amount"));
917
918        let err = CheckError::method_not_found("OrderId", "as_u64", vec!["new".into()]);
919        let msg = format!("{}", err);
920        assert!(msg.contains("method `as_u64` not found"));
921
922        let err = CheckError::variant_not_found("Status", "Inactive", vec!["Active".into()]);
923        let msg = format!("{}", err);
924        assert!(msg.contains("variant `Inactive` not found"));
925
926        let err = CheckError::missing_fields("User", vec!["status".into(), "email".into()]);
927        let msg = format!("{}", err);
928        assert!(msg.contains("missing required field"));
929        assert!(msg.contains("status"));
930
931        let err = CheckError::arg_count_mismatch("new", 0, 1);
932        let msg = format!("{}", err);
933        assert!(msg.contains("expects 0 argument"));
934        assert!(msg.contains("1 provided"));
935    }
936}