cargo_crap/report.rs
1//! Render [`CrapEntry`] lists in any of the supported 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//! | [`sarif`] | `sarif` | GitHub Code Scanning, VS Code (SARIF 2.1.0) |
14//! | [`shields`] | `shields` | README badges (Shields.io endpoint JSON) |
15//! | [`summary`] | `--summary` | aggregate-only output for any format |
16//!
17//! Shared building blocks (severity grade, coverage bar, Δ formatting, source
18//! links, per-crate rollups) live in [`types`], [`links`], and [`per_crate`].
19
20use crate::delta::DeltaReport;
21use crate::merge::{CrapEntry, ScopeDiagnostics};
22use crate::score::Severity;
23use anyhow::{Result, bail};
24use std::io::Write;
25
26mod github;
27mod human;
28mod json;
29mod links;
30mod markdown;
31mod per_crate;
32mod pr_comment;
33mod sarif;
34mod shields;
35mod summary;
36mod types;
37
38#[cfg(test)]
39mod test_support;
40
41// Re-exports — the rest of the crate depends on these names being on `report`.
42pub use json::{DELTA_SCHEMA_URL, Envelope, REPORT_SCHEMA_URL, SCHEMA_VERSION};
43pub use links::SourceLinks;
44pub use summary::{render_delta_summary, render_summary};
45pub use types::set_color_enabled;
46
47/// Output format for the report.
48#[derive(Debug, Clone, Copy)]
49pub enum Format {
50 Human,
51 Json,
52 /// Emit GitHub Actions workflow commands so that each crappy function
53 /// appears as an inline annotation on the PR diff.
54 ///
55 /// Format: `::warning file={path},line={n},title=CRAP ({score})::{message}`
56 ///
57 /// Only functions that exceed the threshold produce an annotation —
58 /// clean functions are silent.
59 GitHub,
60 /// GitHub-Flavored Markdown table — suitable for pasting into PR comments
61 /// or saving to a file rendered by GitHub/GitLab.
62 Markdown,
63 /// Opinionated PR-comment markdown: hides Unchanged rows, surfaces
64 /// regressions and new functions in a primary table, and tucks
65 /// improvements / removed / hot-spots into collapsed `<details>` blocks.
66 /// Capped per section. Use `Markdown` for the exhaustive report.
67 PrComment,
68 /// SARIF 2.1.0 JSON — the format consumed by GitHub Code Scanning,
69 /// VS Code, rust-analyzer, and most static-analysis tooling. Each
70 /// crappy function becomes one `result` with `level: "warning"`,
71 /// pointing at the function's start line.
72 Sarif,
73 /// Shields.io endpoint-badge JSON (spec 15) — a single
74 /// `{schemaVersion, label, message, color}` object reporting how many
75 /// functions exceed the threshold. Serve the file at a stable URL and
76 /// embed it via `https://img.shields.io/endpoint?url=…`. `--baseline`
77 /// is silently ignored: the badge always shows absolute current scores.
78 Shields,
79}
80
81/// Options shared by [`render`] and [`render_delta`], so their signatures
82/// survive new knobs without breaking every call site again.
83///
84/// Construct with struct-update syntax over [`Default`]:
85///
86/// ```
87/// use cargo_crap::report::{Format, RenderOptions};
88/// let opts = RenderOptions {
89/// format: Format::Json,
90/// ..Default::default()
91/// };
92/// # let _ = opts;
93/// ```
94#[derive(Debug, Clone, Copy)]
95pub struct RenderOptions<'a> {
96 /// CRAP score above which a function is flagged.
97 pub threshold: f64,
98 /// Output format to dispatch to.
99 pub format: Format,
100 /// GitHub source links for `markdown` / `pr-comment` cells (spec 12).
101 pub links: Option<&'a SourceLinks>,
102 /// Source/LCOV scope diagnostics (spec 24); embedded in the JSON
103 /// envelope only — other formats report mismatches via the CLI's
104 /// stderr warning.
105 pub diagnostics: Option<&'a ScopeDiagnostics>,
106 /// Show `Unchanged` rows in delta mode (spec 16). Only the human and
107 /// markdown renderers consult it; ignored by [`render`].
108 pub show_unchanged: bool,
109}
110
111impl Default for RenderOptions<'_> {
112 /// CLI defaults: threshold 30, human format, no links, no
113 /// diagnostics, changed-only delta rows.
114 fn default() -> Self {
115 Self {
116 threshold: crate::score::DEFAULT_THRESHOLD,
117 format: Format::Human,
118 links: None,
119 diagnostics: None,
120 show_unchanged: false,
121 }
122 }
123}
124
125/// Render `entries` in the format requested by `opts` to `out`.
126///
127/// For `Format::Human` we emit a table and a summary line. The summary uses
128/// stderr-style coloring if the output is a TTY; `owo-colors` no-ops when
129/// it's not.
130pub fn render(
131 entries: &[CrapEntry],
132 opts: &RenderOptions,
133 out: &mut dyn Write,
134) -> Result<()> {
135 let threshold = opts.threshold;
136 match opts.format {
137 Format::Json => json::render_json(entries, opts.diagnostics, out),
138 Format::Human => human::render_human(entries, threshold, out),
139 Format::GitHub => github::render_github(entries, threshold, out),
140 Format::Markdown => markdown::render_markdown(entries, threshold, opts.links, out),
141 Format::PrComment => pr_comment::render_pr_comment(entries, threshold, opts.links, out),
142 Format::Sarif => sarif::render_sarif(entries, threshold, out),
143 Format::Shields => shields::render_shields(entries, threshold, out),
144 }
145}
146
147/// Render a [`DeltaReport`] in the format requested by `opts`.
148///
149/// Human format: table with a Δ column + summary line.
150/// JSON format: `{"entries": [...], "removed": [...]}` object.
151/// GitHub format: `::warning` for regressed and new-crappy functions only.
152/// `opts.show_unchanged` controls whether `Unchanged` rows appear in the
153/// human and markdown tables (spec 16); it has no effect on the other
154/// formats, which keep their own row policies (json stays exhaustive,
155/// pr-comment hides unchanged by design, github/shields/sarif don't list
156/// unchanged functions).
157pub fn render_delta(
158 report: &DeltaReport,
159 opts: &RenderOptions,
160 out: &mut dyn Write,
161) -> Result<()> {
162 let threshold = opts.threshold;
163 match opts.format {
164 Format::Json => json::render_delta_json(report, opts.diagnostics, out),
165 Format::Human => human::render_delta_human(report, threshold, opts.show_unchanged, out),
166 Format::GitHub => github::render_delta_github(report, threshold, out),
167 Format::Markdown => {
168 markdown::render_delta_markdown(report, threshold, opts.links, opts.show_unchanged, out)
169 },
170 Format::PrComment => {
171 pr_comment::render_delta_pr_comment(report, threshold, opts.links, out)
172 },
173 // SARIF describes the *current* set of findings, not deltas. The
174 // upstream consumers (GitHub Code Scanning, VS Code) don't model
175 // baseline diffs, so combining `--baseline` with `--format sarif`
176 // is rejected rather than silently emitting an unrelated shape.
177 Format::Sarif => bail!(
178 "--format sarif is incompatible with --baseline; use --format json for delta output"
179 ),
180 // The badge has no delta variant (spec 15): the baseline is silently
181 // ignored and the output reflects absolute current scores only.
182 Format::Shields => shields::render_delta_shields(report, threshold, out),
183 }
184}
185
186/// Prepend the hidden HTML marker that lets CI identify and update the PR
187/// comment. Used by both [`markdown`] and [`pr_comment`] renderers.
188pub(crate) fn write_pr_comment_marker(out: &mut dyn Write) -> Result<()> {
189 writeln!(out, "<!-- cargo-crap-report -->")?;
190 writeln!(out)?;
191 Ok(())
192}
193
194/// How many entries exceed the threshold — used by the CLI to decide the
195/// process exit code.
196#[must_use]
197pub fn crappy_count(
198 entries: &[CrapEntry],
199 threshold: f64,
200) -> usize {
201 entries
202 .iter()
203 .filter(|e| Severity::classify(e.crap, threshold) == Severity::Crappy)
204 .count()
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use test_support::sample;
211
212 #[test]
213 fn crappy_count_respects_threshold() {
214 assert_eq!(crappy_count(&sample(), 30.0), 1);
215 assert_eq!(crappy_count(&sample(), 200.0), 0);
216 }
217}