Skip to main content

icb_server/
analytics.rs

1//! Analytics module for the ICB server.
2//!
3//! Provides functions that compute various metrics over a
4//! [`CodePropertyGraph`], such as function and class statistics,
5//! complexity estimations, lines‑of‑code, and per‑file summaries.
6//! These metrics are consumed by the `/api/functions`, `/api/classes`,
7//! and `/api/files` endpoints.
8//!
9//! # Metrics
10//!
11//! * `complexity` – number of AST nodes reachable via `AstChild` edges.
12//! * `loc` – lines of code (`end_line - start_line + 1`) for the
13//!   function/class, useful when AST children are unavailable (Clang).
14
15use icb_common::NodeKind;
16use icb_graph::analysis;
17use icb_graph::graph::{CodePropertyGraph, Edge};
18use petgraph::stable_graph::NodeIndex;
19use petgraph::visit::{EdgeRef, IntoEdgeReferences};
20use serde::Serialize;
21use std::collections::HashMap;
22
23/// A single function (or method) metric.
24#[derive(Debug, Serialize)]
25pub struct FunctionMetric {
26    pub name: String,
27    pub kind: String,
28    pub line: usize,
29    pub file: Option<String>,
30    pub complexity: usize,
31    pub loc: usize,
32    pub is_cycle: bool,
33    pub is_dead: bool,
34    pub callers: usize,
35    pub callees: usize,
36}
37
38/// Computes metrics for every function and class node in the graph.
39///
40/// Builds a name → index lookup once, making per‑node edge counting O(1)
41/// instead of a repeated linear scan over all graph nodes.
42pub fn collect_function_metrics(cpg: &CodePropertyGraph) -> Vec<FunctionMetric> {
43    // Single map from node name to its stable index
44    let name_to_idx: HashMap<String, NodeIndex> = cpg
45        .graph
46        .node_indices()
47        .map(|i| (cpg.graph[i].name.clone().unwrap_or_default(), i))
48        .collect();
49
50    let cycles = analysis::detect_call_cycles(cpg);
51    let dead = analysis::detect_dead_code(cpg, &["main".to_string()]);
52    let complex_list = analysis::detect_complex_functions(cpg, 0);
53
54    cpg.graph
55        .node_weights()
56        .filter(|n| n.kind == NodeKind::Function || n.kind == NodeKind::Class)
57        .map(|node| {
58            let name = node.name.clone().unwrap_or_default();
59
60            let is_cycle = cycles.iter().any(|c| c.functions.contains(&name));
61            let is_dead = dead.iter().any(|n| n.name.as_deref() == Some(&name));
62            let complexity = complex_list
63                .iter()
64                .find(|r| r.function_name == name)
65                .map(|r| r.ast_node_count)
66                .unwrap_or(0);
67
68            let loc = node.end_line.saturating_sub(node.start_line) + 1;
69
70            // O(1) access to the node index
71            let idx = name_to_idx[&name];
72
73            let callers = cpg
74                .graph
75                .edges_directed(idx, petgraph::Direction::Incoming)
76                .filter(|e| matches!(e.weight(), Edge::Call))
77                .count();
78            let callees = cpg
79                .graph
80                .edges(idx)
81                .filter(|e| matches!(e.weight(), Edge::Call))
82                .count();
83
84            FunctionMetric {
85                name,
86                kind: format!("{:?}", node.kind),
87                line: node.start_line,
88                file: None,
89                complexity,
90                loc,
91                is_cycle,
92                is_dead,
93                callers,
94                callees,
95            }
96        })
97        .collect()
98}
99
100/// A single class metric.
101#[derive(Debug, Serialize)]
102pub struct ClassMetric {
103    pub name: String,
104    pub line: usize,
105    pub file: Option<String>,
106    pub methods: usize,
107    pub complexity: usize,
108    pub loc: usize,
109}
110
111/// Collects metrics for every class node.
112///
113/// Uses the same name → index lookup for O(1) edge counting.
114pub fn collect_class_metrics(cpg: &CodePropertyGraph) -> Vec<ClassMetric> {
115    let name_to_idx: HashMap<String, NodeIndex> = cpg
116        .graph
117        .node_indices()
118        .map(|i| (cpg.graph[i].name.clone().unwrap_or_default(), i))
119        .collect();
120
121    let complex_list = analysis::detect_complex_functions(cpg, 0);
122
123    cpg.graph
124        .node_weights()
125        .filter(|n| n.kind == NodeKind::Class)
126        .map(|node| {
127            let name = node.name.clone().unwrap_or_default();
128            let complexity = complex_list
129                .iter()
130                .find(|r| r.function_name == name)
131                .map(|r| r.ast_node_count)
132                .unwrap_or(0);
133
134            let loc = node.end_line.saturating_sub(node.start_line) + 1;
135
136            let idx = name_to_idx[&name];
137
138            let methods = cpg
139                .graph
140                .edges(idx)
141                .filter(|e| matches!(e.weight(), Edge::AstChild))
142                .filter(|e| cpg.graph[e.target()].kind == NodeKind::Function)
143                .count();
144
145            ClassMetric {
146                name,
147                line: node.start_line,
148                file: None,
149                methods,
150                complexity,
151                loc,
152            }
153        })
154        .collect()
155}
156
157/// Per‑file summary.
158#[derive(Debug, Serialize)]
159pub struct FileMetric {
160    pub path: String,
161    pub functions: usize,
162    pub classes: usize,
163    pub total_complexity: usize,
164    pub calls: usize,
165}
166
167/// Creates per‑file aggregate metrics.
168pub fn collect_file_metrics(cpg: &CodePropertyGraph) -> Vec<FileMetric> {
169    let mut files: HashMap<String, (usize, usize, usize, usize)> = HashMap::new();
170    for node in cpg.graph.node_weights() {
171        let file = node.usr.clone().unwrap_or_default();
172        let entry = files.entry(file).or_insert((0, 0, 0, 0));
173        match node.kind {
174            NodeKind::Function => entry.0 += 1,
175            NodeKind::Class => entry.1 += 1,
176            _ => {}
177        }
178    }
179    for edge_ref in cpg.graph.edge_references() {
180        if matches!(edge_ref.weight(), Edge::Call) {
181            let src_node = &cpg.graph[edge_ref.source()];
182            let file = src_node.usr.clone().unwrap_or_default();
183            let entry = files.entry(file).or_insert((0, 0, 0, 0));
184            entry.3 += 1;
185        }
186    }
187    files
188        .into_iter()
189        .map(|(path, (funcs, classes, _compl, calls))| FileMetric {
190            path,
191            functions: funcs,
192            classes,
193            total_complexity: 0,
194            calls,
195        })
196        .collect()
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use icb_graph::graph::{Edge, Node};
203
204    fn build_test_cpg() -> CodePropertyGraph {
205        let mut cpg = CodePropertyGraph::new();
206        let main = cpg.graph.add_node(Node {
207            kind: NodeKind::Function,
208            name: Some("main".into()),
209            usr: Some("main.cpp".into()),
210            start_line: 1,
211            end_line: 5,
212        });
213        let helper = cpg.graph.add_node(Node {
214            kind: NodeKind::Function,
215            name: Some("helper".into()),
216            usr: Some("helper.cpp".into()),
217            start_line: 10,
218            end_line: 12,
219        });
220        let my_class = cpg.graph.add_node(Node {
221            kind: NodeKind::Class,
222            name: Some("MyClass".into()),
223            usr: Some("myclass.cpp".into()),
224            start_line: 20,
225            end_line: 30,
226        });
227        let method = cpg.graph.add_node(Node {
228            kind: NodeKind::Function,
229            name: Some("method".into()),
230            usr: Some("myclass.cpp".into()),
231            start_line: 22,
232            end_line: 25,
233        });
234
235        cpg.graph.add_edge(main, helper, Edge::Call);
236        cpg.graph.add_edge(my_class, method, Edge::AstChild);
237        cpg
238    }
239
240    #[test]
241    fn test_function_metrics() {
242        let cpg = build_test_cpg();
243        let metrics = collect_function_metrics(&cpg);
244        assert_eq!(metrics.len(), 4);
245        let main_metric = metrics.iter().find(|m| m.name == "main").unwrap();
246        assert_eq!(main_metric.loc, 5);
247        assert_eq!(main_metric.callees, 1);
248        let helper_metric = metrics.iter().find(|m| m.name == "helper").unwrap();
249        assert_eq!(helper_metric.loc, 3);
250        assert_eq!(helper_metric.callers, 1);
251    }
252
253    #[test]
254    fn test_class_metrics() {
255        let cpg = build_test_cpg();
256        let metrics = collect_class_metrics(&cpg);
257        assert_eq!(metrics.len(), 1);
258        let class_metric = &metrics[0];
259        assert_eq!(class_metric.name, "MyClass");
260        assert_eq!(class_metric.methods, 1);
261        assert_eq!(class_metric.loc, 11);
262    }
263
264    #[test]
265    fn test_file_metrics() {
266        let cpg = build_test_cpg();
267        let metrics = collect_file_metrics(&cpg);
268        assert_eq!(metrics.len(), 3);
269        let main_file = metrics.iter().find(|f| f.path == "main.cpp").unwrap();
270        assert_eq!(main_file.functions, 1);
271        assert_eq!(main_file.calls, 1);
272    }
273}