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 mut file_paths: Vec<PathBuf> = self.ast_cache.keys().cloned().collect();
88 file_paths.sort_unstable();
89 let mut commands = Vec::new();
90 let mut type_names_to_discover = HashSet::new();
91
92 for file_path in file_paths {
94 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
95 if verbose {
96 println!("🔍 Analyzing file: {}", parsed_file.path.display());
97 }
98
99 let mut file_commands = self.command_parser.extract_commands_from_ast(
101 &parsed_file.ast,
102 parsed_file.path.as_path(),
103 &mut self.type_resolver,
104 )?;
105
106 for command in &mut file_commands {
108 if let Some(func) = self.find_function_in_ast(&parsed_file.ast, &command.name) {
109 let channels = self.channel_parser.extract_channels_from_command(
110 func,
111 &command.name,
112 parsed_file.path.as_path(),
113 &mut self.type_resolver,
114 )?;
115
116 channels.iter().for_each(|ch| {
118 self.extract_type_names(&ch.message_type, &mut type_names_to_discover);
119 });
120
121 command.channels = channels;
122 }
123 }
124
125 let file_events = self.event_parser.extract_events_from_ast(
127 &parsed_file.ast,
128 parsed_file.path.as_path(),
129 &mut self.type_resolver,
130 )?;
131
132 file_commands.iter().for_each(|cmd| {
134 cmd.parameters.iter().for_each(|param| {
135 self.extract_type_names(¶m.rust_type, &mut type_names_to_discover);
136 });
137 self.extract_type_names(&cmd.return_type, &mut type_names_to_discover);
139 });
140
141 file_events.iter().for_each(|event| {
143 self.extract_type_names(&event.payload_type, &mut type_names_to_discover);
144 });
145
146 commands.extend(file_commands);
147 self.discovered_events.extend(file_events);
148
149 self.index_type_definitions(&parsed_file.ast, parsed_file.path.as_path());
151 }
152 }
153
154 if verbose {
155 println!("🔍 Type names to discover: {:?}", type_names_to_discover);
156 }
157
158 self.resolve_types_lazily(&type_names_to_discover)?;
160
161 if verbose {
162 println!(
163 "🏗️ Discovered {} structs total",
164 self.discovered_structs.len()
165 );
166 for (name, info) in &self.discovered_structs {
167 println!(" - {}: {} fields", name, info.fields.len());
168 }
169 println!(
170 "📡 Discovered {} events total",
171 self.discovered_events.len()
172 );
173 for event in &self.discovered_events {
174 println!(" - '{}': {}", event.event_name, event.payload_type);
175 }
176 let all_channels = self.get_all_discovered_channels(&commands);
177 println!("📞 Discovered {} channels total", all_channels.len());
178 for channel in &all_channels {
179 println!(
180 " - '{}' in {}: {}",
181 channel.parameter_name, channel.command_name, channel.message_type
182 );
183 }
184 }
185
186 Ok(commands)
187 }
188
189 pub fn analyze_file(
191 &mut self,
192 file_path: &std::path::Path,
193 ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
194 let path_buf = file_path.to_path_buf();
195
196 match self.ast_cache.parse_and_cache_file(&path_buf) {
198 Ok(_) => {
199 if let Some(parsed_file) = self.ast_cache.get_cloned(&path_buf) {
201 let file_events = self.event_parser.extract_events_from_ast(
203 &parsed_file.ast,
204 path_buf.as_path(),
205 &mut self.type_resolver,
206 )?;
207 self.discovered_events.extend(file_events);
208
209 let mut commands = self.command_parser.extract_commands_from_ast(
211 &parsed_file.ast,
212 path_buf.as_path(),
213 &mut self.type_resolver,
214 )?;
215
216 for command in &mut commands {
218 if let Some(func) =
219 self.find_function_in_ast(&parsed_file.ast, &command.name)
220 {
221 let channels = self.channel_parser.extract_channels_from_command(
222 func,
223 &command.name,
224 path_buf.as_path(),
225 &mut self.type_resolver,
226 )?;
227
228 command.channels = channels;
229 }
230 }
231
232 Ok(commands)
233 } else {
234 Ok(vec![])
235 }
236 }
237 Err(_) => {
238 Ok(vec![])
240 }
241 }
242 }
243
244 fn index_type_definitions(&mut self, ast: &syn::File, file_path: &Path) {
246 self.index_items(&ast.items, file_path);
247 }
248
249 fn index_items(&mut self, items: &[syn::Item], file_path: &Path) {
251 for item in items {
252 match item {
253 syn::Item::Struct(item_struct) => {
254 if self.struct_parser.should_include_struct(item_struct) {
255 let struct_name = item_struct.ident.to_string();
256 self.dependency_graph
257 .add_type_definition(struct_name, file_path.to_path_buf());
258 }
259 }
260 syn::Item::Enum(item_enum) => {
261 if self.struct_parser.should_include_enum(item_enum) {
262 let enum_name = item_enum.ident.to_string();
263 self.dependency_graph
264 .add_type_definition(enum_name, file_path.to_path_buf());
265 }
266 }
267 syn::Item::Mod(item_mod) => {
268 if let Some((_, items)) = &item_mod.content {
269 self.index_items(items, file_path);
270 }
271 }
272 _ => {}
273 }
274 }
275 }
276
277 fn resolve_types_lazily(
279 &mut self,
280 initial_types: &HashSet<String>,
281 ) -> Result<(), Box<dyn std::error::Error>> {
282 let mut types_to_resolve: Vec<String> = initial_types.iter().cloned().collect();
283 let mut resolved_types = HashSet::new();
284
285 while let Some(type_name) = types_to_resolve.pop() {
286 if resolved_types.contains(&type_name)
288 || self.discovered_structs.contains_key(&type_name)
289 {
290 continue;
291 }
292
293 if let Some(file_path) = self
295 .dependency_graph
296 .get_type_definition_path(&type_name)
297 .cloned()
298 {
299 if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
300 if let Some(struct_info) = self.extract_type_from_ast(
302 &parsed_file.ast,
303 &type_name,
304 file_path.as_path(),
305 ) {
306 let mut type_dependencies = HashSet::new();
308 for field in &struct_info.fields {
309 self.extract_type_names(&field.rust_type, &mut type_dependencies);
310 }
311
312 if let Some(variants) = &struct_info.enum_variants {
314 for variant in variants {
315 match &variant.kind {
316 crate::models::EnumVariantKind::Unit => {}
317 crate::models::EnumVariantKind::Tuple(types) => {
318 for type_struct in types {
319 let mut variant_types = HashSet::new();
320 crate::generators::TypeCollector::collect_referenced_types_from_structure(
321 type_struct,
322 &mut variant_types,
323 );
324 type_dependencies.extend(variant_types);
325 }
326 }
327 crate::models::EnumVariantKind::Struct(fields) => {
328 for field in fields {
329 self.extract_type_names(
330 &field.rust_type,
331 &mut type_dependencies,
332 );
333 }
334 }
335 }
336 }
337 }
338
339 for dep_type in &type_dependencies {
341 if !resolved_types.contains(dep_type)
342 && !self.discovered_structs.contains_key(dep_type)
343 && self.dependency_graph.has_type_definition(dep_type)
344 {
345 types_to_resolve.push(dep_type.clone());
346 }
347 }
348
349 self.dependency_graph
351 .add_dependencies(type_name.clone(), type_dependencies.clone());
352 self.dependency_graph
353 .add_resolved_type(type_name.clone(), struct_info.clone());
354 self.discovered_structs
355 .insert(type_name.clone(), struct_info);
356 resolved_types.insert(type_name);
357 }
358 }
359 }
360 }
361
362 Ok(())
363 }
364
365 fn extract_type_from_ast(
367 &mut self,
368 ast: &syn::File,
369 type_name: &str,
370 file_path: &Path,
371 ) -> Option<StructInfo> {
372 self.find_type_in_items(&ast.items, type_name, file_path)
373 }
374
375 fn find_type_in_items(
377 &mut self,
378 items: &[syn::Item],
379 type_name: &str,
380 file_path: &Path,
381 ) -> Option<StructInfo> {
382 for item in items {
383 match item {
384 syn::Item::Struct(item_struct) => {
385 if item_struct.ident == type_name
386 && self.struct_parser.should_include_struct(item_struct)
387 {
388 return self.struct_parser.parse_struct(
389 item_struct,
390 file_path,
391 &mut self.type_resolver,
392 );
393 }
394 }
395 syn::Item::Enum(item_enum) => {
396 if item_enum.ident == type_name
397 && self.struct_parser.should_include_enum(item_enum)
398 {
399 return self.struct_parser.parse_enum(
400 item_enum,
401 file_path,
402 &mut self.type_resolver,
403 );
404 }
405 }
406 syn::Item::Mod(item_mod) => {
407 if let Some((_, items)) = &item_mod.content {
408 if let Some(info) = self.find_type_in_items(items, type_name, file_path) {
409 return Some(info);
410 }
411 }
412 }
413 _ => {}
414 }
415 }
416 None
417 }
418
419 pub fn extract_type_names(&self, rust_type: &str, type_names: &mut HashSet<String>) {
421 self.extract_type_names_recursive(rust_type, type_names);
422 }
423
424 fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
426 let rust_type = rust_type.trim();
427
428 if rust_type.starts_with('&') {
430 let without_ref = rust_type.trim_start_matches('&');
431 self.extract_type_names_recursive(without_ref, type_names);
432 return;
433 }
434
435 let stripped = Self::strip_module_prefix(rust_type);
438
439 if stripped.starts_with("Result<") {
441 if let Some(inner) = stripped
442 .strip_prefix("Result<")
443 .and_then(|s| s.strip_suffix(">"))
444 {
445 if let Some(comma_pos) = inner.find(',') {
446 let ok_type = inner[..comma_pos].trim();
447 let err_type = inner[comma_pos + 1..].trim();
448 self.extract_type_names_recursive(ok_type, type_names);
449 self.extract_type_names_recursive(err_type, type_names);
450 }
451 }
452 return;
453 }
454
455 if stripped.starts_with("Option<") {
457 if let Some(inner) = stripped
458 .strip_prefix("Option<")
459 .and_then(|s| s.strip_suffix(">"))
460 {
461 self.extract_type_names_recursive(inner, type_names);
462 }
463 return;
464 }
465
466 if stripped.starts_with("Vec<") {
468 if let Some(inner) = stripped
469 .strip_prefix("Vec<")
470 .and_then(|s| s.strip_suffix(">"))
471 {
472 self.extract_type_names_recursive(inner, type_names);
473 }
474 return;
475 }
476
477 if stripped.starts_with("HashMap<") || stripped.starts_with("BTreeMap<") {
479 let prefix = if stripped.starts_with("HashMap<") {
480 "HashMap<"
481 } else {
482 "BTreeMap<"
483 };
484 if let Some(inner) = stripped
485 .strip_prefix(prefix)
486 .and_then(|s| s.strip_suffix(">"))
487 {
488 if let Some(comma_pos) = inner.find(',') {
489 let key_type = inner[..comma_pos].trim();
490 let value_type = inner[comma_pos + 1..].trim();
491 self.extract_type_names_recursive(key_type, type_names);
492 self.extract_type_names_recursive(value_type, type_names);
493 }
494 }
495 return;
496 }
497
498 if stripped.starts_with("HashSet<") || stripped.starts_with("BTreeSet<") {
500 let prefix = if stripped.starts_with("HashSet<") {
501 "HashSet<"
502 } else {
503 "BTreeSet<"
504 };
505 if let Some(inner) = stripped
506 .strip_prefix(prefix)
507 .and_then(|s| s.strip_suffix(">"))
508 {
509 self.extract_type_names_recursive(inner, type_names);
510 }
511 return;
512 }
513
514 if rust_type.starts_with('(') && rust_type.ends_with(')') && rust_type != "()" {
516 let inner = &rust_type[1..rust_type.len() - 1];
517 for part in inner.split(',') {
518 self.extract_type_names_recursive(part.trim(), type_names);
519 }
520 return;
521 }
522
523 if !rust_type.is_empty()
525 && !self.type_resolver.get_type_set().contains(rust_type)
526 && !rust_type.starts_with(char::is_lowercase) && rust_type.chars().next().is_some_and(char::is_alphabetic)
528 && !rust_type.contains('<')
529 {
531 let type_name = Self::extract_simple_type_name(rust_type);
533 type_names.insert(type_name);
534 }
535 }
536
537 fn strip_module_prefix(rust_type: &str) -> &str {
540 if let Some(last_double_colon) = rust_type.rfind("::") {
542 let after_colon = &rust_type[last_double_colon + 2..];
544 if after_colon.contains('<') {
545 return after_colon;
546 }
547 }
548 rust_type
549 }
550
551 fn extract_simple_type_name(rust_type: &str) -> String {
554 if let Some(last_double_colon) = rust_type.rfind("::") {
556 rust_type[last_double_colon + 2..].to_string()
557 } else {
558 rust_type.to_string()
559 }
560 }
561
562 pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
564 &self.discovered_structs
565 }
566
567 pub fn get_discovered_events(&self) -> &[EventInfo] {
569 &self.discovered_events
570 }
571
572 pub fn get_type_resolver(&self) -> std::cell::RefCell<&TypeResolver> {
574 std::cell::RefCell::new(&self.type_resolver)
575 }
576
577 pub fn get_all_discovered_channels(&self, commands: &[CommandInfo]) -> Vec<ChannelInfo> {
579 commands
580 .iter()
581 .flat_map(|cmd| cmd.channels.clone())
582 .collect()
583 }
584
585 fn find_function_in_ast<'a>(
587 &self,
588 ast: &'a syn::File,
589 function_name: &str,
590 ) -> Option<&'a syn::ItemFn> {
591 self.find_function_in_items(&ast.items, function_name)
592 }
593
594 fn find_function_in_items<'a>(
596 &self,
597 items: &'a [syn::Item],
598 function_name: &str,
599 ) -> Option<&'a syn::ItemFn> {
600 for item in items {
601 match item {
602 syn::Item::Fn(func) => {
603 if func.sig.ident == function_name {
604 return Some(func);
605 }
606 }
607 syn::Item::Mod(item_mod) => {
608 if let Some((_, items)) = &item_mod.content {
609 if let Some(func) = self.find_function_in_items(items, function_name) {
610 return Some(func);
611 }
612 }
613 }
614 _ => {}
615 }
616 }
617 None
618 }
619
620 pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
622 &self.dependency_graph
623 }
624
625 pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
627 self.dependency_graph.topological_sort_types(types)
628 }
629
630 pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
632 self.dependency_graph.visualize_dependencies(commands)
633 }
634
635 pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
637 self.dependency_graph.generate_dot_graph(commands)
638 }
639}
640
641impl Default for CommandAnalyzer {
642 fn default() -> Self {
643 Self::new()
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650 use std::collections::HashSet;
651
652 fn analyzer() -> CommandAnalyzer {
653 CommandAnalyzer::new()
654 }
655
656 mod initialization {
657 use super::*;
658
659 #[test]
660 fn test_new_creates_analyzer() {
661 let analyzer = CommandAnalyzer::new();
662 assert!(analyzer.get_discovered_structs().is_empty());
663 assert!(analyzer.get_discovered_events().is_empty());
664 }
665
666 #[test]
667 fn test_default_creates_analyzer() {
668 let analyzer = CommandAnalyzer::default();
669 assert!(analyzer.get_discovered_structs().is_empty());
670 assert!(analyzer.get_discovered_events().is_empty());
671 }
672 }
673
674 mod type_name_extraction {
675 use super::*;
676
677 #[test]
678 fn test_extract_simple_type() {
679 let analyzer = analyzer();
680 let mut types = HashSet::new();
681 analyzer.extract_type_names("User", &mut types);
682 assert_eq!(types.len(), 1);
683 assert!(types.contains("User"));
684 }
685
686 #[test]
687 fn test_extract_option_type() {
688 let analyzer = analyzer();
689 let mut types = HashSet::new();
690 analyzer.extract_type_names("Option<User>", &mut types);
691 assert_eq!(types.len(), 1);
692 assert!(types.contains("User"));
693 }
694
695 #[test]
696 fn test_extract_vec_type() {
697 let analyzer = analyzer();
698 let mut types = HashSet::new();
699 analyzer.extract_type_names("Vec<Product>", &mut types);
700 assert_eq!(types.len(), 1);
701 assert!(types.contains("Product"));
702 }
703
704 #[test]
705 fn test_extract_result_type() {
706 let analyzer = analyzer();
707 let mut types = HashSet::new();
708 analyzer.extract_type_names("Result<User, AppError>", &mut types);
709 assert_eq!(types.len(), 2);
710 assert!(types.contains("User"));
711 assert!(types.contains("AppError"));
712 }
713
714 #[test]
715 fn test_extract_hashmap_type() {
716 let analyzer = analyzer();
717 let mut types = HashSet::new();
718 analyzer.extract_type_names("HashMap<String, User>", &mut types);
719 assert_eq!(types.len(), 1);
721 assert!(types.contains("User"));
722 }
723
724 #[test]
725 fn test_extract_btreemap_type() {
726 let analyzer = analyzer();
727 let mut types = HashSet::new();
728 analyzer.extract_type_names("BTreeMap<UserId, Profile>", &mut types);
729 assert_eq!(types.len(), 2);
730 assert!(types.contains("UserId"));
731 assert!(types.contains("Profile"));
732 }
733
734 #[test]
735 fn test_extract_hashset_type() {
736 let analyzer = analyzer();
737 let mut types = HashSet::new();
738 analyzer.extract_type_names("HashSet<User>", &mut types);
739 assert_eq!(types.len(), 1);
740 assert!(types.contains("User"));
741 }
742
743 #[test]
744 fn test_extract_btreeset_type() {
745 let analyzer = analyzer();
746 let mut types = HashSet::new();
747 analyzer.extract_type_names("BTreeSet<Tag>", &mut types);
748 assert_eq!(types.len(), 1);
749 assert!(types.contains("Tag"));
750 }
751
752 #[test]
753 fn test_extract_tuple_type() {
754 let analyzer = analyzer();
755 let mut types = HashSet::new();
756 analyzer.extract_type_names("(User, Product, Order)", &mut types);
757 assert_eq!(types.len(), 3);
758 assert!(types.contains("User"));
759 assert!(types.contains("Product"));
760 assert!(types.contains("Order"));
761 }
762
763 #[test]
764 fn test_extract_reference_type() {
765 let analyzer = analyzer();
766 let mut types = HashSet::new();
767 analyzer.extract_type_names("&User", &mut types);
768 assert_eq!(types.len(), 1);
769 assert!(types.contains("User"));
770 }
771
772 #[test]
773 fn test_extract_nested_types() {
774 let analyzer = analyzer();
775 let mut types = HashSet::new();
776 analyzer.extract_type_names("Vec<Option<User>>", &mut types);
777 assert_eq!(types.len(), 1);
778 assert!(types.contains("User"));
779 }
780
781 #[test]
782 fn test_extract_deeply_nested_types() {
783 let analyzer = analyzer();
784 let mut types = HashSet::new();
785 analyzer.extract_type_names("HashMap<String, Vec<Option<Product>>>", &mut types);
786 assert_eq!(types.len(), 1);
787 assert!(types.contains("Product"));
788 }
789
790 #[test]
791 fn test_skips_primitive_types() {
792 let analyzer = analyzer();
793 let mut types = HashSet::new();
794 analyzer.extract_type_names("String", &mut types);
795 assert_eq!(types.len(), 0);
796 }
797
798 #[test]
799 fn test_skips_built_in_types() {
800 let analyzer = analyzer();
801 let mut types = HashSet::new();
802 analyzer.extract_type_names("i32", &mut types);
803 assert_eq!(types.len(), 0);
804 }
805
806 #[test]
807 fn test_skips_empty_type() {
808 let analyzer = analyzer();
809 let mut types = HashSet::new();
810 analyzer.extract_type_names("", &mut types);
811 assert_eq!(types.len(), 0);
812 }
813
814 #[test]
815 fn test_skips_unit_type() {
816 let analyzer = analyzer();
817 let mut types = HashSet::new();
818 analyzer.extract_type_names("()", &mut types);
819 assert_eq!(types.len(), 0);
820 }
821
822 #[test]
823 fn test_multiple_calls_accumulate() {
824 let analyzer = analyzer();
825 let mut types = HashSet::new();
826 analyzer.extract_type_names("User", &mut types);
827 analyzer.extract_type_names("Product", &mut types);
828 assert_eq!(types.len(), 2);
829 assert!(types.contains("User"));
830 assert!(types.contains("Product"));
831 }
832
833 #[test]
834 fn test_duplicate_types_deduped() {
835 let analyzer = analyzer();
836 let mut types = HashSet::new();
837 analyzer.extract_type_names("User", &mut types);
838 analyzer.extract_type_names("User", &mut types);
839 assert_eq!(types.len(), 1);
840 }
841 }
842
843 mod getters {
844 use super::*;
845
846 #[test]
847 fn test_get_discovered_structs_empty() {
848 let analyzer = analyzer();
849 let structs = analyzer.get_discovered_structs();
850 assert!(structs.is_empty());
851 }
852
853 #[test]
854 fn test_get_discovered_events_empty() {
855 let analyzer = analyzer();
856 let events = analyzer.get_discovered_events();
857 assert!(events.is_empty());
858 }
859
860 #[test]
861 fn test_get_type_resolver() {
862 let analyzer = analyzer();
863 let resolver = analyzer.get_type_resolver();
864 assert!(!resolver.borrow().get_type_set().is_empty());
866 }
867
868 #[test]
869 fn test_get_dependency_graph() {
870 let analyzer = analyzer();
871 let graph = analyzer.get_dependency_graph();
872 assert!(graph.get_resolved_types().is_empty());
874 }
875
876 #[test]
877 fn test_get_all_discovered_channels_empty() {
878 let analyzer = analyzer();
879 let commands = vec![];
880 let channels = analyzer.get_all_discovered_channels(&commands);
881 assert!(channels.is_empty());
882 }
883
884 #[test]
885 fn test_get_all_discovered_channels_with_commands() {
886 let analyzer = analyzer();
887 let command = CommandInfo::new_for_test(
888 "test_cmd",
889 "test.rs",
890 1,
891 vec![],
892 "void",
893 false,
894 vec![
895 ChannelInfo::new_for_test("ch1", "Message1", "test_cmd", "test.rs", 10),
896 ChannelInfo::new_for_test("ch2", "Message2", "test_cmd", "test.rs", 20),
897 ],
898 );
899
900 let commands = vec![command];
901 let channels = analyzer.get_all_discovered_channels(&commands);
902 assert_eq!(channels.len(), 2);
903 }
904 }
905
906 mod topological_sort {
907 use super::*;
908
909 #[test]
910 fn test_topological_sort_empty() {
911 let analyzer = analyzer();
912 let types = HashSet::new();
913 let sorted = analyzer.topological_sort_types(&types);
914 assert!(sorted.is_empty());
915 }
916
917 #[test]
918 fn test_topological_sort_single_type() {
919 let mut analyzer = analyzer();
920 let path = PathBuf::from("test.rs");
921 analyzer
922 .dependency_graph
923 .add_type_definition("User".to_string(), path);
924
925 let mut types = HashSet::new();
926 types.insert("User".to_string());
927
928 let sorted = analyzer.topological_sort_types(&types);
929 assert_eq!(sorted.len(), 1);
930 assert_eq!(sorted[0], "User");
931 }
932 }
933
934 mod ast_helpers {
935 use super::*;
936 use syn::{parse_quote, File as SynFile};
937
938 #[test]
939 fn test_find_function_in_ast() {
940 let analyzer = analyzer();
941 let ast: SynFile = parse_quote! {
942 #[tauri::command]
943 fn my_command() -> String {
944 "test".to_string()
945 }
946
947 fn other_function() {}
948 };
949
950 let result = analyzer.find_function_in_ast(&ast, "my_command");
951 assert!(result.is_some());
952 assert_eq!(result.unwrap().sig.ident, "my_command");
953 }
954
955 #[test]
956 fn test_find_function_in_ast_not_found() {
957 let analyzer = analyzer();
958 let ast: SynFile = parse_quote! {
959 fn my_command() {}
960 };
961
962 let result = analyzer.find_function_in_ast(&ast, "non_existent");
963 assert!(result.is_none());
964 }
965
966 #[test]
967 fn test_find_function_in_ast_empty() {
968 let analyzer = analyzer();
969 let ast: SynFile = parse_quote! {};
970
971 let result = analyzer.find_function_in_ast(&ast, "any_function");
972 assert!(result.is_none());
973 }
974 }
975
976 mod index_type_definitions {
977 use super::*;
978 use syn::{parse_quote, File as SynFile};
979
980 #[test]
981 fn test_index_struct() {
982 let mut analyzer = analyzer();
983 let ast: SynFile = parse_quote! {
984 #[derive(Serialize)]
985 pub struct User {
986 name: String,
987 }
988 };
989 let path = Path::new("test.rs");
990
991 analyzer.index_type_definitions(&ast, path);
992
993 assert!(analyzer.dependency_graph.has_type_definition("User"));
994 }
995
996 #[test]
997 fn test_index_enum() {
998 let mut analyzer = analyzer();
999 let ast: SynFile = parse_quote! {
1000 #[derive(Serialize)]
1001 pub enum Status {
1002 Active,
1003 Inactive,
1004 }
1005 };
1006 let path = Path::new("test.rs");
1007
1008 analyzer.index_type_definitions(&ast, path);
1009
1010 assert!(analyzer.dependency_graph.has_type_definition("Status"));
1011 }
1012
1013 #[test]
1014 fn test_skips_non_serde_types() {
1015 let mut analyzer = analyzer();
1016 let ast: SynFile = parse_quote! {
1017 #[derive(Debug, Clone)]
1018 pub struct User {
1019 name: String,
1020 }
1021 };
1022 let path = Path::new("test.rs");
1023
1024 analyzer.index_type_definitions(&ast, path);
1025
1026 assert!(!analyzer.dependency_graph.has_type_definition("User"));
1027 }
1028 }
1029
1030 mod extract_type_from_ast {
1031 use super::*;
1032 use syn::{parse_quote, File as SynFile};
1033
1034 #[test]
1035 fn test_extract_struct_from_ast() {
1036 let mut analyzer = analyzer();
1037 let ast: SynFile = parse_quote! {
1038 #[derive(Serialize)]
1039 pub struct User {
1040 pub name: String,
1041 }
1042 };
1043 let path = Path::new("test.rs");
1044
1045 let result = analyzer.extract_type_from_ast(&ast, "User", path);
1046 assert!(result.is_some());
1047 let struct_info = result.unwrap();
1048 assert_eq!(struct_info.name, "User");
1049 assert_eq!(struct_info.fields.len(), 1);
1050 }
1051
1052 #[test]
1053 fn test_extract_enum_from_ast() {
1054 let mut analyzer = analyzer();
1055 let ast: SynFile = parse_quote! {
1056 #[derive(Serialize)]
1057 pub enum Status {
1058 Active,
1059 Inactive,
1060 }
1061 };
1062 let path = Path::new("test.rs");
1063
1064 let result = analyzer.extract_type_from_ast(&ast, "Status", path);
1065 assert!(result.is_some());
1066 let enum_info = result.unwrap();
1067 assert_eq!(enum_info.name, "Status");
1068 assert!(enum_info.is_enum);
1069 }
1070
1071 #[test]
1072 fn test_extract_type_not_found() {
1073 let mut analyzer = analyzer();
1074 let ast: SynFile = parse_quote! {
1075 #[derive(Serialize)]
1076 pub struct User {
1077 name: String,
1078 }
1079 };
1080 let path = Path::new("test.rs");
1081
1082 let result = analyzer.extract_type_from_ast(&ast, "Product", path);
1083 assert!(result.is_none());
1084 }
1085
1086 #[test]
1087 fn test_extract_type_without_serde() {
1088 let mut analyzer = analyzer();
1089 let ast: SynFile = parse_quote! {
1090 #[derive(Debug)]
1091 pub struct User {
1092 name: String,
1093 }
1094 };
1095 let path = Path::new("test.rs");
1096
1097 let result = analyzer.extract_type_from_ast(&ast, "User", path);
1098 assert!(result.is_none());
1099 }
1100 }
1101
1102 mod visualization {
1103 use super::*;
1104
1105 #[test]
1106 fn test_visualize_dependencies() {
1107 let analyzer = analyzer();
1108 let commands = vec![];
1109 let viz = analyzer.visualize_dependencies(&commands);
1110 assert!(viz.contains("Dependency Graph"));
1112 }
1113
1114 #[test]
1115 fn test_generate_dot_graph() {
1116 let analyzer = analyzer();
1117 let commands = vec![];
1118 let dot = analyzer.generate_dot_graph(&commands);
1119 assert!(dot.contains("digraph"));
1121 }
1122 }
1123}