fallow_output/
audit_worsened.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7#[serde(rename_all = "kebab-case")]
8pub enum AuditFindingAttribution {
9 Introduced,
14 Worsened,
18 Inherited,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
24#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
25#[serde(rename_all = "kebab-case")]
26pub enum AuditGateExemption {
27 TestFile,
30}
31
32impl AuditGateExemption {
33 #[must_use]
35 pub const fn token(self) -> &'static str {
36 match self {
37 Self::TestFile => "test-file",
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(rename_all = "snake_case")]
46pub enum WorsenedMetricKind {
47 Cyclomatic,
49 Cognitive,
51 Crap,
53 LineCount,
55}
56
57impl WorsenedMetricKind {
58 #[must_use]
60 pub const fn label(self) -> &'static str {
61 match self {
62 Self::Cyclomatic => "cyclomatic",
63 Self::Cognitive => "cognitive",
64 Self::Crap => "CRAP",
65 Self::LineCount => "lines",
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
73#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
74pub struct WorsenedMetric {
75 pub metric: WorsenedMetricKind,
77 #[serde(serialize_with = "serialize_metric_value")]
79 pub base: f64,
80 #[serde(serialize_with = "serialize_metric_value")]
82 pub head: f64,
83}
84
85impl WorsenedMetric {
86 #[must_use]
88 pub fn describe(&self) -> String {
89 format!(
90 "{} {} -> {}",
91 self.metric.label(),
92 format_metric_value(self.base),
93 format_metric_value(self.head)
94 )
95 }
96}
97
98#[must_use]
100pub fn describe_worsened_metrics(metrics: &[WorsenedMetric]) -> String {
101 metrics
102 .iter()
103 .map(WorsenedMetric::describe)
104 .collect::<Vec<_>>()
105 .join(", ")
106}
107
108const MAX_EXACT_WHOLE: f64 = 9_007_199_254_740_992.0;
110
111fn is_whole(value: f64) -> bool {
112 value.fract() == 0.0 && value.abs() < MAX_EXACT_WHOLE
113}
114
115#[must_use]
118pub fn format_metric_value(value: f64) -> String {
119 if is_whole(value) {
120 format!("{value:.0}")
121 } else {
122 format!("{value:.1}")
123 }
124}
125
126#[expect(
128 clippy::trivially_copy_pass_by_ref,
129 reason = "serde serialize_with passes the field by reference"
130)]
131fn serialize_metric_value<S: serde::Serializer>(
132 value: &f64,
133 serializer: S,
134) -> Result<S::Ok, S::Error> {
135 if is_whole(*value) {
136 #[expect(
137 clippy::cast_possible_truncation,
138 reason = "is_whole keeps the value whole and inside the exact i64 range"
139 )]
140 serializer.serialize_i64(*value as i64)
141 } else {
142 serializer.serialize_f64(*value)
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn whole_values_serialize_as_integers() {
152 let metric = WorsenedMetric {
153 metric: WorsenedMetricKind::LineCount,
154 base: 80.0,
155 head: 300.0,
156 };
157 let json = serde_json::to_value(metric).expect("serialize");
158 assert_eq!(
159 json,
160 serde_json::json!({"metric": "line_count", "base": 80, "head": 300})
161 );
162 }
163
164 #[test]
165 fn crap_values_keep_their_fraction() {
166 let metric = WorsenedMetric {
167 metric: WorsenedMetricKind::Crap,
168 base: 31.5,
169 head: 42.25,
170 };
171 let json = serde_json::to_value(metric).expect("serialize");
172 assert_eq!(json["base"], 31.5);
173 assert_eq!(metric.describe(), "CRAP 31.5 -> 42.2");
174 }
175
176 #[test]
177 fn describe_joins_metrics() {
178 let metrics = [
179 WorsenedMetric {
180 metric: WorsenedMetricKind::Cyclomatic,
181 base: 15.0,
182 head: 40.0,
183 },
184 WorsenedMetric {
185 metric: WorsenedMetricKind::LineCount,
186 base: 80.0,
187 head: 300.0,
188 },
189 ];
190 assert_eq!(
191 describe_worsened_metrics(&metrics),
192 "cyclomatic 15 -> 40, lines 80 -> 300"
193 );
194 }
195}