Skip to main content

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            serde_tag: None,
255            enum_variants: None,
256        }
257    }
258
259    #[test]
260    fn test_new_graph() {
261        let graph = TypeDependencyGraph::new();
262        assert!(graph.type_definitions.is_empty());
263        assert!(graph.dependencies.is_empty());
264        assert!(graph.resolved_types.is_empty());
265    }
266
267    #[test]
268    fn test_default_impl() {
269        let graph = TypeDependencyGraph::default();
270        assert!(graph.type_definitions.is_empty());
271    }
272
273    #[test]
274    fn test_add_type_definition() {
275        let mut graph = TypeDependencyGraph::new();
276        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
277
278        assert!(graph.has_type_definition("User"));
279        assert_eq!(
280            graph.get_type_definition_path("User"),
281            Some(&PathBuf::from("user.rs"))
282        );
283    }
284
285    #[test]
286    fn test_add_multiple_type_definitions() {
287        let mut graph = TypeDependencyGraph::new();
288        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
289        graph.add_type_definition("Post".to_string(), PathBuf::from("post.rs"));
290
291        assert!(graph.has_type_definition("User"));
292        assert!(graph.has_type_definition("Post"));
293        assert_eq!(graph.type_definitions.len(), 2);
294    }
295
296    #[test]
297    fn test_has_type_definition() {
298        let mut graph = TypeDependencyGraph::new();
299        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
300
301        assert!(graph.has_type_definition("User"));
302        assert!(!graph.has_type_definition("Post"));
303    }
304
305    #[test]
306    fn test_add_dependency() {
307        let mut graph = TypeDependencyGraph::new();
308        graph.add_dependency("Post".to_string(), "User".to_string());
309
310        let deps = graph.get_dependencies("Post");
311        assert!(deps.is_some());
312        assert!(deps.unwrap().contains("User"));
313    }
314
315    #[test]
316    fn test_add_multiple_dependencies_to_same_type() {
317        let mut graph = TypeDependencyGraph::new();
318        graph.add_dependency("Post".to_string(), "User".to_string());
319        graph.add_dependency("Post".to_string(), "Category".to_string());
320
321        let deps = graph.get_dependencies("Post").unwrap();
322        assert_eq!(deps.len(), 2);
323        assert!(deps.contains("User"));
324        assert!(deps.contains("Category"));
325    }
326
327    #[test]
328    fn test_add_dependencies_set() {
329        let mut graph = TypeDependencyGraph::new();
330        let mut deps = HashSet::new();
331        deps.insert("User".to_string());
332        deps.insert("Category".to_string());
333
334        graph.add_dependencies("Post".to_string(), deps);
335
336        let result_deps = graph.get_dependencies("Post").unwrap();
337        assert_eq!(result_deps.len(), 2);
338        assert!(result_deps.contains("User"));
339        assert!(result_deps.contains("Category"));
340    }
341
342    #[test]
343    fn test_get_dependencies_none() {
344        let graph = TypeDependencyGraph::new();
345        assert!(graph.get_dependencies("NonExistent").is_none());
346    }
347
348    #[test]
349    fn test_add_resolved_type() {
350        let mut graph = TypeDependencyGraph::new();
351        let struct_info = create_test_struct("User", "user.rs");
352
353        graph.add_resolved_type("User".to_string(), struct_info);
354
355        assert!(graph.resolved_types.contains_key("User"));
356        assert_eq!(graph.resolved_types.len(), 1);
357    }
358
359    #[test]
360    fn test_get_resolved_types() {
361        let mut graph = TypeDependencyGraph::new();
362        let struct_info = create_test_struct("User", "user.rs");
363
364        graph.add_resolved_type("User".to_string(), struct_info);
365
366        let resolved = graph.get_resolved_types();
367        assert_eq!(resolved.len(), 1);
368        assert!(resolved.contains_key("User"));
369    }
370
371    // Topological sort tests
372    mod topological_sort {
373        use super::*;
374
375        #[test]
376        fn test_sort_single_type() {
377            let graph = TypeDependencyGraph::new();
378            let mut types = HashSet::new();
379            types.insert("User".to_string());
380
381            let sorted = graph.topological_sort_types(&types);
382            assert_eq!(sorted, vec!["User"]);
383        }
384
385        #[test]
386        fn test_sort_independent_types() {
387            let graph = TypeDependencyGraph::new();
388            let mut types = HashSet::new();
389            types.insert("User".to_string());
390            types.insert("Post".to_string());
391
392            let sorted = graph.topological_sort_types(&types);
393            assert_eq!(sorted.len(), 2);
394            assert!(sorted.contains(&"User".to_string()));
395            assert!(sorted.contains(&"Post".to_string()));
396        }
397
398        #[test]
399        fn test_sort_linear_dependency() {
400            let mut graph = TypeDependencyGraph::new();
401            // Post depends on User
402            graph.add_dependency("Post".to_string(), "User".to_string());
403
404            let mut types = HashSet::new();
405            types.insert("User".to_string());
406            types.insert("Post".to_string());
407
408            let sorted = graph.topological_sort_types(&types);
409            // User should come before Post
410            assert_eq!(sorted, vec!["User", "Post"]);
411        }
412
413        #[test]
414        fn test_sort_diamond_dependency() {
415            let mut graph = TypeDependencyGraph::new();
416            // D depends on B and C
417            // B depends on A
418            // C depends on A
419            graph.add_dependency("D".to_string(), "B".to_string());
420            graph.add_dependency("D".to_string(), "C".to_string());
421            graph.add_dependency("B".to_string(), "A".to_string());
422            graph.add_dependency("C".to_string(), "A".to_string());
423
424            let mut types = HashSet::new();
425            types.insert("A".to_string());
426            types.insert("B".to_string());
427            types.insert("C".to_string());
428            types.insert("D".to_string());
429
430            let sorted = graph.topological_sort_types(&types);
431
432            // A must come first (no dependencies)
433            assert_eq!(sorted[0], "A");
434            // D must come last (depends on everything)
435            assert_eq!(sorted[3], "D");
436            // B and C can be in either order but after A and before D
437            let b_pos = sorted.iter().position(|x| x == "B").unwrap();
438            let c_pos = sorted.iter().position(|x| x == "C").unwrap();
439            assert!(b_pos > 0 && b_pos < 3);
440            assert!(c_pos > 0 && c_pos < 3);
441        }
442
443        #[test]
444        fn test_sort_chain_dependency() {
445            let mut graph = TypeDependencyGraph::new();
446            // A -> B -> C -> D (each depends on previous)
447            graph.add_dependency("D".to_string(), "C".to_string());
448            graph.add_dependency("C".to_string(), "B".to_string());
449            graph.add_dependency("B".to_string(), "A".to_string());
450
451            let mut types = HashSet::new();
452            types.insert("A".to_string());
453            types.insert("B".to_string());
454            types.insert("C".to_string());
455            types.insert("D".to_string());
456
457            let sorted = graph.topological_sort_types(&types);
458            assert_eq!(sorted, vec!["A", "B", "C", "D"]);
459        }
460
461        #[test]
462        fn test_sort_circular_dependency() {
463            let mut graph = TypeDependencyGraph::new();
464            // A -> B -> C -> A (circular)
465            graph.add_dependency("A".to_string(), "B".to_string());
466            graph.add_dependency("B".to_string(), "C".to_string());
467            graph.add_dependency("C".to_string(), "A".to_string());
468
469            let mut types = HashSet::new();
470            types.insert("A".to_string());
471            types.insert("B".to_string());
472            types.insert("C".to_string());
473
474            // Should handle circular dependency without crashing
475            let sorted = graph.topological_sort_types(&types);
476            // All types should still be in the result
477            assert_eq!(sorted.len(), 3);
478        }
479
480        #[test]
481        fn test_sort_self_dependency() {
482            let mut graph = TypeDependencyGraph::new();
483            // A depends on itself
484            graph.add_dependency("A".to_string(), "A".to_string());
485
486            let mut types = HashSet::new();
487            types.insert("A".to_string());
488
489            // Should handle self-dependency without crashing
490            let sorted = graph.topological_sort_types(&types);
491            assert!(!sorted.is_empty());
492        }
493
494        #[test]
495        fn test_sort_empty_set() {
496            let graph = TypeDependencyGraph::new();
497            let types = HashSet::new();
498
499            let sorted = graph.topological_sort_types(&types);
500            assert!(sorted.is_empty());
501        }
502
503        #[test]
504        fn test_sort_type_with_missing_dependency() {
505            let mut graph = TypeDependencyGraph::new();
506            // Post depends on User, but User is not in the types set
507            graph.add_dependency("Post".to_string(), "User".to_string());
508
509            let mut types = HashSet::new();
510            types.insert("Post".to_string());
511
512            let sorted = graph.topological_sort_types(&types);
513            // The topological sort follows dependencies, so User is included even though
514            // it wasn't in the input set
515            assert_eq!(sorted.len(), 2);
516            assert_eq!(sorted, vec!["User", "Post"]);
517        }
518    }
519
520    // Integration tests
521    #[test]
522    fn test_full_graph_workflow() {
523        let mut graph = TypeDependencyGraph::new();
524
525        // Add type definitions
526        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
527        graph.add_type_definition("Post".to_string(), PathBuf::from("post.rs"));
528
529        // Add dependencies
530        graph.add_dependency("Post".to_string(), "User".to_string());
531
532        // Add resolved types
533        graph.add_resolved_type("User".to_string(), create_test_struct("User", "user.rs"));
534        graph.add_resolved_type("Post".to_string(), create_test_struct("Post", "post.rs"));
535
536        // Verify everything
537        assert!(graph.has_type_definition("User"));
538        assert!(graph.has_type_definition("Post"));
539        assert_eq!(graph.get_resolved_types().len(), 2);
540
541        let deps = graph.get_dependencies("Post");
542        assert!(deps.is_some());
543        assert!(deps.unwrap().contains("User"));
544
545        // Test topological sort
546        let mut types = HashSet::new();
547        types.insert("User".to_string());
548        types.insert("Post".to_string());
549        let sorted = graph.topological_sort_types(&types);
550        assert_eq!(sorted, vec!["User", "Post"]);
551    }
552}