gruff-rs 0.4.0

Rust static analyzer and quality linter for CI: dead-code, complexity, security, secrets, and architecture rules with deterministic SARIF/JSON output and baseline support.
use super::styles::css;
use super::{severity_text, DistributionBar, OffenderRow, PillarRow, ReportView, SCHEMA_VERSION};
use crate::{html_escape, pillar_label, Finding, RunDiagnostic, Severity};
use std::fmt::Write as _;

pub(crate) fn document(view: &ReportView<'_>) -> String {
    let mut out = String::with_capacity(16 * 1024);
    out.push_str("<!DOCTYPE html>\n");
    out.push_str("<html lang=\"en\">\n<head>\n");
    out.push_str("<meta charset=\"UTF-8\">\n");
    out.push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n");
    out.push_str(&format!(
        "<title>gruff-rs inspection report - {}</title>\n",
        html_escape(&view.grade_letter)
    ));
    out.push_str("<style>");
    out.push_str(&css(!view.report.diagnostics.is_empty()));
    out.push_str("</style>\n</head>\n<body>\n");
    out.push_str("<main class=\"paper\">");
    out.push_str("<span class=\"corner-tr\"></span><span class=\"corner-bl\"></span>");
    out.push_str(&masthead(view));
    out.push_str(&diagnostics_section(&view.report.diagnostics));
    out.push_str(&verdict_section(view));
    out.push_str(&pillars_section(view));
    out.push_str(&offenders_section(view));
    out.push_str(&distribution_section(view));
    out.push_str(&findings_section(
        &view.report.findings,
        view.findings_total,
    ));
    out.push_str(&footer_section(view));
    out.push_str("</main>\n</body>\n</html>\n");
    out
}

fn masthead(view: &ReportView<'_>) -> String {
    let paths_label = if view.scope.paths.is_empty() {
        ".".to_string()
    } else {
        view.scope.paths.join(", ")
    };
    let scope_label = view.scope.diff_label.as_deref().unwrap_or("full project");
    let tool_version = format!("gruff {}", view.report.tool.version);

    format!(
        concat!(
            "<header class=\"masthead\">",
            "<div class=\"brand\">",
            "<div class=\"wordmark\">gruff</div>",
            "<div class=\"tagline\">rust code quality · inspection report</div>",
            "</div>",
            "<div class=\"meta\">",
            "{paths_row}{scope_row}{format_row}{fail_row}",
            "<div class=\"inspection-id\">{tool_version}</div>",
            "</div></header>"
        ),
        paths_row = meta_row("paths", &paths_label),
        scope_row = meta_row("scope", scope_label),
        format_row = meta_row("format", &view.report.run.format),
        fail_row = meta_row("fail", &view.report.run.fail_on),
        tool_version = html_escape(&tool_version),
    )
}

fn meta_row(label: &str, value: &str) -> String {
    format!(
        "<div><span class=\"label\">{}</span><span class=\"val\">{}</span></div>",
        html_escape(label),
        html_escape(value)
    )
}

fn diagnostics_section(diagnostics: &[RunDiagnostic]) -> String {
    if diagnostics.is_empty() {
        return String::new();
    }
    let mut out = String::new();
    out.push_str("<section class=\"diagnostics\">");
    out.push_str(
        "<h2 class=\"section-head\">diagnostics <span class=\"aside\">run messages</span></h2>",
    );
    out.push_str("<div class=\"diagnostic-list\">");
    for diagnostic in diagnostics {
        out.push_str(&diagnostic_row(diagnostic));
    }
    out.push_str("</div></section>");
    out
}

fn diagnostic_row(diagnostic: &RunDiagnostic) -> String {
    let location = match (diagnostic.file_path.as_deref(), diagnostic.line) {
        (Some(file), Some(line)) => Some(format!("{file}:{line}")),
        (Some(file), None) => Some(file.to_string()),
        (None, _) => None,
    };
    let location_html = match location {
        Some(value) => format!(
            "<span class=\"diagnostic-location\">{}</span>",
            html_escape(&value)
        ),
        None => String::new(),
    };
    format!(
        concat!(
            "<div class=\"diagnostic\">",
            "<span class=\"diagnostic-type\">{}</span>",
            "<span class=\"diagnostic-message\">{}</span>",
            "{}",
            "</div>"
        ),
        html_escape(&diagnostic.diagnostic_type),
        html_escape(&diagnostic.message),
        location_html
    )
}

fn verdict_section(view: &ReportView<'_>) -> String {
    let summary = &view.report.summary;
    format!(
        concat!(
            "<section class=\"verdict\">",
            "<div class=\"grade-stamp grade-{class}\">",
            "<div class=\"grade-letter\">{letter}</div>",
            "<div class=\"grade-score\">{score}</div>",
            "</div>",
            "<div class=\"verdict-body\">",
            "<div class=\"verdict-headline\">Inspection complete.<br><em>{summary}</em></div>",
            "<div class=\"verdict-stats\">",
            "{total_stat}{error_stat}{warning_stat}{advisory_stat}",
            "</div></div></section>"
        ),
        class = view.grade_class,
        letter = html_escape(&view.grade_letter),
        score = html_escape(&view.composite_text),
        summary = html_escape(&view.verdict_summary),
        total_stat = stat_block(&summary.total.to_string(), "findings", ""),
        error_stat = stat_block(&summary.error.to_string(), "error", "fail"),
        warning_stat = stat_block(&summary.warning.to_string(), "warning", "warn"),
        advisory_stat = stat_block(&summary.advisory.to_string(), "advisory", "note"),
    )
}

fn stat_block(value: &str, label: &str, class: &str) -> String {
    format!(
        "<div class=\"stat\"><div class=\"num {class}\">{value}</div><div class=\"lbl\">{label}</div></div>",
        class = html_escape(class),
        value = html_escape(value),
        label = html_escape(label),
    )
}

fn pillars_section(view: &ReportView<'_>) -> String {
    let mut out = String::new();
    out.push_str("<section class=\"pillars\">");
    out.push_str(
        "<h2 class=\"section-head\">pillars <span class=\"aside\">weighted composite</span></h2>",
    );
    out.push_str(concat!(
        "<table class=\"pillar-list\"><thead><tr>",
        "<th scope=\"col\">pillar</th>",
        "<th scope=\"col\" class=\"num\">grade</th>",
        "<th scope=\"col\" class=\"num\">score</th>",
        "<th scope=\"col\" class=\"num\">findings</th>",
        "<th scope=\"col\" class=\"num\">advisory</th>",
        "<th scope=\"col\" class=\"num\">warning</th>",
        "<th scope=\"col\" class=\"num\">error</th>",
        "</tr></thead><tbody>"
    ));
    if view.pillar_rows.is_empty() {
        out.push_str("<tr><td colspan=\"7\">No pillars.</td></tr>");
    } else {
        for row in &view.pillar_rows {
            out.push_str(&pillar_row(row));
        }
    }
    out.push_str("</tbody></table></section>");
    out
}

fn pillar_row(row: &PillarRow) -> String {
    format!(
        concat!(
            "<tr>",
            "<td class=\"pillar-name\">{name}</td>",
            "<td class=\"num\"><span class=\"grade-pill {class}\">{letter}</span></td>",
            "<td class=\"num\">{score:.2}</td>",
            "<td class=\"num\">{findings}</td>",
            "{advisory_cell}{warning_cell}{error_cell}",
            "</tr>"
        ),
        name = html_escape(pillar_label(row.pillar)),
        class = row.grade_class,
        letter = html_escape(&row.grade_letter),
        score = row.score,
        findings = row.findings,
        advisory_cell = severity_count_cell(row.advisories, "note"),
        warning_cell = severity_count_cell(row.warnings, "warn"),
        error_cell = severity_count_cell(row.errors, "fail"),
    )
}

fn severity_count_cell(count: usize, tier: &str) -> String {
    if count == 0 {
        format!("<td class=\"num\">{count}</td>")
    } else {
        format!("<td class=\"num {tier}\">{count}</td>")
    }
}

fn offenders_section(view: &ReportView<'_>) -> String {
    let mut out = String::new();
    out.push_str("<section class=\"offenders\">");
    out.push_str("<h2 class=\"section-head\">top offenders <span class=\"aside\">sorted by score</span></h2>");
    out.push_str(concat!(
        "<table class=\"offender-list\"><thead><tr>",
        "<th scope=\"col\">file</th>",
        "<th scope=\"col\" class=\"num\">cyclo</th>",
        "<th scope=\"col\" class=\"num\">cognit.</th>",
        "<th scope=\"col\" class=\"num\">LOC</th>",
        "<th scope=\"col\" class=\"num\">findings</th>",
        "<th scope=\"col\" class=\"num\">grade</th>",
        "</tr></thead><tbody>"
    ));
    if view.offender_rows.is_empty() {
        out.push_str("<tr><td colspan=\"6\">No offenders found.</td></tr>");
    } else {
        for row in &view.offender_rows {
            out.push_str(&offender_row(row));
        }
    }
    out.push_str("</tbody></table></section>");
    out
}

fn offender_row(row: &OffenderRow<'_>) -> String {
    format!(
        concat!(
            "<tr>",
            "<td class=\"file-path\">{file}</td>",
            "<td class=\"num\">n/a</td>",
            "<td class=\"num\">n/a</td>",
            "<td class=\"num\">n/a</td>",
            "<td class=\"num\">{findings}</td>",
            "<td class=\"num\"><span class=\"grade-pill grade-{class}\">{letter}</span></td>",
            "</tr>"
        ),
        file = html_escape(&row.file.file_path),
        findings = row.file.findings,
        class = row.grade_class,
        letter = html_escape(&row.grade_letter),
    )
}

fn distribution_section(view: &ReportView<'_>) -> String {
    let max = distribution_max(&view.distribution);
    let mut bars = String::new();
    let mut axis = String::new();
    for bucket_bar in &view.distribution {
        push_distribution_bar(&mut bars, &mut axis, bucket_bar, max);
    }

    format!(
        concat!(
            "<section class=\"chart-section\">",
            "<h2 class=\"section-head\">distribution <span class=\"aside\">cyclomatic complexity</span></h2>",
            "<p class=\"chart-summary\">{summary}</p>",
            "<div class=\"chart-card\"><div class=\"title\">cyclomatic complexity · flagged methods</div>",
            "<div class=\"histogram\">{bars}</div>",
            "<div class=\"histogram-axis\">{axis}</div>",
            "</div></section>"
        ),
        summary = html_escape(&view.distribution_summary),
        bars = bars,
        axis = axis,
    )
}

fn distribution_max(distribution: &[DistributionBar]) -> usize {
    distribution
        .iter()
        .map(|bucket_bar| bucket_bar.count)
        .max()
        .unwrap_or(0)
        .max(1)
}

fn push_distribution_bar(
    bars: &mut String,
    axis: &mut String,
    bucket_bar: &DistributionBar,
    max: usize,
) {
    let height = distribution_bar_height(bucket_bar.count, max);
    let _ = write!(
        bars,
        "<div class=\"bar {}\" style=\"height:{}%;\"><span class=\"count\">{}</span></div>",
        html_escape(bucket_bar.class),
        height,
        bucket_bar.count
    );
    let _ = write!(axis, "<span>{}</span>", html_escape(bucket_bar.label));
}

fn distribution_bar_height(count: usize, max: usize) -> i64 {
    if count == 0 {
        4
    } else {
        ((count as f64 / max as f64) * 100.0).round() as i64
    }
    .max(4)
}

fn findings_section(findings: &[Finding], total: usize) -> String {
    let mut out = String::new();
    out.push_str("<section class=\"findings\">");
    out.push_str(&format!(
        "<h2 class=\"section-head\">flagged findings <span class=\"aside\">{} shown</span></h2>",
        total
    ));
    out.push_str("<div class=\"findings-list\">");
    if findings.is_empty() {
        out.push_str("<div class=\"empty\">No findings.</div>");
    } else {
        for finding in findings {
            out.push_str(&finding_row(finding));
        }
    }
    out.push_str("</div></section>");
    out
}

fn finding_row(finding: &Finding) -> String {
    let severity_class = match finding.severity {
        Severity::Error => "fail",
        Severity::Warning => "warn",
        Severity::Advisory => "note",
    };
    let escaped_path = html_escape(&finding.file_path);
    let location = match finding.line {
        Some(line) => format!("{escaped_path}:{line}"),
        None => escaped_path,
    };

    format!(
        concat!(
            "<div class=\"finding\">",
            "<div class=\"severity {sev_class}\">{sev_label}</div>",
            "<div class=\"finding-body\">",
            "<h3 class=\"rule\">{rule}</h3>",
            "<div class=\"msg\">{msg}</div>",
            "<div class=\"loc\"><code>{loc}</code></div>",
            "</div>",
            "<div class=\"points\"><b>{pillar}</b></div>",
            "</div>"
        ),
        sev_class = html_escape(severity_class),
        sev_label = html_escape(severity_text(finding.severity)),
        rule = html_escape(&finding.rule_id),
        msg = html_escape(&finding.message),
        loc = location,
        pillar = html_escape(pillar_label(finding.pillar)),
    )
}

fn footer_section(view: &ReportView<'_>) -> String {
    format!(
        concat!(
            "<footer class=\"footer\">",
            "<div class=\"left\">gruff-rs · v{version}</div>",
            "<div class=\"center\">strong opinions, opinionated defaults</div>",
            "<div class=\"right\">schema · {schema}</div>",
            "</footer>"
        ),
        version = html_escape(&view.report.tool.version),
        schema = html_escape(SCHEMA_VERSION),
    )
}