624 lines
20 KiB
Rust
624 lines
20 KiB
Rust
|
|
use colored::Colorize;
|
|||
|
|
use serde::Serialize;
|
|||
|
|
use std::path::Path;
|
|||
|
|
|
|||
|
|
use crate::complexity::{self, FunctionComplexity};
|
|||
|
|
use crate::render;
|
|||
|
|
|
|||
|
|
// ── Metric extraction ──────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
const METRIC_NAMES: &[&str] = &["loc", "cyclomatic", "cognitive", "nesting", "params"];
|
|||
|
|
|
|||
|
|
struct MetricSet {
|
|||
|
|
/// One vector of values per metric, in METRIC_NAMES order.
|
|||
|
|
columns: Vec<Vec<f64>>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn extract_metrics(fcs: &[FunctionComplexity]) -> MetricSet {
|
|||
|
|
let mut columns: Vec<Vec<f64>> = vec![Vec::new(); METRIC_NAMES.len()];
|
|||
|
|
for fc in fcs {
|
|||
|
|
columns[0].push(fc.line_count as f64);
|
|||
|
|
columns[1].push(fc.cyclomatic as f64);
|
|||
|
|
columns[2].push(fc.cognitive as f64);
|
|||
|
|
columns[3].push(fc.nesting_depth as f64);
|
|||
|
|
columns[4].push(fc.param_count as f64);
|
|||
|
|
}
|
|||
|
|
MetricSet { columns }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Statistics helpers ─────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
fn mean(v: &[f64]) -> f64 {
|
|||
|
|
if v.is_empty() { return 0.0; }
|
|||
|
|
v.iter().sum::<f64>() / v.len() as f64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn std_dev(v: &[f64]) -> f64 {
|
|||
|
|
if v.len() < 2 { return 0.0; }
|
|||
|
|
let m = mean(v);
|
|||
|
|
let var = v.iter().map(|x| (x - m) * (x - m)).sum::<f64>() / v.len() as f64;
|
|||
|
|
var.sqrt()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn pearson(x: &[f64], y: &[f64]) -> f64 {
|
|||
|
|
let n = x.len();
|
|||
|
|
if n < 2 { return 0.0; }
|
|||
|
|
let mx = mean(x);
|
|||
|
|
let my = mean(y);
|
|||
|
|
let mut num = 0.0;
|
|||
|
|
let mut dx2 = 0.0;
|
|||
|
|
let mut dy2 = 0.0;
|
|||
|
|
for i in 0..n {
|
|||
|
|
let dx = x[i] - mx;
|
|||
|
|
let dy = y[i] - my;
|
|||
|
|
num += dx * dy;
|
|||
|
|
dx2 += dx * dx;
|
|||
|
|
dy2 += dy * dy;
|
|||
|
|
}
|
|||
|
|
let denom = (dx2 * dy2).sqrt();
|
|||
|
|
if denom < 1e-12 { 0.0 } else { num / denom }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn skewness(v: &[f64]) -> f64 {
|
|||
|
|
let n = v.len();
|
|||
|
|
if n < 3 { return 0.0; }
|
|||
|
|
let m = mean(v);
|
|||
|
|
let s = std_dev(v);
|
|||
|
|
if s < 1e-12 { return 0.0; }
|
|||
|
|
let m3 = v.iter().map(|x| ((x - m) / s).powi(3)).sum::<f64>();
|
|||
|
|
m3 / n as f64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn kurtosis(v: &[f64]) -> f64 {
|
|||
|
|
let n = v.len();
|
|||
|
|
if n < 4 { return 0.0; }
|
|||
|
|
let m = mean(v);
|
|||
|
|
let s = std_dev(v);
|
|||
|
|
if s < 1e-12 { return 0.0; }
|
|||
|
|
let m4 = v.iter().map(|x| ((x - m) / s).powi(4)).sum::<f64>();
|
|||
|
|
m4 / n as f64 - 3.0 // excess kurtosis
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Histogram ──────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
fn render_histogram(values: &[f64], label: &str, num_bins: usize, verbose: bool) {
|
|||
|
|
if values.is_empty() { return; }
|
|||
|
|
|
|||
|
|
let min_v = values.iter().cloned().fold(f64::INFINITY, f64::min);
|
|||
|
|
let max_v = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
|||
|
|
|
|||
|
|
// Cap bins to the actual range for integer-valued data
|
|||
|
|
let range = max_v - min_v;
|
|||
|
|
let num_bins = if range.abs() < 1e-12 {
|
|||
|
|
1
|
|||
|
|
} else {
|
|||
|
|
let int_range = range.ceil() as usize;
|
|||
|
|
num_bins.min(int_range.max(1))
|
|||
|
|
};
|
|||
|
|
let bin_width = if num_bins == 1 { 1.0 } else { range / num_bins as f64 };
|
|||
|
|
|
|||
|
|
let mut bins = vec![0usize; num_bins];
|
|||
|
|
for &v in values {
|
|||
|
|
let idx = if num_bins == 1 {
|
|||
|
|
0
|
|||
|
|
} else {
|
|||
|
|
((v - min_v) / bin_width).floor() as usize
|
|||
|
|
};
|
|||
|
|
let idx = idx.min(num_bins - 1);
|
|||
|
|
bins[idx] += 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let max_count = *bins.iter().max().unwrap_or(&1).max(&1);
|
|||
|
|
|
|||
|
|
let term_w = render::terminal_width();
|
|||
|
|
let label_w = 12; // " [xxx, yyy)"
|
|||
|
|
let count_w = format!("{}", max_count).len() + 1;
|
|||
|
|
let bar_budget = term_w.saturating_sub(label_w + count_w + 4).max(10);
|
|||
|
|
|
|||
|
|
println!(
|
|||
|
|
"\n {} (n={})",
|
|||
|
|
label.bright_cyan().bold(),
|
|||
|
|
format!("{}", values.len()).bold()
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if verbose {
|
|||
|
|
render::verbose_block(&[
|
|||
|
|
"Histogram: frequency distribution of values. Each row is a bin range.",
|
|||
|
|
"Bar length proportional to count. Color: red = most frequent, green = least.",
|
|||
|
|
"Stats below: skew > 0 = right-tailed (few very high values),",
|
|||
|
|
" excess kurtosis > 0 = heavy tails (more outliers than a normal distribution).",
|
|||
|
|
]);
|
|||
|
|
render::guide_ref("complexity");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for i in 0..num_bins {
|
|||
|
|
let lo = min_v + i as f64 * bin_width;
|
|||
|
|
let hi = lo + bin_width;
|
|||
|
|
let range_label = if num_bins == 1 {
|
|||
|
|
format!("[{:.0}]", lo)
|
|||
|
|
} else if i == num_bins - 1 {
|
|||
|
|
format!("[{:.0},{:.0}]", lo, hi)
|
|||
|
|
} else {
|
|||
|
|
format!("[{:.0},{:.0})", lo, hi)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let bar_len = if max_count > 0 {
|
|||
|
|
(bins[i] as f64 / max_count as f64 * bar_budget as f64).ceil() as usize
|
|||
|
|
} else {
|
|||
|
|
0
|
|||
|
|
}.max(if bins[i] > 0 { 1 } else { 0 });
|
|||
|
|
|
|||
|
|
let ratio = if max_count > 0 { 1.0 - (bins[i] as f64 / max_count as f64) } else { 1.0 };
|
|||
|
|
let bar = render::bar_color(&"█".repeat(bar_len), ratio);
|
|||
|
|
|
|||
|
|
println!(
|
|||
|
|
" {:>10} │ {}{} {}",
|
|||
|
|
range_label.dimmed(),
|
|||
|
|
bar,
|
|||
|
|
" ".repeat(bar_budget.saturating_sub(bar_len)),
|
|||
|
|
format!("{}", bins[i]).bold(),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Distribution shape stats
|
|||
|
|
let m = mean(values);
|
|||
|
|
let sd = std_dev(values);
|
|||
|
|
let sk = skewness(values);
|
|||
|
|
let ku = kurtosis(values);
|
|||
|
|
println!(
|
|||
|
|
" {} mean={:.1} σ={:.1} skew={:.2} kurt={:.2}",
|
|||
|
|
"↳".dimmed(),
|
|||
|
|
m, sd, sk, ku
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Scatter plot ───────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
fn render_scatter(x: &[f64], y: &[f64], x_label: &str, y_label: &str, verbose: bool) {
|
|||
|
|
if x.is_empty() { return; }
|
|||
|
|
|
|||
|
|
let r = pearson(x, y);
|
|||
|
|
|
|||
|
|
println!(
|
|||
|
|
"\n{}",
|
|||
|
|
format!("── {} vs {} (r={:.3}) ", x_label, y_label, r)
|
|||
|
|
.bright_cyan()
|
|||
|
|
.bold()
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if verbose {
|
|||
|
|
render::verbose_block(&[
|
|||
|
|
"Scatter plot: each ● represents one or more functions at that (x, y) position.",
|
|||
|
|
" Green ● = 1 function, Yellow ● = 2-3, Red ● = 4+ (overlapping).",
|
|||
|
|
" · = empty cell. X-axis = LoC, Y-axis = composite complexity.",
|
|||
|
|
"Pearson r measures linear correlation: r > 0.7 = strong positive,",
|
|||
|
|
" r ≈ 0 = no linear relationship, r < -0.7 = strong negative.",
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let plot_w: usize = render::terminal_width().min(72).saturating_sub(8);
|
|||
|
|
let plot_h: usize = 20;
|
|||
|
|
|
|||
|
|
let x_min = x.iter().cloned().fold(f64::INFINITY, f64::min);
|
|||
|
|
let x_max = x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
|||
|
|
let y_min = y.iter().cloned().fold(f64::INFINITY, f64::min);
|
|||
|
|
let y_max = y.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
|||
|
|
|
|||
|
|
let x_range = if (x_max - x_min).abs() < 1e-12 { 1.0 } else { x_max - x_min };
|
|||
|
|
let y_range = if (y_max - y_min).abs() < 1e-12 { 1.0 } else { y_max - y_min };
|
|||
|
|
|
|||
|
|
// Build grid with counts
|
|||
|
|
let mut grid = vec![vec![0u32; plot_w]; plot_h];
|
|||
|
|
for i in 0..x.len() {
|
|||
|
|
let col = ((x[i] - x_min) / x_range * (plot_w - 1) as f64).round() as usize;
|
|||
|
|
let row = ((y[i] - y_min) / y_range * (plot_h - 1) as f64).round() as usize;
|
|||
|
|
let col = col.min(plot_w - 1);
|
|||
|
|
let row = row.min(plot_h - 1);
|
|||
|
|
grid[row][col] += 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Render top to bottom (high y first)
|
|||
|
|
let y_label_w = 6;
|
|||
|
|
for row in (0..plot_h).rev() {
|
|||
|
|
let y_val = y_min + (row as f64 / (plot_h - 1).max(1) as f64) * y_range;
|
|||
|
|
let label = if row == plot_h - 1 || row == 0 || row == plot_h / 2 {
|
|||
|
|
format!("{:>5.0}", y_val)
|
|||
|
|
} else {
|
|||
|
|
" ".to_string()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let mut line = String::new();
|
|||
|
|
for col in 0..plot_w {
|
|||
|
|
let count = grid[row][col];
|
|||
|
|
if count == 0 {
|
|||
|
|
line.push('·');
|
|||
|
|
} else if count == 1 {
|
|||
|
|
line.push_str(&"●".green().to_string());
|
|||
|
|
} else if count < 4 {
|
|||
|
|
line.push_str(&"●".yellow().to_string());
|
|||
|
|
} else {
|
|||
|
|
line.push_str(&"●".red().bold().to_string());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let border = if row == 0 { "└" } else { "│" };
|
|||
|
|
println!("{} {}{}", label.dimmed(), border, line);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// X axis
|
|||
|
|
let x_min_s = format!("{:.0}", x_min);
|
|||
|
|
let x_max_s = format!("{:.0}", x_max);
|
|||
|
|
let mid_x = (x_min + x_max) / 2.0;
|
|||
|
|
let x_mid_s = format!("{:.0}", mid_x);
|
|||
|
|
let axis_padding = plot_w.saturating_sub(x_min_s.len() + x_max_s.len() + x_mid_s.len()) / 2;
|
|||
|
|
println!(
|
|||
|
|
"{} {}{}{}{}{}",
|
|||
|
|
" ".repeat(y_label_w),
|
|||
|
|
x_min_s.dimmed(),
|
|||
|
|
" ".repeat(axis_padding),
|
|||
|
|
x_mid_s.dimmed(),
|
|||
|
|
" ".repeat(axis_padding),
|
|||
|
|
x_max_s.dimmed(),
|
|||
|
|
);
|
|||
|
|
println!(
|
|||
|
|
"{} {} → {} {} ↑",
|
|||
|
|
" ".repeat(y_label_w),
|
|||
|
|
x_label.dimmed(),
|
|||
|
|
y_label.dimmed(),
|
|||
|
|
format!("r={:.3}", r).bold(),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Outlier detection ──────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
fn render_outliers(fcs: &[FunctionComplexity], metrics: &MetricSet, verbose: bool) {
|
|||
|
|
println!(
|
|||
|
|
"\n{}",
|
|||
|
|
"── Outliers (z-score > 2.0) ───────────────────────────"
|
|||
|
|
.bright_cyan()
|
|||
|
|
.bold()
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if verbose {
|
|||
|
|
render::verbose_block(&[
|
|||
|
|
"Outlier detection: functions with a z-score > 2.0 in any metric.",
|
|||
|
|
"z-score = (value - mean) / standard deviation. z > 2.0 means the value is",
|
|||
|
|
"more than 2 standard deviations above the mean — statistically unusual.",
|
|||
|
|
"These are candidates for refactoring or closer inspection.",
|
|||
|
|
"Grouped by metric, showing up to 5 outliers per metric.",
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut any_outlier = false;
|
|||
|
|
|
|||
|
|
for (mi, metric_name) in METRIC_NAMES.iter().enumerate() {
|
|||
|
|
let vals = &metrics.columns[mi];
|
|||
|
|
let m = mean(vals);
|
|||
|
|
let s = std_dev(vals);
|
|||
|
|
if s < 1e-12 { continue; }
|
|||
|
|
|
|||
|
|
let mut outliers: Vec<(usize, f64)> = Vec::new();
|
|||
|
|
for (i, &v) in vals.iter().enumerate() {
|
|||
|
|
let z = (v - m) / s;
|
|||
|
|
if z > 2.0 {
|
|||
|
|
outliers.push((i, z));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
outliers.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
|||
|
|
|
|||
|
|
if !outliers.is_empty() {
|
|||
|
|
any_outlier = true;
|
|||
|
|
println!("\n {} ({} outlier{})", metric_name.yellow().bold(), outliers.len(),
|
|||
|
|
if outliers.len() == 1 { "" } else { "s" });
|
|||
|
|
|
|||
|
|
for (i, z) in outliers.iter().take(5) {
|
|||
|
|
let name = if fcs[*i].name.len() > 45 {
|
|||
|
|
format!("{}...", &fcs[*i].name[..42])
|
|||
|
|
} else {
|
|||
|
|
fcs[*i].name.clone()
|
|||
|
|
};
|
|||
|
|
println!(
|
|||
|
|
" {} {} (z={:.2}, val={:.0})",
|
|||
|
|
"▸".red(),
|
|||
|
|
name.dimmed(),
|
|||
|
|
z,
|
|||
|
|
vals[*i],
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !any_outlier {
|
|||
|
|
println!(" {}", "No outliers detected.".dimmed());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Correlation matrix ─────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
fn render_correlation_matrix(metrics: &MetricSet, verbose: bool) {
|
|||
|
|
let n = METRIC_NAMES.len();
|
|||
|
|
|
|||
|
|
println!(
|
|||
|
|
"\n{}",
|
|||
|
|
"── Correlation Matrix ─────────────────────────────────"
|
|||
|
|
.bright_cyan()
|
|||
|
|
.bold()
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if verbose {
|
|||
|
|
render::verbose_block(&[
|
|||
|
|
"Pairwise Pearson correlation coefficients (r) between all metrics.",
|
|||
|
|
"r ranges from -1.0 (perfect negative) to +1.0 (perfect positive).",
|
|||
|
|
"Color: red/bold = strong positive (r > 0.7), blue/bold = strong negative (r < -0.7),",
|
|||
|
|
" yellow = moderate positive (r > 0.4), cyan = moderate negative (r < -0.4),",
|
|||
|
|
" dim = weak correlation (|r| ≤ 0.4).",
|
|||
|
|
"Diagonal is always 1.000 (a metric perfectly correlates with itself).",
|
|||
|
|
"High correlation between two metrics suggests redundancy or a shared underlying factor.",
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Header row
|
|||
|
|
print!(" {:>12}", "");
|
|||
|
|
for name in METRIC_NAMES {
|
|||
|
|
print!(" {:>10}", name.bold());
|
|||
|
|
}
|
|||
|
|
println!();
|
|||
|
|
|
|||
|
|
for i in 0..n {
|
|||
|
|
print!(" {:>12}", METRIC_NAMES[i].bold());
|
|||
|
|
for j in 0..n {
|
|||
|
|
let r = pearson(&metrics.columns[i], &metrics.columns[j]);
|
|||
|
|
let cell = format!("{:>7.3}", r);
|
|||
|
|
let colored = color_correlation(&cell, r);
|
|||
|
|
print!(" {}", colored);
|
|||
|
|
}
|
|||
|
|
println!();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn color_correlation(text: &str, r: f64) -> String {
|
|||
|
|
let abs_r = r.abs();
|
|||
|
|
if abs_r > 0.7 {
|
|||
|
|
if r > 0.0 { text.red().bold().to_string() } else { text.blue().bold().to_string() }
|
|||
|
|
} else if abs_r > 0.4 {
|
|||
|
|
if r > 0.0 { text.yellow().to_string() } else { text.cyan().to_string() }
|
|||
|
|
} else {
|
|||
|
|
text.dimmed().to_string()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Public entry point ─────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
pub fn render_dist(
|
|||
|
|
rs_files: &[std::path::PathBuf],
|
|||
|
|
project_path: &Path,
|
|||
|
|
metric_filter: Option<&str>,
|
|||
|
|
num_bins: usize,
|
|||
|
|
verbose: bool,
|
|||
|
|
) {
|
|||
|
|
let symbols = crate::ast_parser::parse_project(rs_files);
|
|||
|
|
let fcs = complexity::compute_all(&symbols, project_path);
|
|||
|
|
|
|||
|
|
if fcs.is_empty() {
|
|||
|
|
println!("{}", "No functions found to analyze.".yellow());
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let metrics = extract_metrics(&fcs);
|
|||
|
|
|
|||
|
|
println!(
|
|||
|
|
"\n{}",
|
|||
|
|
"── Distribution Analysis ──────────────────────────────"
|
|||
|
|
.bright_cyan()
|
|||
|
|
.bold()
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if verbose {
|
|||
|
|
render::verbose_block(&[
|
|||
|
|
"Statistical distribution analysis of complexity metrics across all functions.",
|
|||
|
|
"Histograms show frequency distributions. Scatter plot shows LoC vs complexity.",
|
|||
|
|
"Outliers are functions with z-score > 2.0 (more than 2σ above the mean).",
|
|||
|
|
"Correlation matrix shows pairwise Pearson r between all metrics.",
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
println!(
|
|||
|
|
"{} functions across {} files\n",
|
|||
|
|
format!("{}", fcs.len()).bold(),
|
|||
|
|
format!("{}", rs_files.len()).bold(),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Histograms
|
|||
|
|
let labels = &["Function LoC", "Cyclomatic", "Cognitive", "Nesting Depth", "Param Count"];
|
|||
|
|
|
|||
|
|
if let Some(filter) = metric_filter {
|
|||
|
|
// Find the matching metric
|
|||
|
|
let filter_lower = filter.to_lowercase();
|
|||
|
|
if let Some(idx) = METRIC_NAMES.iter().position(|&n| n == filter_lower) {
|
|||
|
|
render_histogram(&metrics.columns[idx], labels[idx], num_bins, verbose);
|
|||
|
|
} else {
|
|||
|
|
println!(
|
|||
|
|
"{} Unknown metric '{}'. Available: {}",
|
|||
|
|
"Error:".red().bold(),
|
|||
|
|
filter,
|
|||
|
|
METRIC_NAMES.join(", "),
|
|||
|
|
);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
for (i, label) in labels.iter().enumerate() {
|
|||
|
|
render_histogram(&metrics.columns[i], label, num_bins, verbose);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Scatter plot: complexity (composite score) vs LoC
|
|||
|
|
let composite_scores: Vec<f64> = fcs.iter().map(|f| f.composite_score).collect();
|
|||
|
|
render_scatter(
|
|||
|
|
&metrics.columns[0], // LoC
|
|||
|
|
&composite_scores,
|
|||
|
|
"Function LoC",
|
|||
|
|
"Complexity",
|
|||
|
|
verbose,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Outlier detection
|
|||
|
|
render_outliers(&fcs, &metrics, verbose);
|
|||
|
|
|
|||
|
|
// Correlation matrix
|
|||
|
|
render_correlation_matrix(&metrics, verbose);
|
|||
|
|
|
|||
|
|
println!();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[derive(Serialize)]
|
|||
|
|
struct HistogramBinJson {
|
|||
|
|
low: f64,
|
|||
|
|
high: f64,
|
|||
|
|
count: usize,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[derive(Serialize)]
|
|||
|
|
struct HistogramJson {
|
|||
|
|
metric: String,
|
|||
|
|
bins: Vec<HistogramBinJson>,
|
|||
|
|
mean: f64,
|
|||
|
|
std_dev: f64,
|
|||
|
|
skewness: f64,
|
|||
|
|
kurtosis: f64,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[derive(Serialize)]
|
|||
|
|
struct OutlierJson {
|
|||
|
|
metric: String,
|
|||
|
|
function: String,
|
|||
|
|
z_score: f64,
|
|||
|
|
value: f64,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[derive(Serialize)]
|
|||
|
|
struct CorrelationEntryJson {
|
|||
|
|
metric_a: String,
|
|||
|
|
metric_b: String,
|
|||
|
|
pearson_r: f64,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[derive(Serialize)]
|
|||
|
|
struct DistJson {
|
|||
|
|
cstat_version: String,
|
|||
|
|
histograms: Vec<HistogramJson>,
|
|||
|
|
outliers: Vec<OutlierJson>,
|
|||
|
|
correlations: Vec<CorrelationEntryJson>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn compute_histogram_bins(values: &[f64], num_bins: usize) -> Vec<HistogramBinJson> {
|
|||
|
|
if values.is_empty() { return vec![]; }
|
|||
|
|
|
|||
|
|
let min_v = values.iter().cloned().fold(f64::INFINITY, f64::min);
|
|||
|
|
let max_v = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
|||
|
|
let range = max_v - min_v;
|
|||
|
|
let num_bins = if range.abs() < 1e-12 {
|
|||
|
|
1
|
|||
|
|
} else {
|
|||
|
|
let int_range = range.ceil() as usize;
|
|||
|
|
num_bins.min(int_range.max(1))
|
|||
|
|
};
|
|||
|
|
let bin_width = if num_bins == 1 { 1.0 } else { range / num_bins as f64 };
|
|||
|
|
|
|||
|
|
let mut counts = vec![0usize; num_bins];
|
|||
|
|
for &v in values {
|
|||
|
|
let idx = if num_bins == 1 {
|
|||
|
|
0
|
|||
|
|
} else {
|
|||
|
|
((v - min_v) / bin_width).floor() as usize
|
|||
|
|
};
|
|||
|
|
let idx = idx.min(num_bins - 1);
|
|||
|
|
counts[idx] += 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
(0..num_bins).map(|i| {
|
|||
|
|
let lo = min_v + i as f64 * bin_width;
|
|||
|
|
let hi = lo + bin_width;
|
|||
|
|
HistogramBinJson { low: lo, high: hi, count: counts[i] }
|
|||
|
|
}).collect()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Render distribution analysis as JSON.
|
|||
|
|
pub fn render_dist_json(
|
|||
|
|
rs_files: &[std::path::PathBuf],
|
|||
|
|
project_path: &std::path::Path,
|
|||
|
|
metric_filter: Option<&str>,
|
|||
|
|
num_bins: usize,
|
|||
|
|
) {
|
|||
|
|
let symbols = crate::ast_parser::parse_project(rs_files);
|
|||
|
|
let fcs = complexity::compute_all(&symbols, project_path);
|
|||
|
|
let metrics = extract_metrics(&fcs);
|
|||
|
|
|
|||
|
|
let labels = &["loc", "cyclomatic", "cognitive", "nesting", "params"];
|
|||
|
|
|
|||
|
|
let indices: Vec<usize> = if let Some(filter) = metric_filter {
|
|||
|
|
let filter_lower = filter.to_lowercase();
|
|||
|
|
if let Some(idx) = METRIC_NAMES.iter().position(|&n| n == filter_lower) {
|
|||
|
|
vec![idx]
|
|||
|
|
} else {
|
|||
|
|
vec![]
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
(0..METRIC_NAMES.len()).collect()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let histograms: Vec<HistogramJson> = indices.iter().map(|&i| {
|
|||
|
|
let vals = &metrics.columns[i];
|
|||
|
|
let bins_json = compute_histogram_bins(vals, num_bins);
|
|||
|
|
HistogramJson {
|
|||
|
|
metric: labels[i].to_string(),
|
|||
|
|
bins: bins_json,
|
|||
|
|
mean: mean(vals),
|
|||
|
|
std_dev: std_dev(vals),
|
|||
|
|
skewness: skewness(vals),
|
|||
|
|
kurtosis: kurtosis(vals),
|
|||
|
|
}
|
|||
|
|
}).collect();
|
|||
|
|
|
|||
|
|
let mut outliers_json: Vec<OutlierJson> = Vec::new();
|
|||
|
|
for (mi, metric_name) in METRIC_NAMES.iter().enumerate() {
|
|||
|
|
let vals = &metrics.columns[mi];
|
|||
|
|
let m = mean(vals);
|
|||
|
|
let s = std_dev(vals);
|
|||
|
|
if s < 1e-12 { continue; }
|
|||
|
|
for (i, &v) in vals.iter().enumerate() {
|
|||
|
|
let z = (v - m) / s;
|
|||
|
|
if z > 2.0 {
|
|||
|
|
outliers_json.push(OutlierJson {
|
|||
|
|
metric: metric_name.to_string(),
|
|||
|
|
function: fcs[i].name.clone(),
|
|||
|
|
z_score: z,
|
|||
|
|
value: v,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
outliers_json.sort_by(|a, b| b.z_score.partial_cmp(&a.z_score).unwrap_or(std::cmp::Ordering::Equal));
|
|||
|
|
|
|||
|
|
let n = METRIC_NAMES.len();
|
|||
|
|
let mut correlations: Vec<CorrelationEntryJson> = Vec::new();
|
|||
|
|
for i in 0..n {
|
|||
|
|
for j in (i+1)..n {
|
|||
|
|
let r = pearson(&metrics.columns[i], &metrics.columns[j]);
|
|||
|
|
correlations.push(CorrelationEntryJson {
|
|||
|
|
metric_a: METRIC_NAMES[i].to_string(),
|
|||
|
|
metric_b: METRIC_NAMES[j].to_string(),
|
|||
|
|
pearson_r: r,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let output = DistJson {
|
|||
|
|
cstat_version: env!("CARGO_PKG_VERSION").to_string(),
|
|||
|
|
histograms,
|
|||
|
|
outliers: outliers_json,
|
|||
|
|
correlations,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
println!("{}", serde_json::to_string(&output).unwrap());
|
|||
|
|
}
|