lean_ctx_patterns/
ninja.rs1macro_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 progress_re() -> &'static regex::Regex {
13 static_regex!(r"^\[(\d+)/(\d+)\]\s+")
14}
15
16pub fn compress(command: &str, output: &str) -> Option<String> {
17 let trimmed = output.trim();
18 if trimmed.is_empty() {
19 return Some("ninja: ok".to_string());
20 }
21
22 if command.contains("-t targets") || command.contains("-t rules") {
23 return Some(compress_query(trimmed));
24 }
25
26 Some(compress_build(trimmed))
27}
28
29fn compress_build(output: &str) -> String {
30 let mut total_steps = 0u32;
31 let mut max_total = 0u32;
32 let mut errors = Vec::new();
33 let mut warnings = Vec::new();
34 let mut warning_seen = std::collections::HashSet::new();
35
36 for line in output.lines() {
37 let trimmed = line.trim();
38
39 if let Some(caps) = progress_re().captures(trimmed) {
40 if let (Ok(current), Ok(total)) = (caps[1].parse::<u32>(), caps[2].parse::<u32>()) {
41 total_steps = current;
42 max_total = total;
43 }
44 continue;
45 }
46
47 if is_error_line(trimmed) {
48 if errors.len() < 20 {
49 errors.push(trimmed.to_string());
50 }
51 continue;
52 }
53
54 if is_warning_line(trimmed) {
55 let key = normalize_warning(trimmed);
56 if warning_seen.insert(key) {
57 warnings.push(trimmed.to_string());
58 }
59 }
60 }
61
62 if !errors.is_empty() {
63 let mut result = format!("ninja: FAILED ({} errors", errors.len());
64 if !warnings.is_empty() {
65 result.push_str(&format!(", {} unique warnings", warnings.len()));
66 }
67 result.push_str(&format!(", {total_steps}/{max_total} steps)"));
68 for e in errors.iter().take(10) {
69 result.push_str(&format!("\n {e}"));
70 }
71 if errors.len() > 10 {
72 result.push_str(&format!("\n ... +{} more errors", errors.len() - 10));
73 }
74 return result;
75 }
76
77 let mut result = format!("ninja: ok ({total_steps}/{max_total} steps)");
78 if !warnings.is_empty() {
79 result.push_str(&format!("\n{} unique warnings:", warnings.len()));
80 for w in warnings.iter().take(10) {
81 result.push_str(&format!("\n {w}"));
82 }
83 if warnings.len() > 10 {
84 result.push_str(&format!("\n ... +{} more", warnings.len() - 10));
85 }
86 }
87 result
88}
89
90fn compress_query(output: &str) -> String {
91 let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
92 if lines.len() <= 20 {
93 return format!("{} entries:\n{}", lines.len(), lines.join("\n"));
94 }
95 format!(
96 "{} entries:\n{}\n... +{} more",
97 lines.len(),
98 lines[..20].join("\n"),
99 lines.len() - 20
100 )
101}
102
103fn is_error_line(line: &str) -> bool {
104 let l = line.to_ascii_lowercase();
105 l.contains("error:") || l.contains("fatal error") || l.contains("ninja: error")
106}
107
108fn is_warning_line(line: &str) -> bool {
109 let l = line.to_ascii_lowercase();
110 l.contains("warning:")
111}
112
113fn normalize_warning(line: &str) -> String {
114 let re = static_regex!(r"[^\s:]+:\d+:\d+:\s*");
115 let without_location = re.replace_all(line, "");
116 without_location.to_string()
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn compresses_successful_build() {
125 let output = "[1/10] Compiling foo.c\n[2/10] Compiling bar.c\n[10/10] Linking app\n";
126 let result = compress("ninja", output).unwrap();
127 assert!(result.contains("10/10"), "should show final progress");
128 assert!(result.contains("ok"), "should indicate success");
129 }
130
131 #[test]
132 fn keeps_errors() {
133 let output =
134 "[1/5] Compiling foo.c\n[2/5] Compiling bar.c\nerror: undefined reference to `main`\n";
135 let result = compress("ninja", output).unwrap();
136 assert!(result.contains("FAILED"), "should indicate failure");
137 assert!(result.contains("undefined reference"), "should keep errors");
138 }
139
140 #[test]
141 fn deduplicates_warnings() {
142 let output = "[1/3] Compiling a.c\nsrc/a.c:10:5: warning: unused variable\nsrc/b.c:20:5: warning: unused variable\n[3/3] Linking\n";
143 let result = compress("ninja", output).unwrap();
144 assert!(
145 result.contains("1 unique warning"),
146 "should deduplicate same warning at different locations: {result}"
147 );
148 }
149
150 #[test]
151 fn empty_output() {
152 let result = compress("ninja", "").unwrap();
153 assert_eq!(result, "ninja: ok");
154 }
155
156 #[test]
157 fn compresses_target_query() {
158 let output = "target1: phony\ntarget2: cc\ntarget3: link\n";
159 let result = compress("ninja -t targets", output).unwrap();
160 assert!(result.contains("3 entries"), "should count targets");
161 }
162}