Skip to main content

fallow_output/
audit_render.rs

1use crate::CssAnalyticsSummary;
2
3/// Effective severity of an audit rule as shown in rendered output.
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub enum AuditDisplaySeverity {
6    /// Rule is disabled and never gates or advises.
7    Off,
8    /// Rule reports advisory findings without failing the audit.
9    Warn,
10    /// Rule findings can fail the audit gate.
11    Error,
12}
13
14/// Which findings an audit rule gates on.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum AuditDisplayGate {
17    /// Only findings introduced since the diff base gate; inherited debt stays advisory.
18    NewOnly,
19    /// Every finding gates, including pre-existing debt.
20    All,
21}
22
23/// Inputs for [`styling_audit_context_label`], describing one styling rule's gate state.
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub struct AuditStylingContextLabelInput<'a> {
26    /// Configured severity of the styling rule.
27    pub severity: AuditDisplaySeverity,
28    /// Config key of the rule, e.g. `rules.css-selector-complexity`.
29    pub rule: &'a str,
30    /// Human-readable description of the diff-base state, e.g. "inherited styling debt from HEAD".
31    pub base_state: Option<&'a str>,
32    /// Whether the rule gates on new findings only or on all findings.
33    pub gate: AuditDisplayGate,
34}
35
36/// Sums every descriptive CSS finding count in `summary`, saturating at `u32::MAX`.
37pub fn styling_candidate_count(summary: &CssAnalyticsSummary) -> u32 {
38    [
39        summary.tailwind_arbitrary_values,
40        summary.duplicate_declaration_blocks,
41        summary.unreferenced_css_classes,
42        summary.unused_theme_tokens,
43        summary.unused_font_faces,
44        summary.unused_property_registrations,
45        summary.unused_layers,
46        summary.scoped_unused_classes,
47        summary.keyframes_unreferenced,
48        summary.keyframes_undefined,
49        summary.unresolved_class_references,
50    ]
51    .into_iter()
52    .fold(0u32, u32::saturating_add)
53}
54
55/// Renders the parenthesized gate-context label for a styling audit finding.
56///
57/// The prefix explains why the finding does or does not gate: "gated" for
58/// error-severity findings, "not gated" for inherited debt under a new-only
59/// gate, and "advisory" otherwise.
60pub fn styling_audit_context_label(input: AuditStylingContextLabelInput<'_>) -> String {
61    let severity_label = match input.severity {
62        AuditDisplaySeverity::Off => "off",
63        AuditDisplaySeverity::Warn => "warn",
64        AuditDisplaySeverity::Error => "error",
65    };
66    let prefix = match (input.severity, input.gate, input.base_state) {
67        (AuditDisplaySeverity::Error, AuditDisplayGate::NewOnly, Some(state))
68            if state.starts_with("inherited") =>
69        {
70            "not gated"
71        }
72        (AuditDisplaySeverity::Error, _, _) => "gated",
73        _ => "advisory",
74    };
75    match input.base_state {
76        Some(state) => format!("({prefix}: {}={severity_label}, {state})", input.rule),
77        None => format!("({prefix}: {}={severity_label})", input.rule),
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn styling_audit_context_label_explains_gate_state() {
87        assert_eq!(
88            styling_audit_context_label(AuditStylingContextLabelInput {
89                severity: AuditDisplaySeverity::Error,
90                rule: "rules.css-selector-complexity",
91                base_state: Some("introduced design-system drift since HEAD"),
92                gate: AuditDisplayGate::NewOnly,
93            }),
94            "(gated: rules.css-selector-complexity=error, introduced design-system drift since HEAD)"
95        );
96        assert_eq!(
97            styling_audit_context_label(AuditStylingContextLabelInput {
98                severity: AuditDisplaySeverity::Error,
99                rule: "rules.css-selector-complexity",
100                base_state: Some("inherited styling debt from HEAD"),
101                gate: AuditDisplayGate::NewOnly,
102            }),
103            "(not gated: rules.css-selector-complexity=error, inherited styling debt from HEAD)"
104        );
105        assert_eq!(
106            styling_audit_context_label(AuditStylingContextLabelInput {
107                severity: AuditDisplaySeverity::Warn,
108                rule: "rules.css-selector-complexity",
109                base_state: None,
110                gate: AuditDisplayGate::All,
111            }),
112            "(advisory: rules.css-selector-complexity=warn)"
113        );
114    }
115
116    #[test]
117    fn styling_candidate_count_saturates_descriptive_css_counts() {
118        let summary = CssAnalyticsSummary {
119            tailwind_arbitrary_values: 2,
120            duplicate_declaration_blocks: 3,
121            unresolved_class_references: 5,
122            ..CssAnalyticsSummary::default()
123        };
124
125        assert_eq!(styling_candidate_count(&summary), 10);
126    }
127}