1pub mod ast_cache;
2pub mod channel_parser;
3pub mod command_parser;
4pub mod dependency_graph;
5pub mod event_parser;
6pub mod serde_parser;
7pub mod struct_parser;
8pub mod type_resolver;
9pub mod validator_parser;
10
11use crate::models::{ChannelInfo, CommandInfo, EventInfo, StructInfo};
12use std::collections::{HashMap, HashSet};
13use std::path::{Path, PathBuf};
14
15use ast_cache::AstCache;
16use channel_parser::ChannelParser;
17use command_parser::CommandParser;
18use dependency_graph::TypeDependencyGraph;
19use event_parser::EventParser;
20use struct_parser::StructParser;
21use type_resolver::TypeResolver;
22
23pub struct CommandAnalyzer {
25 ast_cache: AstCache,
27 command_parser: CommandParser,
29 channel_parser: ChannelParser,
31 event_parser: EventParser,
33 struct_parser: StructParser,
35 type_resolver: TypeResolver,
37 dependency_graph: TypeDependencyGraph,
39 discovered_structs: HashMap<String, StructInfo>,
41 discovered_events: Vec<EventInfo>,
43}
44
45impl CommandAnalyzer {
46 pub fn new() -> Self {
47 Self {
48 ast_cache: AstCache::new(),
49 command_parser: CommandParser::new(),
50 channel_parser: ChannelParser::new(),
51 event_parser: EventParser::new(),
52 struct_parser: StructParser::new(),
53 type_resolver: TypeResolver::new(),
54 dependency_graph: TypeDependencyGraph::new(),
55 discovered_structs: HashMap::new(),
56 discovered_events: Vec::new(),
57 }
58 }
59
60 pub fn add_type_mappings(&mut self, mappings: &HashMap<String, String>) {
62 for (rust_type, ts_type) in mappings {
63 self.type_resolver
64 .add_type_mapping(rust_type.clone(), ts_type.clone());
65 }
66 }
67
68 pub fn analyze_project(
70 &mut self,
71 project_path: &str,
72 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
73 self.analyze_project_with_verbose(project_path, false)
74 }
75
76 pub fn analyze_project_with_verbose(
78 &mut self,
79 project_path: &str,
80 verbose: bool,
81 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
82 self.ast_cache
84 .parse_and_cache_all_files(project_path, verbose)?;
85
86 let file_paths: Vec<PathBuf> = self.ast_cache.keys().cloned().collect();
88 let mut commands = Vec::new();
89 let mut type_names_to_discover = HashSet::new();
90
91 for file_path in file_paths {
93 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
94 if verbose {
95 println!("🔍 Analyzing file: {}", parsed_file.path.display());
96 }
97
98 let mut file_commands = self.command_parser.extract_commands_from_ast(
100 &parsed_file.ast,
101 parsed_file.path.as_path(),
102 &mut self.type_resolver,
103 )?;
104
105 for command in &mut file_commands {
107 if let Some(func) = self.find_function_in_ast(&parsed_file.ast, &command.name) {
108 let channels = self.channel_parser.extract_channels_from_command(
109 func,
110 &command.name,
111 parsed_file.path.as_path(),
112 &mut self.type_resolver,
113 )?;
114
115 channels.iter().for_each(|ch| {
117 self.extract_type_names(&ch.message_type, &mut type_names_to_discover);
118 });
119
120 command.channels = channels;
121 }
122 }
123
124 let file_events = self.event_parser.extract_events_from_ast(
126 &parsed_file.ast,
127 parsed_file.path.as_path(),
128 &mut self.type_resolver,
129 )?;
130
131 file_commands.iter().for_each(|cmd| {
133 cmd.parameters.iter().for_each(|param| {
134 self.extract_type_names(¶m.rust_type, &mut type_names_to_discover);
135 });
136 self.extract_type_names(&cmd.return_type, &mut type_names_to_discover);
138 });
139
140 file_events.iter().for_each(|event| {
142 self.extract_type_names(&event.payload_type, &mut type_names_to_discover);
143 });
144
145 commands.extend(file_commands);
146 self.discovered_events.extend(file_events);
147
148 self.index_type_definitions(&parsed_file.ast, parsed_file.path.as_path());
150 }
151 }
152
153 if verbose {
154 println!("🔍 Type names to discover: {:?}", type_names_to_discover);
155 }
156
157 self.resolve_types_lazily(&type_names_to_discover)?;
159
160 if verbose {
161 println!(
162 "🏗️ Discovered {} structs total",
163 self.discovered_structs.len()
164 );
165 for (name, info) in &self.discovered_structs {
166 println!(" - {}: {} fields", name, info.fields.len());
167 }
168 println!(
169 "📡 Discovered {} events total",
170 self.discovered_events.len()
171 );
172 for event in &self.discovered_events {
173 println!(" - '{}': {}", event.event_name, event.payload_type);
174 }
175 let all_channels = self.get_all_discovered_channels(&commands);
176 println!("📞 Discovered {} channels total", all_channels.len());
177 for channel in &all_channels {
178 println!(
179 " - '{}' in {}: {}",
180 channel.parameter_name, channel.command_name, channel.message_type
181 );
182 }
183 }
184
185 Ok(commands)
186 }
187
188 pub fn analyze_file(
190 &mut self,
191 file_path: &std::path::Path,
192 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
193 let path_buf = file_path.to_path_buf();
194
195 match self.ast_cache.parse_and_cache_file(&path_buf) {
197 Ok(_) => {
198 if let Some(parsed_file) = self.ast_cache.get_cloned(&path_buf) {
200 let file_events = self.event_parser.extract_events_from_ast(
202 &parsed_file.ast,
203 path_buf.as_path(),
204 &mut self.type_resolver,
205 )?;
206 self.discovered_events.extend(file_events);
207
208 let mut commands = self.command_parser.extract_commands_from_ast(
210 &parsed_file.ast,
211 path_buf.as_path(),
212 &mut self.type_resolver,
213 )?;
214
215 for command in &mut commands {
217 if let Some(func) =
218 self.find_function_in_ast(&parsed_file.ast, &command.name)
219 {
220 let channels = self.channel_parser.extract_channels_from_command(
221 func,
222 &command.name,
223 path_buf.as_path(),
224 &mut self.type_resolver,
225 )?;
226
227 command.channels = channels;
228 }
229 }
230
231 Ok(commands)
232 } else {
233 Ok(vec![])
234 }
235 }
236 Err(_) => {
237 Ok(vec![])
239 }
240 }
241 }
242
243 fn index_type_definitions(&mut self, ast: &syn::File, file_path: &Path) {
245 for item in &ast.items {
246 match item {
247 syn::Item::Struct(item_struct) => {
248 if self.struct_parser.should_include_struct(item_struct) {
249 let struct_name = item_struct.ident.to_string();
250 self.dependency_graph
251 .add_type_definition(struct_name, file_path.to_path_buf());
252 }
253 }
254 syn::Item::Enum(item_enum) => {
255 if self.struct_parser.should_include_enum(item_enum) {
256 let enum_name = item_enum.ident.to_string();
257 self.dependency_graph
258 .add_type_definition(enum_name, file_path.to_path_buf());
259 }
260 }
261 _ => {}
262 }
263 }
264 }
265
266 fn resolve_types_lazily(
268 &mut self,
269 initial_types: &HashSet<String>,
270 ) -> Result<(), Box<dyn std::error::Error>> {
271 let mut types_to_resolve: Vec<String> = initial_types.iter().cloned().collect();
272 let mut resolved_types = HashSet::new();
273
274 while let Some(type_name) = types_to_resolve.pop() {
275 if resolved_types.contains(&type_name)
277 || self.discovered_structs.contains_key(&type_name)
278 {
279 continue;
280 }
281
282 if let Some(file_path) = self
284 .dependency_graph
285 .get_type_definition_path(&type_name)
286 .cloned()
287 {
288 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
289 if let Some(struct_info) = self.extract_type_from_ast(
291 &parsed_file.ast,
292 &type_name,
293 file_path.as_path(),
294 ) {
295 let mut type_dependencies = HashSet::new();
297 for field in &struct_info.fields {
298 self.extract_type_names(&field.rust_type, &mut type_dependencies);
299 }
300
301 for dep_type in &type_dependencies {
303 if !resolved_types.contains(dep_type)
304 && !self.discovered_structs.contains_key(dep_type)
305 && self.dependency_graph.has_type_definition(dep_type)
306 {
307 types_to_resolve.push(dep_type.clone());
308 }
309 }
310
311 self.dependency_graph
313 .add_dependencies(type_name.clone(), type_dependencies.clone());
314 self.dependency_graph
315 .add_resolved_type(type_name.clone(), struct_info.clone());
316 self.discovered_structs
317 .insert(type_name.clone(), struct_info);
318 resolved_types.insert(type_name);
319 }
320 }
321 }
322 }
323
324 Ok(())
325 }
326
327 fn extract_type_from_ast(
329 &mut self,
330 ast: &syn::File,
331 type_name: &str,
332 file_path: &Path,
333 ) -> Option<StructInfo> {
334 for item in &ast.items {
335 match item {
336 syn::Item::Struct(item_struct) => {
337 if item_struct.ident == type_name
338 && self.struct_parser.should_include_struct(item_struct)
339 {
340 return self.struct_parser.parse_struct(
341 item_struct,
342 file_path,
343 &mut self.type_resolver,
344 );
345 }
346 }
347 syn::Item::Enum(item_enum) => {
348 if item_enum.ident == type_name
349 && self.struct_parser.should_include_enum(item_enum)
350 {
351 return self.struct_parser.parse_enum(item_enum, file_path);
352 }
353 }
354 _ => {}
355 }
356 }
357 None
358 }
359
360 pub fn extract_type_names(&self, rust_type: &str, type_names: &mut HashSet<String>) {
362 self.extract_type_names_recursive(rust_type, type_names);
363 }
364
365 fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
367 let rust_type = rust_type.trim();
368
369 if rust_type.starts_with("Result<") {
371 if let Some(inner) = rust_type
372 .strip_prefix("Result<")
373 .and_then(|s| s.strip_suffix(">"))
374 {
375 if let Some(comma_pos) = inner.find(',') {
376 let ok_type = inner[..comma_pos].trim();
377 let err_type = inner[comma_pos + 1..].trim();
378 self.extract_type_names_recursive(ok_type, type_names);
379 self.extract_type_names_recursive(err_type, type_names);
380 }
381 }
382 return;
383 }
384
385 if rust_type.starts_with("Option<") {
387 if let Some(inner) = rust_type
388 .strip_prefix("Option<")
389 .and_then(|s| s.strip_suffix(">"))
390 {
391 self.extract_type_names_recursive(inner, type_names);
392 }
393 return;
394 }
395
396 if rust_type.starts_with("Vec<") {
398 if let Some(inner) = rust_type
399 .strip_prefix("Vec<")
400 .and_then(|s| s.strip_suffix(">"))
401 {
402 self.extract_type_names_recursive(inner, type_names);
403 }
404 return;
405 }
406
407 if rust_type.starts_with("HashMap<") || rust_type.starts_with("BTreeMap<") {
409 let prefix = if rust_type.starts_with("HashMap<") {
410 "HashMap<"
411 } else {
412 "BTreeMap<"
413 };
414 if let Some(inner) = rust_type
415 .strip_prefix(prefix)
416 .and_then(|s| s.strip_suffix(">"))
417 {
418 if let Some(comma_pos) = inner.find(',') {
419 let key_type = inner[..comma_pos].trim();
420 let value_type = inner[comma_pos + 1..].trim();
421 self.extract_type_names_recursive(key_type, type_names);
422 self.extract_type_names_recursive(value_type, type_names);
423 }
424 }
425 return;
426 }
427
428 if rust_type.starts_with("HashSet<") || rust_type.starts_with("BTreeSet<") {
430 let prefix = if rust_type.starts_with("HashSet<") {
431 "HashSet<"
432 } else {
433 "BTreeSet<"
434 };
435 if let Some(inner) = rust_type
436 .strip_prefix(prefix)
437 .and_then(|s| s.strip_suffix(">"))
438 {
439 self.extract_type_names_recursive(inner, type_names);
440 }
441 return;
442 }
443
444 if rust_type.starts_with('(') && rust_type.ends_with(')') && rust_type != "()" {
446 let inner = &rust_type[1..rust_type.len() - 1];
447 for part in inner.split(',') {
448 self.extract_type_names_recursive(part.trim(), type_names);
449 }
450 return;
451 }
452
453 if rust_type.starts_with('&') {
455 let without_ref = rust_type.trim_start_matches('&');
456 self.extract_type_names_recursive(without_ref, type_names);
457 return;
458 }
459
460 if !rust_type.is_empty()
462 && !self.type_resolver.get_type_set().contains(rust_type)
463 && !rust_type.starts_with(char::is_lowercase) && rust_type.chars().next().is_some_and(char::is_alphabetic)
465 && !rust_type.contains('<')
466 {
468 type_names.insert(rust_type.to_string());
469 }
470 }
471
472 pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
474 &self.discovered_structs
475 }
476
477 pub fn get_discovered_events(&self) -> &[EventInfo] {
479 &self.discovered_events
480 }
481
482 pub fn get_type_resolver(&self) -> std::cell::RefCell<&TypeResolver> {
484 std::cell::RefCell::new(&self.type_resolver)
485 }
486
487 pub fn get_all_discovered_channels(&self, commands: &[CommandInfo]) -> Vec<ChannelInfo> {
489 commands
490 .iter()
491 .flat_map(|cmd| cmd.channels.clone())
492 .collect()
493 }
494
495 fn find_function_in_ast<'a>(
497 &self,
498 ast: &'a syn::File,
499 function_name: &str,
500 ) -> Option<&'a syn::ItemFn> {
501 for item in &ast.items {
502 if let syn::Item::Fn(func) = item {
503 if func.sig.ident == function_name {
504 return Some(func);
505 }
506 }
507 }
508 None
509 }
510
511 pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
513 &self.dependency_graph
514 }
515
516 pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
518 self.dependency_graph.topological_sort_types(types)
519 }
520
521 pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
523 self.dependency_graph.visualize_dependencies(commands)
524 }
525
526 pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
528 self.dependency_graph.generate_dot_graph(commands)
529 }
530}
531
532impl Default for CommandAnalyzer {
533 fn default() -> Self {
534 Self::new()
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use std::collections::HashSet;
542
543 fn analyzer() -> CommandAnalyzer {
544 CommandAnalyzer::new()
545 }
546
547 mod initialization {
548 use super::*;
549
550 #[test]
551 fn test_new_creates_analyzer() {
552 let analyzer = CommandAnalyzer::new();
553 assert!(analyzer.get_discovered_structs().is_empty());
554 assert!(analyzer.get_discovered_events().is_empty());
555 }
556
557 #[test]
558 fn test_default_creates_analyzer() {
559 let analyzer = CommandAnalyzer::default();
560 assert!(analyzer.get_discovered_structs().is_empty());
561 assert!(analyzer.get_discovered_events().is_empty());
562 }
563 }
564
565 mod type_name_extraction {
566 use super::*;
567
568 #[test]
569 fn test_extract_simple_type() {
570 let analyzer = analyzer();
571 let mut types = HashSet::new();
572 analyzer.extract_type_names("User", &mut types);
573 assert_eq!(types.len(), 1);
574 assert!(types.contains("User"));
575 }
576
577 #[test]
578 fn test_extract_option_type() {
579 let analyzer = analyzer();
580 let mut types = HashSet::new();
581 analyzer.extract_type_names("Option<User>", &mut types);
582 assert_eq!(types.len(), 1);
583 assert!(types.contains("User"));
584 }
585
586 #[test]
587 fn test_extract_vec_type() {
588 let analyzer = analyzer();
589 let mut types = HashSet::new();
590 analyzer.extract_type_names("Vec<Product>", &mut types);
591 assert_eq!(types.len(), 1);
592 assert!(types.contains("Product"));
593 }
594
595 #[test]
596 fn test_extract_result_type() {
597 let analyzer = analyzer();
598 let mut types = HashSet::new();
599 analyzer.extract_type_names("Result<User, AppError>", &mut types);
600 assert_eq!(types.len(), 2);
601 assert!(types.contains("User"));
602 assert!(types.contains("AppError"));
603 }
604
605 #[test]
606 fn test_extract_hashmap_type() {
607 let analyzer = analyzer();
608 let mut types = HashSet::new();
609 analyzer.extract_type_names("HashMap<String, User>", &mut types);
610 assert_eq!(types.len(), 1);
612 assert!(types.contains("User"));
613 }
614
615 #[test]
616 fn test_extract_btreemap_type() {
617 let analyzer = analyzer();
618 let mut types = HashSet::new();
619 analyzer.extract_type_names("BTreeMap<UserId, Profile>", &mut types);
620 assert_eq!(types.len(), 2);
621 assert!(types.contains("UserId"));
622 assert!(types.contains("Profile"));
623 }
624
625 #[test]
626 fn test_extract_hashset_type() {
627 let analyzer = analyzer();
628 let mut types = HashSet::new();
629 analyzer.extract_type_names("HashSet<User>", &mut types);
630 assert_eq!(types.len(), 1);
631 assert!(types.contains("User"));
632 }
633
634 #[test]
635 fn test_extract_btreeset_type() {
636 let analyzer = analyzer();
637 let mut types = HashSet::new();
638 analyzer.extract_type_names("BTreeSet<Tag>", &mut types);
639 assert_eq!(types.len(), 1);
640 assert!(types.contains("Tag"));
641 }
642
643 #[test]
644 fn test_extract_tuple_type() {
645 let analyzer = analyzer();
646 let mut types = HashSet::new();
647 analyzer.extract_type_names("(User, Product, Order)", &mut types);
648 assert_eq!(types.len(), 3);
649 assert!(types.contains("User"));
650 assert!(types.contains("Product"));
651 assert!(types.contains("Order"));
652 }
653
654 #[test]
655 fn test_extract_reference_type() {
656 let analyzer = analyzer();
657 let mut types = HashSet::new();
658 analyzer.extract_type_names("&User", &mut types);
659 assert_eq!(types.len(), 1);
660 assert!(types.contains("User"));
661 }
662
663 #[test]
664 fn test_extract_nested_types() {
665 let analyzer = analyzer();
666 let mut types = HashSet::new();
667 analyzer.extract_type_names("Vec<Option<User>>", &mut types);
668 assert_eq!(types.len(), 1);
669 assert!(types.contains("User"));
670 }
671
672 #[test]
673 fn test_extract_deeply_nested_types() {
674 let analyzer = analyzer();
675 let mut types = HashSet::new();
676 analyzer.extract_type_names("HashMap<String, Vec<Option<Product>>>", &mut types);
677 assert_eq!(types.len(), 1);
678 assert!(types.contains("Product"));
679 }
680
681 #[test]
682 fn test_skips_primitive_types() {
683 let analyzer = analyzer();
684 let mut types = HashSet::new();
685 analyzer.extract_type_names("String", &mut types);
686 assert_eq!(types.len(), 0);
687 }
688
689 #[test]
690 fn test_skips_built_in_types() {
691 let analyzer = analyzer();
692 let mut types = HashSet::new();
693 analyzer.extract_type_names("i32", &mut types);
694 assert_eq!(types.len(), 0);
695 }
696
697 #[test]
698 fn test_skips_empty_type() {
699 let analyzer = analyzer();
700 let mut types = HashSet::new();
701 analyzer.extract_type_names("", &mut types);
702 assert_eq!(types.len(), 0);
703 }
704
705 #[test]
706 fn test_skips_unit_type() {
707 let analyzer = analyzer();
708 let mut types = HashSet::new();
709 analyzer.extract_type_names("()", &mut types);
710 assert_eq!(types.len(), 0);
711 }
712
713 #[test]
714 fn test_multiple_calls_accumulate() {
715 let analyzer = analyzer();
716 let mut types = HashSet::new();
717 analyzer.extract_type_names("User", &mut types);
718 analyzer.extract_type_names("Product", &mut types);
719 assert_eq!(types.len(), 2);
720 assert!(types.contains("User"));
721 assert!(types.contains("Product"));
722 }
723
724 #[test]
725 fn test_duplicate_types_deduped() {
726 let analyzer = analyzer();
727 let mut types = HashSet::new();
728 analyzer.extract_type_names("User", &mut types);
729 analyzer.extract_type_names("User", &mut types);
730 assert_eq!(types.len(), 1);
731 }
732 }
733
734 mod getters {
735 use super::*;
736
737 #[test]
738 fn test_get_discovered_structs_empty() {
739 let analyzer = analyzer();
740 let structs = analyzer.get_discovered_structs();
741 assert!(structs.is_empty());
742 }
743
744 #[test]
745 fn test_get_discovered_events_empty() {
746 let analyzer = analyzer();
747 let events = analyzer.get_discovered_events();
748 assert!(events.is_empty());
749 }
750
751 #[test]
752 fn test_get_type_resolver() {
753 let analyzer = analyzer();
754 let resolver = analyzer.get_type_resolver();
755 assert!(!resolver.borrow().get_type_set().is_empty());
757 }
758
759 #[test]
760 fn test_get_dependency_graph() {
761 let analyzer = analyzer();
762 let graph = analyzer.get_dependency_graph();
763 assert!(graph.get_resolved_types().is_empty());
765 }
766
767 #[test]
768 fn test_get_all_discovered_channels_empty() {
769 let analyzer = analyzer();
770 let commands = vec![];
771 let channels = analyzer.get_all_discovered_channels(&commands);
772 assert!(channels.is_empty());
773 }
774
775 #[test]
776 fn test_get_all_discovered_channels_with_commands() {
777 let analyzer = analyzer();
778 let command = CommandInfo::new_for_test(
779 "test_cmd",
780 "test.rs",
781 1,
782 vec![],
783 "void",
784 false,
785 vec![
786 ChannelInfo::new_for_test("ch1", "Message1", "test_cmd", "test.rs", 10),
787 ChannelInfo::new_for_test("ch2", "Message2", "test_cmd", "test.rs", 20),
788 ],
789 );
790
791 let commands = vec![command];
792 let channels = analyzer.get_all_discovered_channels(&commands);
793 assert_eq!(channels.len(), 2);
794 }
795 }
796
797 mod topological_sort {
798 use super::*;
799
800 #[test]
801 fn test_topological_sort_empty() {
802 let analyzer = analyzer();
803 let types = HashSet::new();
804 let sorted = analyzer.topological_sort_types(&types);
805 assert!(sorted.is_empty());
806 }
807
808 #[test]
809 fn test_topological_sort_single_type() {
810 let mut analyzer = analyzer();
811 let path = PathBuf::from("test.rs");
812 analyzer
813 .dependency_graph
814 .add_type_definition("User".to_string(), path);
815
816 let mut types = HashSet::new();
817 types.insert("User".to_string());
818
819 let sorted = analyzer.topological_sort_types(&types);
820 assert_eq!(sorted.len(), 1);
821 assert_eq!(sorted[0], "User");
822 }
823 }
824
825 mod ast_helpers {
826 use super::*;
827 use syn::{parse_quote, File as SynFile};
828
829 #[test]
830 fn test_find_function_in_ast() {
831 let analyzer = analyzer();
832 let ast: SynFile = parse_quote! {
833 #[tauri::command]
834 fn my_command() -> String {
835 "test".to_string()
836 }
837
838 fn other_function() {}
839 };
840
841 let result = analyzer.find_function_in_ast(&ast, "my_command");
842 assert!(result.is_some());
843 assert_eq!(result.unwrap().sig.ident, "my_command");
844 }
845
846 #[test]
847 fn test_find_function_in_ast_not_found() {
848 let analyzer = analyzer();
849 let ast: SynFile = parse_quote! {
850 fn my_command() {}
851 };
852
853 let result = analyzer.find_function_in_ast(&ast, "non_existent");
854 assert!(result.is_none());
855 }
856
857 #[test]
858 fn test_find_function_in_ast_empty() {
859 let analyzer = analyzer();
860 let ast: SynFile = parse_quote! {};
861
862 let result = analyzer.find_function_in_ast(&ast, "any_function");
863 assert!(result.is_none());
864 }
865 }
866
867 mod index_type_definitions {
868 use super::*;
869 use syn::{parse_quote, File as SynFile};
870
871 #[test]
872 fn test_index_struct() {
873 let mut analyzer = analyzer();
874 let ast: SynFile = parse_quote! {
875 #[derive(Serialize)]
876 pub struct User {
877 name: String,
878 }
879 };
880 let path = Path::new("test.rs");
881
882 analyzer.index_type_definitions(&ast, path);
883
884 assert!(analyzer.dependency_graph.has_type_definition("User"));
885 }
886
887 #[test]
888 fn test_index_enum() {
889 let mut analyzer = analyzer();
890 let ast: SynFile = parse_quote! {
891 #[derive(Serialize)]
892 pub enum Status {
893 Active,
894 Inactive,
895 }
896 };
897 let path = Path::new("test.rs");
898
899 analyzer.index_type_definitions(&ast, path);
900
901 assert!(analyzer.dependency_graph.has_type_definition("Status"));
902 }
903
904 #[test]
905 fn test_skips_non_serde_types() {
906 let mut analyzer = analyzer();
907 let ast: SynFile = parse_quote! {
908 #[derive(Debug, Clone)]
909 pub struct User {
910 name: String,
911 }
912 };
913 let path = Path::new("test.rs");
914
915 analyzer.index_type_definitions(&ast, path);
916
917 assert!(!analyzer.dependency_graph.has_type_definition("User"));
918 }
919 }
920
921 mod extract_type_from_ast {
922 use super::*;
923 use syn::{parse_quote, File as SynFile};
924
925 #[test]
926 fn test_extract_struct_from_ast() {
927 let mut analyzer = analyzer();
928 let ast: SynFile = parse_quote! {
929 #[derive(Serialize)]
930 pub struct User {
931 pub name: String,
932 }
933 };
934 let path = Path::new("test.rs");
935
936 let result = analyzer.extract_type_from_ast(&ast, "User", path);
937 assert!(result.is_some());
938 let struct_info = result.unwrap();
939 assert_eq!(struct_info.name, "User");
940 assert_eq!(struct_info.fields.len(), 1);
941 }
942
943 #[test]
944 fn test_extract_enum_from_ast() {
945 let mut analyzer = analyzer();
946 let ast: SynFile = parse_quote! {
947 #[derive(Serialize)]
948 pub enum Status {
949 Active,
950 Inactive,
951 }
952 };
953 let path = Path::new("test.rs");
954
955 let result = analyzer.extract_type_from_ast(&ast, "Status", path);
956 assert!(result.is_some());
957 let enum_info = result.unwrap();
958 assert_eq!(enum_info.name, "Status");
959 assert!(enum_info.is_enum);
960 }
961
962 #[test]
963 fn test_extract_type_not_found() {
964 let mut analyzer = analyzer();
965 let ast: SynFile = parse_quote! {
966 #[derive(Serialize)]
967 pub struct User {
968 name: String,
969 }
970 };
971 let path = Path::new("test.rs");
972
973 let result = analyzer.extract_type_from_ast(&ast, "Product", path);
974 assert!(result.is_none());
975 }
976
977 #[test]
978 fn test_extract_type_without_serde() {
979 let mut analyzer = analyzer();
980 let ast: SynFile = parse_quote! {
981 #[derive(Debug)]
982 pub struct User {
983 name: String,
984 }
985 };
986 let path = Path::new("test.rs");
987
988 let result = analyzer.extract_type_from_ast(&ast, "User", path);
989 assert!(result.is_none());
990 }
991 }
992
993 mod visualization {
994 use super::*;
995
996 #[test]
997 fn test_visualize_dependencies() {
998 let analyzer = analyzer();
999 let commands = vec![];
1000 let viz = analyzer.visualize_dependencies(&commands);
1001 assert!(viz.contains("Dependency Graph"));
1003 }
1004
1005 #[test]
1006 fn test_generate_dot_graph() {
1007 let analyzer = analyzer();
1008 let commands = vec![];
1009 let dot = analyzer.generate_dot_graph(&commands);
1010 assert!(dot.contains("digraph"));
1012 }
1013 }
1014}