Skip to main content

lean_ctx/core/patterns/
golang.rs

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