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                    // precheck-fix-4: when the field type is a user-defined
258                    // symbol (registered in `registry`, not a primitive, not a
259                    // std container) `check_trait_impl` will return `false`
260                    // whenever the impl was produced by a `#[derive(...)]`
261                    // macro, because `CodeGraphV2` does not synthesize
262                    // `Implements` edges for derive-expanded impls — and the
263                    // `StdImplCache` only ever knows about std types.
264                    //
265                    // Flagging this configuration emits "missing impl on
266                    // <UserType>" false positives every time a struct holds
267                    // another user-defined type that derives the same trait
268                    // (e.g. `pub struct ValidationIssue { level:
269                    // ValidationLevel, code: ValidationCode, ... }` where
270                    // both inner enums `#[derive(Debug, Clone, ...)]`).
271                    //
272                    // Conservative skip: Light verify must lean toward false
273                    // negatives over false positives — `cargo check` (Full
274                    // verify) remains the source of truth for genuine derive
275                    // incompatibilities.
276                    if self.registry.lookup_by_name(&field_type).is_some()
277                        && !self.std_impls.is_primitive(&field_type)
278                        && !self.std_impls.is_std_container(&field_type)
279                    {
280                        continue;
281                    }
282                    missing.push(field_type);
283                }
284            }
285        }
286
287        if missing.is_empty() {
288            CheckResult::Ok
289        } else {
290            CheckResult::Error(vec![CheckError::derive_failed(target, trait_name, missing)])
291        }
292    }
293
294    /// Get the type of a field.
295    ///
296    /// This is a simplified version - in reality we'd need to look at
297    /// the AST or type information stored in DetailStore.
298    fn get_field_type(&self, field_id: crate::SymbolId) -> Option<String> {
299        // Get types used by this field (via TypeFlow)
300        for use_id in self.typeflow.types_used_by(field_id) {
301            if let Some(path) = self.registry.resolve(use_id) {
302                return Some(path.name().to_string());
303            }
304        }
305
306        None
307    }
308
309    // === Member Access Checks ===
310
311    /// Check if a struct/type has a specific field.
312    ///
313    /// Returns Ok if the field exists, Error with available fields if not.
314    ///
315    /// # Examples
316    /// ```rust,ignore
317    /// let checker = GraphChecker::new(&graph, &registry);
318    ///
319    /// // Check if Payment has a 'created_at' field
320    /// match checker.check_field_exists("Payment", "created_at") {
321    ///     Ok(()) => { /* field exists */ }
322    ///     Err(e) => { /* field not found, e contains suggestions */ }
323    /// }
324    /// ```
325    pub fn check_field_exists(&self, type_name: &str, field_name: &str) -> Result<(), CheckError> {
326        let type_id = match self.registry.lookup_by_name(type_name) {
327            Some(id) => id,
328            None => {
329                return Err(CheckError::type_not_found(type_name));
330            }
331        };
332
333        // Get all children (fields) of this type
334        let children: Vec<_> = self.graph.children_of(type_id).collect();
335        let mut available_fields = Vec::new();
336
337        for child_id in &children {
338            if let Some(path) = self.registry.resolve(*child_id) {
339                let name = path.name();
340                if name == field_name {
341                    return Ok(());
342                }
343                available_fields.push(name.to_string());
344            }
345        }
346
347        Err(CheckError::field_not_found(
348            type_name,
349            field_name,
350            available_fields,
351        ))
352    }
353
354    /// Check if a type has a specific method.
355    ///
356    /// Searches for methods by looking up `TypeName::method_name` pattern
357    /// and checking direct children of the type.
358    pub fn check_method_exists(
359        &self,
360        type_name: &str,
361        method_name: &str,
362    ) -> Result<(), CheckError> {
363        // First verify the type exists
364        let type_id = match self.registry.lookup_by_name(type_name) {
365            Some(id) => id,
366            None => {
367                return Err(CheckError::type_not_found(type_name));
368            }
369        };
370
371        // Try direct lookup: TypeName::method_name
372        let qualified_name = format!("{}::{}", type_name, method_name);
373        if self.registry.lookup_by_name(&qualified_name).is_some() {
374            return Ok(());
375        }
376
377        // Search for methods as children of the type
378        let mut available_methods = Vec::new();
379        for child_id in self.graph.children_of(type_id) {
380            if let Some(path) = self.registry.resolve(child_id) {
381                let name = path.name();
382                // Check if it's a function-like symbol
383                if let Some(kind) = self.registry.kind(child_id) {
384                    if matches!(
385                        kind,
386                        crate::SymbolKind::Function | crate::SymbolKind::Method
387                    ) {
388                        if name == method_name {
389                            return Ok(());
390                        }
391                        available_methods.push(name.to_string());
392                    }
393                }
394            }
395        }
396
397        // Search registry for symbols containing the type name and method name
398        for (_, path) in self.registry.iter() {
399            let path_str = path.to_string();
400            // Look for patterns like:
401            // - "crate::module::TypeName::method_name"
402            // - "crate::<impl TypeName>::method_name"
403            // Note: The actual format is `<impl TypeName>`, not `impl<TypeName>`
404            let impl_pattern_correct = format!("<impl {}>", type_name);
405            if (path_str.contains(&format!("{}::{}", type_name, method_name))
406                || path_str.contains(&impl_pattern_correct))
407                && path.name() == method_name
408            {
409                return Ok(());
410            }
411            // Collect available methods for suggestions
412            if path_str.contains(type_name) {
413                if let Some(kind) = self
414                    .registry
415                    .lookup(path)
416                    .and_then(|id| self.registry.kind(id))
417                {
418                    if matches!(
419                        kind,
420                        crate::SymbolKind::Function | crate::SymbolKind::Method
421                    ) && !available_methods.contains(&path.name().to_string())
422                    {
423                        available_methods.push(path.name().to_string());
424                    }
425                }
426            }
427        }
428
429        Err(CheckError::method_not_found(
430            type_name,
431            method_name,
432            available_methods,
433        ))
434    }
435
436    /// Check if an enum has a specific variant.
437    pub fn check_enum_variant_exists(
438        &self,
439        enum_name: &str,
440        variant_name: &str,
441    ) -> Result<(), CheckError> {
442        let enum_id = match self.registry.lookup_by_name(enum_name) {
443            Some(id) => id,
444            None => {
445                return Err(CheckError::type_not_found(enum_name));
446            }
447        };
448
449        // Get all children (variants) of this enum
450        let children: Vec<_> = self.graph.children_of(enum_id).collect();
451        let mut available_variants = Vec::new();
452
453        for child_id in &children {
454            if let Some(path) = self.registry.resolve(*child_id) {
455                let name = path.name();
456                if name == variant_name {
457                    return Ok(());
458                }
459                available_variants.push(name.to_string());
460            }
461        }
462
463        Err(CheckError::variant_not_found(
464            enum_name,
465            variant_name,
466            available_variants,
467        ))
468    }
469
470    /// Check if all required fields are provided for a struct literal.
471    ///
472    /// Returns Ok if all fields are covered, Error with missing fields if not.
473    pub fn check_struct_fields_complete(
474        &self,
475        struct_name: &str,
476        provided_fields: &[&str],
477    ) -> Result<(), CheckError> {
478        let struct_id = match self.registry.lookup_by_name(struct_name) {
479            Some(id) => id,
480            None => {
481                return Err(CheckError::type_not_found(struct_name));
482            }
483        };
484
485        // Get all fields of this struct
486        let children: Vec<_> = self.graph.children_of(struct_id).collect();
487        let mut missing_fields = Vec::new();
488
489        for child_id in &children {
490            if let Some(path) = self.registry.resolve(*child_id) {
491                let field_name = path.name();
492                // Check if it's a field (not a method)
493                if let Some(kind) = self.registry.kind(*child_id) {
494                    if matches!(kind, crate::SymbolKind::Field)
495                        && !provided_fields.contains(&field_name)
496                    {
497                        missing_fields.push(field_name.to_string());
498                    }
499                }
500            }
501        }
502
503        if missing_fields.is_empty() {
504            Ok(())
505        } else {
506            Err(CheckError::missing_fields(struct_name, missing_fields))
507        }
508    }
509
510    /// Get the expected argument count for a function.
511    ///
512    /// Returns None if the function is not found or count cannot be determined.
513    pub fn get_function_arg_count(&self, fn_name: &str) -> Option<usize> {
514        let fn_id = self.registry.lookup_by_name(fn_name)?;
515
516        // Count parameter children
517        let param_count = self
518            .graph
519            .children_of(fn_id)
520            .filter(|child_id| {
521                self.registry
522                    .kind(*child_id)
523                    .map(|k| matches!(k, crate::SymbolKind::Parameter))
524                    .unwrap_or(false)
525            })
526            .count();
527
528        Some(param_count)
529    }
530
531    /// Check if the argument count matches for a function call.
532    pub fn check_function_arg_count(
533        &self,
534        fn_name: &str,
535        actual_count: usize,
536    ) -> Result<(), CheckError> {
537        match self.get_function_arg_count(fn_name) {
538            Some(expected) if expected != actual_count => Err(CheckError::arg_count_mismatch(
539                fn_name,
540                expected,
541                actual_count,
542            )),
543            Some(_) => Ok(()),
544            None => {
545                // Function not found or count unknown - allow it (conservative)
546                Ok(())
547            }
548        }
549    }
550
551    // === Batch Checks ===
552
553    /// Check multiple symbols exist.
554    pub fn check_symbols_exist(&self, names: &[&str]) -> CheckResult {
555        let mut errors = Vec::new();
556
557        for name in names {
558            if !self.check_symbol_exists(name) {
559                errors.push(CheckError::unresolved(*name));
560            }
561        }
562
563        if errors.is_empty() {
564            CheckResult::Ok
565        } else {
566            CheckResult::Error(errors)
567        }
568    }
569
570    /// Check multiple trait implementations.
571    pub fn check_trait_impls(&self, checks: &[(&str, &str)]) -> CheckResult {
572        let mut errors = Vec::new();
573
574        for (type_name, trait_name) in checks {
575            if !self.check_trait_impl(type_name, trait_name) {
576                errors.push(CheckError::trait_not_impl(*type_name, *trait_name));
577            }
578        }
579
580        if errors.is_empty() {
581            CheckResult::Ok
582        } else {
583            CheckResult::Error(errors)
584        }
585    }
586
587    // === Accessors ===
588
589    /// Get a reference to the CodeGraphV2.
590    pub fn graph(&self) -> &CodeGraphV2 {
591        self.graph
592    }
593
594    /// Get a reference to the SymbolRegistry.
595    pub fn registry(&self) -> &SymbolRegistry {
596        self.registry
597    }
598
599    /// Get a reference to the StdImplCache.
600    pub fn std_impls(&self) -> &StdImplCache {
601        &self.std_impls
602    }
603}
604
605impl LightCheck for GraphChecker<'_> {
606    fn check_symbol_exists(&self, name: &str) -> bool {
607        GraphChecker::check_symbol_exists(self, name)
608    }
609
610    fn check_trait_impl(&self, type_name: &str, trait_name: &str) -> bool {
611        GraphChecker::check_trait_impl(self, type_name, trait_name)
612    }
613
614    fn check_derive_possible(&self, target: &str, trait_name: &str) -> CheckResult {
615        GraphChecker::check_derive_possible(self, target, trait_name)
616    }
617
618    fn pre_check(&self, mutation_type: &str, target: &str) -> CheckResult {
619        match mutation_type {
620            "AddDerive" => {
621                // For AddDerive, check if the target type exists
622                if !self.check_symbol_exists(target) {
623                    return CheckResult::Error(vec![CheckError::type_not_found(target)]);
624                }
625                CheckResult::Ok
626            }
627            "RenameSymbol" => {
628                // For rename, check if source exists
629                if !self.check_symbol_exists(target) {
630                    return CheckResult::Error(vec![CheckError::unresolved(target)]);
631                }
632                CheckResult::Ok
633            }
634            _ => CheckResult::Ok,
635        }
636    }
637
638    fn post_check(&self, mutation_type: &str, target: &str) -> CheckResult {
639        match mutation_type {
640            "RenameSymbol" => {
641                // After rename, the new symbol should exist
642                if !self.check_symbol_exists(target) {
643                    return CheckResult::Error(vec![CheckError::unresolved(target)]);
644                }
645                CheckResult::Ok
646            }
647            _ => CheckResult::Ok,
648        }
649    }
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655    use crate::query::GraphBuilderV2;
656    use crate::SymbolKind;
657
658    fn setup_test_env() -> (CodeGraphV2, TypeFlowGraphV2, SymbolRegistry) {
659        let mut registry = SymbolRegistry::new();
660        let mut builder = GraphBuilderV2::new(&mut registry);
661
662        // Register some test symbols
663        let my_struct = builder
664            .add_symbol(
665                SymbolPath::parse("test::MyStruct").unwrap(),
666                SymbolKind::Struct,
667            )
668            .unwrap();
669        let field = builder
670            .add_symbol(
671                SymbolPath::parse("test::MyStruct::field").unwrap(),
672                SymbolKind::Field,
673            )
674            .unwrap();
675        let i32_type = builder
676            .add_symbol(
677                SymbolPath::parse("std::i32").unwrap(),
678                SymbolKind::TypeAlias,
679            )
680            .unwrap();
681
682        builder.add_contains(my_struct, field);
683
684        let graph = builder.build();
685        let mut typeflow = TypeFlowGraphV2::new();
686        // Register field -> i32 type usage in TypeFlow
687        typeflow.add_usage(
688            crate::query::UsageContext::FieldType,
689            crate::query::RefKind::Owned,
690            Some(i32_type),
691            Some(field),
692        );
693        (graph, typeflow, registry)
694    }
695
696    #[test]
697    fn test_check_symbol_exists() {
698        let (graph, typeflow, registry) = setup_test_env();
699        let checker = GraphChecker::new(&graph, &typeflow, &registry);
700
701        // Full path lookup
702        assert!(checker.check_symbol_exists("test::MyStruct"));
703
704        // Short name lookup
705        assert!(checker.check_symbol_exists("MyStruct"));
706
707        // Non-existent
708        assert!(!checker.check_symbol_exists("NonExistent"));
709    }
710
711    #[test]
712    fn test_check_trait_impl_primitives() {
713        let (graph, typeflow, registry) = setup_test_env();
714        let checker = GraphChecker::new(&graph, &typeflow, &registry);
715
716        // Primitives from std cache
717        assert!(checker.check_trait_impl("i32", "Clone"));
718        assert!(checker.check_trait_impl("i32", "Default"));
719        assert!(checker.check_trait_impl("bool", "Eq"));
720
721        // f64 doesn't have Eq
722        assert!(!checker.check_trait_impl("f64", "Eq"));
723    }
724
725    #[test]
726    fn test_check_trait_impl_std_types() {
727        let (graph, typeflow, registry) = setup_test_env();
728        let checker = GraphChecker::new(&graph, &typeflow, &registry);
729
730        assert!(checker.check_trait_impl("String", "Clone"));
731        assert!(checker.check_trait_impl("Vec", "Default"));
732        assert!(checker.check_trait_impl("HashMap", "Default"));
733    }
734
735    #[test]
736    fn test_batch_symbol_check() {
737        let (graph, typeflow, registry) = setup_test_env();
738        let checker = GraphChecker::new(&graph, &typeflow, &registry);
739
740        // All exist
741        let result = checker.check_symbols_exist(&["MyStruct", "field"]);
742        assert!(result.is_ok());
743
744        // One missing
745        let result = checker.check_symbols_exist(&["MyStruct", "NonExistent"]);
746        assert!(result.is_err());
747        assert_eq!(result.errors().len(), 1);
748    }
749
750    // === Member Access Tests ===
751
752    fn setup_extended_test_env() -> (CodeGraphV2, TypeFlowGraphV2, SymbolRegistry) {
753        let mut registry = SymbolRegistry::new();
754        let mut builder = GraphBuilderV2::new(&mut registry);
755
756        // Struct with fields
757        let payment = builder
758            .add_symbol(
759                SymbolPath::parse("test::Payment").unwrap(),
760                SymbolKind::Struct,
761            )
762            .unwrap();
763        let amount_field = builder
764            .add_symbol(
765                SymbolPath::parse("test::Payment::amount").unwrap(),
766                SymbolKind::Field,
767            )
768            .unwrap();
769        let processed_at_field = builder
770            .add_symbol(
771                SymbolPath::parse("test::Payment::processed_at").unwrap(),
772                SymbolKind::Field,
773            )
774            .unwrap();
775        builder.add_contains(payment, amount_field);
776        builder.add_contains(payment, processed_at_field);
777
778        // Struct with method
779        let order_id = builder
780            .add_symbol(
781                SymbolPath::parse("test::OrderId").unwrap(),
782                SymbolKind::Struct,
783            )
784            .unwrap();
785        let new_method = builder
786            .add_symbol(
787                SymbolPath::parse("test::OrderId::new").unwrap(),
788                SymbolKind::Function,
789            )
790            .unwrap();
791        builder.add_contains(order_id, new_method);
792
793        // Enum with variants
794        let status = builder
795            .add_symbol(
796                SymbolPath::parse("test::ProductStatus").unwrap(),
797                SymbolKind::Enum,
798            )
799            .unwrap();
800        let active = builder
801            .add_symbol(
802                SymbolPath::parse("test::ProductStatus::Active").unwrap(),
803                SymbolKind::Variant,
804            )
805            .unwrap();
806        let discontinued = builder
807            .add_symbol(
808                SymbolPath::parse("test::ProductStatus::Discontinued").unwrap(),
809                SymbolKind::Variant,
810            )
811            .unwrap();
812        builder.add_contains(status, active);
813        builder.add_contains(status, discontinued);
814
815        let graph = builder.build();
816        let typeflow = TypeFlowGraphV2::new();
817        (graph, typeflow, registry)
818    }
819
820    #[test]
821    fn test_check_field_exists() {
822        let (graph, typeflow, registry) = setup_extended_test_env();
823        let checker = GraphChecker::new(&graph, &typeflow, &registry);
824
825        // Existing fields
826        assert!(checker.check_field_exists("Payment", "amount").is_ok());
827        assert!(checker
828            .check_field_exists("Payment", "processed_at")
829            .is_ok());
830
831        // Non-existent field (with suggestions)
832        let result = checker.check_field_exists("Payment", "created_at");
833        assert!(result.is_err());
834        if let Err(CheckError::FieldNotFound {
835            type_name,
836            field_name,
837            available_fields,
838        }) = result
839        {
840            assert_eq!(type_name, "Payment");
841            assert_eq!(field_name, "created_at");
842            assert!(available_fields.contains(&"amount".to_string()));
843            assert!(available_fields.contains(&"processed_at".to_string()));
844        } else {
845            panic!("Expected FieldNotFound error");
846        }
847
848        // Non-existent type
849        assert!(matches!(
850            checker.check_field_exists("NonExistent", "field"),
851            Err(CheckError::TypeNotFound { .. })
852        ));
853    }
854
855    #[test]
856    fn test_check_method_exists() {
857        let (graph, typeflow, registry) = setup_extended_test_env();
858        let checker = GraphChecker::new(&graph, &typeflow, &registry);
859
860        // Existing method
861        assert!(checker.check_method_exists("OrderId", "new").is_ok());
862
863        // Non-existent method
864        let result = checker.check_method_exists("OrderId", "as_u64");
865        assert!(result.is_err());
866        if let Err(CheckError::MethodNotFound {
867            type_name,
868            method_name,
869            ..
870        }) = result
871        {
872            assert_eq!(type_name, "OrderId");
873            assert_eq!(method_name, "as_u64");
874        } else {
875            panic!("Expected MethodNotFound error");
876        }
877    }
878
879    #[test]
880    fn test_check_enum_variant_exists() {
881        let (graph, typeflow, registry) = setup_extended_test_env();
882        let checker = GraphChecker::new(&graph, &typeflow, &registry);
883
884        // Existing variants
885        assert!(checker
886            .check_enum_variant_exists("ProductStatus", "Active")
887            .is_ok());
888        assert!(checker
889            .check_enum_variant_exists("ProductStatus", "Discontinued")
890            .is_ok());
891
892        // Non-existent variant (with suggestions)
893        let result = checker.check_enum_variant_exists("ProductStatus", "Inactive");
894        assert!(result.is_err());
895        if let Err(CheckError::EnumVariantNotFound {
896            enum_name,
897            variant_name,
898            available_variants,
899        }) = result
900        {
901            assert_eq!(enum_name, "ProductStatus");
902            assert_eq!(variant_name, "Inactive");
903            assert!(available_variants.contains(&"Active".to_string()));
904            assert!(available_variants.contains(&"Discontinued".to_string()));
905        } else {
906            panic!("Expected EnumVariantNotFound error");
907        }
908    }
909
910    #[test]
911    fn test_check_struct_fields_complete() {
912        let (graph, typeflow, registry) = setup_extended_test_env();
913        let checker = GraphChecker::new(&graph, &typeflow, &registry);
914
915        // All fields provided
916        let result = checker.check_struct_fields_complete("Payment", &["amount", "processed_at"]);
917        assert!(result.is_ok());
918
919        // Missing field
920        let result = checker.check_struct_fields_complete("Payment", &["amount"]);
921        assert!(result.is_err());
922        if let Err(CheckError::MissingRequiredField {
923            struct_name,
924            missing_fields,
925        }) = result
926        {
927            assert_eq!(struct_name, "Payment");
928            assert!(missing_fields.contains(&"processed_at".to_string()));
929        } else {
930            panic!("Expected MissingRequiredField error");
931        }
932    }
933
934    /// Regression test (precheck-fix-4):
935    /// `check_derive_possible(target, "Debug")` should not flag a
936    /// user-defined field type as a "missing impl" just because `Debug`
937    /// is not in our SymbolRegistry (std traits are never indexed here).
938    /// Before the fix, every struct with a field of a registered enum
939    /// (e.g. `pub struct ValidationIssue { level: ValidationLevel, ... }`)
940    /// would falsely report `cannot derive Debug for ValidationIssue:
941    /// missing impl on ValidationLevel`. The conservative skip restores
942    /// `CheckResult::Ok` in this configuration.
943    #[test]
944    fn test_check_derive_possible_user_defined_field_and_std_trait_skips() {
945        let mut registry = SymbolRegistry::new();
946        let mut builder = GraphBuilderV2::new(&mut registry);
947
948        // Target struct with a field whose type is a user-defined enum
949        // that's also registered in the registry. Mirrors the
950        // `ValidationIssue { level: ValidationLevel }` shape.
951        let target = builder
952            .add_symbol(
953                SymbolPath::parse("test::Holder").unwrap(),
954                SymbolKind::Struct,
955            )
956            .unwrap();
957        let field = builder
958            .add_symbol(
959                SymbolPath::parse("test::Holder::inner").unwrap(),
960                SymbolKind::Field,
961            )
962            .unwrap();
963        let inner_ty = builder
964            .add_symbol(SymbolPath::parse("test::Inner").unwrap(), SymbolKind::Enum)
965            .unwrap();
966
967        builder.add_contains(target, field);
968
969        let graph = builder.build();
970        let mut typeflow = TypeFlowGraphV2::new();
971        typeflow.add_usage(
972            crate::query::UsageContext::FieldType,
973            crate::query::RefKind::Owned,
974            Some(inner_ty),
975            Some(field),
976        );
977
978        let checker = GraphChecker::new(&graph, &typeflow, &registry);
979
980        // `Debug` is intentionally not registered as a symbol — it stands
981        // in for the unindexed std trait case.
982        let result = checker.check_derive_possible("Holder", "Debug");
983        assert!(
984            matches!(result, CheckResult::Ok),
985            "user-defined field + unindexed std trait must conservatively pass, got {:?}",
986            result
987        );
988    }
989
990    #[test]
991    fn test_check_error_display() {
992        // Test new error display formats
993        let err = CheckError::field_not_found("Payment", "created_at", vec!["amount".into()]);
994        let msg = format!("{}", err);
995        assert!(msg.contains("field `created_at` not found"));
996        assert!(msg.contains("Payment"));
997        assert!(msg.contains("amount"));
998
999        let err = CheckError::method_not_found("OrderId", "as_u64", vec!["new".into()]);
1000        let msg = format!("{}", err);
1001        assert!(msg.contains("method `as_u64` not found"));
1002
1003        let err = CheckError::variant_not_found("Status", "Inactive", vec!["Active".into()]);
1004        let msg = format!("{}", err);
1005        assert!(msg.contains("variant `Inactive` not found"));
1006
1007        let err = CheckError::missing_fields("User", vec!["status".into(), "email".into()]);
1008        let msg = format!("{}", err);
1009        assert!(msg.contains("missing required field"));
1010        assert!(msg.contains("status"));
1011
1012        let err = CheckError::arg_count_mismatch("new", 0, 1);
1013        let msg = format!("{}", err);
1014        assert!(msg.contains("expects 0 argument"));
1015        assert!(msg.contains("1 provided"));
1016    }
1017}