use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
static OFFENSE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^([^:]+:\d+:\d+): (.+) \(([a-zA-Z0-9_-]+)\)$").unwrap());
static LEVEL_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(WARN|ERROR|INFO)\s+\[([a-zA-Z0-9_-]+)\]").unwrap());
static PROGRESS_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^(level=info|time=)[^\n]*\n?").unwrap());
pub fn compress_golangci(raw: &str, exit_code: i32) -> String {
let cleaned = compactor::normalise(raw);
let s = PROGRESS_RE.replace_all(&cleaned, "");
if exit_code == 0 {
return "golangci-lint: no issues found".to_string();
}
let mut linter_counts: HashMap<String, usize> = HashMap::new();
let mut error_lines: Vec<String> = Vec::new();
for line in s.lines() {
let t = line.trim();
if t.is_empty() {
continue;
}
if let Some(caps) = OFFENSE_RE.captures(t) {
let linter = caps[3].to_string();
*linter_counts.entry(linter).or_insert(0) += 1;
continue;
}
if let Some(caps) = LEVEL_RE.captures(t) {
let linter = caps[2].to_string();
*linter_counts.entry(linter).or_insert(0) += 1;
continue;
}
if t.starts_with("FAIL")
|| t.contains("issue")
|| t.contains("error")
|| t.starts_with("level=error")
{
error_lines.push(line.to_string());
}
}
let mut grouped: Vec<String> = Vec::new();
for (linter, count) in &linter_counts {
if *count > 1 {
grouped.push(format!("{linter} (×{count})"));
} else {
grouped.push(linter.clone());
}
}
grouped.sort();
let mut result = grouped;
result.extend(error_lines);
compactor::collapse_blanks(&result.join("\n"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn success_returns_clean_message() {
let raw =
"level=info msg=\"Running linters...\"\nlevel=info msg=\"Finished linters in 2.1s\"\n";
let out = compress_golangci(raw, 0);
assert!(
out.contains("no issues") || out.is_empty() || !out.contains("Running"),
"{out}"
);
}
#[test]
fn groups_repeated_linter_offenses() {
let raw =
(0..5)
.map(|i| format!("pkg/foo.go:{i}:1: use of unsafe.Pointer (govet)\n"))
.chain((0..3).map(|i| {
format!("pkg/bar.go:{i}:1: exported function lacks comment (golint)\n")
}))
.collect::<String>();
let out = compress_golangci(&raw, 1);
assert!(out.contains("×5") || out.contains("(×5)"), "govet: {out}");
assert!(out.contains("×3") || out.contains("(×3)"), "golint: {out}");
}
#[test]
fn strips_progress_info_lines() {
let raw = "level=info msg=\"Running linters...\"\ntime=\"12:00\" level=info msg=\"Starting analysis\"\npkg/a.go:1:1: something wrong (staticcheck)\n";
let out = compress_golangci(raw, 1);
assert!(!out.contains("Running linters"), "{out}");
assert!(!out.contains("Starting analysis"), "{out}");
}
}