lean_ctx/core/patterns/
clang.rs1use std::collections::HashMap;
2
3macro_rules! static_regex {
4 ($pattern:expr) => {{
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 diag_re() -> &'static regex::Regex {
13 static_regex!(r"^(.+?):(\d+):(\d+):\s+(warning|error|note|fatal error):\s+(.+)")
14}
15
16fn include_stack_re() -> &'static regex::Regex {
17 static_regex!(r"^In file included from .+:\d+:")
18}
19
20pub fn compress(command: &str, output: &str) -> Option<String> {
21 let trimmed = output.trim();
22 if trimmed.is_empty() {
23 return Some("clang: ok".to_string());
24 }
25
26 if command.contains("--version") || command.contains("-v") {
27 return Some(compress_version(trimmed));
28 }
29
30 Some(compress_diagnostics(trimmed))
31}
32
33fn compress_version(output: &str) -> String {
34 let first_line = output.lines().next().unwrap_or("clang");
35 first_line.trim().to_string()
36}
37
38fn compress_diagnostics(output: &str) -> String {
39 let mut errors = Vec::new();
40 let mut warning_groups: HashMap<String, Vec<String>> = HashMap::new();
41 let mut notes = Vec::new();
42 let mut include_stack_depth = 0u32;
43 let mut generated_warnings = 0u32;
44 let mut generated_errors = 0u32;
45
46 for line in output.lines() {
47 let trimmed = line.trim();
48
49 if include_stack_re().is_match(trimmed) {
50 include_stack_depth += 1;
51 continue;
52 }
53
54 if let Some(caps) = diag_re().captures(trimmed) {
55 let file = &caps[1];
56 let severity = &caps[4];
57 let message = &caps[5];
58
59 match severity {
60 "error" | "fatal error" => {
61 generated_errors += 1;
62 if errors.len() < 20 {
63 errors.push(format!("{file}: {message}"));
64 }
65 }
66 "warning" => {
67 generated_warnings += 1;
68 let key = normalize_diagnostic(message);
69 let locations = warning_groups.entry(key).or_default();
70 if locations.len() < 3 {
71 locations.push(file.to_string());
72 }
73 }
74 "note" if notes.len() < 5 => {
75 notes.push(format!("{file}: {message}"));
76 }
77 _ => {}
78 }
79 continue;
80 }
81
82 if trimmed.contains("error generated")
83 || trimmed.contains("errors generated")
84 || trimmed.contains("warning generated")
85 || trimmed.contains("warnings generated")
86 {}
87 }
88
89 if errors.is_empty() && warning_groups.is_empty() {
90 return "clang: ok".to_string();
91 }
92
93 let mut parts = Vec::new();
94
95 if !errors.is_empty() {
96 parts.push(format!("{generated_errors} errors:"));
97 for e in errors.iter().take(10) {
98 parts.push(format!(" {e}"));
99 }
100 if errors.len() > 10 {
101 parts.push(format!(" ... +{} more", errors.len() - 10));
102 }
103 }
104
105 if !warning_groups.is_empty() {
106 parts.push(format!(
107 "{generated_warnings} warnings ({} unique):",
108 warning_groups.len()
109 ));
110 let mut sorted: Vec<_> = warning_groups.iter().collect();
111 sorted.sort_by_key(|(_, locs)| std::cmp::Reverse(locs.len()));
112 for (msg, locs) in sorted.iter().take(10) {
113 let loc_str = if locs.len() <= 2 {
114 locs.join(", ")
115 } else {
116 format!("{}, {} (+{} more)", locs[0], locs[1], locs.len() - 2)
117 };
118 parts.push(format!(" {msg} [{loc_str}]"));
119 }
120 if sorted.len() > 10 {
121 parts.push(format!(" ... +{} more warning types", sorted.len() - 10));
122 }
123 }
124
125 if include_stack_depth > 0 {
126 parts.push(format!(
127 "({include_stack_depth} include-stack lines collapsed)"
128 ));
129 }
130
131 if !notes.is_empty() && errors.is_empty() {
132 parts.push(format!("{} notes (first 5):", notes.len()));
133 for n in ¬es {
134 parts.push(format!(" {n}"));
135 }
136 }
137
138 parts.join("\n")
139}
140
141fn normalize_diagnostic(msg: &str) -> String {
142 let cleaned = msg
143 .trim_end_matches(" [-Wunused-variable]")
144 .trim_end_matches(" [-Wunused-parameter]")
145 .trim_end_matches(" [-Wunused-function]");
146
147 let bracket_re = static_regex!(r"\s*\[-W[^\]]+\]$");
148 let result = bracket_re.replace(cleaned, "");
149
150 let quote_re = static_regex!(r"'[^']*'");
151 quote_re.replace_all(&result, "'…'").to_string()
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn compresses_errors() {
160 let output = "src/main.c:10:5: error: use of undeclared identifier 'foo'\nsrc/main.c:20:5: error: expected ';'\n2 errors generated.\n";
161 let result = compress("clang src/main.c", output).unwrap();
162 assert!(result.contains("2 errors"), "should count errors");
163 assert!(
164 result.contains("undeclared identifier"),
165 "should keep error msg"
166 );
167 }
168
169 #[test]
170 fn deduplicates_warnings() {
171 let output = "a.c:1:5: warning: unused variable 'x' [-Wunused-variable]\nb.c:2:5: warning: unused variable 'y' [-Wunused-variable]\nc.c:3:5: warning: unused variable 'z' [-Wunused-variable]\n3 warnings generated.\n";
172 let result = compress("clang -Wall a.c b.c c.c", output).unwrap();
173 assert!(result.contains("3 warnings"), "should count total warnings");
174 assert!(result.contains("1 unique"), "should deduplicate");
175 }
176
177 #[test]
178 fn collapses_include_stacks() {
179 let output = "In file included from main.c:1:\nIn file included from header.h:5:\nlib.h:10:5: warning: unused function 'helper' [-Wunused-function]\n1 warning generated.\n";
180 let result = compress("clang main.c", output).unwrap();
181 assert!(
182 result.contains("include-stack lines collapsed"),
183 "should report collapsed includes"
184 );
185 }
186
187 #[test]
188 fn clean_output() {
189 let result = compress("clang -o app main.c", "").unwrap();
190 assert_eq!(result, "clang: ok");
191 }
192
193 #[test]
194 fn version_output() {
195 let output = "clang version 17.0.6\nTarget: x86_64-pc-linux-gnu\nThread model: posix\n";
196 let result = compress("clang --version", output).unwrap();
197 assert!(result.contains("clang version 17.0.6"));
198 assert!(
199 !result.contains("Thread model"),
200 "should only keep first line"
201 );
202 }
203}