use super::ProjectReport;
use super::data::SectionResult;
use crate::hal::report::format_time;
fn escape_md(s: &str) -> String {
s.replace('\\', "\\\\").replace('|', "\\|")
}
fn top_of(shown: usize, total: usize) -> String {
if shown < total {
format!("top {shown} of {total}")
} else {
format!("{total}")
}
}
pub fn print_markdown(report: &ProjectReport) {
println!("# Code Metrics Report");
println!();
println!("**Path:** `{}`", report.path);
println!();
println!(
"**Options:** top {} files per section, tests {}, min duplicate block {} lines",
report.top,
if report.include_tests {
"included"
} else {
"excluded"
},
report.min_lines,
);
println!();
println!("## Lines of Code");
println!();
println!(
"Physical line counts by language. Each line is classified as blank, comment, or code. \
Mixed lines (code + comment) count as code. Files are detected by extension or shebang."
);
println!();
if report.loc.is_empty() {
println!("No recognized source files found.");
} else {
println!("| Language | Files | Blank | Comment | Code |");
println!("|----------|------:|------:|--------:|-----:|");
let mut total_files = 0usize;
let mut total_blank = 0usize;
let mut total_comment = 0usize;
let mut total_code = 0usize;
for r in &report.loc {
println!(
"| {} | {} | {} | {} | {} |",
r.name, r.files, r.blank, r.comment, r.code
);
total_files += r.files;
total_blank += r.blank;
total_comment += r.comment;
total_code += r.code;
}
println!(
"| **Total** | **{}** | **{}** | **{}** | **{}** |",
total_files, total_blank, total_comment, total_code
);
}
println!();
println!("## Code Duplication");
println!();
println!("{}", report.duplication.description);
println!();
let d = &report.duplication;
println!("| Metric | Value |");
println!("|--------|------:|");
println!("| Total code lines | {} |", d.total_code_lines);
println!(
"| Duplicated lines | {} ({:.1}%) |",
d.duplicated_lines, d.duplication_percentage
);
println!("| Duplicate groups | {} |", d.duplicate_groups);
println!("| Files with duplicates | {} |", d.files_with_duplicates);
println!("| Largest block | {} lines |", d.largest_block);
let indent = &report.indent;
println!();
println!(
"## Indentation Complexity ({}, by stddev desc)",
top_of(indent.entries.len(), indent.total_count)
);
println!();
println!("{}", indent.description);
println!();
if indent.entries.is_empty() {
println!("No data.");
} else {
println!("| File | Lines | StdDev | Max Depth | Level |");
println!("|------|------:|-------:|----------:|-------|");
for f in &indent.entries {
println!(
"| {} | {} | {:.2} | {} | {} |",
escape_md(&f.path),
f.code_lines,
f.stddev,
f.max_depth,
f.complexity
);
}
}
let hal = &report.halstead;
println!();
println!(
"## Halstead Complexity ({}, by effort desc)",
top_of(hal.entries.len(), hal.total_count)
);
println!();
println!("{}", hal.description);
println!();
if hal.entries.is_empty() {
println!("No data.");
} else {
println!("| File | Volume | Effort | Bugs | Time |");
println!("|------|-------:|-------:|-----:|-----:|");
for f in &hal.entries {
println!(
"| {} | {:.1} | {:.0} | {:.2} | {} |",
escape_md(&f.path),
f.volume,
f.effort,
f.bugs,
format_time(f.time),
);
}
}
let cycom = &report.cyclomatic;
println!();
println!(
"## Cyclomatic Complexity ({}, by total desc)",
top_of(cycom.entries.len(), cycom.total_count)
);
println!();
println!("{}", cycom.description);
println!();
if cycom.entries.is_empty() {
println!("No data.");
} else {
println!("| File | Functions | Total | Max | Avg | Level |");
println!("|------|----------:|------:|----:|----:|-------|");
for f in &cycom.entries {
println!(
"| {} | {} | {} | {} | {:.1} | {} |",
escape_md(&f.path),
f.functions,
f.total,
f.max,
f.avg,
f.level
);
}
}
print_mi_section("Visual Studio", &report.mi_visual_studio, |e| {
(&e.path, e.mi_score, &e.level)
});
print_mi_section("Verifysoft", &report.mi_verifysoft, |e| {
(&e.path, e.mi_score, &e.level)
});
}
fn print_mi_section<T>(
variant: &str,
section: &SectionResult<T>,
fields: impl Fn(&T) -> (&str, f64, &str),
) {
println!();
println!(
"## Maintainability Index \u{2014} {} ({}, by MI asc)",
variant,
top_of(section.entries.len(), section.total_count)
);
println!();
println!("{}", section.description);
println!();
if section.entries.is_empty() {
println!("No data.");
} else {
println!("| File | MI | Level |");
println!("|------|---:|-------|");
for entry in §ion.entries {
let (path, mi_score, level) = fields(entry);
println!("| {} | {:.1} | {} |", escape_md(path), mi_score, level);
}
}
}
#[cfg(test)]
#[path = "markdown_test.rs"]
mod tests;