use crate::graph::unified::concurrent::GraphSnapshot;
use crate::graph::unified::node::{NodeId, NodeKind};
use crate::graph::unified::storage::arena::NodeEntry;
use crate::graph::unified::string::StringId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HubMetric {
#[default]
FanIn,
FanOut,
Combined,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KindMask(u64);
impl KindMask {
#[must_use]
pub const fn empty() -> Self {
Self(0)
}
#[must_use]
pub const fn inserting(self, kind: NodeKind) -> Self {
Self(self.0 | (1u64 << kind_bit(kind)))
}
#[must_use]
pub fn from_kinds(kinds: &[NodeKind]) -> Self {
let mut mask = Self::empty();
for &kind in kinds {
mask = mask.inserting(kind);
}
mask
}
#[must_use]
pub const fn contains(self, kind: NodeKind) -> bool {
self.0 & (1u64 << kind_bit(kind)) != 0
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
}
impl Default for KindMask {
fn default() -> Self {
Self::from_kinds(&[
NodeKind::Function,
NodeKind::Method,
NodeKind::Type,
NodeKind::Class,
NodeKind::Trait,
])
}
}
const fn kind_bit(kind: NodeKind) -> u32 {
match kind {
NodeKind::Function => 0,
NodeKind::Method => 1,
NodeKind::Class => 2,
NodeKind::Interface => 3,
NodeKind::Trait => 4,
NodeKind::Module => 5,
NodeKind::Variable => 6,
NodeKind::Constant => 7,
NodeKind::Type => 8,
NodeKind::Struct => 9,
NodeKind::Enum => 10,
NodeKind::EnumVariant => 11,
NodeKind::Macro => 12,
NodeKind::Parameter => 13,
NodeKind::Property => 14,
NodeKind::CallSite => 15,
NodeKind::Import => 16,
NodeKind::Export => 17,
NodeKind::StyleRule => 18,
NodeKind::StyleAtRule => 19,
NodeKind::StyleVariable => 20,
NodeKind::Lifetime => 21,
NodeKind::Component => 22,
NodeKind::Service => 23,
NodeKind::Resource => 24,
NodeKind::Endpoint => 25,
NodeKind::Test => 26,
NodeKind::TypeParameter => 27,
NodeKind::Annotation => 28,
NodeKind::AnnotationValue => 29,
NodeKind::LambdaTarget => 30,
NodeKind::JavaModule => 31,
NodeKind::EnumConstant => 32,
NodeKind::Channel => 33,
NodeKind::Other => 34,
}
}
#[derive(Debug, Clone, Copy)]
pub struct HubOpts {
pub top: usize,
pub by: HubMetric,
pub kinds: KindMask,
}
impl Default for HubOpts {
fn default() -> Self {
Self {
top: 10,
by: HubMetric::default(),
kinds: KindMask::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HubRank {
pub node: NodeId,
pub name: StringId,
pub kind: NodeKind,
pub fan_in: u32,
pub fan_out: u32,
}
impl HubRank {
#[must_use]
pub fn score(&self, by: HubMetric) -> u64 {
match by {
HubMetric::FanIn => u64::from(self.fan_in),
HubMetric::FanOut => u64::from(self.fan_out),
HubMetric::Combined => u64::from(self.fan_in) * u64::from(self.fan_out),
}
}
}
fn is_stub_free(snapshot: &GraphSnapshot, entry: &NodeEntry) -> bool {
if entry.is_unified_loser() {
return false;
}
let Some(name) = snapshot.strings().resolve(entry.name) else {
return false;
};
if name.is_empty() {
return false;
}
if NodeEntry::is_synthetic_placeholder_name(&name) {
return false;
}
if snapshot.definition_signal_present() && !entry.is_definition() {
return false;
}
true
}
#[must_use]
pub(crate) fn node_is_symbol(snapshot: &GraphSnapshot, entry: &NodeEntry) -> bool {
is_stub_free(snapshot, entry)
}
fn is_degree_edge(kind: &crate::graph::unified::edge::EdgeKind) -> bool {
use crate::graph::unified::edge::EdgeKind;
matches!(kind, EdgeKind::Calls { .. } | EdgeKind::References)
}
#[must_use]
pub fn rank_hubs(snapshot: &GraphSnapshot, opts: &HubOpts) -> Vec<HubRank> {
let node_slots = snapshot.nodes().slot_count();
let mut fan_in = vec![0u32; node_slots];
let mut fan_out = vec![0u32; node_slots];
for edge in snapshot.edges().all_live_forward_edges() {
if !is_degree_edge(&edge.kind) {
continue;
}
let src = edge.source.index() as usize;
let tgt = edge.target.index() as usize;
if let Some(slot) = fan_out.get_mut(src) {
*slot = slot.saturating_add(1);
}
if let Some(slot) = fan_in.get_mut(tgt) {
*slot = slot.saturating_add(1);
}
}
let mut ranked: Vec<(HubRank, String)> = Vec::new();
for (id, entry) in snapshot.iter_nodes() {
if !opts.kinds.contains(entry.kind) {
continue;
}
if !is_stub_free(snapshot, entry) {
continue;
}
let idx = id.index() as usize;
let name = snapshot
.strings()
.resolve(entry.name)
.map_or_else(String::new, |arc| arc.to_string());
ranked.push((
HubRank {
node: id,
name: entry.name,
kind: entry.kind,
fan_in: fan_in.get(idx).copied().unwrap_or(0),
fan_out: fan_out.get(idx).copied().unwrap_or(0),
},
name,
));
}
let by = opts.by;
ranked.sort_by(|(a, a_name), (b, b_name)| {
b.score(by)
.cmp(&a.score(by)) .then_with(|| a.kind.cmp(&b.kind)) .then_with(|| a_name.cmp(b_name)) .then_with(|| a.node.index().cmp(&b.node.index())) });
let mut hubs: Vec<HubRank> = ranked.into_iter().map(|(hub, _)| hub).collect();
if opts.top != 0 && hubs.len() > opts.top {
hubs.truncate(opts.top);
}
hubs
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::edge::{EdgeKind, ResolvedVia};
use crate::graph::unified::file::FileId;
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::PathBuf;
use crate::graph::Language;
use crate::graph::unified::string::StringId;
fn calls() -> EdgeKind {
EdgeKind::Calls {
argument_count: 0,
is_async: false,
resolved_via: ResolvedVia::Direct,
}
}
fn references() -> EdgeKind {
EdgeKind::References
}
fn imports() -> EdgeKind {
EdgeKind::Imports {
alias: None,
is_wildcard: false,
}
}
fn register_file(graph: &mut CodeGraph, path: &str) -> FileId {
graph
.files_mut()
.register_with_language(&PathBuf::from(path), Some(Language::Rust))
.expect("register file")
}
fn add_symbol(graph: &mut CodeGraph, kind: NodeKind, name: &str, file: FileId) -> NodeId {
let sid = graph.strings_mut().intern(name).expect("intern name");
let entry = NodeEntry::new(kind, sid, file)
.with_definition(true)
.with_byte_range(0, 1);
let id = graph.nodes_mut().alloc(entry).expect("alloc node");
graph.indices_mut().add(id, kind, sid, Some(sid), file);
id
}
fn add_stub(graph: &mut CodeGraph, kind: NodeKind, name: &str, file: FileId) -> NodeId {
let sid = graph.strings_mut().intern(name).expect("intern name");
let entry = NodeEntry::new(kind, sid, file).with_byte_range(0, 1);
let id = graph.nodes_mut().alloc(entry).expect("alloc node");
graph.indices_mut().add(id, kind, sid, Some(sid), file);
id
}
fn edge(graph: &CodeGraph, from: NodeId, to: NodeId, kind: EdgeKind, file: FileId) {
graph.edges().add_edge(from, to, kind, file);
}
fn find_by_name<'a>(
hubs: &'a [HubRank],
snapshot: &GraphSnapshot,
name: &str,
) -> Option<&'a HubRank> {
hubs.iter().find(|h| {
snapshot
.strings()
.resolve(h.name)
.is_some_and(|n| &*n == name)
})
}
#[test]
fn fan_in_and_fan_out_counts_are_correct() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let hub = add_symbol(&mut graph, NodeKind::Function, "hub", f);
let a = add_symbol(&mut graph, NodeKind::Function, "a", f);
let b = add_symbol(&mut graph, NodeKind::Function, "b", f);
let c = add_symbol(&mut graph, NodeKind::Function, "c", f);
let sink = add_symbol(&mut graph, NodeKind::Function, "sink", f);
edge(&graph, a, hub, calls(), f);
edge(&graph, b, hub, references(), f);
edge(&graph, c, hub, calls(), f);
edge(&graph, hub, sink, calls(), f);
edge(&graph, hub, a, calls(), f);
edge(&graph, a, b, imports(), f);
let snapshot = graph.snapshot();
let hubs = rank_hubs(&snapshot, &HubOpts::default());
let h = find_by_name(&hubs, &snapshot, "hub").expect("hub ranked");
assert_eq!(h.fan_in, 3, "3 incoming Calls+References");
assert_eq!(h.fan_out, 2, "2 outgoing Calls");
let a_rank = find_by_name(&hubs, &snapshot, "a").expect("a ranked");
assert_eq!(a_rank.fan_in, 1);
assert_eq!(a_rank.fan_out, 1, "a->hub Calls; a->b Imports excluded");
}
#[test]
fn metric_selection_changes_the_top_hub() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let h = add_symbol(&mut graph, NodeKind::Function, "h_high_in", f);
let o = add_symbol(&mut graph, NodeKind::Function, "o_orchestrator", f);
let t = add_symbol(&mut graph, NodeKind::Function, "t_truehub", f);
let s: Vec<NodeId> = (0..4)
.map(|i| add_symbol(&mut graph, NodeKind::Function, &format!("s{i}"), f))
.collect();
let k: Vec<NodeId> = (0..4)
.map(|i| add_symbol(&mut graph, NodeKind::Function, &format!("k{i}"), f))
.collect();
for src in &s {
edge(&graph, *src, h, calls(), f);
}
edge(&graph, h, k[0], calls(), f);
edge(&graph, s[0], o, calls(), f);
for dst in &k {
edge(&graph, o, *dst, calls(), f);
}
for src in s.iter().take(3) {
edge(&graph, *src, t, calls(), f);
}
for dst in k.iter().take(3) {
edge(&graph, t, *dst, calls(), f);
}
let snapshot = graph.snapshot();
let by_in = rank_hubs(
&snapshot,
&HubOpts {
top: 0,
by: HubMetric::FanIn,
kinds: KindMask::default(),
},
);
assert_eq!(
snapshot.strings().resolve(by_in[0].name).as_deref(),
Some("h_high_in"),
"fan-in ranks the most depended-upon symbol first"
);
let by_out = rank_hubs(
&snapshot,
&HubOpts {
top: 0,
by: HubMetric::FanOut,
kinds: KindMask::default(),
},
);
assert_eq!(
snapshot.strings().resolve(by_out[0].name).as_deref(),
Some("o_orchestrator"),
"fan-out ranks the broadest orchestrator first"
);
let by_combined = rank_hubs(
&snapshot,
&HubOpts {
top: 0,
by: HubMetric::Combined,
kinds: KindMask::default(),
},
);
assert_eq!(
snapshot.strings().resolve(by_combined[0].name).as_deref(),
Some("t_truehub"),
"combined ranks the true hub first"
);
}
#[test]
fn tie_break_is_stable_by_name_then_index() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let beta = add_symbol(&mut graph, NodeKind::Function, "beta", f);
let alpha = add_symbol(&mut graph, NodeKind::Function, "alpha", f);
let _ = (beta, alpha);
let snapshot = graph.snapshot();
let hubs = rank_hubs(&snapshot, &HubOpts::default());
let names: Vec<String> = hubs
.iter()
.map(|h| snapshot.strings().resolve(h.name).unwrap().to_string())
.collect();
assert_eq!(names, vec!["alpha", "beta"]);
for _ in 0..5 {
assert_eq!(rank_hubs(&snapshot, &HubOpts::default()), hubs);
}
}
#[test]
fn tie_break_orders_by_kind_before_name() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let _class_a = add_symbol(&mut graph, NodeKind::Class, "a_class", f);
let _fn_z = add_symbol(&mut graph, NodeKind::Function, "z_fn", f);
let snapshot = graph.snapshot();
let hubs = rank_hubs(&snapshot, &HubOpts::default());
assert_eq!(hubs[0].kind, NodeKind::Function);
assert_eq!(
snapshot.strings().resolve(hubs[0].name).as_deref(),
Some("z_fn"),
"kind ordering beats name ordering"
);
}
#[test]
fn only_target_kinds_are_ranked() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let func = add_symbol(&mut graph, NodeKind::Function, "the_fn", f);
let var = add_symbol(&mut graph, NodeKind::Variable, "the_var", f);
let konst = add_symbol(&mut graph, NodeKind::Constant, "the_const", f);
edge(&graph, func, var, calls(), f);
edge(&graph, func, konst, calls(), f);
let snapshot = graph.snapshot();
let hubs = rank_hubs(&snapshot, &HubOpts::default());
for h in &hubs {
assert!(
matches!(
h.kind,
NodeKind::Function
| NodeKind::Method
| NodeKind::Type
| NodeKind::Class
| NodeKind::Trait
),
"unexpected kind {:?} in ranking",
h.kind
);
}
assert!(find_by_name(&hubs, &snapshot, "the_var").is_none());
assert!(find_by_name(&hubs, &snapshot, "the_const").is_none());
}
#[test]
fn tombstoned_and_stub_nodes_are_excluded() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
let real = add_symbol(&mut graph, NodeKind::Function, "real_fn", f);
let stub = add_stub(&mut graph, NodeKind::Function, "stub_fn", f);
let synthetic = add_symbol(&mut graph, NodeKind::Function, "<closure>", f);
let loser_entry = NodeEntry::new(NodeKind::Function, StringId::INVALID, f)
.with_definition(true)
.with_byte_range(0, 1);
let loser = graph.nodes_mut().alloc(loser_entry).expect("alloc loser");
for src_name in ["s0", "s1", "s2", "s3", "s4"] {
let s = add_symbol(&mut graph, NodeKind::Function, src_name, f);
edge(&graph, s, stub, calls(), f);
edge(&graph, s, synthetic, calls(), f);
edge(&graph, s, loser, calls(), f);
edge(&graph, s, real, calls(), f);
}
let snapshot = graph.snapshot();
assert!(snapshot.definition_signal_present());
let hubs = rank_hubs(
&snapshot,
&HubOpts {
top: 0,
by: HubMetric::FanIn,
kinds: KindMask::default(),
},
);
assert!(find_by_name(&hubs, &snapshot, "real_fn").is_some());
assert!(find_by_name(&hubs, &snapshot, "stub_fn").is_none());
assert!(find_by_name(&hubs, &snapshot, "<closure>").is_none());
assert!(
hubs.iter()
.all(|h| snapshot.strings().resolve(h.name).is_some())
);
}
#[test]
fn top_bounds_the_result() {
let mut graph = CodeGraph::new();
let f = register_file(&mut graph, "crate/src/lib.rs");
for i in 0..10 {
add_symbol(&mut graph, NodeKind::Function, &format!("fn{i}"), f);
}
let snapshot = graph.snapshot();
let hubs = rank_hubs(
&snapshot,
&HubOpts {
top: 3,
by: HubMetric::FanIn,
kinds: KindMask::default(),
},
);
assert_eq!(hubs.len(), 3);
}
#[test]
fn kind_mask_membership() {
let mask = KindMask::default();
assert!(mask.contains(NodeKind::Function));
assert!(mask.contains(NodeKind::Trait));
assert!(!mask.contains(NodeKind::Variable));
assert!(KindMask::empty().is_empty());
let custom = KindMask::from_kinds(&[NodeKind::Enum, NodeKind::Struct]);
assert!(custom.contains(NodeKind::Enum));
assert!(custom.contains(NodeKind::Struct));
assert!(!custom.contains(NodeKind::Function));
}
}