Skip to main content

lean_ctx_patterns/
golang.rs

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