use std::fmt::Write as _;
use super::diagnostics;
use super::diagnostics::Finding;
use super::diagnostics::Report;
use super::diagnostics::Severity;
const ANSI_BOLD: &str = "1";
const ANSI_BOLD_RED: &str = "1;31";
const ANSI_BOLD_YELLOW: &str = "1;33";
const ANSI_BOLD_BLUE: &str = "1;34";
const ANSI_DIM: &str = "2";
#[derive(Debug, Clone, Copy)]
pub enum ColorMode {
Enabled,
Disabled,
}
pub fn render_human_report(report: &Report, color: ColorMode) -> String {
if report.findings.is_empty() {
return "No findings.\n".to_string();
}
let mut output = String::new();
for finding in &report.findings {
render_finding(&mut output, finding, color);
}
let _ = writeln!(
output,
"{}",
summary_line(
report.summary.errors,
report.summary.warnings,
report.summary.fixable_with_fix,
report.summary.fixable_with_fix_pub_use,
color
)
);
output
}
fn render_finding(output: &mut String, finding: &Finding, color: ColorMode) {
let severity = severity_label(finding.severity, color);
let headline = diagnostics::finding_headline(finding);
let line_label = finding.line.to_string();
let gutter_width = line_label.len();
let gutter_pad = " ".repeat(gutter_width + 1);
let arrow_pad = " ".repeat(gutter_width);
let _ = writeln!(output, "{severity} {headline}");
let _ = writeln!(
output,
"{}{} {}:{}:{}",
arrow_pad,
blue_bold("-->", color),
finding.path,
finding.line,
finding.column
);
let _ = writeln!(output, "{}{}", gutter_pad, blue_bold("|", color));
let _ = writeln!(
output,
"{:>width$} {} {}",
blue_bold(&line_label, color),
blue_bold("|", color),
finding.source_line,
width = gutter_width
);
let _ = writeln!(
output,
"{}{} {}",
gutter_pad,
blue_bold("|", color),
severity_marker(
finding.severity,
finding.column,
finding.highlight_len,
color
)
);
if let Some(inline_help) = diagnostics::custom_inline_help_text(finding)
.or_else(|| diagnostics::inline_help_text(finding))
{
let _ = writeln!(output, "{}{}", gutter_pad, blue_bold("|", color));
let _ = writeln!(
output,
"{}{} {}",
gutter_pad,
blue_bold("|", color),
blue_bold(&format!("help: {inline_help}"), color)
);
}
let reasons = diagnostics::detail_reasons(finding);
if diagnostics::custom_inline_help_text(finding).is_some()
|| diagnostics::inline_help_text(finding).is_some()
|| !reasons.is_empty()
{
let _ = writeln!(output, "{}{}", gutter_pad, blue_bold("|", color));
}
if !reasons.is_empty() {
for reason in reasons {
let _ = writeln!(
output,
"{}{} {}",
gutter_pad,
diagnostic_label("note", color),
reason
);
}
}
let help_url = diagnostics::finding_help_url(finding);
let _ = writeln!(
output,
"{}{} for further information visit {help_url}",
gutter_pad,
diagnostic_label("help", color)
);
let _ = writeln!(output);
}
fn summary_line(
error_count: usize,
warn_count: usize,
fixable_with_fix_count: usize,
fixable_with_fix_pub_use_count: usize,
color: ColorMode,
) -> String {
let mut parts = vec![
format!("{error_count} error(s)"),
format!("{warn_count} warning(s)"),
];
if fixable_with_fix_count > 0 {
parts.push(format!("{fixable_with_fix_count} fixable with `--fix`"));
}
if fixable_with_fix_pub_use_count > 0 {
parts.push(format!(
"{fixable_with_fix_pub_use_count} fixable with `--fix-pub-use`"
));
}
format!("{} {}", dim("summary:", color), parts.join(", "))
}
fn severity_label(severity: Severity, color: ColorMode) -> String {
match severity {
Severity::Error => paint("error:", ANSI_BOLD_RED, color),
Severity::Warning => paint("warn:", ANSI_BOLD_YELLOW, color),
}
}
fn dim(text: &str, color: ColorMode) -> String { paint(text, ANSI_DIM, color) }
fn blue_bold(text: &str, color: ColorMode) -> String { paint(text, ANSI_BOLD_BLUE, color) }
fn severity_marker(
severity: Severity,
column: usize,
highlight_len: usize,
color: ColorMode,
) -> String {
let indent = " ".repeat(column.saturating_sub(1));
let carets = "^".repeat(highlight_len.max(1));
let code = match severity {
Severity::Error => ANSI_BOLD_RED,
Severity::Warning => ANSI_BOLD_YELLOW,
};
format!("{indent}{}", paint(&carets, code, color))
}
fn diagnostic_label(kind: &str, color: ColorMode) -> String {
let prefix = blue_bold("=", color);
let label = match kind {
"help" => paint("help", ANSI_BOLD, color),
"note" => paint("note", ANSI_BOLD, color),
other => other.to_string(),
};
format!("{prefix} {label}:")
}
fn paint(text: &str, code: &str, color: ColorMode) -> String {
match color {
ColorMode::Enabled => format!("\x1b[{code}m{text}\x1b[0m"),
ColorMode::Disabled => text.to_string(),
}
}