Skip to main content

lean_ctx/core/patterns/
ruff.rs

1use 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    let mut issue_lines: Vec<&str> = Vec::new();
30
31    for line in trimmed.lines() {
32        if let Some(caps) = ruff_line_re().captures(line) {
33            let file = caps[1].to_string();
34            let rule = caps[4].to_string();
35            files.insert(file);
36            *by_rule.entry(rule).or_insert(0) += 1;
37            issue_lines.push(line);
38        }
39    }
40
41    if by_rule.is_empty() {
42        if let Some(caps) = ruff_fixed_re().captures(trimmed) {
43            return format!("{} errors ({} fixable)", &caps[1], &caps[2]);
44        }
45        return compact_output(trimmed, 10);
46    }
47
48    let total: u32 = by_rule.values().sum();
49
50    if total <= 30 {
51        return trimmed.to_string();
52    }
53
54    let mut rules: Vec<(String, u32)> = by_rule.into_iter().collect();
55    rules.sort_by_key(|x| std::cmp::Reverse(x.1));
56
57    let mut parts = Vec::new();
58    parts.push(format!("{total} issues in {} files", files.len()));
59    for line in issue_lines.iter().take(20) {
60        parts.push(format!("  {line}"));
61    }
62    if issue_lines.len() > 20 {
63        parts.push(format!("  ... +{} more issues", issue_lines.len() - 20));
64    }
65    parts.push(String::new());
66    parts.push("by rule:".to_string());
67    for (rule, count) in rules.iter().take(8) {
68        parts.push(format!("  {rule}: {count}"));
69    }
70    if rules.len() > 8 {
71        parts.push(format!("  ... +{} more rules", rules.len() - 8));
72    }
73
74    parts.join("\n")
75}
76
77fn compress_format(output: &str) -> String {
78    let trimmed = output.trim();
79    if trimmed.is_empty() {
80        return "ok (formatted)".to_string();
81    }
82
83    let reformatted: Vec<&str> = trimmed
84        .lines()
85        .filter(|l| l.contains("reformatted") || l.contains("would reformat"))
86        .collect();
87
88    let unchanged: Vec<&str> = trimmed
89        .lines()
90        .filter(|l| l.contains("left unchanged") || l.contains("already formatted"))
91        .collect();
92
93    if !reformatted.is_empty() {
94        return format!("{} files reformatted", reformatted.len());
95    }
96    if !unchanged.is_empty() {
97        return format!("ok ({} files already formatted)", unchanged.len());
98    }
99
100    compact_output(trimmed, 5)
101}
102
103fn compact_output(text: &str, max: usize) -> String {
104    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
105    if lines.len() <= max {
106        return lines.join("\n");
107    }
108    format!(
109        "{}\n... ({} more lines)",
110        lines[..max].join("\n"),
111        lines.len() - max
112    )
113}