use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct FunctionReport {
pub name: String,
pub kind: String,
pub line: u32,
pub cognitive: u32,
pub cyclomatic: u32,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub children: Vec<FunctionReport>,
}
#[derive(Debug, Clone, Serialize)]
pub struct FileReport {
pub path: String,
pub cognitive: u32,
pub cyclomatic: u32,
pub functions: Vec<FunctionReport>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub parse_errors: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct MetricSummary {
pub sum: u32,
pub max: u32,
pub median: u32,
pub p90: u32,
pub p95: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct Summary {
pub file_count: usize,
pub function_count: usize,
pub cognitive: MetricSummary,
pub cyclomatic: MetricSummary,
}
#[derive(Debug, Clone, Serialize)]
pub struct Report {
pub files: Vec<FileReport>,
pub summary: Summary,
}
#[derive(Debug, Clone, Copy)]
pub enum Metric {
Cognitive,
Cyclomatic,
}
impl Metric {
fn as_str(self) -> &'static str {
match self {
Metric::Cognitive => "cognitive",
Metric::Cyclomatic => "cyclomatic",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TopEntry {
pub path: String,
pub name: String,
pub kind: String,
pub line: u32,
pub cognitive: u32,
pub cyclomatic: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct TopReport {
pub metric: String,
pub top: Vec<TopEntry>,
pub summary: Summary,
}
pub fn for_each_function(fns: &[FunctionReport], f: &mut impl FnMut(&FunctionReport)) {
for func in fns {
f(func);
for_each_function(&func.children, f);
}
}
pub fn compute_top(reports: &[FileReport], metric: Metric, n: usize) -> Vec<TopEntry> {
let mut entries = Vec::new();
for r in reports {
for_each_function(&r.functions, &mut |f| {
entries.push(TopEntry {
path: r.path.clone(),
name: f.name.clone(),
kind: f.kind.clone(),
line: f.line,
cognitive: f.cognitive,
cyclomatic: f.cyclomatic,
});
});
}
entries.sort_by(|a, b| {
let (av, bv) = match metric {
Metric::Cognitive => (a.cognitive, b.cognitive),
Metric::Cyclomatic => (a.cyclomatic, b.cyclomatic),
};
bv.cmp(&av)
.then_with(|| a.path.cmp(&b.path))
.then(a.line.cmp(&b.line))
});
entries.truncate(n);
entries
}
pub fn build_top_report(
reports: &[FileReport],
summary: Summary,
metric: Metric,
n: usize,
) -> TopReport {
TopReport {
metric: metric.as_str().to_string(),
top: compute_top(reports, metric, n),
summary,
}
}
fn percentile(sorted: &[u32], p: f64) -> u32 {
if sorted.is_empty() {
return 0;
}
let n = sorted.len();
let rank = ((p / 100.0) * n as f64).ceil() as usize;
let idx = rank.saturating_sub(1).min(n - 1);
sorted[idx]
}
fn metric_summary(mut values: Vec<u32>) -> MetricSummary {
values.sort_unstable();
MetricSummary {
sum: values.iter().sum(),
max: values.last().copied().unwrap_or(0),
median: percentile(&values, 50.0),
p90: percentile(&values, 90.0),
p95: percentile(&values, 95.0),
}
}
pub fn compute_summary(reports: &[FileReport]) -> Summary {
let mut cog = Vec::new();
let mut cyc = Vec::new();
for r in reports {
for_each_function(&r.functions, &mut |f| {
cog.push(f.cognitive);
cyc.push(f.cyclomatic);
});
}
Summary {
file_count: reports.len(),
function_count: cog.len(),
cognitive: metric_summary(cog),
cyclomatic: metric_summary(cyc),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percentile_nearest_rank() {
let v: Vec<u32> = (1..=10).collect();
assert_eq!(percentile(&v, 50.0), 5);
assert_eq!(percentile(&v, 90.0), 9);
assert_eq!(percentile(&v, 95.0), 10);
assert_eq!(percentile(&v, 100.0), 10);
}
#[test]
fn percentile_empty_is_zero() {
assert_eq!(percentile(&[], 50.0), 0);
}
}