use std::path::Path;
use serde::Serialize;
use unicode_width::UnicodeWidthStr;
pub trait PerFunctionRow {
fn name(&self) -> &str;
fn start_line(&self) -> usize;
fn complexity(&self) -> usize;
fn level_str(&self) -> &str;
}
pub trait PerFunctionFile {
type Row: PerFunctionRow;
fn path_str(&self) -> String;
fn rows(&self) -> &[Self::Row];
}
pub fn print_per_function_breakdown<F: PerFunctionFile>(title: &str, files: &[F]) {
if files.is_empty() {
println!("No recognized source files found.");
return;
}
let sep = separator(78);
println!("{title}");
println!("{sep}");
for f in files {
println!();
println!("{}:", f.path_str());
let rows = f.rows();
let max_name_len = rows
.iter()
.map(|r| display_width(r.name()))
.max()
.unwrap_or(10)
.max(10);
for r in rows {
println!(
" {} {:>5} {}",
pad_to(r.name(), max_name_len),
r.complexity(),
r.level_str(),
);
}
}
println!("{sep}");
}
pub fn display_width(s: &str) -> usize {
UnicodeWidthStr::width(s)
}
pub fn pad_to(s: &str, width: usize) -> String {
let padding = width.saturating_sub(UnicodeWidthStr::width(s));
format!("{s}{}", " ".repeat(padding))
}
pub fn github_annotation(level: &str, file: &str, line: usize, title: &str, message: &str) {
println!("::{level} file={file},line={line},title={title}::{message}");
}
pub fn codeclimate_entry(
severity: &str,
file: &str,
line: usize,
title: &str,
description: &str,
) -> serde_json::Value {
serde_json::json!({
"description": description,
"fingerprint": fnv1a_hex(&format!("{file}:{line}:{title}")),
"severity": severity,
"location": { "path": file, "lines": { "begin": line } }
})
}
fn fnv1a_hex(s: &str) -> String {
let mut hash: u64 = 0xcbf29ce484222325;
for byte in s.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
format!("{hash:016x}")
}
pub fn complexity_severity(c: usize) -> &'static str {
match c {
25.. => "critical",
15..=24 => "major",
10..=14 => "minor",
_ => "info",
}
}
fn append_codeclimate_entries<R: PerFunctionRow>(
path: &str,
rows: &[R],
min_complexity: usize,
title: &str,
kind: &str,
out: &mut Vec<serde_json::Value>,
) {
for func in rows {
if func.complexity() < min_complexity {
continue;
}
let description = format!(
"function '{}' has {kind} complexity {} (threshold: {min_complexity})",
func.name(),
func.complexity(),
);
out.push(codeclimate_entry(
complexity_severity(func.complexity()),
path,
func.start_line(),
title,
&description,
));
}
}
pub fn print_codeclimate_complexity<F>(
files: &[F],
min_complexity: usize,
title: &str,
kind: &str,
) -> Result<(), Box<dyn std::error::Error>>
where
F: PerFunctionFile,
{
let mut entries = Vec::new();
for f in files {
append_codeclimate_entries(
&f.path_str(),
f.rows(),
min_complexity,
title,
kind,
&mut entries,
);
}
print_json_stdout(&entries)
}
pub fn max_path_width<'a>(paths: impl Iterator<Item = &'a Path>, min: usize) -> usize {
paths
.map(|p| display_width(&p.display().to_string()))
.max()
.unwrap_or(min)
.max(min)
}
pub fn separator(width: usize) -> String {
"\u{2500}".repeat(width)
}
pub fn print_json_stdout(value: &impl Serialize) -> Result<(), Box<dyn std::error::Error>> {
println!("{}", serde_json::to_string_pretty(value)?);
Ok(())
}
pub fn output_results<T>(
results: &mut Vec<T>,
top: usize,
output: crate::cli::OutputMode,
print_json_fn: impl FnOnce(&[T]) -> Result<(), Box<dyn std::error::Error>>,
print_report_fn: impl FnOnce(&[T]),
print_short_fn: impl FnOnce(&[T]),
print_terse_fn: impl FnOnce(&[T]),
) -> Result<(), Box<dyn std::error::Error>> {
results.truncate(top);
match output {
crate::cli::OutputMode::Terse => {
print_terse_fn(results);
Ok(())
}
crate::cli::OutputMode::Short => {
print_short_fn(results);
Ok(())
}
crate::cli::OutputMode::Json => print_json_fn(results),
crate::cli::OutputMode::Github | crate::cli::OutputMode::Codeclimate => {
Err(crate::cli::ERR_CI_FORMAT_ONLY.into())
}
crate::cli::OutputMode::Table => {
print_report_fn(results);
Ok(())
}
}
}
#[cfg(test)]
#[path = "report_helpers_test.rs"]
mod tests;