lean_ctx/core/patterns/
ruff.rs1use regex::Regex;
2use std::sync::OnceLock;
3
4static RUFF_LINE_RE: OnceLock<Regex> = OnceLock::new();
5static RUFF_FIXED_RE: OnceLock<Regex> = OnceLock::new();
6
7fn ruff_line_re() -> &'static Regex {
8 RUFF_LINE_RE.get_or_init(|| Regex::new(r"^(.+?):(\d+):(\d+):\s+([A-Z]\d+)\s+(.+)$").unwrap())
9}
10fn ruff_fixed_re() -> &'static Regex {
11 RUFF_FIXED_RE.get_or_init(|| Regex::new(r"Found (\d+) errors?.*?(\d+) fixable").unwrap())
12}
13
14pub fn compress(command: &str, output: &str) -> Option<String> {
15 if command.contains("format") || command.contains("fmt") {
16 return Some(compress_format(output));
17 }
18 Some(compress_check(output))
19}
20
21fn compress_check(output: &str) -> String {
22 let trimmed = output.trim();
23 if trimmed.is_empty() || trimmed.contains("All checks passed") {
24 return "clean".to_string();
25 }
26
27 let mut by_rule: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
28 let mut files: std::collections::HashSet<String> = std::collections::HashSet::new();
29
30 for line in trimmed.lines() {
31 if let Some(caps) = ruff_line_re().captures(line) {
32 let file = caps[1].to_string();
33 let rule = caps[4].to_string();
34 files.insert(file);
35 *by_rule.entry(rule).or_insert(0) += 1;
36 }
37 }
38
39 if by_rule.is_empty() {
40 if let Some(caps) = ruff_fixed_re().captures(trimmed) {
41 return format!("{} errors ({} fixable)", &caps[1], &caps[2]);
42 }
43 return compact_output(trimmed, 10);
44 }
45
46 let total: u32 = by_rule.values().sum();
47 let mut rules: Vec<(String, u32)> = by_rule.into_iter().collect();
48 rules.sort_by(|a, b| b.1.cmp(&a.1));
49
50 let mut parts = Vec::new();
51 parts.push(format!("{total} issues in {} files", files.len()));
52 for (rule, count) in rules.iter().take(8) {
53 parts.push(format!(" {rule}: {count}"));
54 }
55 if rules.len() > 8 {
56 parts.push(format!(" ... +{} more rules", rules.len() - 8));
57 }
58
59 parts.join("\n")
60}
61
62fn compress_format(output: &str) -> String {
63 let trimmed = output.trim();
64 if trimmed.is_empty() {
65 return "ok (formatted)".to_string();
66 }
67
68 let reformatted: Vec<&str> = trimmed
69 .lines()
70 .filter(|l| l.contains("reformatted") || l.contains("would reformat"))
71 .collect();
72
73 let unchanged: Vec<&str> = trimmed
74 .lines()
75 .filter(|l| l.contains("left unchanged") || l.contains("already formatted"))
76 .collect();
77
78 if !reformatted.is_empty() {
79 return format!("{} files reformatted", reformatted.len());
80 }
81 if !unchanged.is_empty() {
82 return format!("ok ({} files already formatted)", unchanged.len());
83 }
84
85 compact_output(trimmed, 5)
86}
87
88fn compact_output(text: &str, max: usize) -> String {
89 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
90 if lines.len() <= max {
91 return lines.join("\n");
92 }
93 format!(
94 "{}\n... ({} more lines)",
95 lines[..max].join("\n"),
96 lines.len() - max
97 )
98}