use crate::graph::pdg::ProgramDependenceGraph;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub struct Analytics {
pdg: ProgramDependenceGraph,
}
impl Analytics {
pub fn new(pdg: ProgramDependenceGraph) -> Self {
Self { pdg }
}
pub fn count_nodes_by_type(&self) -> Vec<NodeTypeCount> {
let mut counts = HashMap::<String, i64>::new();
for index in self.pdg.node_indices() {
if let Some(node) = self.pdg.get_node(index) {
*counts
.entry(
crate::storage::generation::graph_codec::graph_node_type_str(
&node.node_type,
)
.to_string(),
)
.or_default() += 1;
}
}
let mut counts: Vec<NodeTypeCount> = counts
.into_iter()
.map(|(node_type, count)| NodeTypeCount { node_type, count })
.collect();
counts.sort_by(|a, b| a.node_type.cmp(&b.node_type));
counts
}
pub fn complexity_distribution(&self) -> Vec<ComplexityBucket> {
let mut counts = HashMap::<&'static str, i64>::new();
for index in self.pdg.node_indices() {
if let Some(node) = self.pdg.get_node(index) {
let bucket = match node.complexity {
complexity if complexity < 5 => "simple",
complexity if complexity < 10 => "moderate",
complexity if complexity < 20 => "complex",
_ => "very_complex",
};
*counts.entry(bucket).or_default() += 1;
}
}
let mut buckets: Vec<ComplexityBucket> = counts
.into_iter()
.map(|(bucket, count)| ComplexityBucket {
bucket: bucket.to_string(),
count,
})
.collect();
buckets.sort_by(|a, b| a.bucket.cmp(&b.bucket));
buckets
}
pub fn count_edges_by_type(&self) -> Vec<EdgeTypeCount> {
let mut counts = HashMap::<String, i64>::new();
for edge_index in self.pdg.edge_indices() {
if let Some(edge) = self.pdg.get_edge(edge_index) {
*counts
.entry(
crate::storage::generation::graph_codec::graph_edge_type_str(
&edge.edge_type,
)
.to_string(),
)
.or_default() += 1;
}
}
let mut counts: Vec<EdgeTypeCount> = counts
.into_iter()
.map(|(edge_type, count)| EdgeTypeCount { edge_type, count })
.collect();
counts.sort_by(|a, b| a.edge_type.cmp(&b.edge_type));
counts
}
pub fn get_hotspots(&self, threshold: i32) -> Vec<Hotspot> {
let mut fan_out_by_node = HashMap::<petgraph::graph::NodeIndex, i64>::new();
for edge_index in self.pdg.edge_indices() {
if let Some((source, _)) = self.pdg.edge_endpoints(edge_index) {
*fan_out_by_node.entry(source).or_default() += 1;
}
}
let threshold = i64::from(threshold);
let mut hotspots = Vec::new();
for node_index in self.pdg.node_indices() {
let Some(node) = self.pdg.get_node(node_index) else {
continue;
};
let complexity = i64::from(node.complexity);
let fan_out = fan_out_by_node.get(&node_index).copied().unwrap_or(0);
if complexity >= threshold && fan_out > threshold / 2 {
hotspots.push(Hotspot {
node_id: node.id.clone(),
symbol_name: node.name.clone(),
file_path: node.file_path.to_string(),
complexity: node.complexity as i32,
fan_out,
});
}
}
hotspots.sort_by(|a, b| {
b.complexity
.cmp(&a.complexity)
.then_with(|| b.fan_out.cmp(&a.fan_out))
});
hotspots
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeTypeCount {
pub node_type: String,
pub count: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityBucket {
pub bucket: String,
pub count: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeTypeCount {
pub edge_type: String,
pub count: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hotspot {
pub node_id: String,
pub symbol_name: String,
pub file_path: String,
pub complexity: i32,
pub fan_out: i64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::pdg::{Edge, EdgeMetadata, EdgeType, Node, NodeType};
use std::sync::Arc;
fn node(id: &str, name: &str, kind: NodeType, complexity: u32) -> Node {
Node {
id: id.to_string(),
node_type: kind,
name: name.to_string(),
file_path: Arc::from("src/lib.rs"),
byte_range: (0, 10),
complexity,
language: "rust".to_string(),
}
}
#[test]
fn test_analytics_graph_queries_preserve_grouping_buckets_and_hotspot_order() {
let mut pdg = ProgramDependenceGraph::new();
let simple = pdg.add_node(node("src/lib.rs:simple", "simple", NodeType::Function, 4));
let moderate = pdg.add_node(node(
"src/lib.rs:moderate",
"moderate",
NodeType::Function,
7,
));
let complex = pdg.add_node(node("src/lib.rs:complex", "complex", NodeType::Class, 12));
let very_complex = pdg.add_node(node(
"src/lib.rs:very_complex",
"very_complex",
NodeType::Method,
20,
));
for (source, target, edge_type) in [
(moderate, simple, EdgeType::Call),
(moderate, complex, EdgeType::Call),
(moderate, very_complex, EdgeType::DataDependency),
(complex, simple, EdgeType::Import),
(very_complex, simple, EdgeType::Call),
] {
pdg.add_edge(
source,
target,
Edge {
edge_type,
metadata: EdgeMetadata::empty(),
},
);
}
let analytics = Analytics::new(pdg);
assert_eq!(
analytics
.count_nodes_by_type()
.into_iter()
.map(|item| (item.node_type, item.count))
.collect::<Vec<_>>(),
vec![
("class".to_string(), 1),
("function".to_string(), 2),
("method".to_string(), 1),
]
);
assert_eq!(
analytics
.complexity_distribution()
.into_iter()
.map(|item| (item.bucket, item.count))
.collect::<Vec<_>>(),
vec![
("complex".to_string(), 1),
("moderate".to_string(), 1),
("simple".to_string(), 1),
("very_complex".to_string(), 1),
]
);
assert_eq!(
analytics
.count_edges_by_type()
.into_iter()
.map(|item| (item.edge_type, item.count))
.collect::<Vec<_>>(),
vec![
("call".to_string(), 3),
("data_dependency".to_string(), 1),
("import".to_string(), 1),
]
);
let hotspots = analytics.get_hotspots(4);
assert_eq!(hotspots.len(), 1);
assert_eq!(hotspots[0].node_id, "src/lib.rs:moderate");
assert_eq!(hotspots[0].fan_out, 3);
}
#[test]
fn test_analytics_creation() {
let analytics = Analytics::new(ProgramDependenceGraph::new());
assert!(analytics.count_nodes_by_type().is_empty());
assert!(analytics.complexity_distribution().is_empty());
assert!(analytics.count_edges_by_type().is_empty());
assert!(analytics.get_hotspots(5).is_empty());
}
}