lean_ctx/core/patterns/
golang.rs1use 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}