lean_ctx/core/patterns/
swiftlint.rs1use crate::core::compressor::strip_ansi;
9use std::collections::{HashMap, HashSet};
10
11macro_rules! static_regex {
12 ($pattern:expr_2021) => {{
13 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
14 RE.get_or_init(|| {
15 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
16 })
17 }};
18}
19
20fn violation_re() -> &'static regex::Regex {
21 static_regex!(r"^(.+?):\d+:\d+:\s+(error|warning):\s+.*\(([a-z_][a-z0-9_]*)\)\s*$")
22}
23
24pub fn compress(_cmd: &str, output: &str) -> Option<String> {
25 let trimmed = output.trim();
26 if trimmed.is_empty() {
27 return Some("swiftlint: ok".to_string());
28 }
29
30 let mut by_rule: HashMap<String, (u32, u32)> = HashMap::new();
31 let mut files: HashSet<String> = HashSet::new();
32 let mut errors = 0u32;
33 let mut warnings = 0u32;
34
35 for raw in trimmed.lines() {
36 let stripped = strip_ansi(raw);
37 let line = stripped.trim();
38 if line.is_empty() || line.starts_with("Linting") || line.starts_with("Done linting!") {
39 continue;
40 }
41 if let Some(caps) = violation_re().captures(line) {
42 files.insert(caps[1].to_string());
43 let rule = caps[3].to_string();
44 let entry = by_rule.entry(rule).or_insert((0, 0));
45 if &caps[2] == "error" {
46 entry.0 += 1;
47 errors += 1;
48 } else {
49 entry.1 += 1;
50 warnings += 1;
51 }
52 }
53 }
54
55 if by_rule.is_empty() {
56 if trimmed.contains("Found 0 violations") || trimmed.contains("0 violations") {
57 return Some("swiftlint: clean".to_string());
58 }
59 return None;
60 }
61
62 let mut parts = vec![format!(
63 "swiftlint: {errors} errors, {warnings} warnings in {} files",
64 files.len()
65 )];
66 let mut rules: Vec<(String, (u32, u32))> = by_rule.into_iter().collect();
67 rules.sort_by(|a, b| {
68 let (ae, aw) = a.1;
69 let (be, bw) = b.1;
70 (be + bw).cmp(&(ae + aw)).then_with(|| a.0.cmp(&b.0))
71 });
72 for (rule, (e, w)) in rules.iter().take(10) {
73 parts.push(format!(" {rule}: {}", e + w));
74 }
75 if rules.len() > 10 {
76 parts.push(format!(" ... +{} more rules", rules.len() - 10));
77 }
78 Some(parts.join("\n"))
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 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.";
86
87 #[test]
88 fn summarizes_by_rule_and_severity() {
89 let r = compress("swiftlint", LINT).unwrap();
90 assert!(r.contains("1 errors, 3 warnings in 2 files"), "{r}");
91 assert!(r.contains("line_length: 2"), "aggregates rule: {r}");
92 assert!(r.contains("force_cast: 1"), "{r}");
93 assert!(!r.contains("Linting 'A.swift'"), "drops progress: {r}");
94 }
95
96 #[test]
97 fn clean_run() {
98 let r = compress(
99 "swiftlint",
100 "Linting 'A.swift' (1/1)\nDone linting! Found 0 violations, 0 serious in 1 file.",
101 )
102 .unwrap();
103 assert_eq!(r, "swiftlint: clean");
104 }
105
106 #[test]
107 fn empty_is_ok() {
108 assert_eq!(compress("swiftlint", "").unwrap(), "swiftlint: ok");
109 }
110}