use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use super::types::{CallGraph, FunctionRef};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchAnalysis {
pub entry_functions: Vec<FunctionInfo>,
pub leaf_functions: Vec<FunctionInfo>,
pub middle_functions: Vec<FunctionInfo>,
pub orphan_functions: Vec<FunctionInfo>,
pub layers: Vec<Vec<FunctionInfo>>,
pub circular_dependencies: Vec<CycleDependency>,
pub stats: ArchStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionInfo {
pub file: String,
pub name: String,
pub qualified_name: Option<String>,
pub outgoing_calls: usize,
pub incoming_calls: usize,
}
impl From<&FunctionRef> for FunctionInfo {
fn from(func: &FunctionRef) -> Self {
FunctionInfo {
file: func.file.clone(),
name: func.name.clone(),
qualified_name: func.qualified_name.clone(),
outgoing_calls: 0,
incoming_calls: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CycleDependency {
pub cycle: Vec<String>,
pub files: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ArchStats {
pub total_functions: usize,
pub entry_count: usize,
pub leaf_count: usize,
pub middle_count: usize,
pub orphan_count: usize,
pub cycle_count: usize,
pub layer_count: usize,
pub max_depth: usize,
}
pub fn analyze_architecture(graph: &CallGraph) -> ArchAnalysis {
let all_functions: HashSet<_> = graph
.edges
.iter()
.flat_map(|e| [e.caller.clone(), e.callee.clone()])
.collect();
let mut entry_functions = Vec::new();
let mut leaf_functions = Vec::new();
let mut middle_functions = Vec::new();
let mut orphan_functions = Vec::new();
for func in &all_functions {
let has_callers = graph.callers.get(func).map_or(false, |c| !c.is_empty());
let has_callees = graph.callees.get(func).map_or(false, |c| !c.is_empty());
let incoming = graph.callers.get(func).map_or(0, |c| c.len());
let outgoing = graph.callees.get(func).map_or(0, |c| c.len());
let mut info = FunctionInfo::from(func);
info.incoming_calls = incoming;
info.outgoing_calls = outgoing;
match (has_callers, has_callees) {
(false, true) => entry_functions.push(info), (true, false) => leaf_functions.push(info), (true, true) => middle_functions.push(info), (false, false) => orphan_functions.push(info), }
}
entry_functions.sort_by(|a, b| a.name.cmp(&b.name));
leaf_functions.sort_by(|a, b| a.name.cmp(&b.name));
middle_functions.sort_by(|a, b| a.name.cmp(&b.name));
orphan_functions.sort_by(|a, b| a.name.cmp(&b.name));
let circular_dependencies = detect_cycles(graph, &all_functions);
let layers = build_layers(graph, &all_functions, &circular_dependencies);
let max_depth = layers.len();
let stats = ArchStats {
total_functions: all_functions.len(),
entry_count: entry_functions.len(),
leaf_count: leaf_functions.len(),
middle_count: middle_functions.len(),
orphan_count: orphan_functions.len(),
cycle_count: circular_dependencies.len(),
layer_count: layers.len(),
max_depth,
};
ArchAnalysis {
entry_functions,
leaf_functions,
middle_functions,
orphan_functions,
layers,
circular_dependencies,
stats,
}
}
fn detect_cycles(graph: &CallGraph, all_functions: &HashSet<FunctionRef>) -> Vec<CycleDependency> {
let mut cycles = Vec::new();
let mut visited_global = HashSet::new();
let mut name_to_func: HashMap<String, &FunctionRef> = HashMap::new();
for func in all_functions {
let key = func.qualified_name.as_deref().unwrap_or(&func.name).to_string();
name_to_func.insert(key, func);
}
for start_func in all_functions {
if visited_global.contains(start_func) {
continue;
}
let mut stack: Vec<(&FunctionRef, Vec<&FunctionRef>)> = vec![(start_func, vec![start_func])];
let mut in_current_path: HashSet<&FunctionRef> = HashSet::new();
in_current_path.insert(start_func);
while let Some((current, path)) = stack.pop() {
visited_global.insert(current.clone());
if let Some(callees) = graph.callees.get(current) {
for callee in callees {
if path.iter().any(|f| f.name == callee.name) {
let cycle_start_idx = path.iter().position(|f| f.name == callee.name).unwrap();
let cycle_funcs: Vec<&FunctionRef> = path[cycle_start_idx..].to_vec();
let cycle_names: Vec<String> = cycle_funcs
.iter()
.map(|f| f.qualified_name.as_deref().unwrap_or(&f.name).to_string())
.collect();
let cycle_files: Vec<String> = cycle_funcs
.iter()
.map(|f| f.file.clone())
.collect::<HashSet<_>>()
.into_iter()
.collect();
let normalized = normalize_cycle(&cycle_names);
let already_exists = cycles.iter().any(|c: &CycleDependency| {
normalize_cycle(&c.cycle) == normalized
});
if !already_exists && cycle_names.len() > 1 {
cycles.push(CycleDependency {
cycle: cycle_names,
files: cycle_files,
});
}
} else if !visited_global.contains(callee) {
let mut new_path = path.clone();
new_path.push(callee);
stack.push((callee, new_path));
}
}
}
}
}
cycles.sort_by_key(|c| c.cycle.len());
cycles
}
fn normalize_cycle(cycle: &[String]) -> Vec<String> {
if cycle.is_empty() {
return Vec::new();
}
let min_idx = cycle
.iter()
.enumerate()
.min_by_key(|(_, name)| *name)
.map(|(idx, _)| idx)
.unwrap_or(0);
let mut normalized = cycle[min_idx..].to_vec();
normalized.extend_from_slice(&cycle[..min_idx]);
normalized
}
fn build_layers(
graph: &CallGraph,
all_functions: &HashSet<FunctionRef>,
cycles: &[CycleDependency],
) -> Vec<Vec<FunctionInfo>> {
if all_functions.is_empty() {
return Vec::new();
}
let cycle_functions: HashSet<String> = cycles
.iter()
.flat_map(|c| c.cycle.iter().cloned())
.collect();
let mut in_degree: HashMap<&FunctionRef, usize> = HashMap::new();
let mut func_to_callees: HashMap<&FunctionRef, Vec<&FunctionRef>> = HashMap::new();
for func in all_functions {
in_degree.insert(func, 0);
}
for func in all_functions {
if let Some(callers) = graph.callers.get(func) {
let count = callers
.iter()
.filter(|caller| {
let caller_name = caller.qualified_name.as_deref().unwrap_or(&caller.name);
let callee_name = func.qualified_name.as_deref().unwrap_or(&func.name);
!(cycle_functions.contains(caller_name) && cycle_functions.contains(callee_name))
})
.count();
in_degree.insert(func, count);
}
if let Some(callees) = graph.callees.get(func) {
func_to_callees.insert(
func,
callees.iter().collect(),
);
}
}
let mut layers: Vec<Vec<FunctionInfo>> = Vec::new();
let mut remaining: HashSet<&FunctionRef> = all_functions.iter().collect();
while !remaining.is_empty() {
let current_layer: Vec<&FunctionRef> = remaining
.iter()
.filter(|f| in_degree.get(*f).copied().unwrap_or(0) == 0)
.copied()
.collect();
if current_layer.is_empty() {
let cycle_layer: Vec<FunctionInfo> = remaining
.iter()
.map(|f| {
let incoming = graph.callers.get(*f).map_or(0, |c| c.len());
let outgoing = graph.callees.get(*f).map_or(0, |c| c.len());
let mut info = FunctionInfo::from(*f);
info.incoming_calls = incoming;
info.outgoing_calls = outgoing;
info
})
.collect();
if !cycle_layer.is_empty() {
layers.push(cycle_layer);
}
break;
}
let mut layer_info: Vec<FunctionInfo> = current_layer
.iter()
.map(|f| {
let incoming = graph.callers.get(*f).map_or(0, |c| c.len());
let outgoing = graph.callees.get(*f).map_or(0, |c| c.len());
let mut info = FunctionInfo::from(*f);
info.incoming_calls = incoming;
info.outgoing_calls = outgoing;
info
})
.collect();
layer_info.sort_by(|a, b| a.name.cmp(&b.name));
layers.push(layer_info);
for func in ¤t_layer {
remaining.remove(func);
if let Some(callees) = func_to_callees.get(func) {
for callee in callees {
if let Some(degree) = in_degree.get_mut(callee) {
*degree = degree.saturating_sub(1);
}
}
}
}
}
layers
}
#[cfg(test)]
mod tests {
use super::*;
use crate::callgraph::types::CallEdge;
fn make_func(file: &str, name: &str) -> FunctionRef {
FunctionRef {
file: file.to_string(),
name: name.to_string(),
qualified_name: None,
}
}
fn make_graph(edges: Vec<(&str, &str, &str, &str)>) -> CallGraph {
let call_edges: Vec<CallEdge> = edges
.into_iter()
.map(|(cf, cn, ef, en)| CallEdge {
caller: make_func(cf, cn),
callee: make_func(ef, en),
call_line: 0,
})
.collect();
let mut graph = CallGraph::from_edges(call_edges);
graph.build_indexes();
graph
}
#[test]
fn test_categorization() {
let graph = make_graph(vec![
("main.rs", "main", "proc.rs", "process"),
("proc.rs", "process", "util.rs", "helper"),
]);
let analysis = analyze_architecture(&graph);
assert_eq!(analysis.entry_functions.len(), 1);
assert_eq!(analysis.entry_functions[0].name, "main");
assert_eq!(analysis.leaf_functions.len(), 1);
assert_eq!(analysis.leaf_functions[0].name, "helper");
assert_eq!(analysis.middle_functions.len(), 1);
assert_eq!(analysis.middle_functions[0].name, "process");
}
#[test]
fn test_cycle_detection() {
let graph = make_graph(vec![
("a.rs", "a", "b.rs", "b"),
("b.rs", "b", "c.rs", "c"),
("c.rs", "c", "a.rs", "a"),
]);
let analysis = analyze_architecture(&graph);
assert!(!analysis.circular_dependencies.is_empty());
let cycle = &analysis.circular_dependencies[0];
assert_eq!(cycle.cycle.len(), 3);
}
#[test]
fn test_layering() {
let graph = make_graph(vec![
("main.rs", "main", "proc.rs", "process"),
("proc.rs", "process", "util.rs", "helper"),
]);
let analysis = analyze_architecture(&graph);
assert_eq!(analysis.layers.len(), 3);
assert_eq!(analysis.layers[0][0].name, "main");
assert_eq!(analysis.layers[1][0].name, "process");
assert_eq!(analysis.layers[2][0].name, "helper");
}
#[test]
fn test_empty_graph() {
let graph = CallGraph::default();
let analysis = analyze_architecture(&graph);
assert!(analysis.entry_functions.is_empty());
assert!(analysis.leaf_functions.is_empty());
assert!(analysis.circular_dependencies.is_empty());
assert!(analysis.layers.is_empty());
}
}