lean_ctx/core/patterns/
golang.rs1macro_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 let mut passed_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 if line.contains("--- PASS:") {
70 if let Some(name) = line.split("--- PASS:").nth(1) {
71 let name = name.split_whitespace().next().unwrap_or("");
72 if !name.is_empty() {
73 passed_tests.push(name.to_string());
74 }
75 }
76 }
77 }
78
79 if results.is_empty() {
80 return compact_output(trimmed, 10);
81 }
82
83 let mut parts = results;
84 if !failed_tests.is_empty() {
85 parts.push(format!("failed: {}", failed_tests.join(", ")));
86 }
87 if failed_tests.is_empty() && !passed_tests.is_empty() {
88 let total = passed_tests.len();
89 let shown: Vec<_> = passed_tests.into_iter().take(5).collect();
90 let suffix = if total > 5 {
91 format!(" ...+{} more", total - 5)
92 } else {
93 String::new()
94 };
95 parts.push(format!("ran: {}{suffix}", shown.join(", ")));
96 }
97 parts.join("\n")
98}
99
100fn compress_bench(output: &str) -> String {
101 let trimmed = output.trim();
102 let mut benchmarks = Vec::new();
103
104 for line in trimmed.lines() {
105 if let Some(caps) = go_bench_re().captures(line) {
106 let name = &caps[1];
107 let ops = &caps[2];
108 let ns = &caps[3];
109 let unit = &caps[4];
110 benchmarks.push(format!("{name}: {ops} ops @ {ns} {unit}/op"));
111 }
112 }
113
114 if benchmarks.is_empty() {
115 return compact_output(trimmed, 10);
116 }
117 format!(
118 "{} benchmarks:\n{}",
119 benchmarks.len(),
120 benchmarks.join("\n")
121 )
122}
123
124fn compress_build(output: &str) -> String {
125 let trimmed = output.trim();
126 if trimmed.is_empty() {
127 return "ok".to_string();
128 }
129
130 let mut errors = Vec::new();
131 for line in trimmed.lines() {
132 if let Some(caps) = go_build_error_re().captures(line) {
133 errors.push(format!("{}:{}: {}", &caps[1], &caps[2], &caps[4]));
134 }
135 }
136
137 if errors.is_empty() {
138 return compact_output(trimmed, 5);
139 }
140 format!("{} errors:\n{}", errors.len(), errors.join("\n"))
141}
142
143fn compress_golint(output: &str) -> String {
144 let trimmed = output.trim();
145 if trimmed.is_empty() {
146 return "clean".to_string();
147 }
148
149 let mut by_linter: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
150 let mut files: std::collections::HashSet<String> = std::collections::HashSet::new();
151
152 for line in trimmed.lines() {
153 if let Some(caps) = golint_re().captures(line) {
154 files.insert(caps[1].to_string());
155 let linter = caps[5].to_string();
156 *by_linter.entry(linter).or_insert(0) += 1;
157 }
158 }
159
160 if by_linter.is_empty() {
161 return compact_output(trimmed, 10);
162 }
163
164 let total: u32 = by_linter.values().sum();
165 let mut linters: Vec<(String, u32)> = by_linter.into_iter().collect();
166 linters.sort_by_key(|x| std::cmp::Reverse(x.1));
167
168 let mut parts = Vec::new();
169 parts.push(format!("{total} issues in {} files", files.len()));
170 for (linter, count) in linters.iter().take(8) {
171 parts.push(format!(" {linter}: {count}"));
172 }
173 if linters.len() > 8 {
174 parts.push(format!(" ... +{} more linters", linters.len() - 8));
175 }
176
177 parts.join("\n")
178}
179
180fn compress_vet(output: &str) -> String {
181 let trimmed = output.trim();
182 if trimmed.is_empty() {
183 return "ok (vet clean)".to_string();
184 }
185 compact_output(trimmed, 10)
186}
187
188fn compress_mod(output: &str) -> String {
189 let trimmed = output.trim();
190 if trimmed.is_empty() {
191 return "ok".to_string();
192 }
193 compact_output(trimmed, 10)
194}
195
196fn compress_fmt(output: &str) -> String {
197 let trimmed = output.trim();
198 if trimmed.is_empty() {
199 return "ok (formatted)".to_string();
200 }
201
202 let files: Vec<&str> = trimmed.lines().filter(|l| !l.trim().is_empty()).collect();
203 format!("{} files reformatted:\n{}", files.len(), files.join("\n"))
204}
205
206fn compact_output(text: &str, max: usize) -> String {
207 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
208 if lines.len() <= max {
209 return lines.join("\n");
210 }
211 format!(
212 "{}\n... ({} more lines)",
213 lines[..max].join("\n"),
214 lines.len() - max
215 )
216}