Skip to main content

alint_core/
report.rs

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