Skip to main content

lean_ctx/core/patterns/
eslint.rs

1macro_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 eslint_file_re() -> &'static regex::Regex {
11    static_regex!(r"^(/\S+|[A-Z]:\\\S+|\S+\.\w+)$")
12}
13fn eslint_error_re() -> &'static regex::Regex {
14    static_regex!(r"^\s+(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}(\S+)$")
15}
16fn eslint_summary_re() -> &'static regex::Regex {
17    static_regex!(r"(\d+)\s+problems?\s*\((\d+)\s+errors?,\s*(\d+)\s+warnings?\)")
18}
19fn biome_diag_re() -> &'static regex::Regex {
20    static_regex!(r"^([\w/.-]+):(\d+):(\d+)\s+(\w+)\s+(.+)$")
21}
22
23pub fn compress(command: &str, output: &str) -> Option<String> {
24    if command.contains("biome") {
25        return Some(compress_biome(output));
26    }
27    if command.contains("stylelint") {
28        return Some(compress_stylelint(output));
29    }
30    Some(compress_eslint(output))
31}
32
33fn compress_eslint(output: &str) -> String {
34    let trimmed = output.trim();
35    if trimmed.is_empty() {
36        return "clean".to_string();
37    }
38
39    let mut by_rule: std::collections::HashMap<String, (u32, u32)> =
40        std::collections::HashMap::new();
41    let mut file_count = 0u32;
42    let mut total_errors = 0u32;
43    let mut total_warnings = 0u32;
44
45    for line in trimmed.lines() {
46        let l = line.trim();
47        if eslint_file_re().is_match(l) {
48            file_count += 1;
49            continue;
50        }
51        if let Some(caps) = eslint_error_re().captures(line) {
52            let severity = &caps[3];
53            let rule = caps[5].to_string();
54            let entry = by_rule.entry(rule).or_insert((0, 0));
55            if severity == "error" {
56                entry.0 += 1;
57                total_errors += 1;
58            } else {
59                entry.1 += 1;
60                total_warnings += 1;
61            }
62        }
63        if let Some(caps) = eslint_summary_re().captures(line) {
64            total_errors = caps[2].parse().unwrap_or(total_errors);
65            total_warnings = caps[3].parse().unwrap_or(total_warnings);
66        }
67    }
68
69    if by_rule.is_empty() && total_errors == 0 && total_warnings == 0 {
70        if trimmed.lines().count() <= 5 {
71            return trimmed.to_string();
72        }
73        return compact_output(trimmed, 10);
74    }
75
76    let mut parts = Vec::new();
77    parts.push(format!(
78        "{total_errors} errors, {total_warnings} warnings in {file_count} files"
79    ));
80
81    let mut rules: Vec<(String, (u32, u32))> = by_rule.into_iter().collect();
82    rules.sort_by_key(|(_, (errors, warnings))| std::cmp::Reverse(*errors + *warnings));
83
84    for (rule, (errors, warnings)) in rules.iter().take(10) {
85        let total = errors + warnings;
86        parts.push(format!("  {rule}: {total}"));
87    }
88    if rules.len() > 10 {
89        parts.push(format!("  ... +{} more rules", rules.len() - 10));
90    }
91
92    parts.join("\n")
93}
94
95fn compress_biome(output: &str) -> String {
96    let trimmed = output.trim();
97    if trimmed.is_empty() || trimmed.contains("No diagnostics") {
98        return "clean".to_string();
99    }
100
101    let mut errors = 0u32;
102    let mut warnings = 0u32;
103    let mut files: std::collections::HashSet<String> = std::collections::HashSet::new();
104
105    for line in trimmed.lines() {
106        if let Some(caps) = biome_diag_re().captures(line) {
107            files.insert(caps[1].to_string());
108            let severity = &caps[4];
109            if severity.contains("error") || severity.contains("ERROR") {
110                errors += 1;
111            } else {
112                warnings += 1;
113            }
114        }
115    }
116
117    if errors == 0 && warnings == 0 {
118        return compact_output(trimmed, 10);
119    }
120
121    format!(
122        "{errors} errors, {warnings} warnings in {} files",
123        files.len()
124    )
125}
126
127fn compress_stylelint(output: &str) -> String {
128    let trimmed = output.trim();
129    if trimmed.is_empty() {
130        return "clean".to_string();
131    }
132    compress_eslint(output)
133}
134
135fn compact_output(text: &str, max: usize) -> String {
136    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
137    if lines.len() <= max {
138        return lines.join("\n");
139    }
140    format!(
141        "{}\n... ({} more lines)",
142        lines[..max].join("\n"),
143        lines.len() - max
144    )
145}