use gitcortex_core::{error::Result, store::GraphStore};
use serde::Serialize;
pub use gitcortex_core::graph::in_degree_by_calls;
#[derive(Debug, Clone, Serialize)]
pub struct GodNode {
pub name: String,
pub qualified_name: String,
pub kind: String,
pub file: String,
pub start_line: u32,
pub in_degree: u32,
}
const DEFAULT_MIN_IN_DEGREE: u32 = 10;
const DEFAULT_LIMIT: usize = 20;
const MAX_LIMIT: usize = 100;
pub fn find_god_nodes<S: GraphStore + ?Sized>(
store: &S,
branch: &str,
min_in_degree: Option<u32>,
limit: Option<usize>,
) -> Result<Vec<GodNode>> {
let min_in_degree = min_in_degree.unwrap_or(DEFAULT_MIN_IN_DEGREE);
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
let nodes = store.list_all_nodes(branch)?;
let edges = store.list_all_edges(branch)?;
let in_degree = in_degree_by_calls(&edges);
let mut scored: Vec<(GodNode, u32)> = nodes
.into_iter()
.filter_map(|n| {
let deg = in_degree.get(&n.id.as_str()).copied().unwrap_or(0);
if deg < min_in_degree {
return None;
}
Some((
GodNode {
name: n.name,
qualified_name: n.qualified_name,
kind: n.kind.to_string(),
file: n.file.display().to_string(),
start_line: n.span.start_line,
in_degree: deg,
},
deg,
))
})
.collect();
scored.sort_by(|a, b| {
b.1.cmp(&a.1)
.then_with(|| a.0.qualified_name.cmp(&b.0.qualified_name))
});
Ok(scored.into_iter().take(limit).map(|(g, _)| g).collect())
}
#[cfg(test)]
mod tests {
use gitcortex_core::graph::{Edge, NodeId};
use gitcortex_core::schema::EdgeKind;
use super::in_degree_by_calls;
#[test]
fn in_degree_empty_edges_returns_empty_map() {
assert!(in_degree_by_calls(&[]).is_empty());
}
#[test]
fn in_degree_counts_calls_edges_only() {
let a = NodeId::new();
let b = NodeId::new();
let edges = vec![
Edge::new(a.clone(), b.clone(), EdgeKind::Calls),
Edge::new(a.clone(), b.clone(), EdgeKind::Contains), Edge::new(a.clone(), b.clone(), EdgeKind::Uses), ];
let map = in_degree_by_calls(&edges);
assert_eq!(map.get(&b.as_str()), Some(&1));
assert!(
!map.contains_key(&a.as_str()),
"src should not appear as dst"
);
}
#[test]
fn in_degree_multiple_callers_accumulate() {
let dst = NodeId::new();
let callers: Vec<NodeId> = (0..5).map(|_| NodeId::new()).collect();
let edges: Vec<Edge> = callers
.iter()
.map(|src| Edge::new(src.clone(), dst.clone(), EdgeKind::Calls))
.collect();
let map = in_degree_by_calls(&edges);
assert_eq!(map.get(&dst.as_str()), Some(&5));
}
#[test]
fn in_degree_src_node_absent_unless_also_dst() {
let a = NodeId::new();
let b = NodeId::new();
let c = NodeId::new();
let edges = vec![
Edge::new(a.clone(), b.clone(), EdgeKind::Calls),
Edge::new(b.clone(), c.clone(), EdgeKind::Calls),
];
let map = in_degree_by_calls(&edges);
assert!(
!map.contains_key(&a.as_str()),
"pure caller should not appear"
);
assert_eq!(map.get(&b.as_str()), Some(&1));
assert_eq!(map.get(&c.as_str()), Some(&1));
}
#[test]
fn in_degree_sort_order_descending_ties_by_qname() {
let high = NodeId::new();
let low = NodeId::new();
let callers: Vec<NodeId> = (0..3).map(|_| NodeId::new()).collect();
let mut edges: Vec<Edge> = callers
.iter()
.map(|src| Edge::new(src.clone(), high.clone(), EdgeKind::Calls))
.collect();
edges.push(Edge::new(NodeId::new(), low.clone(), EdgeKind::Calls));
let map = in_degree_by_calls(&edges);
assert!(map[&high.as_str()] > map[&low.as_str()]);
}
}