use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static PROGRESS_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Linting '[^']+' \(\d+/\d+\)\n?").unwrap());
static FINDING_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^([^:]+\.swift:\d+:\d+): (warning|error): ([a-zA-Z_]+): (.+)$").unwrap()
});
pub fn compress_swiftlint(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = PROGRESS_RE.replace_all(&cleaned, "");
if s.trim().is_empty() {
return "swiftlint: no issues".to_string();
}
let mut by_rule: std::collections::HashMap<String, (Vec<String>, bool)> =
std::collections::HashMap::new(); let mut other_lines: Vec<&str> = Vec::new();
for line in s.lines() {
let t = line.trim();
if t.is_empty() {
continue;
}
if let Some(caps) = FINDING_RE.captures(t) {
let loc = caps[1].to_string();
let is_error = &caps[2] == "error";
let rule = caps[3].to_string();
let entry = by_rule.entry(rule).or_insert((Vec::new(), false));
entry.0.push(loc);
entry.1 |= is_error;
continue;
}
if t.starts_with("Done") || t.contains("violations") || t.contains("error") {
other_lines.push(line);
}
}
if by_rule.is_empty() {
return compactor::collapse_blanks(&s);
}
let total: usize = by_rule.values().map(|(v, _)| v.len()).sum();
let error_count: usize = by_rule
.values()
.filter(|(_, is_err)| *is_err)
.map(|(v, _)| v.len())
.sum();
let mut rules: Vec<(&String, &(Vec<String>, bool))> = by_rule.iter().collect();
rules.sort_by(|(a_name, (_, a_err)), (b_name, (_, b_err))| {
b_err.cmp(a_err).then(a_name.cmp(b_name))
});
let mut result: Vec<String> = Vec::new();
for (rule, (locs, is_error)) in &rules {
let level = if *is_error { "error" } else { "warning" };
if locs.len() == 1 {
result.push(format!("{level}: {rule} — {}", locs[0]));
} else {
result.push(format!("{level}: {rule} (×{}) — {}", locs.len(), locs[0]));
}
}
result.extend(other_lines.iter().map(|l| l.to_string()));
result.push(format!(
"swiftlint: {total} issues ({error_count} errors, {} warnings)",
total - error_count
));
result.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_progress_lines() {
let raw = "Linting 'Foo.swift' (1/5)\nLinting 'Bar.swift' (2/5)\nSources/Foo.swift:10:5: warning: line_length: Line should be 120 characters or less.\n";
let out = compress_swiftlint(&raw);
assert!(!out.contains("Linting 'Foo.swift'"), "{out}");
assert!(
out.contains("line_length") || out.contains("warning"),
"{out}"
);
}
#[test]
fn groups_by_rule() {
let raw = "Foo.swift:10:5: warning: trailing_whitespace: Trailing Whitespace Violation.\nBar.swift:20:1: warning: trailing_whitespace: Trailing Whitespace Violation.\nBaz.swift:5:3: error: force_cast: Force Cast Violation.\n";
let out = compress_swiftlint(&raw);
assert!(out.contains("trailing_whitespace"), "{out}");
assert!(out.contains("×2") || out.contains("force_cast"), "{out}");
}
#[test]
fn errors_shown_before_warnings() {
let raw = "Foo.swift:1:1: warning: line_length: too long.\nBar.swift:2:2: error: force_cast: bad cast.\n";
let out = compress_swiftlint(&raw);
let err_pos = out.find("force_cast").unwrap_or(usize::MAX);
let warn_pos = out.find("line_length").unwrap_or(usize::MAX);
assert!(
err_pos < warn_pos,
"errors should come before warnings: {out}"
);
}
}