Skip to main content

lean_ctx_patterns/
swiftlint.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! SwiftLint output compression.
4//!
5//! SwiftLint emits a `Linting 'File' (i/n)` progress line per file and one
6//! `path:line:col: severity: Message (rule_id)` line per violation. We drop the
7//! progress, summarize violations by rule + severity and keep the final
8//! `Done linting!` total.
9
10use crate::ansi::strip_ansi;
11use std::collections::{HashMap, HashSet};
12
13macro_rules! static_regex {
14    ($pattern:expr_2021) => {{
15        static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
16        RE.get_or_init(|| {
17            regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
18        })
19    }};
20}
21
22fn violation_re() -> &'static regex::Regex {
23    static_regex!(r"^(.+?):\d+:\d+:\s+(error|warning):\s+.*\(([a-z_][a-z0-9_]*)\)\s*$")
24}
25
26pub fn compress(_cmd: &str, output: &str) -> Option<String> {
27    let trimmed = output.trim();
28    if trimmed.is_empty() {
29        return Some("swiftlint: ok".to_string());
30    }
31
32    let mut by_rule: HashMap<String, (u32, u32)> = HashMap::new();
33    let mut files: HashSet<String> = HashSet::new();
34    let mut errors = 0u32;
35    let mut warnings = 0u32;
36
37    for raw in trimmed.lines() {
38        let stripped = strip_ansi(raw);
39        let line = stripped.trim();
40        if line.is_empty() || line.starts_with("Linting") || line.starts_with("Done linting!") {
41            continue;
42        }
43        if let Some(caps) = violation_re().captures(line) {
44            files.insert(caps[1].to_string());
45            let rule = caps[3].to_string();
46            let entry = by_rule.entry(rule).or_insert((0, 0));
47            if &caps[2] == "error" {
48                entry.0 += 1;
49                errors += 1;
50            } else {
51                entry.1 += 1;
52                warnings += 1;
53            }
54        }
55    }
56
57    if by_rule.is_empty() {
58        if trimmed.contains("Found 0 violations") || trimmed.contains("0 violations") {
59            return Some("swiftlint: clean".to_string());
60        }
61        return None;
62    }
63
64    let mut parts = vec![format!(
65        "swiftlint: {errors} errors, {warnings} warnings in {} files",
66        files.len()
67    )];
68    let mut rules: Vec<(String, (u32, u32))> = by_rule.into_iter().collect();
69    rules.sort_by(|a, b| {
70        let (ae, aw) = a.1;
71        let (be, bw) = b.1;
72        (be + bw).cmp(&(ae + aw)).then_with(|| a.0.cmp(&b.0))
73    });
74    for (rule, (e, w)) in rules.iter().take(10) {
75        parts.push(format!("  {rule}: {}", e + w));
76    }
77    if rules.len() > 10 {
78        parts.push(format!("  ... +{} more rules", rules.len() - 10));
79    }
80    Some(parts.join("\n"))
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    const LINT: &str = "Linting Swift files in current working directory\nLinting 'A.swift' (1/3)\nLinting 'B.swift' (2/3)\nLinting 'C.swift' (3/3)\n/path/A.swift:10:5: warning: Line Length Violation: Line should be 120 chars or less (line_length)\n/path/A.swift:22:1: warning: Trailing Whitespace Violation: no trailing whitespace (trailing_whitespace)\n/path/B.swift:5:1: error: Force Cast Violation: avoid force casts (force_cast)\n/path/A.swift:30:5: warning: Line Length Violation: too long (line_length)\nDone linting! Found 4 violations, 1 serious in 3 files.";
88
89    #[test]
90    fn summarizes_by_rule_and_severity() {
91        let r = compress("swiftlint", LINT).unwrap();
92        assert!(r.contains("1 errors, 3 warnings in 2 files"), "{r}");
93        assert!(r.contains("line_length: 2"), "aggregates rule: {r}");
94        assert!(r.contains("force_cast: 1"), "{r}");
95        assert!(!r.contains("Linting 'A.swift'"), "drops progress: {r}");
96    }
97
98    #[test]
99    fn clean_run() {
100        let r = compress(
101            "swiftlint",
102            "Linting 'A.swift' (1/1)\nDone linting! Found 0 violations, 0 serious in 1 file.",
103        )
104        .unwrap();
105        assert_eq!(r, "swiftlint: clean");
106    }
107
108    #[test]
109    fn empty_is_ok() {
110        assert_eq!(compress("swiftlint", "").unwrap(), "swiftlint: ok");
111    }
112}