cargo_crap/report.rs
1//! Render [`CrapEntry`] lists in any of five output formats.
2//!
3//! This module is the dispatch layer. The actual rendering for each format
4//! lives in a dedicated submodule:
5//!
6//! | Submodule | Format(s) | Audience |
7//! |---|---|---|
8//! | [`human`] | `human` | terminal users (coloured comfy-table) |
9//! | [`json`] | `json` | tools, baselines (versioned envelope) |
10//! | [`github`] | `github` | GitHub Actions (`::warning` annotations) |
11//! | [`markdown`] | `markdown` | exhaustive GFM table for artifacts |
12//! | [`pr_comment`] | `pr-comment` | opinionated PR comment (capped, collapsed) |
13//! | [`summary`] | `--summary` | aggregate-only output for any format |
14//!
15//! Shared building blocks (severity grade, coverage bar, Δ formatting, source
16//! links, per-crate rollups) live in [`types`], [`links`], and [`per_crate`].
17
18use crate::delta::DeltaReport;
19use crate::merge::CrapEntry;
20use crate::score::Severity;
21use anyhow::{Result, bail};
22use std::io::Write;
23
24mod github;
25mod human;
26mod json;
27mod links;
28mod markdown;
29mod per_crate;
30mod pr_comment;
31mod sarif;
32mod summary;
33mod types;
34
35#[cfg(test)]
36mod test_support;
37
38// Re-exports — the rest of the crate depends on these names being on `report`.
39pub use json::{DELTA_SCHEMA_URL, Envelope, REPORT_SCHEMA_URL, SCHEMA_VERSION};
40pub use links::SourceLinks;
41pub use summary::{render_delta_summary, render_summary};
42
43/// Output format for the report.
44#[derive(Debug, Clone, Copy)]
45pub enum Format {
46 Human,
47 Json,
48 /// Emit GitHub Actions workflow commands so that each crappy function
49 /// appears as an inline annotation on the PR diff.
50 ///
51 /// Format: `::warning file={path},line={n},title=CRAP ({score})::{message}`
52 ///
53 /// Only functions that exceed the threshold produce an annotation —
54 /// clean functions are silent.
55 GitHub,
56 /// GitHub-Flavored Markdown table — suitable for pasting into PR comments
57 /// or saving to a file rendered by GitHub/GitLab.
58 Markdown,
59 /// Opinionated PR-comment markdown: hides Unchanged rows, surfaces
60 /// regressions and new functions in a primary table, and tucks
61 /// improvements / removed / hot-spots into collapsed `<details>` blocks.
62 /// Capped per section. Use `Markdown` for the exhaustive report.
63 PrComment,
64 /// SARIF 2.1.0 JSON — the format consumed by GitHub Code Scanning,
65 /// VS Code, rust-analyzer, and most static-analysis tooling. Each
66 /// crappy function becomes one `result` with `level: "warning"`,
67 /// pointing at the function's start line.
68 Sarif,
69}
70
71/// Render `entries` in the requested format to `out`.
72///
73/// For `Format::Human` we emit a table and a summary line. The summary uses
74/// stderr-style coloring if the output is a TTY; `owo-colors` no-ops when
75/// it's not.
76pub fn render(
77 entries: &[CrapEntry],
78 threshold: f64,
79 format: Format,
80 links: Option<&SourceLinks>,
81 out: &mut dyn Write,
82) -> Result<()> {
83 match format {
84 Format::Json => json::render_json(entries, out),
85 Format::Human => human::render_human(entries, threshold, out),
86 Format::GitHub => github::render_github(entries, threshold, out),
87 Format::Markdown => markdown::render_markdown(entries, threshold, links, out),
88 Format::PrComment => pr_comment::render_pr_comment(entries, threshold, links, out),
89 Format::Sarif => sarif::render_sarif(entries, threshold, out),
90 }
91}
92
93/// Render a [`DeltaReport`] in the requested format.
94///
95/// Human format: table with a Δ column + summary line.
96/// JSON format: `{"entries": [...], "removed": [...]}` object.
97/// GitHub format: `::warning` for regressed and new-crappy functions only.
98pub fn render_delta(
99 report: &DeltaReport,
100 threshold: f64,
101 format: Format,
102 links: Option<&SourceLinks>,
103 out: &mut dyn Write,
104) -> Result<()> {
105 match format {
106 Format::Json => json::render_delta_json(report, out),
107 Format::Human => human::render_delta_human(report, threshold, out),
108 Format::GitHub => github::render_delta_github(report, threshold, out),
109 Format::Markdown => markdown::render_delta_markdown(report, threshold, links, out),
110 Format::PrComment => pr_comment::render_delta_pr_comment(report, threshold, links, out),
111 // SARIF describes the *current* set of findings, not deltas. The
112 // upstream consumers (GitHub Code Scanning, VS Code) don't model
113 // baseline diffs, so combining `--baseline` with `--format sarif`
114 // is rejected rather than silently emitting an unrelated shape.
115 Format::Sarif => bail!(
116 "--format sarif is incompatible with --baseline; use --format json for delta output"
117 ),
118 }
119}
120
121/// Prepend the hidden HTML marker that lets CI identify and update the PR
122/// comment. Used by both [`markdown`] and [`pr_comment`] renderers.
123pub(crate) fn write_pr_comment_marker(out: &mut dyn Write) -> Result<()> {
124 writeln!(out, "<!-- cargo-crap-report -->")?;
125 writeln!(out)?;
126 Ok(())
127}
128
129/// How many entries exceed the threshold — used by the CLI to decide the
130/// process exit code.
131#[must_use]
132pub fn crappy_count(
133 entries: &[CrapEntry],
134 threshold: f64,
135) -> usize {
136 entries
137 .iter()
138 .filter(|e| Severity::classify(e.crap, threshold) == Severity::Crappy)
139 .count()
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use test_support::sample;
146
147 #[test]
148 fn crappy_count_respects_threshold() {
149 assert_eq!(crappy_count(&sample(), 30.0), 1);
150 assert_eq!(crappy_count(&sample(), 200.0), 0);
151 }
152}