tauri_typegen/analysis/
dependency_graph.rs

1use crate::models::{CommandInfo, StructInfo};
2use std::collections::{HashMap, HashSet};
3use std::path::PathBuf;
4
5/// Dependency graph for lazy type resolution
6#[derive(Debug, Default)]
7pub struct TypeDependencyGraph {
8    /// Maps type name to the files where it's defined
9    pub type_definitions: HashMap<String, PathBuf>,
10    /// Maps type name to types it depends on
11    pub dependencies: HashMap<String, HashSet<String>>,
12    /// Maps type name to its resolved StructInfo
13    pub resolved_types: HashMap<String, StructInfo>,
14}
15
16impl TypeDependencyGraph {
17    pub fn new() -> Self {
18        Self::default()
19    }
20
21    /// Add a type definition to the graph
22    pub fn add_type_definition(&mut self, type_name: String, file_path: PathBuf) {
23        self.type_definitions.insert(type_name, file_path);
24    }
25
26    /// Add a dependency relationship between types
27    pub fn add_dependency(&mut self, dependent: String, dependency: String) {
28        self.dependencies
29            .entry(dependent)
30            .or_default()
31            .insert(dependency);
32    }
33
34    /// Add multiple dependencies for a type
35    pub fn add_dependencies(&mut self, dependent: String, dependencies: HashSet<String>) {
36        self.dependencies.insert(dependent, dependencies);
37    }
38
39    /// Add a resolved type to the graph
40    pub fn add_resolved_type(&mut self, type_name: String, struct_info: StructInfo) {
41        self.resolved_types.insert(type_name, struct_info);
42    }
43
44    /// Get all resolved types
45    pub fn get_resolved_types(&self) -> &HashMap<String, StructInfo> {
46        &self.resolved_types
47    }
48
49    /// Get dependencies for a type
50    pub fn get_dependencies(&self, type_name: &str) -> Option<&HashSet<String>> {
51        self.dependencies.get(type_name)
52    }
53
54    /// Check if a type is defined in the graph
55    pub fn has_type_definition(&self, type_name: &str) -> bool {
56        self.type_definitions.contains_key(type_name)
57    }
58
59    /// Get the file path where a type is defined
60    pub fn get_type_definition_path(&self, type_name: &str) -> Option<&PathBuf> {
61        self.type_definitions.get(type_name)
62    }
63
64    /// Perform topological sort on the given types using the dependency graph
65    pub fn topological_sort_types(&self, types: &HashSet<String>) -> Vec<String> {
66        let mut sorted = Vec::new();
67        let mut visited = HashSet::new();
68        let mut visiting = HashSet::new();
69
70        for type_name in types {
71            if !visited.contains(type_name) {
72                self.topological_visit(type_name, &mut sorted, &mut visited, &mut visiting);
73            }
74        }
75
76        sorted
77    }
78
79    /// Recursive helper for topological sorting with cycle detection
80    fn topological_visit(
81        &self,
82        type_name: &str,
83        sorted: &mut Vec<String>,
84        visited: &mut HashSet<String>,
85        visiting: &mut HashSet<String>,
86    ) {
87        // Check for cycles
88        if visiting.contains(type_name) {
89            eprintln!(
90                "Warning: Circular dependency detected involving type: {}",
91                type_name
92            );
93            return;
94        }
95
96        if visited.contains(type_name) {
97            return;
98        }
99
100        visiting.insert(type_name.to_string());
101
102        // Visit dependencies first
103        if let Some(deps) = self.dependencies.get(type_name) {
104            for dep in deps {
105                self.topological_visit(dep, sorted, visited, visiting);
106            }
107        }
108
109        visiting.remove(type_name);
110        visited.insert(type_name.to_string());
111        sorted.push(type_name.to_string());
112    }
113
114    /// Build visualization of the dependency graph
115    pub fn visualize_dependencies(&self, entry_commands: &[crate::models::CommandInfo]) -> String {
116        let mut output = String::new();
117        output.push_str("🌐 Type Dependency Graph\n");
118        output.push_str("======================\n\n");
119
120        // Show command entry points
121        output.push_str("šŸ“‹ Command Entry Points:\n");
122        for cmd in entry_commands {
123            output.push_str(&format!(
124                "• {} ({}:{})\n",
125                cmd.name, cmd.file_path, cmd.line_number
126            ));
127
128            // Show parameters
129            for param in &cmd.parameters {
130                output.push_str(&format!("  ā”œā”€ {}: {}\n", param.name, param.rust_type));
131            }
132
133            // Show return type
134            output.push_str(&format!("  └─ returns: {}\n", cmd.return_type));
135        }
136
137        output.push_str("\nšŸ—ļø  Discovered Types:\n");
138        for (type_name, struct_info) in &self.resolved_types {
139            let type_kind = if struct_info.is_enum {
140                "enum"
141            } else {
142                "struct"
143            };
144            output.push_str(&format!(
145                "• {} ({}) - {} fields - defined in {}\n",
146                type_name,
147                type_kind,
148                struct_info.fields.len(),
149                struct_info.file_path
150            ));
151
152            // Show dependencies
153            if let Some(deps) = self.dependencies.get(type_name) {
154                if !deps.is_empty() {
155                    let deps_list: Vec<String> = deps.iter().cloned().collect();
156                    output.push_str(&format!("  └─ depends on: {}\n", deps_list.join(", ")));
157                }
158            }
159        }
160
161        // Show dependency chains
162        output.push_str("\nšŸ”— Dependency Chains:\n");
163        for type_name in self.resolved_types.keys() {
164            self.show_dependency_chain(type_name, &mut output, 0);
165        }
166
167        output.push_str(&format!(
168            "\nšŸ“Š Summary:\n• {} commands analyzed\n• {} types discovered\n• {} files with type definitions\n",
169            entry_commands.len(),
170            self.resolved_types.len(),
171            self.type_definitions.len()
172        ));
173
174        output
175    }
176
177    /// Recursively show dependency chain for a type
178    fn show_dependency_chain(&self, type_name: &str, output: &mut String, indent: usize) {
179        let indent_str = "  ".repeat(indent);
180        output.push_str(&format!("{}ā”œā”€ {}\n", indent_str, type_name));
181
182        if let Some(deps) = self.dependencies.get(type_name) {
183            for dep in deps {
184                if indent < 3 {
185                    // Prevent too deep recursion in visualization
186                    self.show_dependency_chain(dep, output, indent + 1);
187                }
188            }
189        }
190    }
191
192    /// Generate a DOT graph representation of the dependency graph
193    pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
194        let mut output = String::new();
195        output.push_str("digraph Dependencies {\n");
196        output.push_str("  rankdir=LR;\n");
197        output.push_str("  node [shape=box];\n");
198        output.push('\n');
199
200        // Add command nodes
201        for command in commands {
202            output.push_str(&format!(
203                "  \"{}\" [color=blue, style=filled, fillcolor=lightblue];\n",
204                command.name
205            ));
206        }
207
208        // Add type nodes
209        for type_name in self.resolved_types.keys() {
210            output.push_str(&format!("  \"{}\" [color=green];\n", type_name));
211        }
212
213        // Add edges from commands to their parameter/return types
214        for command in commands {
215            for param in &command.parameters {
216                if self.resolved_types.contains_key(&param.rust_type) {
217                    output.push_str(&format!(
218                        "  \"{}\" -> \"{}\" [label=\"param\"];\n",
219                        command.name, param.rust_type
220                    ));
221                }
222            }
223            if self.resolved_types.contains_key(&command.return_type) {
224                output.push_str(&format!(
225                    "  \"{}\" -> \"{}\" [label=\"return\"];\n",
226                    command.name, command.return_type
227                ));
228            }
229        }
230
231        // Add type dependency edges
232        for (type_name, deps) in &self.dependencies {
233            for dep in deps {
234                output.push_str(&format!("  \"{}\" -> \"{}\";\n", type_name, dep));
235            }
236        }
237
238        output.push_str("}\n");
239        output
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    fn create_test_struct(name: &str, file: &str) -> StructInfo {
248        StructInfo {
249            name: name.to_string(),
250            fields: vec![],
251            file_path: file.to_string(),
252            is_enum: false,
253            serde_rename_all: None,
254        }
255    }
256
257    #[test]
258    fn test_new_graph() {
259        let graph = TypeDependencyGraph::new();
260        assert!(graph.type_definitions.is_empty());
261        assert!(graph.dependencies.is_empty());
262        assert!(graph.resolved_types.is_empty());
263    }
264
265    #[test]
266    fn test_default_impl() {
267        let graph = TypeDependencyGraph::default();
268        assert!(graph.type_definitions.is_empty());
269    }
270
271    #[test]
272    fn test_add_type_definition() {
273        let mut graph = TypeDependencyGraph::new();
274        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
275
276        assert!(graph.has_type_definition("User"));
277        assert_eq!(
278            graph.get_type_definition_path("User"),
279            Some(&PathBuf::from("user.rs"))
280        );
281    }
282
283    #[test]
284    fn test_add_multiple_type_definitions() {
285        let mut graph = TypeDependencyGraph::new();
286        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
287        graph.add_type_definition("Post".to_string(), PathBuf::from("post.rs"));
288
289        assert!(graph.has_type_definition("User"));
290        assert!(graph.has_type_definition("Post"));
291        assert_eq!(graph.type_definitions.len(), 2);
292    }
293
294    #[test]
295    fn test_has_type_definition() {
296        let mut graph = TypeDependencyGraph::new();
297        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
298
299        assert!(graph.has_type_definition("User"));
300        assert!(!graph.has_type_definition("Post"));
301    }
302
303    #[test]
304    fn test_add_dependency() {
305        let mut graph = TypeDependencyGraph::new();
306        graph.add_dependency("Post".to_string(), "User".to_string());
307
308        let deps = graph.get_dependencies("Post");
309        assert!(deps.is_some());
310        assert!(deps.unwrap().contains("User"));
311    }
312
313    #[test]
314    fn test_add_multiple_dependencies_to_same_type() {
315        let mut graph = TypeDependencyGraph::new();
316        graph.add_dependency("Post".to_string(), "User".to_string());
317        graph.add_dependency("Post".to_string(), "Category".to_string());
318
319        let deps = graph.get_dependencies("Post").unwrap();
320        assert_eq!(deps.len(), 2);
321        assert!(deps.contains("User"));
322        assert!(deps.contains("Category"));
323    }
324
325    #[test]
326    fn test_add_dependencies_set() {
327        let mut graph = TypeDependencyGraph::new();
328        let mut deps = HashSet::new();
329        deps.insert("User".to_string());
330        deps.insert("Category".to_string());
331
332        graph.add_dependencies("Post".to_string(), deps);
333
334        let result_deps = graph.get_dependencies("Post").unwrap();
335        assert_eq!(result_deps.len(), 2);
336        assert!(result_deps.contains("User"));
337        assert!(result_deps.contains("Category"));
338    }
339
340    #[test]
341    fn test_get_dependencies_none() {
342        let graph = TypeDependencyGraph::new();
343        assert!(graph.get_dependencies("NonExistent").is_none());
344    }
345
346    #[test]
347    fn test_add_resolved_type() {
348        let mut graph = TypeDependencyGraph::new();
349        let struct_info = create_test_struct("User", "user.rs");
350
351        graph.add_resolved_type("User".to_string(), struct_info);
352
353        assert!(graph.resolved_types.contains_key("User"));
354        assert_eq!(graph.resolved_types.len(), 1);
355    }
356
357    #[test]
358    fn test_get_resolved_types() {
359        let mut graph = TypeDependencyGraph::new();
360        let struct_info = create_test_struct("User", "user.rs");
361
362        graph.add_resolved_type("User".to_string(), struct_info);
363
364        let resolved = graph.get_resolved_types();
365        assert_eq!(resolved.len(), 1);
366        assert!(resolved.contains_key("User"));
367    }
368
369    // Topological sort tests
370    mod topological_sort {
371        use super::*;
372
373        #[test]
374        fn test_sort_single_type() {
375            let graph = TypeDependencyGraph::new();
376            let mut types = HashSet::new();
377            types.insert("User".to_string());
378
379            let sorted = graph.topological_sort_types(&types);
380            assert_eq!(sorted, vec!["User"]);
381        }
382
383        #[test]
384        fn test_sort_independent_types() {
385            let graph = TypeDependencyGraph::new();
386            let mut types = HashSet::new();
387            types.insert("User".to_string());
388            types.insert("Post".to_string());
389
390            let sorted = graph.topological_sort_types(&types);
391            assert_eq!(sorted.len(), 2);
392            assert!(sorted.contains(&"User".to_string()));
393            assert!(sorted.contains(&"Post".to_string()));
394        }
395
396        #[test]
397        fn test_sort_linear_dependency() {
398            let mut graph = TypeDependencyGraph::new();
399            // Post depends on User
400            graph.add_dependency("Post".to_string(), "User".to_string());
401
402            let mut types = HashSet::new();
403            types.insert("User".to_string());
404            types.insert("Post".to_string());
405
406            let sorted = graph.topological_sort_types(&types);
407            // User should come before Post
408            assert_eq!(sorted, vec!["User", "Post"]);
409        }
410
411        #[test]
412        fn test_sort_diamond_dependency() {
413            let mut graph = TypeDependencyGraph::new();
414            // D depends on B and C
415            // B depends on A
416            // C depends on A
417            graph.add_dependency("D".to_string(), "B".to_string());
418            graph.add_dependency("D".to_string(), "C".to_string());
419            graph.add_dependency("B".to_string(), "A".to_string());
420            graph.add_dependency("C".to_string(), "A".to_string());
421
422            let mut types = HashSet::new();
423            types.insert("A".to_string());
424            types.insert("B".to_string());
425            types.insert("C".to_string());
426            types.insert("D".to_string());
427
428            let sorted = graph.topological_sort_types(&types);
429
430            // A must come first (no dependencies)
431            assert_eq!(sorted[0], "A");
432            // D must come last (depends on everything)
433            assert_eq!(sorted[3], "D");
434            // B and C can be in either order but after A and before D
435            let b_pos = sorted.iter().position(|x| x == "B").unwrap();
436            let c_pos = sorted.iter().position(|x| x == "C").unwrap();
437            assert!(b_pos > 0 && b_pos < 3);
438            assert!(c_pos > 0 && c_pos < 3);
439        }
440
441        #[test]
442        fn test_sort_chain_dependency() {
443            let mut graph = TypeDependencyGraph::new();
444            // A -> B -> C -> D (each depends on previous)
445            graph.add_dependency("D".to_string(), "C".to_string());
446            graph.add_dependency("C".to_string(), "B".to_string());
447            graph.add_dependency("B".to_string(), "A".to_string());
448
449            let mut types = HashSet::new();
450            types.insert("A".to_string());
451            types.insert("B".to_string());
452            types.insert("C".to_string());
453            types.insert("D".to_string());
454
455            let sorted = graph.topological_sort_types(&types);
456            assert_eq!(sorted, vec!["A", "B", "C", "D"]);
457        }
458
459        #[test]
460        fn test_sort_circular_dependency() {
461            let mut graph = TypeDependencyGraph::new();
462            // A -> B -> C -> A (circular)
463            graph.add_dependency("A".to_string(), "B".to_string());
464            graph.add_dependency("B".to_string(), "C".to_string());
465            graph.add_dependency("C".to_string(), "A".to_string());
466
467            let mut types = HashSet::new();
468            types.insert("A".to_string());
469            types.insert("B".to_string());
470            types.insert("C".to_string());
471
472            // Should handle circular dependency without crashing
473            let sorted = graph.topological_sort_types(&types);
474            // All types should still be in the result
475            assert_eq!(sorted.len(), 3);
476        }
477
478        #[test]
479        fn test_sort_self_dependency() {
480            let mut graph = TypeDependencyGraph::new();
481            // A depends on itself
482            graph.add_dependency("A".to_string(), "A".to_string());
483
484            let mut types = HashSet::new();
485            types.insert("A".to_string());
486
487            // Should handle self-dependency without crashing
488            let sorted = graph.topological_sort_types(&types);
489            assert!(!sorted.is_empty());
490        }
491
492        #[test]
493        fn test_sort_empty_set() {
494            let graph = TypeDependencyGraph::new();
495            let types = HashSet::new();
496
497            let sorted = graph.topological_sort_types(&types);
498            assert!(sorted.is_empty());
499        }
500
501        #[test]
502        fn test_sort_type_with_missing_dependency() {
503            let mut graph = TypeDependencyGraph::new();
504            // Post depends on User, but User is not in the types set
505            graph.add_dependency("Post".to_string(), "User".to_string());
506
507            let mut types = HashSet::new();
508            types.insert("Post".to_string());
509
510            let sorted = graph.topological_sort_types(&types);
511            // The topological sort follows dependencies, so User is included even though
512            // it wasn't in the input set
513            assert_eq!(sorted.len(), 2);
514            assert_eq!(sorted, vec!["User", "Post"]);
515        }
516    }
517
518    // Integration tests
519    #[test]
520    fn test_full_graph_workflow() {
521        let mut graph = TypeDependencyGraph::new();
522
523        // Add type definitions
524        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
525        graph.add_type_definition("Post".to_string(), PathBuf::from("post.rs"));
526
527        // Add dependencies
528        graph.add_dependency("Post".to_string(), "User".to_string());
529
530        // Add resolved types
531        graph.add_resolved_type("User".to_string(), create_test_struct("User", "user.rs"));
532        graph.add_resolved_type("Post".to_string(), create_test_struct("Post", "post.rs"));
533
534        // Verify everything
535        assert!(graph.has_type_definition("User"));
536        assert!(graph.has_type_definition("Post"));
537        assert_eq!(graph.get_resolved_types().len(), 2);
538
539        let deps = graph.get_dependencies("Post");
540        assert!(deps.is_some());
541        assert!(deps.unwrap().contains("User"));
542
543        // Test topological sort
544        let mut types = HashSet::new();
545        types.insert("User".to_string());
546        types.insert("Post".to_string());
547        let sorted = graph.topological_sort_types(&types);
548        assert_eq!(sorted, vec!["User", "Post"]);
549    }
550}