1mod dominators;
6mod paths;
7mod test_cfg;
8mod types;
9
10pub use dominators::DominatorTree;
11pub use paths::{Path, PathBuilder};
12pub use test_cfg::TestCfg;
13pub use types::Loop;
14
15use crate::error::Result;
16use crate::storage::UnifiedGraphStore;
17use crate::types::{BlockId, SymbolId};
18use std::sync::Arc;
19
20#[derive(Clone)]
21pub struct CfgModule {
22 store: Arc<UnifiedGraphStore>,
23}
24
25#[derive(Clone, Debug)]
26pub struct FunctionCfg {
27 pub symbol_id: SymbolId,
28 pub name: String,
29 pub cfg: TestCfg,
30}
31
32impl CfgModule {
33 pub(crate) fn new(store: Arc<UnifiedGraphStore>) -> Self {
34 Self { store }
35 }
36
37 pub async fn index(&self) -> Result<()> {
38 Ok(())
39 }
40
41 pub async fn extract_function_cfg(
42 &self,
43 _file_path: &std::path::Path,
44 function_name: &str,
45 ) -> Result<Option<TestCfg>> {
46 if !self.store.db_path.exists() {
47 return Ok(None);
48 }
49 let conn = rusqlite::Connection::open(&self.store.db_path)
50 .map_err(|e| crate::error::ForgeError::DatabaseError(format!("Open db: {}", e)))?;
51 let entity_id: Option<i64> = conn
52 .query_row(
53 "SELECT id FROM graph_entities WHERE name = ?1
54 AND kind IN ('fn', 'function') LIMIT 1",
55 rusqlite::params![function_name],
56 |row| row.get(0),
57 )
58 .ok();
59 match entity_id {
60 Some(id) => load_test_cfg(&self.store.db_path, id),
61 None => Ok(None),
62 }
63 }
64
65 pub fn paths(&self, function: SymbolId) -> PathBuilder {
66 PathBuilder {
67 function: Some(function),
68 store: Some(Arc::clone(&self.store)),
69 ..PathBuilder::default()
70 }
71 }
72
73 pub async fn dominators(&self, function: SymbolId) -> Result<DominatorTree> {
74 if let Some(cfg) = load_test_cfg(&self.store.db_path, function.0)? {
75 let dom_tree = cfg.compute_dominators();
76 return Ok(dom_tree);
77 }
78
79 let cfg = TestCfg::chain(0, 5);
80 Ok(cfg.compute_dominators())
81 }
82
83 pub async fn loops(&self, function: SymbolId) -> Result<Vec<Loop>> {
84 if let Some(cfg) = load_test_cfg(&self.store.db_path, function.0)? {
85 return Ok(cfg.detect_loops());
86 }
87
88 let cfg = TestCfg::simple_loop();
89 let loops = cfg.detect_loops();
90
91 Ok(loops)
92 }
93
94 pub async fn detect_cycles(&self) -> Result<CycleReport> {
95 if !self.store.db_path.exists() {
96 return Ok(CycleReport { cycles: Vec::new() });
97 }
98 mirage::forge::detect_cycles(&self.store.db_path)
99 .map(|report| CycleReport {
100 cycles: report
101 .cycles
102 .into_iter()
103 .map(|c| CallCycle {
104 members: c,
105 depth: 0,
106 })
107 .collect(),
108 })
109 .map_err(|e| {
110 crate::error::ForgeError::DatabaseError(format!("Cycle detection failed: {}", e))
111 })
112 }
113
114 pub async fn dead_symbols(&self, entry_symbol: &str) -> Result<Vec<DeadSymbol>> {
115 if !self.store.db_path.exists() {
116 return Ok(Vec::new());
117 }
118 mirage::forge::find_dead_symbols(entry_symbol, &self.store.db_path)
119 .map(|symbols| {
120 symbols
121 .into_iter()
122 .map(|d| DeadSymbol {
123 name: d.name,
124 kind: d.kind,
125 file_path: d.file_path,
126 })
127 .collect()
128 })
129 .map_err(|e| {
130 crate::error::ForgeError::DatabaseError(format!(
131 "Dead symbol analysis failed: {}",
132 e
133 ))
134 })
135 }
136
137 pub async fn reachable_symbols(&self, symbol_id: &str) -> Result<Vec<DeadSymbol>> {
138 if !self.store.db_path.exists() {
139 return Ok(Vec::new());
140 }
141 mirage::forge::reachable_symbols(symbol_id, &self.store.db_path)
142 .map(|symbols| {
143 symbols
144 .into_iter()
145 .map(|s| DeadSymbol {
146 name: s.name,
147 kind: s.kind,
148 file_path: s.file_path,
149 })
150 .collect()
151 })
152 .map_err(|e| {
153 crate::error::ForgeError::DatabaseError(format!(
154 "Reachability analysis failed: {}",
155 e
156 ))
157 })
158 }
159
160 pub async fn callees(
161 &self,
162 function_name: &str,
163 file_filter: Option<&str>,
164 ) -> Result<Vec<i64>> {
165 if !self.store.db_path.exists() {
166 return Ok(Vec::new());
167 }
168 mirage::forge::get_callees(function_name, file_filter, &self.store.db_path).map_err(|e| {
169 crate::error::ForgeError::DatabaseError(format!("Callee lookup failed: {}", e))
170 })
171 }
172
173 pub async fn resolve_function(&self, name: &str, file_filter: Option<&str>) -> Result<i64> {
174 if !self.store.db_path.exists() {
175 return Err(crate::error::ForgeError::DatabaseError(
176 "graph DB not found".to_string(),
177 ));
178 }
179 mirage::forge::resolve_function(name, file_filter, &self.store.db_path).map_err(|e| {
180 crate::error::ForgeError::DatabaseError(format!("Function resolution failed: {}", e))
181 })
182 }
183
184 pub async fn database_status(&self) -> Result<Option<DatabaseStatus>> {
185 if !self.store.db_path.exists() {
186 return Ok(None);
187 }
188 mirage::forge::database_status(&self.store.db_path)
189 .map(|status| {
190 Some(DatabaseStatus {
191 cfg_blocks: status.cfg_blocks,
192 cfg_paths: status.cfg_paths,
193 cfg_dominators: status.cfg_dominators,
194 mirage_schema_version: status.mirage_schema_version,
195 magellan_schema_version: status.magellan_schema_version,
196 })
197 })
198 .map_err(|e| {
199 crate::error::ForgeError::DatabaseError(format!("Status check failed: {}", e))
200 })
201 }
202}
203
204#[derive(Debug, Clone)]
205pub struct CycleReport {
206 pub cycles: Vec<CallCycle>,
207}
208
209#[derive(Debug, Clone)]
210pub struct CallCycle {
211 pub members: Vec<String>,
212 pub depth: usize,
213}
214
215#[derive(Debug, Clone)]
216pub struct DeadSymbol {
217 pub name: String,
218 pub kind: String,
219 pub file_path: String,
220}
221
222#[derive(Debug, Clone)]
223pub struct DatabaseStatus {
224 pub cfg_blocks: i64,
225 pub cfg_paths: i64,
226 pub cfg_dominators: i64,
227 pub mirage_schema_version: i32,
228 pub magellan_schema_version: i32,
229}
230
231fn load_test_cfg(
232 db_path: &std::path::Path,
233 function_id: i64,
234) -> crate::error::Result<Option<TestCfg>> {
235 use rusqlite::{params, Connection};
236
237 let graph_db = db_path;
238 if !graph_db.exists() {
239 return Ok(None);
240 }
241
242 let backend = match mirage::Backend::detect_and_open(graph_db) {
243 Ok(b) => b,
244 Err(_) => return Ok(None),
245 };
246
247 let blocks = match backend.get_cfg_blocks(function_id) {
248 Ok(b) => b,
249 Err(_) => return Ok(None),
250 };
251
252 if blocks.is_empty() {
253 return Ok(None);
254 }
255
256 let entry = BlockId(blocks[0].id);
257 let mut cfg = TestCfg::new(entry);
258
259 let mut has_real_edges = false;
260 if let Ok(conn) = Connection::open(graph_db) {
261 let query = r#"
262 SELECT source_idx, target_idx, edge_type
263 FROM cfg_edges
264 WHERE function_id = ?1
265 ORDER BY id
266 "#;
267 if let Ok(mut stmt) = conn.prepare(query) {
268 if let Ok(rows) = stmt.query_map(params![function_id], |row| {
269 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
270 }) {
271 for row in rows.flatten() {
272 let src = BlockId(blocks.get(row.0 as usize).map(|b| b.id).unwrap_or(row.0));
273 let dst = BlockId(blocks.get(row.1 as usize).map(|b| b.id).unwrap_or(row.1));
274 cfg.add_edge(src, dst);
275 has_real_edges = true;
276 }
277 }
278 }
279 }
280
281 if !has_real_edges {
282 for i in 0..blocks.len().saturating_sub(1) {
283 cfg.add_edge(BlockId(blocks[i].id), BlockId(blocks[i + 1].id));
284 }
285 }
286
287 for b in &blocks {
288 if b.terminator == "return" || b.terminator == "throw" {
289 cfg.add_exit(BlockId(b.id));
290 }
291 }
292 if cfg.exits.is_empty() {
293 if let Some(last) = blocks.last() {
294 cfg.add_exit(BlockId(last.id));
295 }
296 }
297
298 Ok(Some(cfg))
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::storage::BackendKind;
305
306 #[tokio::test]
307 async fn test_cfg_module_creation() {
308 let store = Arc::new(
309 UnifiedGraphStore::open(tempfile::tempdir().unwrap().path(), BackendKind::SQLite)
310 .await
311 .unwrap(),
312 );
313 let module = CfgModule::new(Arc::clone(&store));
314
315 assert_eq!(module.store.db_path(), store.db_path());
316 }
317
318 #[tokio::test]
319 async fn test_path_builder_filters() {
320 let store = Arc::new(
321 UnifiedGraphStore::open(tempfile::tempdir().unwrap().path(), BackendKind::SQLite)
322 .await
323 .unwrap(),
324 );
325
326 let dummy_module = CfgModule {
327 store: Arc::clone(&store),
328 };
329
330 let builder = dummy_module.paths(SymbolId(1)).normal_only().max_length(10);
331
332 assert!(builder.normal_only);
333 assert_eq!(builder.max_length, Some(10));
334 }
335
336 #[tokio::test]
337 async fn test_dominators_basic() {
338 let store = Arc::new(
339 UnifiedGraphStore::open(tempfile::tempdir().unwrap().path(), BackendKind::SQLite)
340 .await
341 .unwrap(),
342 );
343 let module = CfgModule::new(store);
344
345 let doms = module.dominators(SymbolId(1)).await.unwrap();
346 assert_eq!(doms.root, BlockId(0));
347 assert_eq!(doms.dominators.len(), 4);
348 }
349
350 #[tokio::test]
351 async fn test_loops_detection() {
352 let store = Arc::new(
353 UnifiedGraphStore::open(tempfile::tempdir().unwrap().path(), BackendKind::SQLite)
354 .await
355 .unwrap(),
356 );
357 let module = CfgModule::new(store);
358
359 let loops = module.loops(SymbolId(1)).await.unwrap();
360 assert_eq!(loops.len(), 1);
361 }
362
363 #[tokio::test]
364 async fn test_paths_execute_no_db_returns_synthetic() {
365 let store = Arc::new(
366 UnifiedGraphStore::open(tempfile::tempdir().unwrap().path(), BackendKind::SQLite)
367 .await
368 .unwrap(),
369 );
370 let module = CfgModule::new(store);
371
372 let paths = module.paths(SymbolId(1)).execute().await.unwrap();
373 assert!(!paths.is_empty());
374 assert_eq!(paths[0].blocks.len(), 1);
375 assert_eq!(paths[0].blocks[0], BlockId(1));
376 }
377
378 fn make_cfg_fixture_db(db_path: &std::path::Path, fn_name: &str) -> i64 {
379 use crate::storage::{open_graph, GraphConfig, NodeSpec};
380 let config = GraphConfig::sqlite();
381 let backend = open_graph(db_path, &config).unwrap();
382 let node = NodeSpec {
383 kind: "fn".to_string(),
384 name: fn_name.to_string(),
385 file_path: Some("src/lib.rs".to_string()),
386 data: serde_json::Value::Null,
387 };
388 let entity_id = backend.insert_node(node).unwrap();
389 drop(backend);
390
391 let conn = rusqlite::Connection::open(db_path).unwrap();
392 conn.execute_batch(
393 "CREATE TABLE IF NOT EXISTS cfg_blocks (
394 id INTEGER PRIMARY KEY AUTOINCREMENT,
395 function_id INTEGER NOT NULL,
396 kind TEXT NOT NULL,
397 terminator TEXT NOT NULL,
398 byte_start INTEGER,
399 byte_end INTEGER,
400 start_line INTEGER,
401 start_col INTEGER,
402 end_line INTEGER,
403 end_col INTEGER,
404 coord_x INTEGER DEFAULT 0,
405 coord_y INTEGER DEFAULT 0,
406 coord_z INTEGER DEFAULT 0,
407 cfg_condition TEXT
408 );",
409 )
410 .unwrap();
411 conn.execute(
412 "INSERT INTO cfg_blocks
413 (function_id, kind, terminator, byte_start, byte_end,
414 start_line, start_col, end_line, end_col)
415 VALUES (?1, 'entry', 'return', 0, 50, 1, 0, 5, 0)",
416 rusqlite::params![entity_id],
417 )
418 .unwrap();
419 entity_id
420 }
421
422 #[tokio::test]
423 async fn test_extract_function_cfg_finds_fn_entity() {
424 let dir = tempfile::tempdir().unwrap();
425 let db_path = dir.path().join("cfg_test.db");
426
427 make_cfg_fixture_db(&db_path, "compute_flow");
428
429 let store = Arc::new(
430 crate::storage::UnifiedGraphStore::open_with_path(
431 dir.path(),
432 &db_path,
433 BackendKind::SQLite,
434 )
435 .await
436 .unwrap(),
437 );
438 let module = CfgModule::new(store);
439
440 let result = module
441 .extract_function_cfg(std::path::Path::new("src/lib.rs"), "compute_flow")
442 .await
443 .unwrap();
444
445 assert!(
446 result.is_some(),
447 "should return Some(cfg) when function entity and CFG blocks exist"
448 );
449 }
450}