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(item_enum, file_path);
352                    }
353                }
354                _ => {}
355            }
356        }
357        None
358    }
359
360    /// Extract type names from a Rust type string
361    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    /// Recursively extract type names from complex types
366    fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
367        let rust_type = rust_type.trim();
368
369        // Handle Result<T, E> - extract both T and E
370        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        // Handle Option<T> - extract T
386        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        // Handle Vec<T> - extract T
397        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        // Handle HashMap<K, V> and BTreeMap<K, V> - extract K and V
408        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        // Handle HashSet<T> and BTreeSet<T> - extract T
429        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        // Handle tuple types like (T, U, V)
445        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        // Handle references
454        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        // Check if this is a custom type name
461        if !rust_type.is_empty()
462            && !self.type_resolver.get_type_set().contains(rust_type)
463            && !rust_type.starts_with(char::is_lowercase) // Skip built-in types
464            && rust_type.chars().next().is_some_and(char::is_alphabetic)
465            && !rust_type.contains('<')
466        // Skip generic type names with parameters
467        {
468            type_names.insert(rust_type.to_string());
469        }
470    }
471
472    /// Get discovered structs
473    pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
474        &self.discovered_structs
475    }
476
477    /// Get discovered events
478    pub fn get_discovered_events(&self) -> &[EventInfo] {
479        &self.discovered_events
480    }
481
482    /// Get reference to the type resolver
483    pub fn get_type_resolver(&self) -> std::cell::RefCell<&TypeResolver> {
484        std::cell::RefCell::new(&self.type_resolver)
485    }
486
487    /// Get all discovered channels from all commands
488    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    /// Find a function by name in an AST
496    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    /// Get the dependency graph for visualization
512    pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
513        &self.dependency_graph
514    }
515
516    /// Sort types topologically to ensure dependencies are declared before being used
517    pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
518        self.dependency_graph.topological_sort_types(types)
519    }
520
521    /// Generate a text-based visualization of the dependency graph
522    pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
523        self.dependency_graph.visualize_dependencies(commands)
524    }
525
526    /// Generate a DOT graph visualization of the dependency graph
527    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            // String is a primitive, should only extract User
611            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            // Just verify it returns a RefCell
756            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            // Verify graph exists (check resolved types)
764            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            // Just verify it returns a string
1002            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            // Verify basic DOT format
1011            assert!(dot.contains("digraph"));
1012        }
1013    }
1014}