cargo-mend 0.3.2

Opinionated visibility auditing for Rust crates and workspaces
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(),
    }
}