Skip to main content

tauri_typegen/analysis/
mod.rs

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
23/// Analyzer that orchestrates all analysis sub-modules
24pub struct CommandAnalyzer {
25    /// AST cache for parsed files
26    ast_cache: AstCache,
27    /// Command parser for extracting Tauri commands
28    command_parser: CommandParser,
29    /// Channel parser for extracting channel parameters
30    channel_parser: ChannelParser,
31    /// Event parser for extracting event emissions
32    event_parser: EventParser,
33    /// Struct parser for extracting type definitions
34    struct_parser: StructParser,
35    /// Type resolver for Rust to TypeScript type mappings
36    type_resolver: TypeResolver,
37    /// Dependency graph for type resolution
38    dependency_graph: TypeDependencyGraph,
39    /// Discovered struct definitions
40    discovered_structs: HashMap<String, StructInfo>,
41    /// Discovered event emissions
42    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    /// Add custom type mappings from configuration
61    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    /// Analyze a complete project for Tauri commands and types
69    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    /// Analyze a complete project for Tauri commands and types with verbose output
77    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        // Single pass: Parse all Rust files and cache ASTs
83        self.ast_cache
84            .parse_and_cache_all_files(project_path, verbose)?;
85
86        // Extract commands from cached ASTs
87        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        // Process each file - using functional style where possible
92        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                // Extract commands from this file's AST
99                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                // Extract channels for each command
106                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                        // Collect type names from channel message types
116                        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                // Extract events from this file's AST
125                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                // Collect type names from command parameters and return types using functional style
132                file_commands.iter().for_each(|cmd| {
133                    cmd.parameters.iter().for_each(|param| {
134                        self.extract_type_names(&param.rust_type, &mut type_names_to_discover);
135                    });
136                    // Use the Rust return type (not TypeScript) to properly extract nested type names
137                    self.extract_type_names(&cmd.return_type, &mut type_names_to_discover);
138                });
139
140                // Collect type names from event payloads
141                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                // Build type definition index from this file
149                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        // Lazy type resolution: Resolve types on demand using dependency graph
158        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    /// Analyze a single file for Tauri commands (backward compatibility for tests)
189    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        // Parse and cache this single file - handle syntax errors gracefully
196        match self.ast_cache.parse_and_cache_file(&path_buf) {
197            Ok(_) => {
198                // Extract commands and events from the cached AST
199                if let Some(parsed_file) = self.ast_cache.get_cloned(&path_buf) {
200                    // Extract events
201                    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                    // Extract commands
209                    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                    // Extract channels for each command
216                    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                // Return empty vector for files with syntax errors (backward compatibility)
238                Ok(vec![])
239            }
240        }
241    }
242
243    /// Build an index of type definitions from an AST
244    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    /// Lazily resolve types using the dependency graph
267    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            // Skip if already resolved
276            if resolved_types.contains(&type_name)
277                || self.discovered_structs.contains_key(&type_name)
278            {
279                continue;
280            }
281
282            // Try to resolve this type
283            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                    // Find and parse the specific type from the cached AST
290                    if let Some(struct_info) = self.extract_type_from_ast(
291                        &parsed_file.ast,
292                        &type_name,
293                        file_path.as_path(),
294                    ) {
295                        // Collect dependencies of this type
296                        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                        // Add dependencies to the resolution queue
302                        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                        // Store the resolved type
312                        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    /// Extract a specific type from a cached AST
328    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    /// Extract type names from a Rust type string
365    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    /// Recursively extract type names from complex types
370    fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
371        let rust_type = rust_type.trim();
372
373        // Handle Result<T, E> - extract both T and E
374        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        // Handle Option<T> - extract T
390        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        // Handle Vec<T> - extract T
401        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        // Handle HashMap<K, V> and BTreeMap<K, V> - extract K and V
412        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        // Handle HashSet<T> and BTreeSet<T> - extract T
433        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        // Handle tuple types like (T, U, V)
449        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        // Handle references
458        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        // Check if this is a custom type name
465        if !rust_type.is_empty()
466            && !self.type_resolver.get_type_set().contains(rust_type)
467            && !rust_type.starts_with(char::is_lowercase) // Skip built-in types
468            && rust_type.chars().next().is_some_and(char::is_alphabetic)
469            && !rust_type.contains('<')
470        // Skip generic type names with parameters
471        {
472            type_names.insert(rust_type.to_string());
473        }
474    }
475
476    /// Get discovered structs
477    pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
478        &self.discovered_structs
479    }
480
481    /// Get discovered events
482    pub fn get_discovered_events(&self) -> &[EventInfo] {
483        &self.discovered_events
484    }
485
486    /// Get reference to the type resolver
487    pub fn get_type_resolver(&self) -> std::cell::RefCell<&TypeResolver> {
488        std::cell::RefCell::new(&self.type_resolver)
489    }
490
491    /// Get all discovered channels from all commands
492    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    /// Find a function by name in an AST
500    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    /// Get the dependency graph for visualization
516    pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
517        &self.dependency_graph
518    }
519
520    /// Sort types topologically to ensure dependencies are declared before being used
521    pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
522        self.dependency_graph.topological_sort_types(types)
523    }
524
525    /// Generate a text-based visualization of the dependency graph
526    pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
527        self.dependency_graph.visualize_dependencies(commands)
528    }
529
530    /// Generate a DOT graph visualization of the dependency graph
531    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            // String is a primitive, should only extract User
615            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            // Just verify it returns a RefCell
760            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            // Verify graph exists (check resolved types)
768            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            // Just verify it returns a string
1006            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            // Verify basic DOT format
1015            assert!(dot.contains("digraph"));
1016        }
1017    }
1018}