1use super::result::{CheckError, CheckResult};
4use super::traits::LightCheck;
5use crate::query::{CodeGraphV2, StdImplCache, TypeFlowGraphV2};
6use crate::symbol::{SymbolPath, SymbolRegistry};
7
8pub 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 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 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 pub fn check_symbol_exists(&self, name: &str) -> bool {
75 if let Ok(path) = SymbolPath::parse(name) {
77 if self.registry.lookup(&path).is_some() {
78 return true;
79 }
80 }
81
82 !self.registry.find_by_name(name).is_empty()
84 }
85
86 pub fn check_symbol_with_suggestions(&self, name: &str) -> Result<(), CheckError> {
88 if self.check_symbol_exists(name) {
89 Ok(())
90 } else {
91 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 pub fn check_symbol_unique(&self, name: &str) -> Result<crate::SymbolId, CheckError> {
118 if let Ok(path) = SymbolPath::parse(name) {
120 if let Some(id) = self.registry.lookup(&path) {
121 return Ok(id);
122 }
123 }
124
125 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 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 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 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 pub fn check_trait_impl(&self, type_name: &str, trait_name: &str) -> bool {
179 if self.std_impls.has_impl(type_name, trait_name) {
181 return true;
182 }
183
184 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 self.check_generic_std_impl(type_name, trait_name)
194 }
195 }
196 }
197
198 fn check_generic_std_impl(&self, type_name: &str, trait_name: &str) -> bool {
203 let base_type = type_name.split('<').next().unwrap_or(type_name).trim();
205
206 if self.std_impls.is_std_container(base_type) {
207 self.std_impls.has_impl(base_type, trait_name)
210 } else {
211 false
212 }
213 }
214
215 pub fn check_derive_possible(&self, target: &str, trait_name: &str) -> CheckResult {
225 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 let children: Vec<_> = self.graph.children_of(target_id).collect();
235
236 if children.is_empty() {
237 return CheckResult::Ok;
239 }
240
241 let mut missing = Vec::new();
243
244 for child_id in children.into_iter() {
245 if let Some(field_type) = self.get_field_type(child_id) {
247 if !self.check_trait_impl(&field_type, trait_name) {
248 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 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 fn get_field_type(&self, field_id: crate::SymbolId) -> Option<String> {
274 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 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 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 pub fn check_method_exists(
334 &self,
335 type_name: &str,
336 method_name: &str,
337 ) -> Result<(), CheckError> {
338 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 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 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 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 for (_, path) in self.registry.iter() {
374 let path_str = path.to_string();
375 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 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 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 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 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 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 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 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 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 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 Ok(())
522 }
523 }
524 }
525
526 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 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 pub fn graph(&self) -> &CodeGraphV2 {
566 self.graph
567 }
568
569 pub fn registry(&self) -> &SymbolRegistry {
571 self.registry
572 }
573
574 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 if !self.check_symbol_exists(target) {
598 return CheckResult::Error(vec![CheckError::type_not_found(target)]);
599 }
600 CheckResult::Ok
601 }
602 "RenameSymbol" => {
603 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 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 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 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, ®istry);
675
676 assert!(checker.check_symbol_exists("test::MyStruct"));
678
679 assert!(checker.check_symbol_exists("MyStruct"));
681
682 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, ®istry);
690
691 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 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, ®istry);
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, ®istry);
714
715 let result = checker.check_symbols_exist(&["MyStruct", "field"]);
717 assert!(result.is_ok());
718
719 let result = checker.check_symbols_exist(&["MyStruct", "NonExistent"]);
721 assert!(result.is_err());
722 assert_eq!(result.errors().len(), 1);
723 }
724
725 fn setup_extended_test_env() -> (CodeGraphV2, TypeFlowGraphV2, SymbolRegistry) {
728 let mut registry = SymbolRegistry::new();
729 let mut builder = GraphBuilderV2::new(&mut registry);
730
731 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 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 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, ®istry);
799
800 assert!(checker.check_field_exists("Payment", "amount").is_ok());
802 assert!(checker
803 .check_field_exists("Payment", "processed_at")
804 .is_ok());
805
806 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 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, ®istry);
834
835 assert!(checker.check_method_exists("OrderId", "new").is_ok());
837
838 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, ®istry);
858
859 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 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, ®istry);
889
890 let result = checker.check_struct_fields_complete("Payment", &["amount", "processed_at"]);
892 assert!(result.is_ok());
893
894 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 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}