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(
352 item_enum,
353 file_path,
354 &mut self.type_resolver,
355 );
356 }
357 }
358 _ => {}
359 }
360 }
361 None
362 }
363
364 pub fn extract_type_names(&self, rust_type: &str, type_names: &mut HashSet<String>) {
366 self.extract_type_names_recursive(rust_type, type_names);
367 }
368
369 fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
371 let rust_type = rust_type.trim();
372
373 if rust_type.starts_with("Result<") {
375 if let Some(inner) = rust_type
376 .strip_prefix("Result<")
377 .and_then(|s| s.strip_suffix(">"))
378 {
379 if let Some(comma_pos) = inner.find(',') {
380 let ok_type = inner[..comma_pos].trim();
381 let err_type = inner[comma_pos + 1..].trim();
382 self.extract_type_names_recursive(ok_type, type_names);
383 self.extract_type_names_recursive(err_type, type_names);
384 }
385 }
386 return;
387 }
388
389 if rust_type.starts_with("Option<") {
391 if let Some(inner) = rust_type
392 .strip_prefix("Option<")
393 .and_then(|s| s.strip_suffix(">"))
394 {
395 self.extract_type_names_recursive(inner, type_names);
396 }
397 return;
398 }
399
400 if rust_type.starts_with("Vec<") {
402 if let Some(inner) = rust_type
403 .strip_prefix("Vec<")
404 .and_then(|s| s.strip_suffix(">"))
405 {
406 self.extract_type_names_recursive(inner, type_names);
407 }
408 return;
409 }
410
411 if rust_type.starts_with("HashMap<") || rust_type.starts_with("BTreeMap<") {
413 let prefix = if rust_type.starts_with("HashMap<") {
414 "HashMap<"
415 } else {
416 "BTreeMap<"
417 };
418 if let Some(inner) = rust_type
419 .strip_prefix(prefix)
420 .and_then(|s| s.strip_suffix(">"))
421 {
422 if let Some(comma_pos) = inner.find(',') {
423 let key_type = inner[..comma_pos].trim();
424 let value_type = inner[comma_pos + 1..].trim();
425 self.extract_type_names_recursive(key_type, type_names);
426 self.extract_type_names_recursive(value_type, type_names);
427 }
428 }
429 return;
430 }
431
432 if rust_type.starts_with("HashSet<") || rust_type.starts_with("BTreeSet<") {
434 let prefix = if rust_type.starts_with("HashSet<") {
435 "HashSet<"
436 } else {
437 "BTreeSet<"
438 };
439 if let Some(inner) = rust_type
440 .strip_prefix(prefix)
441 .and_then(|s| s.strip_suffix(">"))
442 {
443 self.extract_type_names_recursive(inner, type_names);
444 }
445 return;
446 }
447
448 if rust_type.starts_with('(') && rust_type.ends_with(')') && rust_type != "()" {
450 let inner = &rust_type[1..rust_type.len() - 1];
451 for part in inner.split(',') {
452 self.extract_type_names_recursive(part.trim(), type_names);
453 }
454 return;
455 }
456
457 if rust_type.starts_with('&') {
459 let without_ref = rust_type.trim_start_matches('&');
460 self.extract_type_names_recursive(without_ref, type_names);
461 return;
462 }
463
464 if !rust_type.is_empty()
466 && !self.type_resolver.get_type_set().contains(rust_type)
467 && !rust_type.starts_with(char::is_lowercase) && rust_type.chars().next().is_some_and(char::is_alphabetic)
469 && !rust_type.contains('<')
470 {
472 type_names.insert(rust_type.to_string());
473 }
474 }
475
476 pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
478 &self.discovered_structs
479 }
480
481 pub fn get_discovered_events(&self) -> &[EventInfo] {
483 &self.discovered_events
484 }
485
486 pub fn get_type_resolver(&self) -> std::cell::RefCell<&TypeResolver> {
488 std::cell::RefCell::new(&self.type_resolver)
489 }
490
491 pub fn get_all_discovered_channels(&self, commands: &[CommandInfo]) -> Vec<ChannelInfo> {
493 commands
494 .iter()
495 .flat_map(|cmd| cmd.channels.clone())
496 .collect()
497 }
498
499 fn find_function_in_ast<'a>(
501 &self,
502 ast: &'a syn::File,
503 function_name: &str,
504 ) -> Option<&'a syn::ItemFn> {
505 for item in &ast.items {
506 if let syn::Item::Fn(func) = item {
507 if func.sig.ident == function_name {
508 return Some(func);
509 }
510 }
511 }
512 None
513 }
514
515 pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
517 &self.dependency_graph
518 }
519
520 pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
522 self.dependency_graph.topological_sort_types(types)
523 }
524
525 pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
527 self.dependency_graph.visualize_dependencies(commands)
528 }
529
530 pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
532 self.dependency_graph.generate_dot_graph(commands)
533 }
534}
535
536impl Default for CommandAnalyzer {
537 fn default() -> Self {
538 Self::new()
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545 use std::collections::HashSet;
546
547 fn analyzer() -> CommandAnalyzer {
548 CommandAnalyzer::new()
549 }
550
551 mod initialization {
552 use super::*;
553
554 #[test]
555 fn test_new_creates_analyzer() {
556 let analyzer = CommandAnalyzer::new();
557 assert!(analyzer.get_discovered_structs().is_empty());
558 assert!(analyzer.get_discovered_events().is_empty());
559 }
560
561 #[test]
562 fn test_default_creates_analyzer() {
563 let analyzer = CommandAnalyzer::default();
564 assert!(analyzer.get_discovered_structs().is_empty());
565 assert!(analyzer.get_discovered_events().is_empty());
566 }
567 }
568
569 mod type_name_extraction {
570 use super::*;
571
572 #[test]
573 fn test_extract_simple_type() {
574 let analyzer = analyzer();
575 let mut types = HashSet::new();
576 analyzer.extract_type_names("User", &mut types);
577 assert_eq!(types.len(), 1);
578 assert!(types.contains("User"));
579 }
580
581 #[test]
582 fn test_extract_option_type() {
583 let analyzer = analyzer();
584 let mut types = HashSet::new();
585 analyzer.extract_type_names("Option<User>", &mut types);
586 assert_eq!(types.len(), 1);
587 assert!(types.contains("User"));
588 }
589
590 #[test]
591 fn test_extract_vec_type() {
592 let analyzer = analyzer();
593 let mut types = HashSet::new();
594 analyzer.extract_type_names("Vec<Product>", &mut types);
595 assert_eq!(types.len(), 1);
596 assert!(types.contains("Product"));
597 }
598
599 #[test]
600 fn test_extract_result_type() {
601 let analyzer = analyzer();
602 let mut types = HashSet::new();
603 analyzer.extract_type_names("Result<User, AppError>", &mut types);
604 assert_eq!(types.len(), 2);
605 assert!(types.contains("User"));
606 assert!(types.contains("AppError"));
607 }
608
609 #[test]
610 fn test_extract_hashmap_type() {
611 let analyzer = analyzer();
612 let mut types = HashSet::new();
613 analyzer.extract_type_names("HashMap<String, User>", &mut types);
614 assert_eq!(types.len(), 1);
616 assert!(types.contains("User"));
617 }
618
619 #[test]
620 fn test_extract_btreemap_type() {
621 let analyzer = analyzer();
622 let mut types = HashSet::new();
623 analyzer.extract_type_names("BTreeMap<UserId, Profile>", &mut types);
624 assert_eq!(types.len(), 2);
625 assert!(types.contains("UserId"));
626 assert!(types.contains("Profile"));
627 }
628
629 #[test]
630 fn test_extract_hashset_type() {
631 let analyzer = analyzer();
632 let mut types = HashSet::new();
633 analyzer.extract_type_names("HashSet<User>", &mut types);
634 assert_eq!(types.len(), 1);
635 assert!(types.contains("User"));
636 }
637
638 #[test]
639 fn test_extract_btreeset_type() {
640 let analyzer = analyzer();
641 let mut types = HashSet::new();
642 analyzer.extract_type_names("BTreeSet<Tag>", &mut types);
643 assert_eq!(types.len(), 1);
644 assert!(types.contains("Tag"));
645 }
646
647 #[test]
648 fn test_extract_tuple_type() {
649 let analyzer = analyzer();
650 let mut types = HashSet::new();
651 analyzer.extract_type_names("(User, Product, Order)", &mut types);
652 assert_eq!(types.len(), 3);
653 assert!(types.contains("User"));
654 assert!(types.contains("Product"));
655 assert!(types.contains("Order"));
656 }
657
658 #[test]
659 fn test_extract_reference_type() {
660 let analyzer = analyzer();
661 let mut types = HashSet::new();
662 analyzer.extract_type_names("&User", &mut types);
663 assert_eq!(types.len(), 1);
664 assert!(types.contains("User"));
665 }
666
667 #[test]
668 fn test_extract_nested_types() {
669 let analyzer = analyzer();
670 let mut types = HashSet::new();
671 analyzer.extract_type_names("Vec<Option<User>>", &mut types);
672 assert_eq!(types.len(), 1);
673 assert!(types.contains("User"));
674 }
675
676 #[test]
677 fn test_extract_deeply_nested_types() {
678 let analyzer = analyzer();
679 let mut types = HashSet::new();
680 analyzer.extract_type_names("HashMap<String, Vec<Option<Product>>>", &mut types);
681 assert_eq!(types.len(), 1);
682 assert!(types.contains("Product"));
683 }
684
685 #[test]
686 fn test_skips_primitive_types() {
687 let analyzer = analyzer();
688 let mut types = HashSet::new();
689 analyzer.extract_type_names("String", &mut types);
690 assert_eq!(types.len(), 0);
691 }
692
693 #[test]
694 fn test_skips_built_in_types() {
695 let analyzer = analyzer();
696 let mut types = HashSet::new();
697 analyzer.extract_type_names("i32", &mut types);
698 assert_eq!(types.len(), 0);
699 }
700
701 #[test]
702 fn test_skips_empty_type() {
703 let analyzer = analyzer();
704 let mut types = HashSet::new();
705 analyzer.extract_type_names("", &mut types);
706 assert_eq!(types.len(), 0);
707 }
708
709 #[test]
710 fn test_skips_unit_type() {
711 let analyzer = analyzer();
712 let mut types = HashSet::new();
713 analyzer.extract_type_names("()", &mut types);
714 assert_eq!(types.len(), 0);
715 }
716
717 #[test]
718 fn test_multiple_calls_accumulate() {
719 let analyzer = analyzer();
720 let mut types = HashSet::new();
721 analyzer.extract_type_names("User", &mut types);
722 analyzer.extract_type_names("Product", &mut types);
723 assert_eq!(types.len(), 2);
724 assert!(types.contains("User"));
725 assert!(types.contains("Product"));
726 }
727
728 #[test]
729 fn test_duplicate_types_deduped() {
730 let analyzer = analyzer();
731 let mut types = HashSet::new();
732 analyzer.extract_type_names("User", &mut types);
733 analyzer.extract_type_names("User", &mut types);
734 assert_eq!(types.len(), 1);
735 }
736 }
737
738 mod getters {
739 use super::*;
740
741 #[test]
742 fn test_get_discovered_structs_empty() {
743 let analyzer = analyzer();
744 let structs = analyzer.get_discovered_structs();
745 assert!(structs.is_empty());
746 }
747
748 #[test]
749 fn test_get_discovered_events_empty() {
750 let analyzer = analyzer();
751 let events = analyzer.get_discovered_events();
752 assert!(events.is_empty());
753 }
754
755 #[test]
756 fn test_get_type_resolver() {
757 let analyzer = analyzer();
758 let resolver = analyzer.get_type_resolver();
759 assert!(!resolver.borrow().get_type_set().is_empty());
761 }
762
763 #[test]
764 fn test_get_dependency_graph() {
765 let analyzer = analyzer();
766 let graph = analyzer.get_dependency_graph();
767 assert!(graph.get_resolved_types().is_empty());
769 }
770
771 #[test]
772 fn test_get_all_discovered_channels_empty() {
773 let analyzer = analyzer();
774 let commands = vec![];
775 let channels = analyzer.get_all_discovered_channels(&commands);
776 assert!(channels.is_empty());
777 }
778
779 #[test]
780 fn test_get_all_discovered_channels_with_commands() {
781 let analyzer = analyzer();
782 let command = CommandInfo::new_for_test(
783 "test_cmd",
784 "test.rs",
785 1,
786 vec![],
787 "void",
788 false,
789 vec![
790 ChannelInfo::new_for_test("ch1", "Message1", "test_cmd", "test.rs", 10),
791 ChannelInfo::new_for_test("ch2", "Message2", "test_cmd", "test.rs", 20),
792 ],
793 );
794
795 let commands = vec![command];
796 let channels = analyzer.get_all_discovered_channels(&commands);
797 assert_eq!(channels.len(), 2);
798 }
799 }
800
801 mod topological_sort {
802 use super::*;
803
804 #[test]
805 fn test_topological_sort_empty() {
806 let analyzer = analyzer();
807 let types = HashSet::new();
808 let sorted = analyzer.topological_sort_types(&types);
809 assert!(sorted.is_empty());
810 }
811
812 #[test]
813 fn test_topological_sort_single_type() {
814 let mut analyzer = analyzer();
815 let path = PathBuf::from("test.rs");
816 analyzer
817 .dependency_graph
818 .add_type_definition("User".to_string(), path);
819
820 let mut types = HashSet::new();
821 types.insert("User".to_string());
822
823 let sorted = analyzer.topological_sort_types(&types);
824 assert_eq!(sorted.len(), 1);
825 assert_eq!(sorted[0], "User");
826 }
827 }
828
829 mod ast_helpers {
830 use super::*;
831 use syn::{parse_quote, File as SynFile};
832
833 #[test]
834 fn test_find_function_in_ast() {
835 let analyzer = analyzer();
836 let ast: SynFile = parse_quote! {
837 #[tauri::command]
838 fn my_command() -> String {
839 "test".to_string()
840 }
841
842 fn other_function() {}
843 };
844
845 let result = analyzer.find_function_in_ast(&ast, "my_command");
846 assert!(result.is_some());
847 assert_eq!(result.unwrap().sig.ident, "my_command");
848 }
849
850 #[test]
851 fn test_find_function_in_ast_not_found() {
852 let analyzer = analyzer();
853 let ast: SynFile = parse_quote! {
854 fn my_command() {}
855 };
856
857 let result = analyzer.find_function_in_ast(&ast, "non_existent");
858 assert!(result.is_none());
859 }
860
861 #[test]
862 fn test_find_function_in_ast_empty() {
863 let analyzer = analyzer();
864 let ast: SynFile = parse_quote! {};
865
866 let result = analyzer.find_function_in_ast(&ast, "any_function");
867 assert!(result.is_none());
868 }
869 }
870
871 mod index_type_definitions {
872 use super::*;
873 use syn::{parse_quote, File as SynFile};
874
875 #[test]
876 fn test_index_struct() {
877 let mut analyzer = analyzer();
878 let ast: SynFile = parse_quote! {
879 #[derive(Serialize)]
880 pub struct User {
881 name: String,
882 }
883 };
884 let path = Path::new("test.rs");
885
886 analyzer.index_type_definitions(&ast, path);
887
888 assert!(analyzer.dependency_graph.has_type_definition("User"));
889 }
890
891 #[test]
892 fn test_index_enum() {
893 let mut analyzer = analyzer();
894 let ast: SynFile = parse_quote! {
895 #[derive(Serialize)]
896 pub enum Status {
897 Active,
898 Inactive,
899 }
900 };
901 let path = Path::new("test.rs");
902
903 analyzer.index_type_definitions(&ast, path);
904
905 assert!(analyzer.dependency_graph.has_type_definition("Status"));
906 }
907
908 #[test]
909 fn test_skips_non_serde_types() {
910 let mut analyzer = analyzer();
911 let ast: SynFile = parse_quote! {
912 #[derive(Debug, Clone)]
913 pub struct User {
914 name: String,
915 }
916 };
917 let path = Path::new("test.rs");
918
919 analyzer.index_type_definitions(&ast, path);
920
921 assert!(!analyzer.dependency_graph.has_type_definition("User"));
922 }
923 }
924
925 mod extract_type_from_ast {
926 use super::*;
927 use syn::{parse_quote, File as SynFile};
928
929 #[test]
930 fn test_extract_struct_from_ast() {
931 let mut analyzer = analyzer();
932 let ast: SynFile = parse_quote! {
933 #[derive(Serialize)]
934 pub struct User {
935 pub name: String,
936 }
937 };
938 let path = Path::new("test.rs");
939
940 let result = analyzer.extract_type_from_ast(&ast, "User", path);
941 assert!(result.is_some());
942 let struct_info = result.unwrap();
943 assert_eq!(struct_info.name, "User");
944 assert_eq!(struct_info.fields.len(), 1);
945 }
946
947 #[test]
948 fn test_extract_enum_from_ast() {
949 let mut analyzer = analyzer();
950 let ast: SynFile = parse_quote! {
951 #[derive(Serialize)]
952 pub enum Status {
953 Active,
954 Inactive,
955 }
956 };
957 let path = Path::new("test.rs");
958
959 let result = analyzer.extract_type_from_ast(&ast, "Status", path);
960 assert!(result.is_some());
961 let enum_info = result.unwrap();
962 assert_eq!(enum_info.name, "Status");
963 assert!(enum_info.is_enum);
964 }
965
966 #[test]
967 fn test_extract_type_not_found() {
968 let mut analyzer = analyzer();
969 let ast: SynFile = parse_quote! {
970 #[derive(Serialize)]
971 pub struct User {
972 name: String,
973 }
974 };
975 let path = Path::new("test.rs");
976
977 let result = analyzer.extract_type_from_ast(&ast, "Product", path);
978 assert!(result.is_none());
979 }
980
981 #[test]
982 fn test_extract_type_without_serde() {
983 let mut analyzer = analyzer();
984 let ast: SynFile = parse_quote! {
985 #[derive(Debug)]
986 pub struct User {
987 name: String,
988 }
989 };
990 let path = Path::new("test.rs");
991
992 let result = analyzer.extract_type_from_ast(&ast, "User", path);
993 assert!(result.is_none());
994 }
995 }
996
997 mod visualization {
998 use super::*;
999
1000 #[test]
1001 fn test_visualize_dependencies() {
1002 let analyzer = analyzer();
1003 let commands = vec![];
1004 let viz = analyzer.visualize_dependencies(&commands);
1005 assert!(viz.contains("Dependency Graph"));
1007 }
1008
1009 #[test]
1010 fn test_generate_dot_graph() {
1011 let analyzer = analyzer();
1012 let commands = vec![];
1013 let dot = analyzer.generate_dot_graph(&commands);
1014 assert!(dot.contains("digraph"));
1016 }
1017 }
1018}