tauri_typegen/analysis/
mod.rs

1pub mod ast_cache;
2pub mod command_parser;
3pub mod dependency_graph;
4pub mod struct_parser;
5pub mod type_resolver;
6pub mod validator_parser;
7
8use crate::models::{CommandInfo, StructInfo};
9use std::collections::{HashMap, HashSet};
10use std::path::{Path, PathBuf};
11
12use ast_cache::AstCache;
13use command_parser::CommandParser;
14use dependency_graph::TypeDependencyGraph;
15use struct_parser::StructParser;
16use type_resolver::TypeResolver;
17
18/// Comprehensive analyzer that orchestrates all analysis sub-modules
19pub struct CommandAnalyzer {
20    /// AST cache for parsed files
21    ast_cache: AstCache,
22    /// Command parser for extracting Tauri commands
23    command_parser: CommandParser,
24    /// Struct parser for extracting type definitions
25    struct_parser: StructParser,
26    /// Type resolver for Rust to TypeScript type mappings
27    type_resolver: TypeResolver,
28    /// Dependency graph for type resolution
29    dependency_graph: TypeDependencyGraph,
30    /// Discovered struct definitions
31    discovered_structs: HashMap<String, StructInfo>,
32}
33
34impl CommandAnalyzer {
35    pub fn new() -> Self {
36        Self {
37            ast_cache: AstCache::new(),
38            command_parser: CommandParser::new(),
39            struct_parser: StructParser::new(),
40            type_resolver: TypeResolver::new(),
41            dependency_graph: TypeDependencyGraph::new(),
42            discovered_structs: HashMap::new(),
43        }
44    }
45
46    /// Analyze a complete project for Tauri commands and types
47    pub fn analyze_project(
48        &mut self,
49        project_path: &str,
50    ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
51        self.analyze_project_with_verbose(project_path, false)
52    }
53
54    /// Analyze a complete project for Tauri commands and types with verbose output
55    pub fn analyze_project_with_verbose(
56        &mut self,
57        project_path: &str,
58        verbose: bool,
59    ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
60        // Single pass: Parse all Rust files and cache ASTs
61        self.ast_cache
62            .parse_and_cache_all_files(project_path, verbose)?;
63
64        // Extract commands from cached ASTs
65        let file_paths: Vec<PathBuf> = self.ast_cache.keys().cloned().collect();
66        let mut commands = Vec::new();
67        let mut type_names_to_discover = HashSet::new();
68
69        // Process each file - using functional style where possible
70        for file_path in file_paths {
71            if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
72                if verbose {
73                    println!("🔍 Analyzing file: {}", parsed_file.path.display());
74                }
75
76                // Extract commands from this file's AST
77                let file_commands = self.command_parser.extract_commands_from_ast(
78                    &parsed_file.ast,
79                    parsed_file.path.as_path(),
80                    &mut self.type_resolver,
81                )?;
82
83                // Collect type names from command parameters and return types using functional style
84                file_commands.iter().for_each(|cmd| {
85                    cmd.parameters.iter().for_each(|param| {
86                        self.extract_type_names(&param.rust_type, &mut type_names_to_discover);
87                    });
88                    self.extract_type_names(&cmd.return_type, &mut type_names_to_discover);
89                });
90
91                commands.extend(file_commands);
92
93                // Build type definition index from this file
94                self.index_type_definitions(&parsed_file.ast, parsed_file.path.as_path());
95            }
96        }
97
98        if verbose {
99            println!("🔍 Type names to discover: {:?}", type_names_to_discover);
100        }
101
102        // Lazy type resolution: Resolve types on demand using dependency graph
103        self.resolve_types_lazily(&type_names_to_discover)?;
104
105        if verbose {
106            println!(
107                "🏗️  Discovered {} structs total",
108                self.discovered_structs.len()
109            );
110            for (name, info) in &self.discovered_structs {
111                println!("  - {}: {} fields", name, info.fields.len());
112            }
113        }
114
115        Ok(commands)
116    }
117
118    /// Analyze a single file for Tauri commands (backward compatibility for tests)
119    pub fn analyze_file(
120        &mut self,
121        file_path: &std::path::Path,
122    ) -> Result<Vec<CommandInfo>, Box<dyn std::error::Error>> {
123        let path_buf = file_path.to_path_buf();
124
125        // Parse and cache this single file - handle syntax errors gracefully
126        match self.ast_cache.parse_and_cache_file(&path_buf) {
127            Ok(_) => {
128                // Extract commands from the cached AST
129                if let Some(parsed_file) = self.ast_cache.get_cloned(&path_buf) {
130                    self.command_parser.extract_commands_from_ast(
131                        &parsed_file.ast,
132                        path_buf.as_path(),
133                        &mut self.type_resolver,
134                    )
135                } else {
136                    Ok(vec![])
137                }
138            }
139            Err(_) => {
140                // Return empty vector for files with syntax errors (backward compatibility)
141                Ok(vec![])
142            }
143        }
144    }
145
146    /// Build an index of type definitions from an AST
147    fn index_type_definitions(&mut self, ast: &syn::File, file_path: &Path) {
148        for item in &ast.items {
149            match item {
150                syn::Item::Struct(item_struct) => {
151                    if self.struct_parser.should_include_struct(item_struct) {
152                        let struct_name = item_struct.ident.to_string();
153                        self.dependency_graph
154                            .add_type_definition(struct_name, file_path.to_path_buf());
155                    }
156                }
157                syn::Item::Enum(item_enum) => {
158                    if self.struct_parser.should_include_enum(item_enum) {
159                        let enum_name = item_enum.ident.to_string();
160                        self.dependency_graph
161                            .add_type_definition(enum_name, file_path.to_path_buf());
162                    }
163                }
164                _ => {}
165            }
166        }
167    }
168
169    /// Lazily resolve types using the dependency graph
170    fn resolve_types_lazily(
171        &mut self,
172        initial_types: &HashSet<String>,
173    ) -> Result<(), Box<dyn std::error::Error>> {
174        let mut types_to_resolve: Vec<String> = initial_types.iter().cloned().collect();
175        let mut resolved_types = HashSet::new();
176
177        while let Some(type_name) = types_to_resolve.pop() {
178            // Skip if already resolved
179            if resolved_types.contains(&type_name)
180                || self.discovered_structs.contains_key(&type_name)
181            {
182                continue;
183            }
184
185            // Try to resolve this type
186            if let Some(file_path) = self
187                .dependency_graph
188                .get_type_definition_path(&type_name)
189                .cloned()
190            {
191                if let Some(parsed_file) = self.ast_cache.get_cloned(&file_path) {
192                    // Find and parse the specific type from the cached AST
193                    if let Some(struct_info) = self.extract_type_from_ast(
194                        &parsed_file.ast,
195                        &type_name,
196                        file_path.as_path(),
197                    ) {
198                        // Collect dependencies of this type
199                        let mut type_dependencies = HashSet::new();
200                        for field in &struct_info.fields {
201                            self.extract_type_names(&field.rust_type, &mut type_dependencies);
202                        }
203
204                        // Add dependencies to the resolution queue
205                        for dep_type in &type_dependencies {
206                            if !resolved_types.contains(dep_type)
207                                && !self.discovered_structs.contains_key(dep_type)
208                                && self.dependency_graph.has_type_definition(dep_type)
209                            {
210                                types_to_resolve.push(dep_type.clone());
211                            }
212                        }
213
214                        // Store the resolved type
215                        self.dependency_graph
216                            .add_dependencies(type_name.clone(), type_dependencies.clone());
217                        self.dependency_graph
218                            .add_resolved_type(type_name.clone(), struct_info.clone());
219                        self.discovered_structs
220                            .insert(type_name.clone(), struct_info);
221                        resolved_types.insert(type_name);
222                    }
223                }
224            }
225        }
226
227        Ok(())
228    }
229
230    /// Extract a specific type from a cached AST
231    fn extract_type_from_ast(
232        &mut self,
233        ast: &syn::File,
234        type_name: &str,
235        file_path: &Path,
236    ) -> Option<StructInfo> {
237        for item in &ast.items {
238            match item {
239                syn::Item::Struct(item_struct) => {
240                    if item_struct.ident == type_name
241                        && self.struct_parser.should_include_struct(item_struct)
242                    {
243                        return self.struct_parser.parse_struct(
244                            item_struct,
245                            file_path,
246                            &mut self.type_resolver,
247                        );
248                    }
249                }
250                syn::Item::Enum(item_enum) => {
251                    if item_enum.ident == type_name
252                        && self.struct_parser.should_include_enum(item_enum)
253                    {
254                        return self.struct_parser.parse_enum(
255                            item_enum,
256                            file_path,
257                            &mut self.type_resolver,
258                        );
259                    }
260                }
261                _ => {}
262            }
263        }
264        None
265    }
266
267    /// Extract type names from a Rust type string
268    pub fn extract_type_names(&self, rust_type: &str, type_names: &mut HashSet<String>) {
269        self.extract_type_names_recursive(rust_type, type_names);
270    }
271
272    /// Recursively extract type names from complex types
273    fn extract_type_names_recursive(&self, rust_type: &str, type_names: &mut HashSet<String>) {
274        let rust_type = rust_type.trim();
275
276        // Handle Result<T, E> - extract both T and E
277        if rust_type.starts_with("Result<") {
278            if let Some(inner) = rust_type
279                .strip_prefix("Result<")
280                .and_then(|s| s.strip_suffix(">"))
281            {
282                if let Some(comma_pos) = inner.find(',') {
283                    let ok_type = inner[..comma_pos].trim();
284                    let err_type = inner[comma_pos + 1..].trim();
285                    self.extract_type_names_recursive(ok_type, type_names);
286                    self.extract_type_names_recursive(err_type, type_names);
287                }
288            }
289            return;
290        }
291
292        // Handle Option<T> - extract T
293        if rust_type.starts_with("Option<") {
294            if let Some(inner) = rust_type
295                .strip_prefix("Option<")
296                .and_then(|s| s.strip_suffix(">"))
297            {
298                self.extract_type_names_recursive(inner, type_names);
299            }
300            return;
301        }
302
303        // Handle Vec<T> - extract T
304        if rust_type.starts_with("Vec<") {
305            if let Some(inner) = rust_type
306                .strip_prefix("Vec<")
307                .and_then(|s| s.strip_suffix(">"))
308            {
309                self.extract_type_names_recursive(inner, type_names);
310            }
311            return;
312        }
313
314        // Handle HashMap<K, V> and BTreeMap<K, V> - extract K and V
315        if rust_type.starts_with("HashMap<") || rust_type.starts_with("BTreeMap<") {
316            let prefix = if rust_type.starts_with("HashMap<") {
317                "HashMap<"
318            } else {
319                "BTreeMap<"
320            };
321            if let Some(inner) = rust_type
322                .strip_prefix(prefix)
323                .and_then(|s| s.strip_suffix(">"))
324            {
325                if let Some(comma_pos) = inner.find(',') {
326                    let key_type = inner[..comma_pos].trim();
327                    let value_type = inner[comma_pos + 1..].trim();
328                    self.extract_type_names_recursive(key_type, type_names);
329                    self.extract_type_names_recursive(value_type, type_names);
330                }
331            }
332            return;
333        }
334
335        // Handle HashSet<T> and BTreeSet<T> - extract T
336        if rust_type.starts_with("HashSet<") || rust_type.starts_with("BTreeSet<") {
337            let prefix = if rust_type.starts_with("HashSet<") {
338                "HashSet<"
339            } else {
340                "BTreeSet<"
341            };
342            if let Some(inner) = rust_type
343                .strip_prefix(prefix)
344                .and_then(|s| s.strip_suffix(">"))
345            {
346                self.extract_type_names_recursive(inner, type_names);
347            }
348            return;
349        }
350
351        // Handle tuple types like (T, U, V)
352        if rust_type.starts_with('(') && rust_type.ends_with(')') && rust_type != "()" {
353            let inner = &rust_type[1..rust_type.len() - 1];
354            for part in inner.split(',') {
355                self.extract_type_names_recursive(part.trim(), type_names);
356            }
357            return;
358        }
359
360        // Handle references
361        if rust_type.starts_with('&') {
362            let without_ref = rust_type.trim_start_matches('&');
363            self.extract_type_names_recursive(without_ref, type_names);
364            return;
365        }
366
367        // Check if this is a custom type name
368        if !rust_type.is_empty()
369            && !self.type_resolver.get_type_mappings().contains_key(rust_type)
370            && !rust_type.starts_with(char::is_lowercase) // Skip built-in types
371            && rust_type.chars().next().is_some_and(char::is_alphabetic)
372            && !rust_type.contains('<')
373        // Skip generic type names with parameters
374        {
375            type_names.insert(rust_type.to_string());
376        }
377    }
378
379    /// Get discovered structs
380    pub fn get_discovered_structs(&self) -> &HashMap<String, StructInfo> {
381        &self.discovered_structs
382    }
383
384    /// Get the dependency graph for visualization
385    pub fn get_dependency_graph(&self) -> &TypeDependencyGraph {
386        &self.dependency_graph
387    }
388
389    /// Sort types topologically to ensure dependencies are declared before being used
390    pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
391        self.dependency_graph.topological_sort_types(types)
392    }
393
394    /// Generate a text-based visualization of the dependency graph
395    pub fn visualize_dependencies(&self, commands: &[CommandInfo]) -> String {
396        self.dependency_graph.visualize_dependencies(commands)
397    }
398
399    /// Generate a DOT graph visualization of the dependency graph
400    pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
401        self.dependency_graph.generate_dot_graph(commands)
402    }
403
404    /// Map a Rust type to its TypeScript equivalent
405    pub fn map_rust_type_to_typescript(&mut self, rust_type: &str) -> String {
406        self.type_resolver.map_rust_type_to_typescript(rust_type)
407    }
408}
409
410impl Default for CommandAnalyzer {
411    fn default() -> Self {
412        Self::new()
413    }
414}