1use 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#[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
38pub fn collect_function_metrics(cpg: &CodePropertyGraph) -> Vec<FunctionMetric> {
43 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 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#[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
111pub 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#[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
167pub 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}