Skip to main content

lean_ctx/core/patterns/
golang.rs

1use regex::Regex;
2use std::sync::OnceLock;
3
4static GO_TEST_RESULT_RE: OnceLock<Regex> = OnceLock::new();
5static GO_BENCH_RE: OnceLock<Regex> = OnceLock::new();
6static GOLINT_RE: OnceLock<Regex> = OnceLock::new();
7static GO_BUILD_ERROR_RE: OnceLock<Regex> = OnceLock::new();
8
9fn go_test_result_re() -> &'static Regex {
10    GO_TEST_RESULT_RE.get_or_init(|| Regex::new(r"^(ok|FAIL)\s+(\S+)\s+(\S+)").unwrap())
11}
12fn go_bench_re() -> &'static Regex {
13    GO_BENCH_RE.get_or_init(|| {
14        Regex::new(r"^Benchmark(\S+)\s+(\d+)\s+(\d+\.?\d*)\s*(ns|µs|ms)/op").unwrap()
15    })
16}
17fn golint_re() -> &'static Regex {
18    GOLINT_RE.get_or_init(|| Regex::new(r"^(.+?):(\d+):(\d+):\s+(.+?)\s+\((.+?)\)$").unwrap())
19}
20fn go_build_error_re() -> &'static Regex {
21    GO_BUILD_ERROR_RE.get_or_init(|| Regex::new(r"^(.+?):(\d+):(\d+):\s+(.+)$").unwrap())
22}
23
24pub fn compress(command: &str, output: &str) -> Option<String> {
25    if command.contains("golangci-lint") || command.contains("golint") {
26        return Some(compress_golint(output));
27    }
28    if command.contains("test") {
29        if command.contains("-bench") || command.contains("bench") {
30            return Some(compress_bench(output));
31        }
32        return Some(compress_test(output));
33    }
34    if command.contains("build") {
35        return Some(compress_build(output));
36    }
37    if command.contains("vet") {
38        return Some(compress_vet(output));
39    }
40    if command.contains("mod") {
41        return Some(compress_mod(output));
42    }
43    if command.contains("fmt") {
44        return Some(compress_fmt(output));
45    }
46    None
47}
48
49fn compress_test(output: &str) -> String {
50    let trimmed = output.trim();
51    if trimmed.is_empty() {
52        return "ok".to_string();
53    }
54
55    let mut results = Vec::new();
56    let mut failed_tests = Vec::new();
57
58    for line in trimmed.lines() {
59        if let Some(caps) = go_test_result_re().captures(line) {
60            let status = &caps[1];
61            let pkg = &caps[2];
62            let duration = &caps[3];
63            results.push(format!("{status} {pkg} ({duration})"));
64        }
65        if line.contains("--- FAIL:") {
66            let name = line.replace("--- FAIL:", "").trim().to_string();
67            failed_tests.push(name);
68        }
69    }
70
71    if results.is_empty() {
72        return compact_output(trimmed, 10);
73    }
74
75    let mut parts = results;
76    if !failed_tests.is_empty() {
77        parts.push(format!("failed: {}", failed_tests.join(", ")));
78    }
79    parts.join("\n")
80}
81
82fn compress_bench(output: &str) -> String {
83    let trimmed = output.trim();
84    let mut benchmarks = Vec::new();
85
86    for line in trimmed.lines() {
87        if let Some(caps) = go_bench_re().captures(line) {
88            let name = &caps[1];
89            let ops = &caps[2];
90            let ns = &caps[3];
91            let unit = &caps[4];
92            benchmarks.push(format!("{name}: {ops} ops @ {ns} {unit}/op"));
93        }
94    }
95
96    if benchmarks.is_empty() {
97        return compact_output(trimmed, 10);
98    }
99    format!(
100        "{} benchmarks:\n{}",
101        benchmarks.len(),
102        benchmarks.join("\n")
103    )
104}
105
106fn compress_build(output: &str) -> String {
107    let trimmed = output.trim();
108    if trimmed.is_empty() {
109        return "ok".to_string();
110    }
111
112    let mut errors = Vec::new();
113    for line in trimmed.lines() {
114        if let Some(caps) = go_build_error_re().captures(line) {
115            errors.push(format!("{}:{}: {}", &caps[1], &caps[2], &caps[4]));
116        }
117    }
118
119    if errors.is_empty() {
120        return compact_output(trimmed, 5);
121    }
122    format!("{} errors:\n{}", errors.len(), errors.join("\n"))
123}
124
125fn compress_golint(output: &str) -> String {
126    let trimmed = output.trim();
127    if trimmed.is_empty() {
128        return "clean".to_string();
129    }
130
131    let mut by_linter: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
132    let mut files: std::collections::HashSet<String> = std::collections::HashSet::new();
133
134    for line in trimmed.lines() {
135        if let Some(caps) = golint_re().captures(line) {
136            files.insert(caps[1].to_string());
137            let linter = caps[5].to_string();
138            *by_linter.entry(linter).or_insert(0) += 1;
139        }
140    }
141
142    if by_linter.is_empty() {
143        return compact_output(trimmed, 10);
144    }
145
146    let total: u32 = by_linter.values().sum();
147    let mut linters: Vec<(String, u32)> = by_linter.into_iter().collect();
148    linters.sort_by_key(|x| std::cmp::Reverse(x.1));
149
150    let mut parts = Vec::new();
151    parts.push(format!("{total} issues in {} files", files.len()));
152    for (linter, count) in linters.iter().take(8) {
153        parts.push(format!("  {linter}: {count}"));
154    }
155    if linters.len() > 8 {
156        parts.push(format!("  ... +{} more linters", linters.len() - 8));
157    }
158
159    parts.join("\n")
160}
161
162fn compress_vet(output: &str) -> String {
163    let trimmed = output.trim();
164    if trimmed.is_empty() {
165        return "ok (vet clean)".to_string();
166    }
167    compact_output(trimmed, 10)
168}
169
170fn compress_mod(output: &str) -> String {
171    let trimmed = output.trim();
172    if trimmed.is_empty() {
173        return "ok".to_string();
174    }
175    compact_output(trimmed, 10)
176}
177
178fn compress_fmt(output: &str) -> String {
179    let trimmed = output.trim();
180    if trimmed.is_empty() {
181        return "ok (formatted)".to_string();
182    }
183
184    let files: Vec<&str> = trimmed.lines().filter(|l| !l.trim().is_empty()).collect();
185    format!("{} files reformatted:\n{}", files.len(), files.join("\n"))
186}
187
188fn compact_output(text: &str, max: usize) -> String {
189    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
190    if lines.len() <= max {
191        return lines.join("\n");
192    }
193    format!(
194        "{}\n... ({} more lines)",
195        lines[..max].join("\n"),
196        lines.len() - max
197    )
198}