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        let mut type_names: Vec<&String> = types.iter().collect();
70        type_names.sort_unstable();
71
72        for type_name in type_names {
73            if !visited.contains(type_name) {
74                self.topological_visit(type_name, &mut sorted, &mut visited, &mut visiting);
75            }
76        }
77
78        sorted
79    }
80
81    /// Recursive helper for topological sorting with cycle detection
82    fn topological_visit(
83        &self,
84        type_name: &str,
85        sorted: &mut Vec<String>,
86        visited: &mut HashSet<String>,
87        visiting: &mut HashSet<String>,
88    ) {
89        // Check for cycles
90        if visiting.contains(type_name) {
91            eprintln!(
92                "Warning: Circular dependency detected involving type: {}",
93                type_name
94            );
95            return;
96        }
97
98        if visited.contains(type_name) {
99            return;
100        }
101
102        visiting.insert(type_name.to_string());
103
104        // Visit dependencies first
105        if let Some(deps) = self.dependencies.get(type_name) {
106            let mut sorted_deps: Vec<&String> = deps.iter().collect();
107            sorted_deps.sort_unstable();
108
109            for dep in sorted_deps {
110                self.topological_visit(dep, sorted, visited, visiting);
111            }
112        }
113
114        visiting.remove(type_name);
115        visited.insert(type_name.to_string());
116        sorted.push(type_name.to_string());
117    }
118
119    fn sorted_commands<'a>(&self, commands: &'a [CommandInfo]) -> Vec<&'a CommandInfo> {
120        let mut sorted: Vec<_> = commands.iter().collect();
121        sorted.sort_by(|a, b| {
122            a.name
123                .cmp(&b.name)
124                .then_with(|| a.file_path.cmp(&b.file_path))
125                .then_with(|| a.line_number.cmp(&b.line_number))
126        });
127        sorted
128    }
129
130    fn sorted_resolved_type_names(&self) -> Vec<&String> {
131        let mut names: Vec<_> = self.resolved_types.keys().collect();
132        names.sort_unstable();
133        names
134    }
135
136    fn sorted_dependencies_for(&self, type_name: &str) -> Vec<&String> {
137        let mut deps: Vec<_> = self
138            .dependencies
139            .get(type_name)
140            .map(|deps| deps.iter().collect())
141            .unwrap_or_default();
142        deps.sort_unstable();
143        deps
144    }
145
146    fn sorted_dependency_owners(&self) -> Vec<&String> {
147        let mut owners: Vec<_> = self.dependencies.keys().collect();
148        owners.sort_unstable();
149        owners
150    }
151
152    /// Build visualization of the dependency graph
153    pub fn visualize_dependencies(&self, entry_commands: &[crate::models::CommandInfo]) -> String {
154        let mut output = String::new();
155        output.push_str("🌐 Type Dependency Graph\n");
156        output.push_str("======================\n\n");
157
158        // Show command entry points
159        output.push_str("šŸ“‹ Command Entry Points:\n");
160        for cmd in self.sorted_commands(entry_commands) {
161            output.push_str(&format!(
162                "• {} ({}:{})\n",
163                cmd.name, cmd.file_path, cmd.line_number
164            ));
165
166            // Show parameters
167            for param in &cmd.parameters {
168                output.push_str(&format!("  ā”œā”€ {}: {}\n", param.name, param.rust_type));
169            }
170
171            // Show return type
172            output.push_str(&format!("  └─ returns: {}\n", cmd.return_type));
173        }
174
175        output.push_str("\nšŸ—ļø  Discovered Types:\n");
176        for type_name in self.sorted_resolved_type_names() {
177            let struct_info = &self.resolved_types[type_name];
178            let type_kind = if struct_info.is_enum {
179                "enum"
180            } else {
181                "struct"
182            };
183            output.push_str(&format!(
184                "• {} ({}) - {} fields - defined in {}\n",
185                type_name,
186                type_kind,
187                struct_info.fields.len(),
188                struct_info.file_path
189            ));
190
191            // Show dependencies
192            let deps_list: Vec<String> = self
193                .sorted_dependencies_for(type_name)
194                .into_iter()
195                .cloned()
196                .collect();
197            if !deps_list.is_empty() {
198                output.push_str(&format!("  └─ depends on: {}\n", deps_list.join(", ")));
199            }
200        }
201
202        // Show dependency chains
203        output.push_str("\nšŸ”— Dependency Chains:\n");
204        for type_name in self.sorted_resolved_type_names() {
205            self.show_dependency_chain(type_name, &mut output, 0);
206        }
207
208        output.push_str(&format!(
209            "\nšŸ“Š Summary:\n• {} commands analyzed\n• {} types discovered\n• {} files with type definitions\n",
210            entry_commands.len(),
211            self.resolved_types.len(),
212            self.type_definitions.len()
213        ));
214
215        output
216    }
217
218    /// Recursively show dependency chain for a type
219    fn show_dependency_chain(&self, type_name: &str, output: &mut String, indent: usize) {
220        let indent_str = "  ".repeat(indent);
221        output.push_str(&format!("{}ā”œā”€ {}\n", indent_str, type_name));
222
223        for dep in self.sorted_dependencies_for(type_name) {
224            if indent < 3 {
225                // Prevent too deep recursion in visualization
226                self.show_dependency_chain(dep, output, indent + 1);
227            }
228        }
229    }
230
231    /// Generate a DOT graph representation of the dependency graph
232    pub fn generate_dot_graph(&self, commands: &[CommandInfo]) -> String {
233        let mut output = String::new();
234        output.push_str("digraph Dependencies {\n");
235        output.push_str("  rankdir=LR;\n");
236        output.push_str("  node [shape=box];\n");
237        output.push('\n');
238
239        // Add command nodes
240        for command in self.sorted_commands(commands) {
241            output.push_str(&format!(
242                "  \"{}\" [color=blue, style=filled, fillcolor=lightblue];\n",
243                command.name
244            ));
245        }
246
247        // Add type nodes
248        for type_name in self.sorted_resolved_type_names() {
249            output.push_str(&format!("  \"{}\" [color=green];\n", type_name));
250        }
251
252        // Add edges from commands to their parameter/return types
253        for command in self.sorted_commands(commands) {
254            for param in &command.parameters {
255                if self.resolved_types.contains_key(&param.rust_type) {
256                    output.push_str(&format!(
257                        "  \"{}\" -> \"{}\" [label=\"param\"];\n",
258                        command.name, param.rust_type
259                    ));
260                }
261            }
262            if self.resolved_types.contains_key(&command.return_type) {
263                output.push_str(&format!(
264                    "  \"{}\" -> \"{}\" [label=\"return\"];\n",
265                    command.name, command.return_type
266                ));
267            }
268        }
269
270        // Add type dependency edges
271        for type_name in self.sorted_dependency_owners() {
272            for dep in self.sorted_dependencies_for(type_name) {
273                output.push_str(&format!("  \"{}\" -> \"{}\";\n", type_name, dep));
274            }
275        }
276
277        output.push_str("}\n");
278        output
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    fn create_test_struct(name: &str, file: &str) -> StructInfo {
287        StructInfo {
288            name: name.to_string(),
289            fields: vec![],
290            file_path: file.to_string(),
291            is_enum: false,
292            serde_rename_all: None,
293            serde_tag: None,
294            enum_variants: None,
295        }
296    }
297
298    fn create_test_command(name: &str, file: &str, line: usize) -> CommandInfo {
299        CommandInfo::new_for_test(name, file, line, vec![], "String", false, vec![])
300    }
301
302    #[test]
303    fn test_new_graph() {
304        let graph = TypeDependencyGraph::new();
305        assert!(graph.type_definitions.is_empty());
306        assert!(graph.dependencies.is_empty());
307        assert!(graph.resolved_types.is_empty());
308    }
309
310    #[test]
311    fn test_default_impl() {
312        let graph = TypeDependencyGraph::default();
313        assert!(graph.type_definitions.is_empty());
314    }
315
316    #[test]
317    fn test_add_type_definition() {
318        let mut graph = TypeDependencyGraph::new();
319        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
320
321        assert!(graph.has_type_definition("User"));
322        assert_eq!(
323            graph.get_type_definition_path("User"),
324            Some(&PathBuf::from("user.rs"))
325        );
326    }
327
328    #[test]
329    fn test_add_multiple_type_definitions() {
330        let mut graph = TypeDependencyGraph::new();
331        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
332        graph.add_type_definition("Post".to_string(), PathBuf::from("post.rs"));
333
334        assert!(graph.has_type_definition("User"));
335        assert!(graph.has_type_definition("Post"));
336        assert_eq!(graph.type_definitions.len(), 2);
337    }
338
339    #[test]
340    fn test_has_type_definition() {
341        let mut graph = TypeDependencyGraph::new();
342        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
343
344        assert!(graph.has_type_definition("User"));
345        assert!(!graph.has_type_definition("Post"));
346    }
347
348    #[test]
349    fn test_add_dependency() {
350        let mut graph = TypeDependencyGraph::new();
351        graph.add_dependency("Post".to_string(), "User".to_string());
352
353        let deps = graph.get_dependencies("Post");
354        assert!(deps.is_some());
355        assert!(deps.unwrap().contains("User"));
356    }
357
358    #[test]
359    fn test_add_multiple_dependencies_to_same_type() {
360        let mut graph = TypeDependencyGraph::new();
361        graph.add_dependency("Post".to_string(), "User".to_string());
362        graph.add_dependency("Post".to_string(), "Category".to_string());
363
364        let deps = graph.get_dependencies("Post").unwrap();
365        assert_eq!(deps.len(), 2);
366        assert!(deps.contains("User"));
367        assert!(deps.contains("Category"));
368    }
369
370    #[test]
371    fn test_add_dependencies_set() {
372        let mut graph = TypeDependencyGraph::new();
373        let mut deps = HashSet::new();
374        deps.insert("User".to_string());
375        deps.insert("Category".to_string());
376
377        graph.add_dependencies("Post".to_string(), deps);
378
379        let result_deps = graph.get_dependencies("Post").unwrap();
380        assert_eq!(result_deps.len(), 2);
381        assert!(result_deps.contains("User"));
382        assert!(result_deps.contains("Category"));
383    }
384
385    #[test]
386    fn test_get_dependencies_none() {
387        let graph = TypeDependencyGraph::new();
388        assert!(graph.get_dependencies("NonExistent").is_none());
389    }
390
391    #[test]
392    fn test_add_resolved_type() {
393        let mut graph = TypeDependencyGraph::new();
394        let struct_info = create_test_struct("User", "user.rs");
395
396        graph.add_resolved_type("User".to_string(), struct_info);
397
398        assert!(graph.resolved_types.contains_key("User"));
399        assert_eq!(graph.resolved_types.len(), 1);
400    }
401
402    #[test]
403    fn test_get_resolved_types() {
404        let mut graph = TypeDependencyGraph::new();
405        let struct_info = create_test_struct("User", "user.rs");
406
407        graph.add_resolved_type("User".to_string(), struct_info);
408
409        let resolved = graph.get_resolved_types();
410        assert_eq!(resolved.len(), 1);
411        assert!(resolved.contains_key("User"));
412    }
413
414    // Topological sort tests
415    mod topological_sort {
416        use super::*;
417
418        #[test]
419        fn test_sort_single_type() {
420            let graph = TypeDependencyGraph::new();
421            let mut types = HashSet::new();
422            types.insert("User".to_string());
423
424            let sorted = graph.topological_sort_types(&types);
425            assert_eq!(sorted, vec!["User"]);
426        }
427
428        #[test]
429        fn test_sort_independent_types() {
430            let graph = TypeDependencyGraph::new();
431            let mut types = HashSet::new();
432            types.insert("User".to_string());
433            types.insert("Post".to_string());
434
435            let sorted = graph.topological_sort_types(&types);
436            assert_eq!(sorted, vec!["Post", "User"]);
437        }
438
439        #[test]
440        fn test_sort_linear_dependency() {
441            let mut graph = TypeDependencyGraph::new();
442            // Post depends on User
443            graph.add_dependency("Post".to_string(), "User".to_string());
444
445            let mut types = HashSet::new();
446            types.insert("User".to_string());
447            types.insert("Post".to_string());
448
449            let sorted = graph.topological_sort_types(&types);
450            // User should come before Post
451            assert_eq!(sorted, vec!["User", "Post"]);
452        }
453
454        #[test]
455        fn test_sort_diamond_dependency() {
456            let mut graph = TypeDependencyGraph::new();
457            // D depends on B and C
458            // B depends on A
459            // C depends on A
460            graph.add_dependency("D".to_string(), "B".to_string());
461            graph.add_dependency("D".to_string(), "C".to_string());
462            graph.add_dependency("B".to_string(), "A".to_string());
463            graph.add_dependency("C".to_string(), "A".to_string());
464
465            let mut types = HashSet::new();
466            types.insert("A".to_string());
467            types.insert("B".to_string());
468            types.insert("C".to_string());
469            types.insert("D".to_string());
470
471            let sorted = graph.topological_sort_types(&types);
472
473            // A must come first (no dependencies)
474            assert_eq!(sorted[0], "A");
475            // D must come last (depends on everything)
476            assert_eq!(sorted[3], "D");
477            assert_eq!(sorted, vec!["A", "B", "C", "D"]);
478        }
479
480        #[test]
481        fn test_sort_chain_dependency() {
482            let mut graph = TypeDependencyGraph::new();
483            // A -> B -> C -> D (each depends on previous)
484            graph.add_dependency("D".to_string(), "C".to_string());
485            graph.add_dependency("C".to_string(), "B".to_string());
486            graph.add_dependency("B".to_string(), "A".to_string());
487
488            let mut types = HashSet::new();
489            types.insert("A".to_string());
490            types.insert("B".to_string());
491            types.insert("C".to_string());
492            types.insert("D".to_string());
493
494            let sorted = graph.topological_sort_types(&types);
495            assert_eq!(sorted, vec!["A", "B", "C", "D"]);
496        }
497
498        #[test]
499        fn test_sort_circular_dependency() {
500            let mut graph = TypeDependencyGraph::new();
501            // A -> B -> C -> A (circular)
502            graph.add_dependency("A".to_string(), "B".to_string());
503            graph.add_dependency("B".to_string(), "C".to_string());
504            graph.add_dependency("C".to_string(), "A".to_string());
505
506            let mut types = HashSet::new();
507            types.insert("A".to_string());
508            types.insert("B".to_string());
509            types.insert("C".to_string());
510
511            // Should handle circular dependency without crashing
512            let sorted = graph.topological_sort_types(&types);
513            // All types should still be in the result
514            assert_eq!(sorted.len(), 3);
515        }
516
517        #[test]
518        fn test_sort_self_dependency() {
519            let mut graph = TypeDependencyGraph::new();
520            // A depends on itself
521            graph.add_dependency("A".to_string(), "A".to_string());
522
523            let mut types = HashSet::new();
524            types.insert("A".to_string());
525
526            // Should handle self-dependency without crashing
527            let sorted = graph.topological_sort_types(&types);
528            assert!(!sorted.is_empty());
529        }
530
531        #[test]
532        fn test_sort_empty_set() {
533            let graph = TypeDependencyGraph::new();
534            let types = HashSet::new();
535
536            let sorted = graph.topological_sort_types(&types);
537            assert!(sorted.is_empty());
538        }
539
540        #[test]
541        fn test_sort_type_with_missing_dependency() {
542            let mut graph = TypeDependencyGraph::new();
543            // Post depends on User, but User is not in the types set
544            graph.add_dependency("Post".to_string(), "User".to_string());
545
546            let mut types = HashSet::new();
547            types.insert("Post".to_string());
548
549            let sorted = graph.topological_sort_types(&types);
550            // The topological sort follows dependencies, so User is included even though
551            // it wasn't in the input set
552            assert_eq!(sorted.len(), 2);
553            assert_eq!(sorted, vec!["User", "Post"]);
554        }
555    }
556
557    // Integration tests
558    #[test]
559    fn test_full_graph_workflow() {
560        let mut graph = TypeDependencyGraph::new();
561
562        // Add type definitions
563        graph.add_type_definition("User".to_string(), PathBuf::from("user.rs"));
564        graph.add_type_definition("Post".to_string(), PathBuf::from("post.rs"));
565
566        // Add dependencies
567        graph.add_dependency("Post".to_string(), "User".to_string());
568
569        // Add resolved types
570        graph.add_resolved_type("User".to_string(), create_test_struct("User", "user.rs"));
571        graph.add_resolved_type("Post".to_string(), create_test_struct("Post", "post.rs"));
572
573        // Verify everything
574        assert!(graph.has_type_definition("User"));
575        assert!(graph.has_type_definition("Post"));
576        assert_eq!(graph.get_resolved_types().len(), 2);
577
578        let deps = graph.get_dependencies("Post");
579        assert!(deps.is_some());
580        assert!(deps.unwrap().contains("User"));
581
582        // Test topological sort
583        let mut types = HashSet::new();
584        types.insert("User".to_string());
585        types.insert("Post".to_string());
586        let sorted = graph.topological_sort_types(&types);
587        assert_eq!(sorted, vec!["User", "Post"]);
588    }
589
590    #[test]
591    fn test_visualize_dependencies_is_deterministic() {
592        let mut graph1 = TypeDependencyGraph::new();
593        graph1.add_resolved_type("Beta".to_string(), create_test_struct("Beta", "beta.rs"));
594        graph1.add_resolved_type("Alpha".to_string(), create_test_struct("Alpha", "alpha.rs"));
595        graph1.add_dependency("Beta".to_string(), "Delta".to_string());
596        graph1.add_dependency("Beta".to_string(), "Gamma".to_string());
597        graph1.add_dependency("Alpha".to_string(), "Gamma".to_string());
598
599        let mut graph2 = TypeDependencyGraph::new();
600        graph2.add_resolved_type("Alpha".to_string(), create_test_struct("Alpha", "alpha.rs"));
601        graph2.add_resolved_type("Beta".to_string(), create_test_struct("Beta", "beta.rs"));
602        graph2.add_dependency("Alpha".to_string(), "Gamma".to_string());
603        graph2.add_dependency("Beta".to_string(), "Gamma".to_string());
604        graph2.add_dependency("Beta".to_string(), "Delta".to_string());
605
606        let commands1 = vec![
607            create_test_command("beta_command", "beta.rs", 20),
608            create_test_command("alpha_command", "alpha.rs", 10),
609        ];
610        let commands2 = vec![
611            create_test_command("alpha_command", "alpha.rs", 10),
612            create_test_command("beta_command", "beta.rs", 20),
613        ];
614
615        assert_eq!(
616            graph1.visualize_dependencies(&commands1),
617            graph2.visualize_dependencies(&commands2)
618        );
619    }
620
621    #[test]
622    fn test_generate_dot_graph_is_deterministic() {
623        let mut graph1 = TypeDependencyGraph::new();
624        graph1.add_resolved_type("Beta".to_string(), create_test_struct("Beta", "beta.rs"));
625        graph1.add_resolved_type("Alpha".to_string(), create_test_struct("Alpha", "alpha.rs"));
626        graph1.add_dependency("Beta".to_string(), "Delta".to_string());
627        graph1.add_dependency("Beta".to_string(), "Gamma".to_string());
628        graph1.add_dependency("Alpha".to_string(), "Gamma".to_string());
629
630        let mut graph2 = TypeDependencyGraph::new();
631        graph2.add_resolved_type("Alpha".to_string(), create_test_struct("Alpha", "alpha.rs"));
632        graph2.add_resolved_type("Beta".to_string(), create_test_struct("Beta", "beta.rs"));
633        graph2.add_dependency("Alpha".to_string(), "Gamma".to_string());
634        graph2.add_dependency("Beta".to_string(), "Gamma".to_string());
635        graph2.add_dependency("Beta".to_string(), "Delta".to_string());
636
637        let commands1 = vec![
638            create_test_command("beta_command", "beta.rs", 20),
639            create_test_command("alpha_command", "alpha.rs", 10),
640        ];
641        let commands2 = vec![
642            create_test_command("alpha_command", "alpha.rs", 10),
643            create_test_command("beta_command", "beta.rs", 20),
644        ];
645
646        assert_eq!(
647            graph1.generate_dot_graph(&commands1),
648            graph2.generate_dot_graph(&commands2)
649        );
650    }
651}