lean_ctx/core/patterns/
eslint.rs1macro_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}