forgekit_core/analysis/
modules.rs1use crate::error::{ForgeError, Result};
6use std::collections::{HashMap, HashSet};
7use std::path::Path;
8
9pub struct ModuleAnalyzer<'a> {
11 db_path: &'a Path,
12}
13
14impl<'a> ModuleAnalyzer<'a> {
15 pub fn new(db_path: &'a Path) -> Self {
17 Self { db_path }
18 }
19
20 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 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 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 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#[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#[derive(Debug)]
157pub struct ModuleDependencyGraph {
158 pub modules: HashMap<String, ModuleInfo>,
159 pub dependencies: HashMap<String, HashSet<String>>,
160}
161
162impl ModuleDependencyGraph {
163 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 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(¤t) {
182 continue;
183 }
184 visited.insert(current.clone());
185 max_depth = max_depth.max(depth);
186
187 if let Some(deps) = self.dependencies.get(¤t) {
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 assert!(!analyzer.db_path.exists());
212 }
213}