use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
static INSPECTING_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Inspecting \d+ files?\n?").unwrap());
static OFFENSE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^([^:]+):\d+:\d+: ([CWEF]): ([A-Za-z0-9:/]+): ").unwrap());
static DOTS_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^[\.CFEW]+\n?").unwrap());
static SUMMARY_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\d+ files? inspected[^\n]+\n?").unwrap());
pub fn compress_rubocop(raw: &str, exit_code: i32) -> String {
let cleaned = compactor::normalise(raw);
let s = INSPECTING_RE.replace_all(&cleaned, "");
let s = DOTS_RE.replace_all(&s, "");
if exit_code == 0 {
let summary: Vec<&str> = s.lines().filter(|l| SUMMARY_RE.is_match(l)).collect();
if !summary.is_empty() {
return summary.join("\n");
}
return "rubocop: no offenses detected".to_string();
}
let mut cop_counts: HashMap<String, usize> = HashMap::new();
let mut error_lines: Vec<String> = Vec::new();
let mut summary_lines: Vec<String> = Vec::new();
for line in s.lines() {
if SUMMARY_RE.is_match(line) {
summary_lines.push(line.to_string());
continue;
}
if let Some(caps) = OFFENSE_RE.captures(line) {
let severity = caps[2].to_string();
let cop = caps[3].to_string();
if severity == "E" || severity == "F" {
error_lines.push(line.to_string());
} else {
*cop_counts.entry(cop).or_insert(0) += 1;
}
continue;
}
if line.starts_with(" ") {
continue;
}
if !line.trim().is_empty() {
error_lines.push(line.to_string());
}
}
let mut grouped: Vec<String> = Vec::new();
for (cop, count) in &cop_counts {
if *count > 1 {
grouped.push(format!("{cop} (×{count})"));
} else {
grouped.push(cop.clone());
}
}
grouped.sort();
let mut result = grouped;
result.extend(error_lines);
result.extend(summary_lines);
compactor::collapse_blanks(&result.join("\n"))
}
pub fn compress_bundle_audit(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let out: Vec<&str> = cleaned
.lines()
.filter(|l| {
let t = l.trim();
t.starts_with("Name:")
|| t.starts_with("Version:")
|| t.starts_with("Advisory:")
|| t.starts_with("Criticality:")
|| t.starts_with("URL:")
|| t.starts_with("Title:")
|| t.contains("vulnerabilit")
|| t.contains("Vulnerabilities found")
|| t.contains("No vulnerabilities found")
|| t.starts_with("Insecure Source")
})
.collect();
if out.is_empty() {
cleaned
} else {
out.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn success_returns_summary_only() {
let raw = "Inspecting 42 files\n..........................................\n42 files inspected, no offenses detected\n";
let out = compress_rubocop(raw, 0);
assert!(!out.contains("Inspecting"), "{out}");
assert!(!out.contains("......"), "{out}");
assert!(
out.contains("no offenses") || out.contains("42 files"),
"{out}"
);
}
#[test]
fn failure_groups_repeated_cops() {
let block = |cop: &str, n: usize| -> String {
(0..n)
.map(|i| {
format!("app/foo.rb:{i}:1: C: {cop}: Message here\n let x = 1\n ^\n")
})
.collect()
};
let raw = format!(
"{}{}",
block("Style/FrozenStringLiteralComment", 8),
block("Layout/TrailingWhitespace", 3)
);
let raw = format!(
"Inspecting 2 files\n..FF..FF\n\n{raw}2 files inspected, 11 offenses detected\n"
);
let out = compress_rubocop(&raw, 1);
assert!(
out.contains("×8") || out.contains("(×8)"),
"should group 8 occurrences: {out}"
);
assert!(
!out.contains("let x = 1"),
"source snippets should be stripped: {out}"
);
}
#[test]
fn bundle_audit_keeps_vulnerability_info() {
let raw = "Name: rack\nVersion: 2.0.8\nAdvisory: CVE-2022-12345\nCriticality: High\nTitle: Remote code execution\nURL: https://nvd.nist.gov/...\n1 vulnerability found!\n";
let out = compress_bundle_audit(raw);
assert!(out.contains("CVE-2022-12345"), "{out}");
assert!(out.contains("High"), "{out}");
}
}