Skip to main content

forgekit_core/analysis/
modules.rs

1//! Module dependency analysis
2//!
3//! Analyzes imports and dependencies between modules.
4
5use crate::error::{ForgeError, Result};
6use std::collections::{HashMap, HashSet};
7use std::path::Path;
8
9/// Module dependency analyzer
10pub struct ModuleAnalyzer<'a> {
11    db_path: &'a Path,
12}
13
14impl<'a> ModuleAnalyzer<'a> {
15    /// Create a new module analyzer
16    pub fn new(db_path: &'a Path) -> Self {
17        Self { db_path }
18    }
19
20    /// Analyze module dependencies from the graph database
21    pub fn analyze_dependencies(&self) -> Result<ModuleDependencyGraph> {
22        use sqlitegraph::{open_graph, snapshot::SnapshotId, GraphConfig};
23
24        let config = GraphConfig::sqlite();
25        let backend = open_graph(self.db_path, &config)
26            .map_err(|e| ForgeError::DatabaseError(format!("Failed to open graph: {}", e)))?;
27
28        let snapshot = SnapshotId::current();
29        let mut modules: HashMap<String, ModuleInfo> = HashMap::new();
30
31        let entity_ids = backend
32            .entity_ids()
33            .map_err(|e| ForgeError::DatabaseError(format!("Failed to list entities: {}", e)))?;
34
35        // Collect modules
36        for id in entity_ids.clone() {
37            if let Ok(node) = backend.get_node(snapshot, id) {
38                if node.kind == "module" {
39                    modules.insert(
40                        node.name.clone(),
41                        ModuleInfo {
42                            id,
43                            name: node.name,
44                            file_path: node.file_path.clone().unwrap_or_default(),
45                            symbols: Vec::new(),
46                            imports: HashSet::new(),
47                            exports: HashSet::new(),
48                        },
49                    );
50                }
51            }
52        }
53
54        // Build cross-file dependencies
55        let mut dependencies: HashMap<String, HashSet<String>> = HashMap::new();
56
57        for id in entity_ids {
58            if let Ok(node) = backend.get_node(snapshot, id) {
59                let from_file = node.file_path.clone().unwrap_or_default();
60
61                if let Ok(outgoing) = backend.fetch_outgoing(id) {
62                    for target_id in outgoing {
63                        if let Ok(target) = backend.get_node(snapshot, target_id) {
64                            let to_file = target.file_path.unwrap_or_default();
65                            if from_file != to_file && !from_file.is_empty() && !to_file.is_empty()
66                            {
67                                dependencies
68                                    .entry(from_file.clone())
69                                    .or_default()
70                                    .insert(to_file);
71                            }
72                        }
73                    }
74                }
75            }
76        }
77
78        Ok(ModuleDependencyGraph {
79            modules,
80            dependencies,
81        })
82    }
83
84    /// Find circular dependencies
85    pub fn find_cycles(&self) -> Result<Vec<Vec<String>>> {
86        let graph = self.analyze_dependencies()?;
87
88        let mut cycles = Vec::new();
89        let mut visited = HashSet::new();
90        let mut path = Vec::new();
91        let mut path_set = HashSet::new();
92
93        fn dfs(
94            node: &str,
95            dependencies: &HashMap<String, HashSet<String>>,
96            visited: &mut HashSet<String>,
97            path: &mut Vec<String>,
98            path_set: &mut HashSet<String>,
99            cycles: &mut Vec<Vec<String>>,
100        ) {
101            if path_set.contains(node) {
102                if let Some(start) = path.iter().position(|x| x == node) {
103                    let cycle: Vec<String> = path[start..].to_vec();
104                    cycles.push(cycle);
105                }
106                return;
107            }
108
109            if visited.contains(node) {
110                return;
111            }
112
113            visited.insert(node.to_string());
114            path.push(node.to_string());
115            path_set.insert(node.to_string());
116
117            if let Some(deps) = dependencies.get(node) {
118                for dep in deps {
119                    dfs(dep, dependencies, visited, path, path_set, cycles);
120                }
121            }
122
123            path.pop();
124            path_set.remove(node);
125        }
126
127        for file in graph.dependencies.keys() {
128            if !visited.contains(file) {
129                dfs(
130                    file,
131                    &graph.dependencies,
132                    &mut visited,
133                    &mut path,
134                    &mut path_set,
135                    &mut cycles,
136                );
137            }
138        }
139
140        Ok(cycles)
141    }
142}
143
144/// Information about a module
145#[derive(Debug, Clone)]
146pub struct ModuleInfo {
147    pub id: i64,
148    pub name: String,
149    pub file_path: String,
150    pub symbols: Vec<String>,
151    pub imports: HashSet<String>,
152    pub exports: HashSet<String>,
153}
154
155/// Module dependency graph
156#[derive(Debug)]
157pub struct ModuleDependencyGraph {
158    pub modules: HashMap<String, ModuleInfo>,
159    pub dependencies: HashMap<String, HashSet<String>>,
160}
161
162impl ModuleDependencyGraph {
163    /// Get all modules that depend on the given module
164    pub fn dependents(&self, module_file: &str) -> Vec<&str> {
165        self.dependencies
166            .iter()
167            .filter(|(_, deps)| deps.contains(module_file))
168            .map(|(file, _)| file.as_str())
169            .collect()
170    }
171
172    /// Get the dependency depth
173    pub fn dependency_depth(&self) -> usize {
174        let mut max_depth = 0;
175
176        for start in self.dependencies.keys() {
177            let mut visited = HashSet::new();
178            let mut queue: Vec<(String, usize)> = vec![(start.clone(), 0)];
179
180            while let Some((current, depth)) = queue.pop() {
181                if visited.contains(&current) {
182                    continue;
183                }
184                visited.insert(current.clone());
185                max_depth = max_depth.max(depth);
186
187                if let Some(deps) = self.dependencies.get(&current) {
188                    for dep in deps {
189                        queue.push((dep.clone(), depth + 1));
190                    }
191                }
192            }
193        }
194
195        max_depth
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use tempfile::tempdir;
203
204    #[test]
205    fn test_analyzer_creation() {
206        let temp = tempdir().unwrap();
207        let db_path = temp.path().join("test.db");
208
209        let analyzer = ModuleAnalyzer::new(&db_path);
210        // Verify creation
211        assert!(!analyzer.db_path.exists());
212    }
213}