use ahash::{AHashMap, AHashSet};
use anyhow::Result;
use rusqlite::params;
use sqlitegraph::errors::SqliteGraphError;
use sqlitegraph::{GraphBackend, SnapshotId};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use crate::graph::schema::SymbolNode;
use super::CodeGraph;
fn reachable_from(
backend: &dyn GraphBackend,
start: i64,
) -> Result<AHashSet<i64>, SqliteGraphError> {
let mut visited = AHashSet::new();
let mut queue = VecDeque::new();
visited.insert(start);
queue.push_back(start);
while let Some(node) = queue.pop_front() {
for neighbor in backend.fetch_outgoing(node)? {
if visited.insert(neighbor) {
queue.push_back(neighbor);
}
}
}
Ok(visited)
}
fn reverse_reachable_from(
backend: &dyn GraphBackend,
start: i64,
) -> Result<AHashSet<i64>, SqliteGraphError> {
let mut visited = AHashSet::new();
let mut queue = VecDeque::new();
visited.insert(start);
queue.push_back(start);
while let Some(node) = queue.pop_front() {
for neighbor in backend.fetch_incoming(node)? {
if visited.insert(neighbor) {
queue.push_back(neighbor);
}
}
}
Ok(visited)
}
#[derive(Debug, Clone)]
struct SccCollapseResult {
_node_to_supernode: AHashMap<i64, i64>,
supernode_members: AHashMap<i64, AHashSet<i64>>,
supernode_edges: Vec<(i64, i64)>,
_num_sccs: usize,
}
fn collapse_sccs(backend: &dyn GraphBackend) -> Result<SccCollapseResult, SqliteGraphError> {
let scc_result = strongly_connected_components(backend)?;
if scc_result.components.is_empty() {
return Ok(SccCollapseResult {
_node_to_supernode: AHashMap::new(),
supernode_members: AHashMap::new(),
supernode_edges: Vec::new(),
_num_sccs: 0,
});
}
let mut node_to_supernode: AHashMap<i64, i64> = AHashMap::new();
let mut supernode_members: AHashMap<i64, AHashSet<i64>> = AHashMap::new();
for component in &scc_result.components {
let supernode_id = component[0]; let mut members = AHashSet::new();
for &node in component {
node_to_supernode.insert(node, supernode_id);
members.insert(node);
}
supernode_members.insert(supernode_id, members);
}
let mut supernode_edges: Vec<(i64, i64)> = Vec::new();
let mut seen_edges: HashSet<(i64, i64)> = HashSet::new();
for (&node, &supernode) in &node_to_supernode {
for neighbor in backend.fetch_outgoing(node)? {
if let Some(&neighbor_supernode) = node_to_supernode.get(&neighbor) {
if supernode != neighbor_supernode {
let edge = (supernode, neighbor_supernode);
if seen_edges.insert(edge) {
supernode_edges.push(edge);
}
}
}
}
}
supernode_edges.sort();
Ok(SccCollapseResult {
_node_to_supernode: node_to_supernode,
supernode_members,
supernode_edges,
_num_sccs: scc_result.components.len(),
})
}
#[derive(Debug, Clone)]
struct InternalPathEnumerationResult {
paths: Vec<Vec<i64>>,
total_found: usize,
pruned_by_bounds: usize,
_max_depth_reached: usize,
}
#[derive(Debug, Clone)]
struct PathEnumerationConfig {
max_depth: usize,
max_paths: usize,
revisit_cap: usize,
exit_nodes: Option<AHashSet<i64>>,
_error_nodes: Option<AHashSet<i64>>,
}
impl Default for PathEnumerationConfig {
fn default() -> Self {
Self {
max_depth: 100,
max_paths: 1000,
revisit_cap: 100,
exit_nodes: None,
_error_nodes: None,
}
}
}
fn enumerate_paths(
backend: &dyn GraphBackend,
entry: i64,
config: &PathEnumerationConfig,
) -> Result<InternalPathEnumerationResult, SqliteGraphError> {
let mut paths = Vec::new();
let mut current_path = Vec::new();
let mut visit_count: AHashMap<i64, usize> = AHashMap::new();
let mut total_found = 0usize;
let mut pruned_by_bounds = 0usize;
let mut max_depth_reached = 0usize;
dfs_enumerate(
backend,
entry,
config,
&mut current_path,
&mut visit_count,
&mut paths,
&mut total_found,
&mut pruned_by_bounds,
&mut max_depth_reached,
)?;
Ok(InternalPathEnumerationResult {
paths,
total_found,
pruned_by_bounds,
_max_depth_reached: max_depth_reached,
})
}
#[allow(clippy::too_many_arguments)]
fn dfs_enumerate(
backend: &dyn GraphBackend,
node: i64,
config: &PathEnumerationConfig,
current_path: &mut Vec<i64>,
visit_count: &mut AHashMap<i64, usize>,
all_paths: &mut Vec<Vec<i64>>,
total_found: &mut usize,
pruned_by_bounds: &mut usize,
max_depth_reached: &mut usize,
) -> Result<(), SqliteGraphError> {
let count = visit_count.entry(node).or_insert(0);
*count += 1;
if *count > config.revisit_cap {
visit_count.entry(node).and_modify(|e| *e -= 1);
return Ok(());
}
current_path.push(node);
let current_depth = current_path.len();
*max_depth_reached = (*max_depth_reached).max(current_depth);
if current_depth >= config.max_depth {
*pruned_by_bounds += 1;
current_path.pop();
visit_count.entry(node).and_modify(|e| *e -= 1);
return Ok(());
}
if let Some(ref exits) = config.exit_nodes {
if exits.contains(&node) && current_depth > 1 {
if all_paths.len() < config.max_paths {
all_paths.push(current_path.clone());
}
*total_found += 1;
current_path.pop();
visit_count.entry(node).and_modify(|e| *e -= 1);
return Ok(());
}
}
let neighbors = backend.fetch_outgoing(node)?;
let mut had_successors = false;
for neighbor in neighbors {
had_successors = true;
dfs_enumerate(
backend,
neighbor,
config,
current_path,
visit_count,
all_paths,
total_found,
pruned_by_bounds,
max_depth_reached,
)?;
}
if !had_successors && config.exit_nodes.is_none() && current_depth > 1 {
if all_paths.len() < config.max_paths {
all_paths.push(current_path.clone());
}
*total_found += 1;
}
current_path.pop();
visit_count.entry(node).and_modify(|e| *e -= 1);
Ok(())
}
#[derive(Debug, Clone)]
struct SccResult {
components: Vec<Vec<i64>>,
node_to_component: AHashMap<i64, usize>,
}
fn strongly_connected_components(
backend: &dyn GraphBackend,
) -> Result<SccResult, SqliteGraphError> {
let all_ids = backend.all_entity_ids()?;
if all_ids.is_empty() {
return Ok(SccResult {
components: Vec::new(),
node_to_component: AHashMap::new(),
});
}
let mut index = 0usize;
let mut stack: Vec<i64> = Vec::new();
let mut on_stack: HashSet<i64> = HashSet::new();
let mut indices: AHashMap<i64, usize> = AHashMap::new();
let mut lowlinks: AHashMap<i64, usize> = AHashMap::new();
let mut components: Vec<Vec<i64>> = Vec::new();
let mut node_to_component: AHashMap<i64, usize> = AHashMap::new();
fn strongconnect(
v: i64,
backend: &dyn GraphBackend,
index: &mut usize,
stack: &mut Vec<i64>,
on_stack: &mut HashSet<i64>,
indices: &mut AHashMap<i64, usize>,
lowlinks: &mut AHashMap<i64, usize>,
components: &mut Vec<Vec<i64>>,
node_to_component: &mut AHashMap<i64, usize>,
) -> Result<(), SqliteGraphError> {
indices.insert(v, *index);
lowlinks.insert(v, *index);
*index += 1;
stack.push(v);
on_stack.insert(v);
for w in backend.fetch_outgoing(v)? {
if !indices.contains_key(&w) {
strongconnect(
w,
backend,
index,
stack,
on_stack,
indices,
lowlinks,
components,
node_to_component,
)?;
let v_low = lowlinks[&v];
let w_low = lowlinks[&w];
lowlinks.insert(v, v_low.min(w_low));
} else if on_stack.contains(&w) {
let v_low = lowlinks[&v];
let w_idx = indices[&w];
lowlinks.insert(v, v_low.min(w_idx));
}
}
if lowlinks[&v] == indices[&v] {
let mut component = Vec::new();
loop {
let w = stack.pop().unwrap(); on_stack.remove(&w);
node_to_component.insert(w, components.len());
component.push(w);
if w == v {
break;
}
}
components.push(component);
}
Ok(())
}
for &node in &all_ids {
if !indices.contains_key(&node) {
strongconnect(
node,
backend,
&mut index,
&mut stack,
&mut on_stack,
&mut indices,
&mut lowlinks,
&mut components,
&mut node_to_component,
)?;
}
}
Ok(SccResult {
components,
node_to_component,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymbolInfo {
pub symbol_id: Option<String>,
pub fqn: Option<String>,
pub file_path: String,
pub kind: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeadSymbol {
pub symbol: SymbolInfo,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CycleKind {
MutualRecursion,
SelfLoop,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cycle {
pub members: Vec<SymbolInfo>,
pub kind: CycleKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CycleReport {
pub cycles: Vec<Cycle>,
pub total_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Supernode {
pub id: i64,
pub members: Vec<SymbolInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CondensationGraph {
pub supernodes: Vec<Supernode>,
pub edges: Vec<(i64, i64)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CondensationResult {
pub graph: CondensationGraph,
pub original_to_supernode: HashMap<String, i64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SliceDirection {
Backward,
Forward,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgramSlice {
pub target: SymbolInfo,
pub direction: SliceDirection,
pub included_symbols: Vec<SymbolInfo>,
pub symbol_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SliceResult {
pub slice: ProgramSlice,
pub statistics: SliceStatistics,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SliceStatistics {
pub total_symbols: usize,
pub data_dependencies: usize,
pub control_dependencies: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecutionPath {
pub symbols: Vec<SymbolInfo>,
pub length: usize,
}
#[derive(Debug, Clone)]
pub struct PathEnumerationResult {
pub paths: Vec<ExecutionPath>,
pub total_enumerated: usize,
pub bounded_hit: bool,
pub statistics: PathStatistics,
}
#[derive(Debug, Clone)]
pub struct PathStatistics {
pub avg_length: f64,
pub min_length: usize,
pub max_length: usize,
pub unique_symbols: usize,
}
impl CodeGraph {
pub fn resolve_symbol_entity(&self, symbol_id_or_fqn: &str) -> Result<i64> {
let conn = self.chunks.connect()?;
let mut stmt = conn
.prepare_cached(
"SELECT id FROM graph_entities
WHERE kind = 'Symbol'
AND json_extract(data, '$.symbol_id') = ?1",
)
.map_err(|e| anyhow::anyhow!("Failed to prepare symbol ID query: {}", e))?;
let result = stmt.query_row(params![symbol_id_or_fqn], |row| row.get::<_, i64>(0));
match result {
Ok(entity_id) => return Ok(entity_id),
Err(rusqlite::Error::QueryReturnedNoRows) => {
}
Err(e) => {
return Err(anyhow::anyhow!("Failed to query symbol ID: {}", e));
}
}
let mut stmt = conn
.prepare_cached(
"SELECT id FROM graph_entities
WHERE kind = 'Symbol'
AND (json_extract(data, '$.fqn') = ?1
OR json_extract(data, '$.display_fqn') = ?1
OR json_extract(data, '$.canonical_fqn') = ?1)",
)
.map_err(|e| anyhow::anyhow!("Failed to prepare FQN query: {}", e))?;
stmt.query_row(params![symbol_id_or_fqn], |row| row.get::<_, i64>(0))
.map_err(|e| match e {
rusqlite::Error::QueryReturnedNoRows => {
anyhow::anyhow!(
"Symbol '{}' not found in database (tried symbol_id, fqn, display_fqn, canonical_fqn)",
symbol_id_or_fqn
)
}
_ => anyhow::anyhow!("Failed to query symbol by FQN: {}", e),
})
}
pub fn symbol_by_entity_id(&self, entity_id: i64) -> Result<SymbolInfo> {
let snapshot = SnapshotId::current();
let node = self
.calls
.backend
.get_node(snapshot, entity_id)
.map_err(|e| anyhow::anyhow!("Failed to get entity {}: {}", entity_id, e))?;
if node.kind != "Symbol" {
return Err(anyhow::anyhow!(
"Entity {} is not a Symbol (kind: {})",
entity_id,
node.kind
));
}
let symbol_node: SymbolNode = serde_json::from_value(node.data)
.map_err(|e| anyhow::anyhow!("Failed to parse SymbolNode data: {}", e))?;
Ok(SymbolInfo {
symbol_id: symbol_node.symbol_id,
fqn: symbol_node.fqn.or_else(|| symbol_node.display_fqn.clone()),
file_path: node.file_path.unwrap_or_else(|| "?".to_string()),
kind: symbol_node.kind,
})
}
fn all_call_graph_entities(&self) -> Result<Vec<i64>> {
let conn = self.chunks.connect()?;
let mut stmt = conn
.prepare_cached(
"SELECT DISTINCT from_id FROM graph_edges WHERE edge_type = 'CALLER'
UNION
SELECT DISTINCT to_id FROM graph_edges WHERE edge_type = 'CALLS'",
)
.map_err(|e| anyhow::anyhow!("Failed to prepare call graph query: {}", e))?;
let entity_ids = stmt
.query_map([], |row| row.get::<_, i64>(0))
.map_err(|e| anyhow::anyhow!("Failed to execute call graph query: {}", e))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| anyhow::anyhow!("Failed to collect call graph results: {}", e))?;
Ok(entity_ids)
}
pub fn reachable_symbols(
&self,
symbol_id: &str,
_max_depth: Option<usize>,
) -> Result<Vec<SymbolInfo>> {
let entity_id = self.resolve_symbol_entity(symbol_id)?;
let backend = &*self.calls.backend;
let reachable_entity_ids = reachable_from(backend, entity_id)?;
let mut symbols = Vec::new();
for id in reachable_entity_ids {
if id == entity_id {
continue;
}
if let Ok(info) = self.symbol_by_entity_id(id) {
symbols.push(info);
}
}
symbols.sort_by(|a, b| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
.then_with(|| a.kind.cmp(&b.kind))
});
Ok(symbols)
}
pub fn reverse_reachable_symbols(
&self,
symbol_id: &str,
_max_depth: Option<usize>,
) -> Result<Vec<SymbolInfo>> {
let entity_id = self.resolve_symbol_entity(symbol_id)?;
let backend = &*self.calls.backend;
let reachable_entity_ids = reverse_reachable_from(backend, entity_id)?;
let mut symbols = Vec::new();
for id in reachable_entity_ids {
if id == entity_id {
continue;
}
if let Ok(info) = self.symbol_by_entity_id(id) {
symbols.push(info);
}
}
symbols.sort_by(|a, b| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
.then_with(|| a.kind.cmp(&b.kind))
});
Ok(symbols)
}
pub fn dead_symbols(&self, entry_symbol_id: &str) -> Result<Vec<DeadSymbol>> {
let entry_entity = self.resolve_symbol_entity(entry_symbol_id)?;
let all_entities = self.all_call_graph_entities()?;
let backend = &*self.calls.backend;
let reachable_ids = reachable_from(backend, entry_entity)?;
let reachable_set: HashSet<i64> = reachable_ids.into_iter().collect();
let mut dead_symbols = Vec::new();
for entity_id in all_entities {
if entity_id == entry_entity {
continue;
}
if !reachable_set.contains(&entity_id) {
if let Ok(info) = self.symbol_by_entity_id(entity_id) {
dead_symbols.push(DeadSymbol {
reason: "unreachable from entry point".to_string(),
symbol: info,
});
}
}
}
dead_symbols.sort_by(|a, b| {
a.symbol
.file_path
.cmp(&b.symbol.file_path)
.then_with(|| a.symbol.fqn.as_ref().cmp(&b.symbol.fqn.as_ref()))
.then_with(|| a.symbol.kind.cmp(&b.symbol.kind))
});
Ok(dead_symbols)
}
pub fn detect_cycles(&self) -> Result<CycleReport> {
let backend = &*self.calls.backend;
let scc_result = strongly_connected_components(backend)?;
let cycles: Vec<_> = scc_result
.components
.into_iter()
.filter(|scc| scc.len() > 1)
.map(|members| {
let symbol_infos: Vec<_> = members
.into_iter()
.filter_map(|id| self.symbol_by_entity_id(id).ok())
.collect();
Cycle {
members: symbol_infos,
kind: CycleKind::MutualRecursion,
}
})
.filter(|cycle| !cycle.members.is_empty())
.collect();
let total_count = cycles.len();
let mut cycles = cycles;
cycles.sort_by(|a, b| match (a.members.first(), b.members.first()) {
(Some(am), Some(bm)) => match (am.fqn.as_ref(), bm.fqn.as_ref()) {
(Some(af), Some(bf)) => af.cmp(bf),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
},
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
});
Ok(CycleReport {
cycles,
total_count,
})
}
pub fn find_cycles_containing(&self, symbol_id: &str) -> Result<Vec<Cycle>> {
let entity_id = self.resolve_symbol_entity(symbol_id)?;
let backend = &*self.calls.backend;
let scc_result = strongly_connected_components(backend)?;
let target_component_idx = scc_result.node_to_component.get(&entity_id);
let target_idx = match target_component_idx {
Some(&idx) => idx,
None => return Ok(Vec::new()), };
let target_component = &scc_result.components[target_idx];
if target_component.len() <= 1 {
return Ok(Vec::new());
}
let symbol_infos: Vec<_> = target_component
.iter()
.filter_map(|&id| self.symbol_by_entity_id(id).ok())
.collect();
let cycle = Cycle {
members: symbol_infos,
kind: CycleKind::MutualRecursion,
};
Ok(vec![cycle])
}
pub fn condense_call_graph(&self) -> Result<CondensationResult> {
let backend = &*self.calls.backend;
let collapse_result = collapse_sccs(backend)?;
let mut supernodes = Vec::new();
let mut original_to_supernode = HashMap::new();
for (&supernode_id, member_ids) in &collapse_result.supernode_members {
let symbol_infos: Vec<_> = member_ids
.iter()
.filter_map(|&id| self.symbol_by_entity_id(id).ok())
.collect();
for symbol_info in &symbol_infos {
if let Some(ref sym_id) = symbol_info.symbol_id {
original_to_supernode.insert(sym_id.clone(), supernode_id);
}
}
supernodes.push(Supernode {
id: supernode_id,
members: symbol_infos,
});
}
supernodes.sort_by_key(|a| a.id);
let graph = CondensationGraph {
supernodes,
edges: collapse_result.supernode_edges,
};
Ok(CondensationResult {
graph,
original_to_supernode,
})
}
pub fn backward_slice(&self, symbol_id: &str) -> Result<SliceResult> {
let entity_id = self.resolve_symbol_entity(symbol_id)?;
let backend = &*self.calls.backend;
let target = self.symbol_by_entity_id(entity_id)?;
let caller_entity_ids = reverse_reachable_from(backend, entity_id)?;
let mut included_symbols = Vec::new();
for id in caller_entity_ids {
if id == entity_id {
continue;
}
if let Ok(info) = self.symbol_by_entity_id(id) {
included_symbols.push(info);
}
}
included_symbols.sort_by(|a, b| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
.then_with(|| a.kind.cmp(&b.kind))
});
let symbol_count = included_symbols.len();
Ok(SliceResult {
slice: ProgramSlice {
target,
direction: SliceDirection::Backward,
included_symbols,
symbol_count,
},
statistics: SliceStatistics {
total_symbols: symbol_count,
data_dependencies: 0, control_dependencies: symbol_count,
},
})
}
pub fn forward_slice(&self, symbol_id: &str) -> Result<SliceResult> {
let entity_id = self.resolve_symbol_entity(symbol_id)?;
let backend = &*self.calls.backend;
let target = self.symbol_by_entity_id(entity_id)?;
let callee_entity_ids = reachable_from(backend, entity_id)?;
let mut included_symbols = Vec::new();
for id in callee_entity_ids {
if id == entity_id {
continue;
}
if let Ok(info) = self.symbol_by_entity_id(id) {
included_symbols.push(info);
}
}
included_symbols.sort_by(|a, b| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
.then_with(|| a.kind.cmp(&b.kind))
});
let symbol_count = included_symbols.len();
Ok(SliceResult {
slice: ProgramSlice {
target,
direction: SliceDirection::Forward,
included_symbols,
symbol_count,
},
statistics: SliceStatistics {
total_symbols: symbol_count,
data_dependencies: 0, control_dependencies: symbol_count,
},
})
}
pub fn enumerate_paths(
&self,
start_symbol_id: &str,
end_symbol_id: Option<&str>,
max_depth: usize,
max_paths: usize,
) -> Result<PathEnumerationResult> {
let start_entity_id = self.resolve_symbol_entity(start_symbol_id)?;
let backend = &*self.calls.backend;
let exit_nodes: Option<AHashSet<i64>> = if let Some(end_id) = end_symbol_id {
let end_entity_id = self.resolve_symbol_entity(end_id)?;
let mut set = AHashSet::new();
set.insert(end_entity_id);
Some(set)
} else {
None
};
let config = PathEnumerationConfig {
max_depth,
max_paths,
revisit_cap: 100, exit_nodes,
_error_nodes: None,
};
let enum_result = enumerate_paths(backend, start_entity_id, &config)?;
let mut paths = Vec::new();
let mut all_symbols = HashSet::new();
let mut min_length = usize::MAX;
let mut max_length = 0;
let mut total_length = 0;
for path in enum_result.paths {
let mut symbols = Vec::new();
for &entity_id in &path {
if let Ok(info) = self.symbol_by_entity_id(entity_id) {
all_symbols.insert(info.symbol_id.clone().unwrap_or_default());
symbols.push(info);
}
}
let length = symbols.len();
if length > 0 {
min_length = min_length.min(length);
max_length = max_length.max(length);
total_length += length;
paths.push(ExecutionPath { symbols, length });
}
}
paths.sort_by(|a, b| {
match (
a.symbols.first().and_then(|s| s.fqn.as_ref()),
b.symbols.first().and_then(|s| s.fqn.as_ref()),
) {
(Some(a_fqn), Some(b_fqn)) => {
a_fqn.cmp(b_fqn).then_with(|| a.length.cmp(&b.length))
}
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => a.length.cmp(&b.length),
}
});
let avg_length = if paths.is_empty() {
0.0
} else {
total_length as f64 / paths.len() as f64
};
let bounded_hit = enum_result.pruned_by_bounds > 0;
Ok(PathEnumerationResult {
paths,
total_enumerated: enum_result.total_found,
bounded_hit,
statistics: PathStatistics {
avg_length,
min_length: if min_length == usize::MAX {
0
} else {
min_length
},
max_length,
unique_symbols: all_symbols.len(),
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CodeGraph;
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
fn next_test_dir() -> std::path::PathBuf {
let n = TEST_DIR_COUNTER.fetch_add(1, Ordering::SeqCst);
std::env::temp_dir().join(format!("magellan_test_{}_{}", std::process::id(), n))
}
fn create_test_graph() -> Result<(CodeGraph, String, String)> {
let temp_dir = std::env::temp_dir().join(format!(
"magellan_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
std::fs::create_dir_all(&temp_dir)?;
let db_path = temp_dir.join("test.db");
let source = r#"
fn main() {
helper_a();
helper_b();
}
fn helper_a() {
leaf();
}
fn helper_b() {
leaf();
}
fn leaf() {
println!("leaf");
}
fn unused_function() {
leaf();
}
"#;
let mut graph = CodeGraph::open(&db_path)?;
let test_file = temp_dir.join("test.rs");
std::fs::write(&test_file, source)?;
let path_str = test_file.to_string_lossy().to_string();
let source_bytes = std::fs::read(&test_file)?;
graph.index_file(&path_str, &source_bytes)?;
graph.index_calls(&path_str, &source_bytes)?;
let symbols = crate::graph::query::symbols_in_file(&mut graph, &path_str)?;
let main_id = symbols
.iter()
.find(|s| s.name.as_deref() == Some("main"))
.and_then(|s| s.fqn.clone())
.unwrap_or_default();
let unused_id = symbols
.iter()
.find(|s| s.name.as_deref() == Some("unused_function"))
.and_then(|s| s.fqn.clone())
.unwrap_or_default();
Ok((graph, main_id, unused_id))
}
#[test]
fn test_resolve_symbol_entity_not_found() {
let (graph, _, _) = create_test_graph().unwrap();
let result = graph.resolve_symbol_entity("nonexistent_id_123456789012");
assert!(result.is_err());
}
#[test]
fn test_symbol_by_entity_id() {
let (graph, _, _) = create_test_graph().unwrap();
let entity_ids = graph.calls.backend.entity_ids().unwrap();
let snapshot = SnapshotId::current();
for entity_id in entity_ids {
if let Ok(node) = graph.calls.backend.get_node(snapshot, entity_id) {
if node.kind == "Symbol" {
let info = graph.symbol_by_entity_id(entity_id);
assert!(info.is_ok());
let symbol_info = info.unwrap();
assert!(!symbol_info.file_path.is_empty());
assert!(!symbol_info.kind.is_empty());
return;
}
}
}
panic!("No Symbol entity found in test graph");
}
#[test]
fn test_reachable_symbols_basic() {
let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
let entity_ids = graph.calls.backend.entity_ids().unwrap();
eprintln!("Total entities: {}", entity_ids.len());
let snapshot = SnapshotId::current();
let mut found_symbols = 0;
for entity_id in &entity_ids {
match graph.calls.backend.get_node(snapshot, *entity_id) {
Ok(node) => {
eprintln!(" Entity {}: kind={}", entity_id, node.kind);
if node.kind == "Symbol" {
found_symbols += 1;
}
}
Err(e) => {
eprintln!(" Entity {}: ERROR getting node: {:?}", entity_id, e);
}
}
}
assert!(
found_symbols > 0,
"Should find Symbol entities in test graph, got {} symbols from {} entities",
found_symbols,
entity_ids.len()
);
}
#[test]
fn test_reachable_symbols_max_depth() {
let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
let snapshot = SnapshotId::current();
let entity_ids = graph.calls.backend.entity_ids().unwrap();
let main_entity_id = entity_ids.into_iter().find(|&id| {
if let Ok(node) = graph.calls.backend.get_node(snapshot, id) {
if let Ok(data) = serde_json::from_value::<serde_json::Value>(node.data) {
if let Some(name) = data.get("name").and_then(|v| v.as_str()) {
return name == "main";
}
}
}
false
});
if let Some(entity_id) = main_entity_id {
let result = graph.calls.backend.get_node(snapshot, entity_id);
assert!(result.is_ok(), "Should be able to get main node");
}
}
#[test]
fn test_dead_symbols() {
let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
let entity_ids = graph.calls.backend.entity_ids().unwrap();
assert!(entity_ids.len() > 0, "Should have call graph entities");
}
#[test]
fn test_reverse_reachable_symbols() {
let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
let entity_ids = graph.calls.backend.entity_ids().unwrap();
assert!(entity_ids.len() > 0, "Should have call graph entities");
}
}