cargo_build_rx/finding.rs
1//! The data model every check produces: [`Finding`] and its parts.
2//!
3//! A check returns a `Vec<Finding>`. Each finding carries a [`Severity`]
4//! (how strongly the tool recommends acting), a [`Category`] (which check
5//! produced it), an [`Impact`] (rough size of the build-time win), and an
6//! optional [`Fix`] describing the concrete change to make.
7
8use serde::Serialize;
9
10/// How strongly the tool recommends acting on a finding.
11///
12/// Variants are ordered from most to least actionable, so the derived
13/// [`Ord`] sorts `Fix` before `Warn` before `Info`.
14///
15/// ```
16/// use cargo_build_rx::finding::Severity;
17/// assert!(Severity::Fix < Severity::Warn);
18/// assert!(Severity::Warn < Severity::Info);
19/// ```
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
21pub enum Severity {
22 /// Nearly always correct to change; the strongest recommendation.
23 Fix,
24 /// Worth reviewing; the win is real but depends on the project.
25 Warn,
26 /// Informational; surfaced for awareness, not necessarily action.
27 Info,
28}
29
30impl Severity {
31 /// The fixed-width uppercase label shown in terminal output.
32 #[must_use]
33 pub fn label(self) -> &'static str {
34 match self {
35 Severity::Fix => "FIX",
36 Severity::Warn => "WARN",
37 Severity::Info => "INFO",
38 }
39 }
40}
41
42impl std::fmt::Display for Severity {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.write_str(self.label())
45 }
46}
47
48/// Which check produced a finding. Used for filtering and JSON consumers.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
50pub enum Category {
51 /// Linker selection and debug-info packaging.
52 Linker,
53 /// `[profile.*]` settings that affect dev compile time.
54 Profile,
55 /// The same crate compiled in several distinct versions.
56 Dependencies,
57 /// Procedural-macro crates on the build critical path.
58 ProcMacros,
59 /// Crates that ship a `build.rs` script.
60 BuildScripts,
61 /// Heavy default feature sets on direct dependencies.
62 Features,
63 /// Heavy dev-dependencies that add to compile time.
64 DevDeps,
65 /// The installed Rust toolchain.
66 Toolchain,
67 /// Workspace-wide feature unification.
68 Workspace,
69 /// Incremental-compilation configuration.
70 Incremental,
71}
72
73/// Rough size of the build-time win from acting on a finding.
74///
75/// Ordered most to least impactful for sorting alongside [`Severity`].
76#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
77pub enum Impact {
78 /// A large, broadly applicable build-time win.
79 High,
80 /// A moderate win.
81 Medium,
82 /// A small or situational win.
83 Low,
84}
85
86impl Impact {
87 /// The capitalized label shown in terminal output (e.g. `"High"`).
88 #[must_use]
89 pub fn label(self) -> &'static str {
90 match self {
91 Impact::High => "High",
92 Impact::Medium => "Medium",
93 Impact::Low => "Low",
94 }
95 }
96}
97
98impl std::fmt::Display for Impact {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.write_str(self.label())
101 }
102}
103
104/// One diagnostic produced by a check.
105#[derive(Debug, Clone, Serialize)]
106pub struct Finding {
107 /// How strongly the tool recommends acting.
108 pub severity: Severity,
109 /// Which check produced this finding.
110 pub category: Category,
111 /// Rough size of the build-time win.
112 pub impact: Impact,
113 /// One-line summary of the issue.
114 pub title: String,
115 /// Longer explanation, possibly multiple lines.
116 pub description: String,
117 /// The concrete change to make, when one applies.
118 pub fix: Option<Fix>,
119}
120
121/// A concrete, actionable change attached to a [`Finding`].
122#[derive(Debug, Clone, Serialize)]
123pub struct Fix {
124 /// Human-readable summary of what the fix does.
125 pub description: String,
126 /// The structured form of the change, for tooling.
127 pub kind: FixKind,
128}
129
130/// The structured shape of a [`Fix`], so consumers can act on it programmatically.
131#[derive(Debug, Clone, Serialize)]
132pub enum FixKind {
133 /// A key to add under `.cargo/config.toml`.
134 CargoConfig {
135 /// The TOML key path, e.g. `[target.aarch64-apple-darwin]`.
136 key_path: String,
137 /// The value(s) to place under that key.
138 value: String,
139 },
140 /// A key to add under a `Cargo.toml` section.
141 CargoToml {
142 /// The section, e.g. `profile.dev`.
143 section: String,
144 /// The key to set within the section.
145 key: String,
146 /// The value to set.
147 value: String,
148 },
149 /// A shell command to run.
150 ShellCommand(String),
151 /// A manual step described in prose.
152 Manual(String),
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn severity_orders_fix_before_warn_before_info() {
161 let mut v = [Severity::Info, Severity::Fix, Severity::Warn];
162 v.sort();
163 assert_eq!(v, [Severity::Fix, Severity::Warn, Severity::Info]);
164 }
165
166 #[test]
167 fn every_fixkind_variant_serializes() {
168 let variants = [
169 FixKind::CargoConfig {
170 key_path: "[target.aarch64-apple-darwin]".into(),
171 value: "linker = \"clang\"".into(),
172 },
173 FixKind::CargoToml {
174 section: "profile.dev".into(),
175 key: "debug".into(),
176 value: "1".into(),
177 },
178 FixKind::ShellCommand("cargo update".into()),
179 FixKind::Manual("do the thing".into()),
180 ];
181 for kind in variants {
182 let json = serde_json::to_value(&kind).expect("FixKind must serialize");
183 // Externally-tagged enum: exactly one variant key, and it round-trips
184 // back to a string for the tag.
185 assert!(json.is_object());
186 assert_eq!(json.as_object().unwrap().len(), 1);
187 }
188 }
189
190 #[test]
191 fn finding_serializes_with_expected_fields() {
192 let finding = Finding {
193 severity: Severity::Warn,
194 category: Category::Profile,
195 impact: Impact::Medium,
196 title: "t".into(),
197 description: "d".into(),
198 fix: None,
199 };
200 let json = serde_json::to_value(&finding).unwrap();
201 for key in ["severity", "category", "impact", "title", "description", "fix"] {
202 assert!(json.get(key).is_some(), "missing field {key}");
203 }
204 assert_eq!(json["severity"], "Warn");
205 }
206}