1use serde::Serialize;
5
6#[derive(Debug, Clone, Serialize)]
12pub struct FunctionReport {
13 pub name: String,
14 pub kind: String,
16 pub line: u32,
18 pub cognitive: u32,
19 pub cyclomatic: u32,
20 #[serde(skip_serializing_if = "Vec::is_empty")]
21 pub children: Vec<FunctionReport>,
22}
23
24#[derive(Debug, Clone, Serialize)]
26pub struct FileReport {
27 pub path: String,
28 pub cognitive: u32,
30 pub cyclomatic: u32,
31 pub functions: Vec<FunctionReport>,
32 #[serde(skip_serializing_if = "Vec::is_empty")]
33 pub parse_errors: Vec<String>,
34}
35
36#[derive(Debug, Clone, Serialize)]
42pub struct MetricSummary {
43 pub sum: u32,
44 pub max: u32,
45 pub median: u32,
46 pub p90: u32,
47 pub p95: u32,
48}
49
50#[derive(Debug, Clone, Serialize)]
52pub struct Summary {
53 pub file_count: usize,
54 pub function_count: usize,
55 pub cognitive: MetricSummary,
56 pub cyclomatic: MetricSummary,
57}
58
59#[derive(Debug, Clone, Serialize)]
61pub struct Report {
62 pub files: Vec<FileReport>,
63 pub summary: Summary,
64}
65
66#[derive(Debug, Clone, Copy)]
68pub enum Metric {
69 Cognitive,
70 Cyclomatic,
71}
72
73impl Metric {
74 fn as_str(self) -> &'static str {
75 match self {
76 Metric::Cognitive => "cognitive",
77 Metric::Cyclomatic => "cyclomatic",
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize)]
85pub struct TopEntry {
86 pub path: String,
87 pub name: String,
88 pub kind: String,
89 pub line: u32,
90 pub cognitive: u32,
91 pub cyclomatic: u32,
92}
93
94#[derive(Debug, Clone, Serialize)]
96pub struct TopReport {
97 pub metric: String,
99 pub top: Vec<TopEntry>,
100 pub summary: Summary,
101}
102
103pub fn for_each_function(fns: &[FunctionReport], f: &mut impl FnMut(&FunctionReport)) {
105 for func in fns {
106 f(func);
107 for_each_function(&func.children, f);
108 }
109}
110
111pub fn compute_top(reports: &[FileReport], metric: Metric, n: usize) -> Vec<TopEntry> {
115 let mut entries = Vec::new();
116 for r in reports {
117 for_each_function(&r.functions, &mut |f| {
118 entries.push(TopEntry {
119 path: r.path.clone(),
120 name: f.name.clone(),
121 kind: f.kind.clone(),
122 line: f.line,
123 cognitive: f.cognitive,
124 cyclomatic: f.cyclomatic,
125 });
126 });
127 }
128 entries.sort_by(|a, b| {
129 let (av, bv) = match metric {
130 Metric::Cognitive => (a.cognitive, b.cognitive),
131 Metric::Cyclomatic => (a.cyclomatic, b.cyclomatic),
132 };
133 bv.cmp(&av)
134 .then_with(|| a.path.cmp(&b.path))
135 .then(a.line.cmp(&b.line))
136 });
137 entries.truncate(n);
138 entries
139}
140
141pub fn build_top_report(
143 reports: &[FileReport],
144 summary: Summary,
145 metric: Metric,
146 n: usize,
147) -> TopReport {
148 TopReport {
149 metric: metric.as_str().to_string(),
150 top: compute_top(reports, metric, n),
151 summary,
152 }
153}
154
155fn percentile(sorted: &[u32], p: f64) -> u32 {
158 if sorted.is_empty() {
159 return 0;
160 }
161 let n = sorted.len();
162 let rank = ((p / 100.0) * n as f64).ceil() as usize;
163 let idx = rank.saturating_sub(1).min(n - 1);
164 sorted[idx]
165}
166
167fn metric_summary(mut values: Vec<u32>) -> MetricSummary {
168 values.sort_unstable();
169 MetricSummary {
170 sum: values.iter().sum(),
171 max: values.last().copied().unwrap_or(0),
172 median: percentile(&values, 50.0),
173 p90: percentile(&values, 90.0),
174 p95: percentile(&values, 95.0),
175 }
176}
177
178pub fn compute_summary(reports: &[FileReport]) -> Summary {
182 let mut cog = Vec::new();
183 let mut cyc = Vec::new();
184 for r in reports {
185 for_each_function(&r.functions, &mut |f| {
186 cog.push(f.cognitive);
187 cyc.push(f.cyclomatic);
188 });
189 }
190 Summary {
191 file_count: reports.len(),
192 function_count: cog.len(),
193 cognitive: metric_summary(cog),
194 cyclomatic: metric_summary(cyc),
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn percentile_nearest_rank() {
204 let v: Vec<u32> = (1..=10).collect();
205 assert_eq!(percentile(&v, 50.0), 5);
206 assert_eq!(percentile(&v, 90.0), 9);
207 assert_eq!(percentile(&v, 95.0), 10);
208 assert_eq!(percentile(&v, 100.0), 10);
209 }
210
211 #[test]
212 fn percentile_empty_is_zero() {
213 assert_eq!(percentile(&[], 50.0), 0);
214 }
215}