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