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/// Comprehensive 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    /// Analyze a complete project for Tauri commands and types
61    pub fn analyze_project(
62        &mut self,
63        project_path: &str,
64    ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
65        self.analyze_project_with_verbose(project_path, false)
66    }
67
68    /// Analyze a complete project for Tauri commands and types with verbose output
69    pub fn analyze_project_with_verbose(
70        &mut self,
71        project_path: &str,
72        verbose: bool,
73    ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
74        // Single pass: Parse all Rust files and cache ASTs
75        self.ast_cache
76            .parse_and_cache_all_files(project_path, verbose)?;
77
78        // Extract commands from cached ASTs
79        let file_paths: Vec<PathBuf> = self.ast_cache.keys().cloned().collect();
80        let mut commands = Vec::new();
81        let mut type_names_to_discover = HashSet::new();
82
83        // Process each file - using functional style where possible
84        for file_path in file_paths {
85            if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
86                if verbose {
87                    println!("🔍 Analyzing file: {}", parsed_file.path.display());
88                }
89
90                // Extract commands from this file's AST
91                let mut file_commands = self.command_parser.extract_commands_from_ast(
92                    &parsed_file.ast,
93                    parsed_file.path.as_path(),
94                    &mut self.type_resolver,
95                )?;
96
97                // Extract channels for each command
98                for command in &mut file_commands {
99                    if let Some(func) = self.find_function_in_ast(&parsed_file.ast, &command.name) {
100                        let channels = self.channel_parser.extract_channels_from_command(
101                            func,
102                            &command.name,
103                            parsed_file.path.as_path(),
104                            &mut self.type_resolver,
105                        )?;
106
107                        // Collect type names from channel message types
108                        channels.iter().for_each(|ch| {
109                            self.extract_type_names(&ch.message_type, &mut type_names_to_discover);
110                        });
111
112                        command.channels = channels;
113                    }
114                }
115
116                // Extract events from this file's AST
117                let file_events = self.event_parser.extract_events_from_ast(
118                    &parsed_file.ast,
119                    parsed_file.path.as_path(),
120                    &mut self.type_resolver,
121                )?;
122
123                // Collect type names from command parameters and return types using functional style
124                file_commands.iter().for_each(|cmd| {
125                    cmd.parameters.iter().for_each(|param| {
126                        self.extract_type_names(&param.rust_type, &mut type_names_to_discover);
127                    });
128                    // Use the Rust return type (not TypeScript) to properly extract nested type names
129                    self.extract_type_names(&cmd.return_type, &mut type_names_to_discover);
130                });
131
132                // Collect type names from event payloads
133                file_events.iter().for_each(|event| {
134                    self.extract_type_names(&event.payload_type, &mut type_names_to_discover);
135                });
136
137                commands.extend(file_commands);
138                self.discovered_events.extend(file_events);
139
140                // Build type definition index from this file
141                self.index_type_definitions(&parsed_file.ast, parsed_file.path.as_path());
142            }
143        }
144
145        if verbose {
146            println!("🔍 Type names to discover: {:?}", type_names_to_discover);
147        }
148
149        // Lazy type resolution: Resolve types on demand using dependency graph
150        self.resolve_types_lazily(&type_names_to_discover)?;
151
152        if verbose {
153            println!(
154                "🏗️  Discovered {} structs total",
155                self.discovered_structs.len()
156            );
157            for (name, info) in &self.discovered_structs {
158                println!("  - {}: {} fields", name, info.fields.len());
159            }
160            println!(
161                "📡 Discovered {} events total",
162                self.discovered_events.len()
163            );
164            for event in &self.discovered_events {
165                println!("  - '{}': {}", event.event_name, event.payload_type);
166            }
167            let all_channels = self.get_all_discovered_channels(&commands);
168            println!("📞 Discovered {} channels total", all_channels.len());
169            for channel in &all_channels {
170                println!(
171                    "  - '{}' in {}: {}",
172                    channel.parameter_name, channel.command_name, channel.message_type
173                );
174            }
175        }
176
177        Ok(commands)
178    }
179
180    /// Analyze a single file for Tauri commands (backward compatibility for tests)
181    pub fn analyze_file(
182        &mut self,
183        file_path: &std::path::Path,
184    ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
185        let path_buf = file_path.to_path_buf();
186
187        // Parse and cache this single file - handle syntax errors gracefully
188        match self.ast_cache.parse_and_cache_file(&path_buf) {
189            Ok(_) => {
190                // Extract commands and events from the cached AST
191                if let Some(parsed_file) = self.ast_cache.get_cloned(&path_buf) {
192                    // Extract events
193                    let file_events = self.event_parser.extract_events_from_ast(
194                        &parsed_file.ast,
195                        path_buf.as_path(),
196                        &mut self.type_resolver,
197                    )?;
198                    self.discovered_events.extend(file_events);
199
200                    // Extract commands
201                    let mut commands = self.command_parser.extract_commands_from_ast(
202                        &parsed_file.ast,
203                        path_buf.as_path(),
204                        &mut self.type_resolver,
205                    )?;
206
207                    // Extract channels for each command
208                    for command in &mut commands {
209                        if let Some(func) =
210                            self.find_function_in_ast(&parsed_file.ast, &command.name)
211                        {
212                            let channels = self.channel_parser.extract_channels_from_command(
213                                func,
214                                &command.name,
215                                path_buf.as_path(),
216                                &mut self.type_resolver,
217                            )?;
218                            command.channels = channels;
219                        }
220                    }
221
222                    Ok(commands)
223                } else {
224                    Ok(vec![])
225                }
226            }
227            Err(_) => {
228                // Return empty vector for files with syntax errors (backward compatibility)
229                Ok(vec![])
230            }
231        }
232    }
233
234    /// Build an index of type definitions from an AST
235    fn index_type_definitions(&mut self, ast: &syn::File, file_path: &Path) {
236        for item in &ast.items {
237            match item {
238                syn::Item::Struct(item_struct) => {
239                    if self.struct_parser.should_include_struct(item_struct) {
240                        let struct_name = item_struct.ident.to_string();
241                        self.dependency_graph
242                            .add_type_definition(struct_name, file_path.to_path_buf());
243                    }
244                }
245                syn::Item::Enum(item_enum) => {
246                    if self.struct_parser.should_include_enum(item_enum) {
247                        let enum_name = item_enum.ident.to_string();
248                        self.dependency_graph
249                            .add_type_definition(enum_name, file_path.to_path_buf());
250                    }
251                }
252                _ => {}
253            }
254        }
255    }
256
257    /// Lazily resolve types using the dependency graph
258    fn resolve_types_lazily(
259        &mut self,
260        initial_types: &HashSet<String>,
261    ) -> Result<(), Box<dyn std::error::Error>> {
262        let mut types_to_resolve: Vec<String> = initial_types.iter().cloned().collect();
263        let mut resolved_types = HashSet::new();
264
265        while let Some(type_name) = types_to_resolve.pop() {
266            // Skip if already resolved
267            if resolved_types.contains(&type_name)
268                || self.discovered_structs.contains_key(&type_name)
269            {
270                continue;
271            }
272
273            // Try to resolve this type
274            if let Some(file_path) = self
275                .dependency_graph
276                .get_type_definition_path(&type_name)
277                .cloned()
278            {
279                if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
280                    // Find and parse the specific type from the cached AST
281                    if let Some(struct_info) = self.extract_type_from_ast(
282                        &parsed_file.ast,
283                        &type_name,
284                        file_path.as_path(),
285                    ) {
286                        // Collect dependencies of this type
287                        let mut type_dependencies = HashSet::new();
288                        for field in &struct_info.fields {
289                            self.extract_type_names(&field.rust_type, &mut type_dependencies);
290                        }
291
292                        // Add dependencies to the resolution queue
293                        for dep_type in &type_dependencies {
294                            if !resolved_types.contains(dep_type)
295                                && !self.discovered_structs.contains_key(dep_type)
296                                && self.dependency_graph.has_type_definition(dep_type)
297                            {
298                                types_to_resolve.push(dep_type.clone());
299                            }
300                        }
301
302                        // Store the resolved type
303                        self.dependency_graph
304                            .add_dependencies(type_name.clone(), type_dependencies.clone());
305                        self.dependency_graph
306                            .add_resolved_type(type_name.clone(), struct_info.clone());
307                        self.discovered_structs
308                            .insert(type_name.clone(), struct_info);
309                        resolved_types.insert(type_name);
310                    }
311                }
312            }
313        }
314
315        Ok(())
316    }
317
318    /// Extract a specific type from a cached AST
319    fn extract_type_from_ast(
320        &mut self,
321        ast: &syn::File,
322        type_name: &str,
323        file_path: &Path,
324    ) -> Option<StructInfo> {
325        for item in &ast.items {
326            match item {
327                syn::Item::Struct(item_struct) => {
328                    if item_struct.ident == type_name
329                        && self.struct_parser.should_include_struct(item_struct)
330                    {
331                        return self.struct_parser.parse_struct(
332                            item_struct,
333                            file_path,
334                            &mut self.type_resolver,
335                        );
336                    }
337                }
338                syn::Item::Enum(item_enum) => {
339                    if item_enum.ident == type_name
340                        && self.struct_parser.should_include_enum(item_enum)
341                    {
342                        return self.struct_parser.parse_enum(
343                            item_enum,
344                            file_path,
345                            &mut self.type_resolver,
346                        );
347                    }
348                }
349                _ => {}
350            }
351        }
352        None
353    }
354
355    /// Extract type names from a Rust type string
356    pub fn extract_type_names(&self, rust_type: &str, type_names: &mut HashSet<String>) {
357        self.extract_type_names_recursive(rust_type, type_names);
358    }
359
360    /// Recursively extract type names from complex types
361    fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
362        let rust_type = rust_type.trim();
363
364        // Handle Result<T, E> - extract both T and E
365        if rust_type.starts_with("Result<") {
366            if let Some(inner) = rust_type
367                .strip_prefix("Result<")
368                .and_then(|s| s.strip_suffix(">"))
369            {
370                if let Some(comma_pos) = inner.find(',') {
371                    let ok_type = inner[..comma_pos].trim();
372                    let err_type = inner[comma_pos + 1..].trim();
373                    self.extract_type_names_recursive(ok_type, type_names);
374                    self.extract_type_names_recursive(err_type, type_names);
375                }
376            }
377            return;
378        }
379
380        // Handle Option<T> - extract T
381        if rust_type.starts_with("Option<") {
382            if let Some(inner) = rust_type
383                .strip_prefix("Option<")
384                .and_then(|s| s.strip_suffix(">"))
385            {
386                self.extract_type_names_recursive(inner, type_names);
387            }
388            return;
389        }
390
391        // Handle Vec<T> - extract T
392        if rust_type.starts_with("Vec<") {
393            if let Some(inner) = rust_type
394                .strip_prefix("Vec<")
395                .and_then(|s| s.strip_suffix(">"))
396            {
397                self.extract_type_names_recursive(inner, type_names);
398            }
399            return;
400        }
401
402        // Handle HashMap<K, V> and BTreeMap<K, V> - extract K and V
403        if rust_type.starts_with("HashMap<") || rust_type.starts_with("BTreeMap<") {
404            let prefix = if rust_type.starts_with("HashMap<") {
405                "HashMap<"
406            } else {
407                "BTreeMap<"
408            };
409            if let Some(inner) = rust_type
410                .strip_prefix(prefix)
411                .and_then(|s| s.strip_suffix(">"))
412            {
413                if let Some(comma_pos) = inner.find(',') {
414                    let key_type = inner[..comma_pos].trim();
415                    let value_type = inner[comma_pos + 1..].trim();
416                    self.extract_type_names_recursive(key_type, type_names);
417                    self.extract_type_names_recursive(value_type, type_names);
418                }
419            }
420            return;
421        }
422
423        // Handle HashSet<T> and BTreeSet<T> - extract T
424        if rust_type.starts_with("HashSet<") || rust_type.starts_with("BTreeSet<") {
425            let prefix = if rust_type.starts_with("HashSet<") {
426                "HashSet<"
427            } else {
428                "BTreeSet<"
429            };
430            if let Some(inner) = rust_type
431                .strip_prefix(prefix)
432                .and_then(|s| s.strip_suffix(">"))
433            {
434                self.extract_type_names_recursive(inner, type_names);
435            }
436            return;
437        }
438
439        // Handle tuple types like (T, U, V)
440        if rust_type.starts_with('(') && rust_type.ends_with(')') && rust_type != "()" {
441            let inner = &rust_type[1..rust_type.len() - 1];
442            for part in inner.split(',') {
443                self.extract_type_names_recursive(part.trim(), type_names);
444            }
445            return;
446        }
447
448        // Handle references
449        if rust_type.starts_with('&') {
450            let without_ref = rust_type.trim_start_matches('&');
451            self.extract_type_names_recursive(without_ref, type_names);
452            return;
453        }
454
455        // Check if this is a custom type name
456        if !rust_type.is_empty()
457            && !self.type_resolver.get_type_mappings().contains_key(rust_type)
458            && !rust_type.starts_with(char::is_lowercase) // Skip built-in types
459            && rust_type.chars().next().is_some_and(char::is_alphabetic)
460            && !rust_type.contains('<')
461        // Skip generic type names with parameters
462        {
463            type_names.insert(rust_type.to_string());
464        }
465    }
466
467    /// Get discovered structs
468    pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
469        &self.discovered_structs
470    }
471
472    /// Get discovered events
473    pub fn get_discovered_events(&self) -> &[EventInfo] {
474        &self.discovered_events
475    }
476
477    /// Get all discovered channels from all commands
478    pub fn get_all_discovered_channels(&self, commands: &[CommandInfo]) -> Vec<ChannelInfo> {
479        commands
480            .iter()
481            .flat_map(|cmd| cmd.channels.clone())
482            .collect()
483    }
484
485    /// Find a function by name in an AST
486    fn find_function_in_ast<'a>(
487        &self,
488        ast: &'a syn::File,
489        function_name: &str,
490    ) -> Option<&'a syn::ItemFn> {
491        for item in &ast.items {
492            if let syn::Item::Fn(func) = item {
493                if func.sig.ident == function_name {
494                    return Some(func);
495                }
496            }
497        }
498        None
499    }
500
501    /// Get the dependency graph for visualization
502    pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
503        &self.dependency_graph
504    }
505
506    /// Sort types topologically to ensure dependencies are declared before being used
507    pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
508        self.dependency_graph.topological_sort_types(types)
509    }
510
511    /// Generate a text-based visualization of the dependency graph
512    pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
513        self.dependency_graph.visualize_dependencies(commands)
514    }
515
516    /// Generate a DOT graph visualization of the dependency graph
517    pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
518        self.dependency_graph.generate_dot_graph(commands)
519    }
520
521    /// Map a Rust type to its TypeScript equivalent
522    pub fn map_rust_type_to_typescript(&mut self, rust_type: &str) -> String {
523        self.type_resolver.map_rust_type_to_typescript(rust_type)
524    }
525}
526
527impl Default for CommandAnalyzer {
528    fn default() -> Self {
529        Self::new()
530    }
531}