lean_ctx_patterns/
ruby.rs1pub fn compress(cmd_lower: &str, output: &str) -> Option<String> {
4 if cmd_lower.contains("rubocop") {
5 return compress_rubocop(output);
6 }
7 if cmd_lower.contains("bundle install") || cmd_lower.contains("bundle update") {
8 return compress_bundle(output);
9 }
10 if cmd_lower.contains("rake test") || cmd_lower.contains("rails test") {
11 return compress_minitest(output);
12 }
13 None
14}
15
16fn compress_rubocop(output: &str) -> Option<String> {
17 let mut offenses = Vec::new();
18 let mut files_inspected = 0u32;
19 let mut total_offenses = 0u32;
20
21 for line in output.lines() {
22 let trimmed = line.trim();
23 if trimmed.contains("files inspected") {
24 for word in trimmed.split_whitespace() {
25 if let Ok(n) = word.parse::<u32>() {
26 files_inspected = n;
27 break;
28 }
29 }
30 if let Some(pos) = trimmed.find("offense") {
31 let before = &trimmed[..pos];
32 for word in before.split(", ").last().unwrap_or("").split_whitespace() {
33 if let Ok(n) = word.parse::<u32>() {
34 total_offenses = n;
35 }
36 }
37 }
38 } else if trimmed.contains(": C:")
39 || trimmed.contains(": W:")
40 || trimmed.contains(": E:")
41 || trimmed.contains(": F:")
42 {
43 offenses.push(trimmed.to_string());
44 }
45 }
46
47 if files_inspected == 0 && offenses.is_empty() {
48 return None;
49 }
50
51 let mut result = format!("rubocop: {files_inspected} files, {total_offenses} offenses");
52
53 if total_offenses == 0 {
54 result.push_str(" (clean)");
55 return Some(result);
56 }
57
58 let grouped = group_by_cop(&offenses);
59 for (cop, count) in grouped.iter().take(10) {
60 result.push_str(&format!("\n {cop}: {count}x"));
61 }
62
63 if offenses.len() > 10 {
64 result.push_str(&format!("\n ... +{} more", offenses.len() - 10));
65 }
66
67 Some(result)
68}
69
70fn group_by_cop(offenses: &[String]) -> Vec<(String, usize)> {
71 let mut map = std::collections::HashMap::new();
72 for offense in offenses {
73 let cop = offense
74 .split('[')
75 .next_back()
76 .and_then(|s| s.strip_suffix(']'))
77 .unwrap_or("unknown")
78 .to_string();
79 *map.entry(cop).or_insert(0usize) += 1;
80 }
81 let mut sorted: Vec<_> = map.into_iter().collect();
82 sorted.sort_by_key(|x| std::cmp::Reverse(x.1));
83 sorted
84}
85
86fn compress_bundle(output: &str) -> Option<String> {
87 let mut installed = 0u32;
88 let mut using = 0u32;
89
90 for line in output.lines() {
91 let trimmed = line.trim();
92 if trimmed.starts_with("Installing ") {
93 installed += 1;
94 } else if trimmed.starts_with("Using ") {
95 using += 1;
96 }
97 }
98
99 if installed == 0 && using == 0 {
100 return None;
101 }
102
103 let mut result = String::from("bundle: ");
104 if installed > 0 {
105 result.push_str(&format!("{installed} installed"));
106 }
107 if using > 0 {
108 if installed > 0 {
109 result.push_str(", ");
110 }
111 result.push_str(&format!("{using} using (cached)"));
112 }
113
114 for line in output.lines().rev().take(3) {
115 let trimmed = line.trim();
116 if trimmed.starts_with("Bundle complete") || trimmed.starts_with("Bundled gems") {
117 result.push_str(&format!("\n {trimmed}"));
118 break;
119 }
120 }
121
122 Some(result)
123}
124
125fn compress_minitest(output: &str) -> Option<String> {
126 let mut total = 0u32;
127 let mut failures = 0u32;
128 let mut errors = 0u32;
129 let mut skips = 0u32;
130 let mut time = String::new();
131 let mut failure_details = Vec::new();
132
133 for line in output.lines() {
134 let trimmed = line.trim();
135 if trimmed.contains("runs,") && trimmed.contains("assertions") {
136 for part in trimmed.split(", ") {
137 let part = part.trim();
138 if part.ends_with("runs") {
139 if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
140 total = n;
141 }
142 } else if part.ends_with("failures") {
143 if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
144 failures = n;
145 }
146 } else if part.ends_with("errors") {
147 if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
148 errors = n;
149 }
150 } else if part.ends_with("skips")
151 && let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok())
152 {
153 skips = n;
154 }
155 }
156 if let Some(pos) = trimmed.find(" in ") {
157 time = trimmed[pos + 4..]
158 .split(',')
159 .next()
160 .unwrap_or("")
161 .trim()
162 .to_string();
163 }
164 }
165 if trimmed.starts_with("Failure:") || trimmed.starts_with("Error:") {
166 failure_details.push(trimmed.to_string());
167 }
168 }
169
170 if total == 0 {
171 return None;
172 }
173
174 let passed = total
175 .saturating_sub(failures)
176 .saturating_sub(errors)
177 .saturating_sub(skips);
178 let mut result = format!("minitest: {passed} passed");
179 if failures > 0 {
180 result.push_str(&format!(", {failures} failed"));
181 }
182 if errors > 0 {
183 result.push_str(&format!(", {errors} errors"));
184 }
185 if skips > 0 {
186 result.push_str(&format!(", {skips} skipped"));
187 }
188 if !time.is_empty() {
189 result.push_str(&format!(" ({time})"));
190 }
191
192 for detail in failure_details.iter().take(5) {
193 result.push_str(&format!("\n {detail}"));
194 }
195
196 Some(result)
197}