#![allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::too_many_lines
)]
mod sections;
use std::collections::BTreeMap;
use std::fmt::Write;
use big_code_analysis::{FuncSpace, LANG, SpaceKind};
#[derive(Debug)]
pub(crate) struct FunctionSummary {
pub file: String,
pub name: String,
pub kind: SpaceKind,
pub language: LANG,
pub start_line: usize,
#[allow(dead_code)]
pub end_line: usize,
pub sloc: usize,
pub ploc: usize,
#[expect(dead_code)]
pub lloc: usize,
pub cloc: usize,
pub tokens: usize,
pub cyclomatic: f64,
pub cognitive: f64,
pub halstead_volume: f64,
#[expect(dead_code)]
pub halstead_difficulty: f64,
pub halstead_effort: f64,
pub halstead_bugs: f64,
#[expect(dead_code)]
pub halstead_time: f64,
#[expect(dead_code)]
pub mi_original: f64,
#[expect(dead_code)]
pub mi_sei: f64,
pub mi_visual_studio: f64,
pub nargs: usize,
pub nexits: usize,
pub nom: usize,
pub abc: f64,
pub wmc: f64,
pub npa: f64,
pub npm: f64,
}
pub(crate) fn extract_summaries(
space: &FuncSpace,
file: &str,
language: LANG,
strip_prefix: &str,
out: &mut Vec<FunctionSummary>,
) {
let display_file = file.strip_prefix(strip_prefix).unwrap_or(file);
extract_summaries_inner(space, display_file, language, out);
}
fn extract_summaries_inner(
space: &FuncSpace,
display_file: &str,
language: LANG,
out: &mut Vec<FunctionSummary>,
) {
let mut stack: Vec<&FuncSpace> = vec![space];
while let Some(current) = stack.pop() {
let m = ¤t.metrics;
out.push(FunctionSummary {
file: display_file.to_string(),
name: current.name.clone().unwrap_or_default(),
kind: current.kind,
language,
start_line: current.start_line,
end_line: current.end_line,
sloc: m.loc.sloc() as usize,
ploc: m.loc.ploc() as usize,
lloc: m.loc.lloc() as usize,
cloc: m.loc.cloc() as usize,
tokens: m.tokens.tokens_sum() as usize,
cyclomatic: m.cyclomatic.cyclomatic(),
cognitive: m.cognitive.cognitive(),
halstead_volume: m.halstead.volume(),
halstead_difficulty: m.halstead.difficulty(),
halstead_effort: m.halstead.effort(),
halstead_bugs: m.halstead.bugs(),
halstead_time: m.halstead.time(),
mi_original: m.mi.mi_original(),
mi_sei: m.mi.mi_sei(),
mi_visual_studio: m.mi.mi_visual_studio(),
nargs: m.nargs.nargs_total() as usize,
nexits: m.nexits.exit_sum() as usize,
nom: m.nom.total() as usize,
abc: m.abc.magnitude(),
wmc: m.wmc.total_wmc(),
npa: m.npa.total_npa(),
npm: m.npm.total_npm(),
});
stack.extend(current.spaces.iter().rev());
}
}
fn escape_cell(s: &str) -> String {
s.replace('|', "\\|").replace(['\n', '\r'], " ")
}
fn escape_name(s: &str) -> String {
let sanitized = s.replace('`', "\u{02CB}");
format!("`{}`", escape_cell(&sanitized))
}
pub(super) fn thousands(n: usize) -> String {
let s = n.to_string();
let len = s.len();
if len <= 3 {
return s;
}
let mut result = String::with_capacity(len + (len - 1) / 3);
for (i, ch) in s.chars().enumerate() {
if i > 0 && (len - i).is_multiple_of(3) {
result.push(',');
}
result.push(ch);
}
result
}
pub(super) fn title_case(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut capitalize_next = true;
for c in s.chars() {
if capitalize_next {
result.extend(c.to_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
if matches!(c, '/' | ' ' | '-') {
capitalize_next = true;
}
}
result
}
pub(super) fn sort_by_metric_desc(
items: &mut [&FunctionSummary],
metric: impl Fn(&FunctionSummary) -> f64,
) {
items.sort_by(|a, b| {
metric(b)
.total_cmp(&metric(a))
.then_with(|| a.file.cmp(&b.file))
.then_with(|| a.start_line.cmp(&b.start_line))
.then_with(|| a.name.cmp(&b.name))
});
}
pub(super) fn sort_by_metric_asc(
items: &mut [&FunctionSummary],
metric: impl Fn(&FunctionSummary) -> f64,
) {
items.sort_by(|a, b| {
metric(a)
.total_cmp(&metric(b))
.then_with(|| a.file.cmp(&b.file))
.then_with(|| a.start_line.cmp(&b.start_line))
.then_with(|| a.name.cmp(&b.name))
});
}
pub(super) fn is_class_like(kind: SpaceKind) -> bool {
matches!(
kind,
SpaceKind::Class
| SpaceKind::Struct
| SpaceKind::Trait
| SpaceKind::Impl
| SpaceKind::Namespace
| SpaceKind::Interface
)
}
pub(super) fn mi_rating(mi: f64) -> &'static str {
if mi >= 20.0 {
"GOOD"
} else if mi >= 10.0 {
"MODERATE"
} else {
"LOW"
}
}
#[derive(Clone, Copy)]
enum Align {
Left,
Right,
}
fn write_table(out: &mut String, headers: &[&str], aligns: &[Align], rows: &[Vec<String>]) {
debug_assert_eq!(headers.len(), aligns.len());
let widths = column_widths(headers, rows);
out.push('|');
for (i, h) in headers.iter().enumerate() {
push_cell(out, h, widths[i], aligns[i]);
out.push('|');
}
out.push('\n');
out.push('|');
for (i, &a) in aligns.iter().enumerate() {
push_separator(out, a, widths[i]);
}
out.push('\n');
for row in rows {
debug_assert_eq!(row.len(), headers.len());
out.push('|');
for (i, cell) in row.iter().enumerate() {
push_cell(out, cell, widths[i], aligns[i]);
out.push('|');
}
out.push('\n');
}
}
fn column_widths(headers: &[&str], rows: &[Vec<String>]) -> Vec<usize> {
headers
.iter()
.enumerate()
.map(|(i, h)| {
let cell_w = rows.iter().map(|r| r[i].chars().count()).max().unwrap_or(0);
h.chars().count().max(cell_w).max(3)
})
.collect()
}
fn push_cell(out: &mut String, cell: &str, width: usize, align: Align) {
let pad = width - cell.chars().count();
out.push(' ');
match align {
Align::Left => {
out.push_str(cell);
out.extend(std::iter::repeat_n(' ', pad));
}
Align::Right => {
out.extend(std::iter::repeat_n(' ', pad));
out.push_str(cell);
}
}
out.push(' ');
}
fn push_separator(out: &mut String, align: Align, width: usize) {
out.push(' ');
match align {
Align::Left => out.extend(std::iter::repeat_n('-', width)),
Align::Right => {
out.extend(std::iter::repeat_n('-', width - 1));
out.push(':');
}
}
out.push(' ');
out.push('|');
}
pub(crate) fn generate_report(summaries: &[FunctionSummary], top_n: usize) -> String {
let mut out = String::new();
let by_lang = group_by_language(summaries);
let totals = GlobalTotals::from_entries(summaries);
write_global_header(&mut out, &totals, &by_lang);
if by_lang.is_empty() {
return out;
}
write_per_language_overview(&mut out, &by_lang);
for (&lang_name, lang_summaries) in &by_lang {
write_language_section(&mut out, lang_name, lang_summaries, top_n);
}
out
}
fn group_by_language(summaries: &[FunctionSummary]) -> BTreeMap<&str, Vec<&FunctionSummary>> {
let mut map = BTreeMap::<&str, Vec<&FunctionSummary>>::new();
for s in summaries {
map.entry(s.language.get_name()).or_default().push(s);
}
map
}
struct GlobalTotals {
files: usize,
sloc: usize,
ploc: usize,
cloc: usize,
functions: usize,
classes: usize,
}
impl GlobalTotals {
fn from_entries(summaries: &[FunctionSummary]) -> Self {
let mut t = Self {
files: 0,
sloc: 0,
ploc: 0,
cloc: 0,
functions: 0,
classes: 0,
};
for s in summaries {
if s.kind == SpaceKind::Unit {
t.files += 1;
t.sloc += s.sloc;
t.ploc += s.ploc;
t.cloc += s.cloc;
}
if s.kind == SpaceKind::Function {
t.functions += 1;
}
if is_class_like(s.kind) {
t.classes += 1;
}
}
t
}
fn comment_ratio(&self) -> f64 {
if self.sloc > 0 {
(self.cloc as f64 / self.sloc as f64) * 100.0
} else {
0.0
}
}
}
fn write_global_header(
out: &mut String,
totals: &GlobalTotals,
by_lang: &BTreeMap<&str, Vec<&FunctionSummary>>,
) {
let languages_list: String = by_lang
.keys()
.map(|k| title_case(k))
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(out, "# Code Quality Metrics Summary\n");
let _ = writeln!(
out,
"**Files analyzed:** {} **Languages:** {}",
thousands(totals.files),
languages_list,
);
let _ = writeln!(
out,
"**Total SLOC:** {} **PLOC:** {} **Comments:** {}",
thousands(totals.sloc),
thousands(totals.ploc),
thousands(totals.cloc),
);
let _ = writeln!(
out,
"**Functions/methods:** {} **Classes/impls/traits:** {}",
thousands(totals.functions),
thousands(totals.classes),
);
let comment_ratio = totals.comment_ratio();
let _ = writeln!(out, "**Comment ratio:** {comment_ratio:.1}%");
}
fn write_per_language_overview(out: &mut String, by_lang: &BTreeMap<&str, Vec<&FunctionSummary>>) {
let _ = writeln!(out, "\n## Per-language overview\n");
let mut overview_rows: Vec<Vec<String>> = Vec::with_capacity(by_lang.len());
for (&lang_name, lang_summaries) in by_lang {
overview_rows.push(lang_overview_row(lang_name, lang_summaries));
}
write_table(
out,
&[
"Language",
"Files",
"SLOC",
"Functions",
"Avg MI",
"Avg CC",
"Avg Cognitive",
],
&[
Align::Left,
Align::Right,
Align::Right,
Align::Right,
Align::Right,
Align::Right,
Align::Right,
],
&overview_rows,
);
}
fn lang_overview_row(lang_name: &str, lang_summaries: &[&FunctionSummary]) -> Vec<String> {
let (unit_count, lang_sloc, avg_mi) = unit_aggregates(lang_summaries);
let (func_count, avg_cc, avg_cog) = function_aggregates(lang_summaries);
vec![
title_case(lang_name),
thousands(unit_count),
thousands(lang_sloc),
thousands(func_count),
format!("{avg_mi:.1}"),
format!("{avg_cc:.1}"),
format!("{avg_cog:.1}"),
]
}
fn unit_aggregates(lang_summaries: &[&FunctionSummary]) -> (usize, usize, f64) {
let mut count = 0usize;
let mut sloc = 0usize;
let mut mi_sum = 0.0f64;
for s in lang_summaries.iter().filter(|s| s.kind == SpaceKind::Unit) {
count += 1;
sloc += s.sloc;
mi_sum += s.mi_visual_studio;
}
let avg_mi = if count > 0 {
mi_sum / count as f64
} else {
0.0
};
(count, sloc, avg_mi)
}
fn function_aggregates(lang_summaries: &[&FunctionSummary]) -> (usize, f64, f64) {
let mut count = 0usize;
let mut cc_sum = 0.0f64;
let mut cog_sum = 0.0f64;
for s in lang_summaries
.iter()
.filter(|s| s.kind == SpaceKind::Function)
{
count += 1;
cc_sum += s.cyclomatic;
cog_sum += s.cognitive;
}
if count > 0 {
(count, cc_sum / count as f64, cog_sum / count as f64)
} else {
(0, 0.0, 0.0)
}
}
fn write_language_section(
out: &mut String,
lang_name: &str,
entries: &[&FunctionSummary],
top_n: usize,
) {
let display_name = title_case(lang_name);
let _ = writeln!(out, "\n## {display_name}\n");
let (units, funcs) = sections::split_units_and_functions(entries);
sections::write_summary(out, &units);
sections::write_mi_lowest(out, &units, top_n);
sections::write_cyclomatic_hotspots(out, &funcs, top_n);
sections::write_cognitive_hotspots(out, &funcs, top_n);
sections::write_halstead_hotspots(out, &funcs, top_n);
sections::write_largest_by_sloc(out, &funcs, top_n);
sections::write_many_params(out, &funcs, top_n);
sections::write_actionable_summary(out, &funcs);
sections::write_wmc_hotspots(out, entries, top_n);
sections::write_nexits_hotspots(out, &funcs, top_n);
sections::write_abc_hotspots(out, &funcs, top_n);
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::doc_markdown,
clippy::needless_raw_string_hashes,
clippy::too_many_lines
)]
mod tests {
use super::*;
use big_code_analysis::{CodeMetrics, FuncSpace, SpaceKind};
fn collapse_spaces(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut prev_space = false;
for c in s.chars() {
if c == ' ' {
if !prev_space {
out.push(' ');
}
prev_space = true;
} else {
out.push(c);
prev_space = false;
}
}
out
}
fn make_space(name: &str, kind: SpaceKind, start: usize, end: usize) -> FuncSpace {
FuncSpace {
name: Some(name.to_string()),
start_line: start,
end_line: end,
kind,
spaces: Vec::new(),
metrics: CodeMetrics::default(),
suppressed: big_code_analysis::SuppressionScope::default(),
}
}
fn make_summary(name: &str, file: &str, kind: SpaceKind, language: LANG) -> FunctionSummary {
FunctionSummary {
file: file.to_string(),
name: name.to_string(),
kind,
language,
start_line: 1,
end_line: 10,
sloc: 20,
ploc: 25,
lloc: 15,
cloc: 5,
tokens: 30,
cyclomatic: 3.0,
cognitive: 2.0,
halstead_volume: 100.0,
halstead_difficulty: 5.0,
halstead_effort: 500.0,
halstead_bugs: 0.1,
halstead_time: 28.0,
mi_original: 80.0,
mi_sei: 85.0,
mi_visual_studio: 50.0,
nargs: 2,
nexits: 1,
nom: 1,
abc: 5.0,
wmc: 3.0,
npa: 0.0,
npm: 0.0,
}
}
#[test]
fn extract_single_space() {
let space = make_space("root.rs", SpaceKind::Unit, 1, 10);
let mut out = Vec::new();
extract_summaries(&space, "src/root.rs", LANG::Rust, "", &mut out);
assert_eq!(out.len(), 1);
assert_eq!(out[0].file, "src/root.rs");
assert_eq!(out[0].name, "root.rs");
assert_eq!(out[0].kind, SpaceKind::Unit);
assert_eq!(out[0].start_line, 1);
assert_eq!(out[0].end_line, 10);
}
#[test]
fn extract_nested_spaces() {
let mut root = make_space("root.rs", SpaceKind::Unit, 1, 20);
let func_a = make_space("func_a", SpaceKind::Function, 2, 8);
let mut class_b = make_space("ClassB", SpaceKind::Class, 10, 18);
let func_c = make_space("method_c", SpaceKind::Function, 12, 16);
class_b.spaces.push(func_c);
root.spaces.push(func_a);
root.spaces.push(class_b);
let mut out = Vec::new();
extract_summaries(&root, "src/root.rs", LANG::Rust, "", &mut out);
assert_eq!(out.len(), 4);
assert_eq!(out[0].kind, SpaceKind::Unit);
assert_eq!(out[1].kind, SpaceKind::Function);
assert_eq!(out[1].name, "func_a");
assert_eq!(out[2].kind, SpaceKind::Class);
assert_eq!(out[2].name, "ClassB");
assert_eq!(out[3].kind, SpaceKind::Function);
assert_eq!(out[3].name, "method_c");
assert_eq!(out[3].start_line, 12);
assert_eq!(out[3].end_line, 16);
}
#[test]
fn strip_prefix_removes_matching_prefix() {
let space = make_space("root.rs", SpaceKind::Unit, 1, 5);
let mut out = Vec::new();
extract_summaries(&space, "src/lib/root.rs", LANG::Rust, "src/lib/", &mut out);
assert_eq!(out[0].file, "root.rs");
}
#[test]
fn strip_prefix_passthrough_on_mismatch() {
let space = make_space("root.rs", SpaceKind::Unit, 1, 5);
let mut out = Vec::new();
extract_summaries(&space, "other/root.rs", LANG::Rust, "src/lib/", &mut out);
assert_eq!(out[0].file, "other/root.rs");
}
#[test]
fn empty_tree_produces_one_summary() {
let space = make_space("empty.rs", SpaceKind::Unit, 0, 0);
let mut out = Vec::new();
extract_summaries(&space, "empty.rs", LANG::Rust, "", &mut out);
assert_eq!(out.len(), 1);
}
#[test]
fn language_propagated_to_all_children() {
let mut root = make_space("root.py", SpaceKind::Unit, 1, 10);
root.spaces.push(make_space("f", SpaceKind::Function, 2, 5));
let mut out = Vec::new();
extract_summaries(&root, "root.py", LANG::Python, "", &mut out);
assert_eq!(out.len(), 2);
assert!(out.iter().all(|s| s.language == LANG::Python));
}
#[test]
fn deeply_nested_spaces_do_not_overflow_stack() {
const DEPTH: usize = 50_000;
const TIGHT_STACK: usize = 256 * 1024;
let handle = std::thread::Builder::new()
.stack_size(TIGHT_STACK)
.spawn(|| {
let mut current = make_space("f_inner", SpaceKind::Function, DEPTH, DEPTH);
for i in (0..DEPTH).rev() {
let mut parent = if i == 0 {
make_space("root.rs", SpaceKind::Unit, 1, DEPTH + 1)
} else {
make_space(&format!("f_{i}"), SpaceKind::Function, i, DEPTH + 1)
};
parent.spaces.push(current);
current = parent;
}
let mut out = Vec::new();
extract_summaries(¤t, "root.rs", LANG::Rust, "", &mut out);
assert_eq!(out.len(), DEPTH + 1);
assert_eq!(out[0].kind, SpaceKind::Unit);
assert_eq!(out[0].name, "root.rs");
assert_eq!(out[DEPTH].name, "f_inner");
std::mem::forget(current);
})
.expect("spawn worker thread with bounded stack");
handle
.join()
.expect("iterative extract_summaries must not overflow even a 256 KiB stack");
}
#[test]
fn two_language_report_contains_both_sections() {
let summaries = vec![
make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust),
make_summary("do_stuff", "src/lib.rs", SpaceKind::Function, LANG::Rust),
make_summary("main.py", "main.py", SpaceKind::Unit, LANG::Python),
make_summary("run", "main.py", SpaceKind::Function, LANG::Python),
];
let report = generate_report(&summaries, 20);
assert!(report.contains("## Rust"), "missing Rust section header");
assert!(
report.contains("## Python"),
"missing Python section header"
);
assert!(
report.contains("## Per-language overview"),
"missing overview"
);
let normalized = collapse_spaces(&report);
assert!(
normalized.contains("| Rust |"),
"missing Rust overview row in:\n{report}"
);
assert!(
normalized.contains("| Python |"),
"missing Python overview row in:\n{report}"
);
assert!(report.contains("**Files analyzed:** 2"));
assert!(report.contains("**Functions/methods:** 2"));
}
#[test]
fn halstead_section_omitted_when_no_effort() {
let mut unit = make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust);
unit.halstead_effort = 0.0;
let mut func = make_summary("f", "src/lib.rs", SpaceKind::Function, LANG::Rust);
func.halstead_effort = 0.0;
func.halstead_volume = 0.0;
func.halstead_bugs = 0.0;
let report = generate_report(&[unit, func], 20);
assert!(
!report.contains("### Halstead Effort Hotspots"),
"Halstead section should be omitted"
);
}
#[test]
fn top_n_truncation() {
let mut summaries = Vec::new();
summaries.push(make_summary(
"lib.rs",
"src/lib.rs",
SpaceKind::Unit,
LANG::Rust,
));
for i in 0..30 {
let mut f = make_summary(
&format!("func_{i}"),
"src/lib.rs",
SpaceKind::Function,
LANG::Rust,
);
f.start_line = i + 1;
f.cyclomatic = (i + 1) as f64;
f.cognitive = (i + 1) as f64;
f.halstead_effort = (i + 1) as f64 * 100.0;
f.sloc = (i + 1) * 5;
summaries.push(f);
}
let report = generate_report(&summaries, 5);
let sections = [
"### Cyclomatic Complexity Hotspots",
"### Cognitive Complexity Hotspots",
"### Halstead Effort Hotspots",
"### Largest Functions by SLOC",
];
for section_hdr in sections {
let section_start = report
.find(section_hdr)
.unwrap_or_else(|| panic!("missing section: {section_hdr}"));
let section_text = &report[section_start..];
let section_end = section_text[1..]
.find("\n## ")
.or_else(|| section_text[1..].find("\n### "))
.map_or(section_text.len(), |p| p + 1);
let section_body = §ion_text[..section_end];
let data_rows = section_body
.lines()
.filter(|l| l.starts_with("| `"))
.count();
assert_eq!(
data_rows, 5,
"expected 5 data rows in {section_hdr}, got {data_rows}"
);
}
}
#[test]
fn determinism() {
let summaries = vec![
make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust),
make_summary("alpha", "src/lib.rs", SpaceKind::Function, LANG::Rust),
make_summary("beta", "src/lib.rs", SpaceKind::Function, LANG::Rust),
make_summary("main.py", "main.py", SpaceKind::Unit, LANG::Python),
make_summary("run", "main.py", SpaceKind::Function, LANG::Python),
];
let a = generate_report(&summaries, 10);
let b = generate_report(&summaries, 10);
assert_eq!(a, b, "report must be byte-equal across runs");
}
#[test]
fn cell_escaping_pipe() {
let mut f = make_summary("foo|bar", "dir/a|b.rs", SpaceKind::Function, LANG::Rust);
f.cyclomatic = 5.0;
let unit = make_summary("a|b.rs", "dir/a|b.rs", SpaceKind::Unit, LANG::Rust);
let report = generate_report(&[unit, f], 20);
assert!(
report.contains("foo\\|bar"),
"pipe in name not escaped: {report}"
);
assert!(
report.contains("a\\|b.rs"),
"pipe in file not escaped: {report}"
);
}
#[test]
fn cell_escaping_backtick() {
let mut f = make_summary("foo`bar", "src/lib.rs", SpaceKind::Function, LANG::Rust);
f.cyclomatic = 5.0;
let unit = make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust);
let report = generate_report(&[unit, f], 20);
assert!(
report.contains("foo\u{02CB}bar"),
"backtick in name not replaced"
);
}
#[test]
fn nan_safe_sort_does_not_panic() {
let mut unit = make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust);
unit.mi_visual_studio = f64::NAN;
let mut f = make_summary("f", "src/lib.rs", SpaceKind::Function, LANG::Rust);
f.cyclomatic = f64::NAN;
f.cognitive = f64::NAN;
f.halstead_effort = f64::NAN;
let report = generate_report(&[unit, f], 20);
assert!(report.contains("# Code Quality Metrics Summary"));
}
#[test]
fn sort_by_metric_desc_handles_nan() {
let mut a = make_summary("a", "a.rs", SpaceKind::Function, LANG::Rust);
a.cyclomatic = f64::NAN;
let mut b = make_summary("b", "b.rs", SpaceKind::Function, LANG::Rust);
b.cyclomatic = 5.0;
let mut c = make_summary("c", "c.rs", SpaceKind::Function, LANG::Rust);
c.cyclomatic = 10.0;
let mut items: Vec<&FunctionSummary> = vec![&a, &b, &c];
sort_by_metric_desc(&mut items, |s| s.cyclomatic);
assert_eq!(items[0].name, "a");
assert_eq!(items[1].name, "c");
assert_eq!(items[2].name, "b");
}
#[test]
fn sort_by_metric_asc_handles_nan() {
let mut a = make_summary("a", "a.rs", SpaceKind::Unit, LANG::Rust);
a.mi_visual_studio = f64::NAN;
let mut b = make_summary("b", "b.rs", SpaceKind::Unit, LANG::Rust);
b.mi_visual_studio = 30.0;
let mut c = make_summary("c", "c.rs", SpaceKind::Unit, LANG::Rust);
c.mi_visual_studio = 10.0;
let mut items: Vec<&FunctionSummary> = vec![&a, &b, &c];
sort_by_metric_asc(&mut items, |s| s.mi_visual_studio);
assert_eq!(items[0].name, "c");
assert_eq!(items[1].name, "b");
assert_eq!(items[2].name, "a");
}
#[test]
fn empty_input() {
let report = generate_report(&[], 20);
assert!(report.contains("**Files analyzed:** 0"));
assert!(report.contains("**Functions/methods:** 0"));
assert!(!report.contains("## Per-language overview"));
}
#[test]
fn thousands_formatting() {
assert_eq!(thousands(0), "0");
assert_eq!(thousands(999), "999");
assert_eq!(thousands(1_000), "1,000");
assert_eq!(thousands(1_234_567), "1,234,567");
assert_eq!(thousands(10_000_000), "10,000,000");
}
#[test]
fn write_table_pads_left_and_right_columns() {
let mut out = String::new();
write_table(
&mut out,
&["Name", "Count"],
&[Align::Left, Align::Right],
&[
vec!["a".to_string(), "1".to_string()],
vec!["longname".to_string(), "1234".to_string()],
],
);
let expected = "\
| Name | Count |
| -------- | ----: |
| a | 1 |
| longname | 1234 |
";
assert_eq!(out, expected);
}
#[test]
fn write_table_handles_empty_rows() {
let mut out = String::new();
write_table(&mut out, &["A", "B"], &[Align::Left, Align::Right], &[]);
let expected = "\
| A | B |
| --- | --: |
";
assert_eq!(out, expected);
}
#[test]
fn write_table_widens_to_longest_cell() {
let mut out = String::new();
write_table(
&mut out,
&["X", "Y"],
&[Align::Left, Align::Right],
&[vec!["wide-cell".to_string(), "100".to_string()]],
);
let expected = "\
| X | Y |
| --------- | --: |
| wide-cell | 100 |
";
assert_eq!(out, expected);
}
#[test]
fn write_table_counts_chars_not_bytes_for_multibyte_cells() {
let mut out = String::new();
write_table(
&mut out,
&["Name"],
&[Align::Left],
&[vec!["abc".to_string()], vec!["a\u{02CB}c".to_string()]],
);
let expected = "\
| Name |
| ---- |
| abc |
| a\u{02CB}c |
";
assert_eq!(out, expected);
}
#[test]
fn title_case_basic() {
assert_eq!(title_case("rust"), "Rust");
assert_eq!(title_case("python"), "Python");
assert_eq!(title_case("c/c++"), "C/C++");
assert_eq!(title_case(""), "");
}
#[test]
fn escape_name_wraps_in_backticks() {
assert_eq!(escape_name("hello"), "`hello`");
assert_eq!(escape_name("a|b"), "`a\\|b`");
assert_eq!(escape_name("a`b"), "`a\u{02CB}b`");
assert_eq!(escape_name("a\nb"), "`a b`");
}
#[test]
fn actionable_summary_clean() {
let summaries = vec![
make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust),
make_summary("f", "src/lib.rs", SpaceKind::Function, LANG::Rust),
];
let report = generate_report(&summaries, 20);
assert!(
report.contains("No major quality concerns detected."),
"clean codebase should show no-concerns message"
);
}
#[test]
fn actionable_summary_with_concerns() {
let unit = make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust);
let mut f = make_summary("big_func", "src/lib.rs", SpaceKind::Function, LANG::Rust);
f.cyclomatic = 25.0;
f.cognitive = 20.0;
f.sloc = 150;
f.nargs = 5;
f.halstead_bugs = 2.0;
let report = generate_report(&[unit, f], 20);
assert!(report.contains("functions with CC > 10"));
assert!(report.contains("functions with cognitive complexity > 15"));
assert!(report.contains("functions with SLOC > 100"));
assert!(report.contains("functions with more than 3 parameters"));
assert!(report.contains("functions with estimated Halstead bugs > 1.0"));
}
#[test]
fn mi_table_shows_lowest_first() {
let mut unit_good = make_summary("good.rs", "good.rs", SpaceKind::Unit, LANG::Rust);
unit_good.mi_visual_studio = 80.0;
let mut unit_bad = make_summary("bad.rs", "bad.rs", SpaceKind::Unit, LANG::Rust);
unit_bad.mi_visual_studio = 15.0;
let report = generate_report(&[unit_good, unit_bad], 20);
let mi_section = report
.find("### Maintainability Index")
.expect("MI section missing");
let after_mi = &report[mi_section..];
let bad_pos = after_mi.find("bad.rs").expect("bad.rs missing in MI");
let good_pos = after_mi.find("good.rs").expect("good.rs missing in MI");
assert!(
bad_pos < good_pos,
"lowest MI file should appear first in MI table"
);
}
#[test]
fn wmc_section_present_with_class_summaries() {
let unit = make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust);
let mut cls = make_summary("MyClass", "src/lib.rs", SpaceKind::Class, LANG::Rust);
cls.wmc = 12.0;
cls.nom = 4;
cls.npa = 2.0;
cls.npm = 3.0;
cls.sloc = 80;
let func = make_summary("f", "src/lib.rs", SpaceKind::Function, LANG::Rust);
let report = generate_report(&[unit, cls, func], 20);
assert!(
report.contains("### Class/Trait/Impl Hotspots (WMC)"),
"WMC section should be present when class-kind summaries exist"
);
let normalized = collapse_spaces(&report);
assert!(
normalized.contains("| `MyClass`"),
"class name should appear as backtick-wrapped cell"
);
assert!(
normalized.contains("| 12 | 4 | 2 | 3 | 80 | 30 |"),
"WMC row should contain wmc=12, nom=4, npa=2, npm=3, sloc=80, tokens=30 in:\n{report}"
);
}
#[test]
fn wmc_section_omitted_without_classes() {
let unit = make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust);
let func = make_summary("f", "src/lib.rs", SpaceKind::Function, LANG::Rust);
let report = generate_report(&[unit, func], 20);
assert!(
!report.contains("### Class/Trait/Impl Hotspots (WMC)"),
"WMC section should be absent when no class-kind summaries exist"
);
}
#[test]
fn nexits_section_present() {
let unit = make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust);
let mut func = make_summary("multi_exit", "src/lib.rs", SpaceKind::Function, LANG::Rust);
func.nexits = 3;
func.cyclomatic = 7.0;
func.sloc = 40;
let report = generate_report(&[unit, func], 20);
assert!(
report.contains("### Functions with the most exit points (NEXITS)"),
"NEXITS section should be present when functions have exits > 0"
);
let normalized = collapse_spaces(&report);
assert!(
normalized.contains("| `multi_exit`"),
"function name should appear as backtick-wrapped cell"
);
assert!(
normalized.contains("| 3 | 7 | 40 | 30 |"),
"NEXITS row should contain exits=3, cc=7, sloc=40, tokens=30 in:\n{report}"
);
}
#[test]
fn abc_section_present() {
let unit = make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust);
let mut func = make_summary("complex", "src/lib.rs", SpaceKind::Function, LANG::Rust);
func.abc = 15.5;
func.sloc = 35;
let report = generate_report(&[unit, func], 20);
assert!(
report.contains("### ABC Magnitude Hotspots"),
"ABC section should be present when functions have abc > 0"
);
let normalized = collapse_spaces(&report);
assert!(
normalized.contains("| `complex`"),
"function name should appear as backtick-wrapped cell"
);
assert!(
normalized.contains("| 15.5 | 35 | 30 |"),
"ABC row should contain abc=15.5, sloc=35, tokens=30 in:\n{report}"
);
}
#[test]
fn top_n_truncation_wmc_nexits_abc() {
let mut summaries = Vec::new();
summaries.push(make_summary(
"lib.rs",
"src/lib.rs",
SpaceKind::Unit,
LANG::Rust,
));
for i in 0..10 {
let mut cls = make_summary(
&format!("Class_{i}"),
"src/lib.rs",
SpaceKind::Class,
LANG::Rust,
);
cls.wmc = (i + 1) as f64;
cls.start_line = 100 + i;
summaries.push(cls);
}
for i in 0..10 {
let mut f = make_summary(
&format!("func_{i}"),
"src/lib.rs",
SpaceKind::Function,
LANG::Rust,
);
f.nexits = i + 1;
f.abc = (i + 1) as f64 * 2.0;
f.start_line = 200 + i;
summaries.push(f);
}
let report = generate_report(&summaries, 3);
let sections = [
"### Class/Trait/Impl Hotspots (WMC)",
"### Functions with the most exit points (NEXITS)",
"### ABC Magnitude Hotspots",
];
for section_hdr in sections {
let section_start = report
.find(section_hdr)
.unwrap_or_else(|| panic!("missing section: {section_hdr}"));
let section_text = &report[section_start..];
let section_end = section_text[1..]
.find("\n## ")
.or_else(|| section_text[1..].find("\n### "))
.map_or(section_text.len(), |p| p + 1);
let section_body = §ion_text[..section_end];
let data_rows = section_body
.lines()
.filter(|l| l.starts_with("| `"))
.count();
assert_eq!(
data_rows, 3,
"expected 3 data rows in {section_hdr}, got {data_rows}"
);
}
}
#[test]
fn tokens_column_present_in_hotspot_tables() {
let unit = make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust);
let mut func = make_summary("hot", "src/lib.rs", SpaceKind::Function, LANG::Rust);
func.cyclomatic = 5.0;
func.cognitive = 4.0;
func.halstead_effort = 200.0;
func.nargs = 4;
func.nexits = 2;
func.abc = 8.0;
func.tokens = 42;
let mut cls = make_summary("Cls", "src/lib.rs", SpaceKind::Class, LANG::Rust);
cls.wmc = 6.0;
cls.tokens = 99;
let report = generate_report(&[unit, func, cls], 20);
for header in [
"### Maintainability Index",
"### Cyclomatic Complexity Hotspots",
"### Cognitive Complexity Hotspots",
"### Halstead Effort Hotspots",
"### Largest Functions by SLOC",
"### Functions With Many Parameters (>3)",
"### Class/Trait/Impl Hotspots (WMC)",
"### Functions with the most exit points (NEXITS)",
"### ABC Magnitude Hotspots",
] {
let start = report
.find(header)
.unwrap_or_else(|| panic!("missing section: {header}"));
let header_row = report[start..]
.lines()
.find(|l| l.starts_with('|'))
.expect("header row");
assert!(
header_row.contains("Tokens"),
"Tokens column missing from {header} header row:\n{header_row}"
);
}
let normalized = collapse_spaces(&report);
assert!(
normalized.contains("| 42 |"),
"function token count should appear in normalized report"
);
assert!(
normalized.contains("| 99 |"),
"class token count should appear in normalized report"
);
}
#[test]
fn nexits_present_abc_absent() {
let unit = make_summary("lib.rs", "src/lib.rs", SpaceKind::Unit, LANG::Rust);
let mut func = make_summary(
"early_return",
"src/lib.rs",
SpaceKind::Function,
LANG::Rust,
);
func.nexits = 2;
func.abc = 0.0;
let report = generate_report(&[unit, func], 20);
assert!(
report.contains("### Functions with the most exit points (NEXITS)"),
"NEXITS section should be present"
);
assert!(
!report.contains("### ABC Magnitude Hotspots"),
"ABC section should be absent when all abc values are 0"
);
}
}