Skip to main content

lean_ctx_patterns/
eslint.rs

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