Skip to main content

lean_ctx_patterns/
ruby.rs

1// SPDX-License-Identifier: Apache-2.0
2
3pub 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}