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