Skip to main content

alint_core/
report.rs

1use std::sync::Arc;
2
3use crate::level::Level;
4use crate::rule::{RuleResult, Violation};
5
6#[derive(Debug, Clone)]
7pub struct Report {
8    pub results: Vec<RuleResult>,
9}
10
11impl Report {
12    pub fn has_errors(&self) -> bool {
13        self.results
14            .iter()
15            .any(|r| r.level == Level::Error && !r.violations.is_empty())
16    }
17
18    pub fn has_warnings(&self) -> bool {
19        self.results
20            .iter()
21            .any(|r| r.level == Level::Warning && !r.violations.is_empty())
22    }
23
24    pub fn total_violations(&self) -> usize {
25        self.results.iter().map(|r| r.violations.len()).sum()
26    }
27
28    pub fn failing_rules(&self) -> usize {
29        self.results.iter().filter(|r| !r.passed()).count()
30    }
31
32    pub fn passing_rules(&self) -> usize {
33        self.results.iter().filter(|r| r.passed()).count()
34    }
35}
36
37/// Outcome of running [`Engine::fix`](crate::Engine::fix) against a
38/// repository. One [`FixRuleResult`] per rule that produced violations;
39/// rules that passed are omitted.
40#[derive(Debug, Clone)]
41pub struct FixReport {
42    pub results: Vec<FixRuleResult>,
43}
44
45#[derive(Debug, Clone)]
46pub struct FixRuleResult {
47    pub rule_id: Arc<str>,
48    pub level: Level,
49    pub items: Vec<FixItem>,
50}
51
52#[derive(Debug, Clone)]
53pub struct FixItem {
54    pub violation: Violation,
55    pub status: FixStatus,
56}
57
58#[derive(Debug, Clone)]
59pub enum FixStatus {
60    /// The fix was applied (or would be, under `--dry-run`).
61    Applied(String),
62    /// The rule has a fixer but it declined to act (e.g. file already
63    /// exists, violation lacked a path).
64    Skipped(String),
65    /// The rule has no fixer; violation stands.
66    Unfixable,
67}
68
69impl FixReport {
70    pub fn applied(&self) -> usize {
71        self.items()
72            .filter(|i| matches!(i.status, FixStatus::Applied(_)))
73            .count()
74    }
75
76    pub fn skipped(&self) -> usize {
77        self.items()
78            .filter(|i| matches!(i.status, FixStatus::Skipped(_)))
79            .count()
80    }
81
82    pub fn unfixable(&self) -> usize {
83        self.items()
84            .filter(|i| matches!(i.status, FixStatus::Unfixable))
85            .count()
86    }
87
88    /// Any rule at `level: error` whose violations were not all fixed.
89    pub fn has_unfixable_errors(&self) -> bool {
90        self.results
91            .iter()
92            .any(|r| r.level == Level::Error && has_unresolved(&r.items))
93    }
94
95    pub fn has_unfixable_warnings(&self) -> bool {
96        self.results
97            .iter()
98            .any(|r| r.level == Level::Warning && has_unresolved(&r.items))
99    }
100
101    fn items(&self) -> impl Iterator<Item = &FixItem> {
102        self.results.iter().flat_map(|r| &r.items)
103    }
104}
105
106fn has_unresolved(items: &[FixItem]) -> bool {
107    items
108        .iter()
109        .any(|i| matches!(i.status, FixStatus::Skipped(_) | FixStatus::Unfixable))
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    fn rr(rule_id: &str, level: Level, n_violations: usize) -> RuleResult {
117        RuleResult {
118            rule_id: rule_id.into(),
119            level,
120            policy_url: None,
121            violations: (0..n_violations)
122                .map(|i| Violation::new(format!("v{i}")))
123                .collect(),
124            notes: Vec::new(),
125            is_fixable: false,
126        }
127    }
128
129    fn frr(rule_id: &str, level: Level, statuses: Vec<FixStatus>) -> FixRuleResult {
130        FixRuleResult {
131            rule_id: rule_id.into(),
132            level,
133            items: statuses
134                .into_iter()
135                .map(|status| FixItem {
136                    violation: Violation::new("v"),
137                    status,
138                })
139                .collect(),
140        }
141    }
142
143    #[test]
144    fn empty_report_has_no_errors_or_warnings() {
145        let r = Report { results: vec![] };
146        assert!(!r.has_errors());
147        assert!(!r.has_warnings());
148        assert_eq!(r.total_violations(), 0);
149        assert_eq!(r.failing_rules(), 0);
150        assert_eq!(r.passing_rules(), 0);
151    }
152
153    #[test]
154    fn passing_rules_count_passing_results() {
155        // A passing RuleResult has zero violations.
156        let r = Report {
157            results: vec![rr("a", Level::Error, 0), rr("b", Level::Warning, 0)],
158        };
159        assert_eq!(r.passing_rules(), 2);
160        assert_eq!(r.failing_rules(), 0);
161        assert!(!r.has_errors());
162    }
163
164    #[test]
165    fn has_errors_true_when_error_level_has_violations() {
166        let r = Report {
167            results: vec![rr("a", Level::Error, 1), rr("b", Level::Warning, 5)],
168        };
169        assert!(r.has_errors());
170        assert!(r.has_warnings());
171        assert_eq!(r.total_violations(), 6);
172        assert_eq!(r.failing_rules(), 2);
173    }
174
175    #[test]
176    fn has_errors_false_when_only_warnings_have_violations() {
177        let r = Report {
178            results: vec![rr("a", Level::Error, 0), rr("b", Level::Warning, 3)],
179        };
180        assert!(!r.has_errors());
181        assert!(r.has_warnings());
182    }
183
184    #[test]
185    fn fix_report_applied_skipped_unfixable_counts_summed_across_rules() {
186        let r = FixReport {
187            results: vec![
188                frr(
189                    "a",
190                    Level::Error,
191                    vec![
192                        FixStatus::Applied("ok".into()),
193                        FixStatus::Applied("ok".into()),
194                        FixStatus::Skipped("nope".into()),
195                    ],
196                ),
197                frr(
198                    "b",
199                    Level::Warning,
200                    vec![FixStatus::Unfixable, FixStatus::Applied("ok".into())],
201                ),
202            ],
203        };
204        assert_eq!(r.applied(), 3);
205        assert_eq!(r.skipped(), 1);
206        assert_eq!(r.unfixable(), 1);
207    }
208
209    #[test]
210    fn has_unfixable_errors_true_when_error_rule_has_unresolved() {
211        let r = FixReport {
212            results: vec![frr("a", Level::Error, vec![FixStatus::Unfixable])],
213        };
214        assert!(r.has_unfixable_errors());
215        assert!(!r.has_unfixable_warnings());
216    }
217
218    #[test]
219    fn has_unfixable_errors_false_when_all_applied() {
220        let r = FixReport {
221            results: vec![frr(
222                "a",
223                Level::Error,
224                vec![FixStatus::Applied("done".into())],
225            )],
226        };
227        assert!(!r.has_unfixable_errors());
228    }
229
230    #[test]
231    fn has_unfixable_errors_false_when_skip_only_at_warning_level() {
232        // Skips at warning level matter for `has_unfixable_warnings`,
233        // not `has_unfixable_errors` — severity gates the check.
234        let r = FixReport {
235            results: vec![frr(
236                "a",
237                Level::Warning,
238                vec![FixStatus::Skipped("nope".into())],
239            )],
240        };
241        assert!(!r.has_unfixable_errors());
242        assert!(r.has_unfixable_warnings());
243    }
244
245    #[test]
246    fn rule_result_passed_method_is_correct() {
247        let passing = rr("a", Level::Error, 0);
248        let failing = rr("b", Level::Error, 1);
249        assert!(passing.passed());
250        assert!(!failing.passed());
251    }
252}