Skip to main content

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}