Skip to main content

lean_ctx/core/patterns/
eslint.rs

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