1use crate::ansi::strip_ansi;
10
11macro_rules! static_regex {
12 ($pattern:expr_2021) => {{
13 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
14 RE.get_or_init(|| {
15 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
16 })
17 }};
18}
19
20fn violation_re() -> &'static regex::Regex {
21 static_regex!(r"^(.+?):\d+:\d+:(.+)$")
22}
23
24pub fn compress(_cmd: &str, output: &str) -> Option<String> {
25 let trimmed = output.trim();
26 if trimmed.is_empty() {
27 return Some("buf: ok".to_string());
28 }
29
30 let mut violations: Vec<String> = Vec::new();
31 let mut other: Vec<String> = Vec::new();
32 for raw in trimmed.lines() {
33 let line = strip_ansi(raw);
34 let t = line.trim();
35 if t.is_empty() {
36 continue;
37 }
38 if violation_re().is_match(t) {
39 violations.push(t.to_string());
40 } else {
41 other.push(t.to_string());
42 }
43 }
44
45 if violations.is_empty() {
46 if other.is_empty() {
48 return Some("buf: ok".to_string());
49 }
50 return Some(other.join("\n"));
51 }
52
53 let mut parts = vec![format!("buf: {} violation(s)", violations.len())];
54 for v in violations.iter().take(20) {
55 parts.push(format!(" {v}"));
56 }
57 if violations.len() > 20 {
58 parts.push(format!(" ... +{} more", violations.len() - 20));
59 }
60 Some(parts.join("\n"))
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn counts_and_keeps_violations() {
69 let out = "proto/foo.proto:10:1:Field name should be lower_snake_case.\nproto/bar.proto:5:3:Enum value should be UPPER_SNAKE_CASE.";
70 let r = compress("buf lint", out).unwrap();
71 assert!(r.contains("buf: 2 violation(s)"), "{r}");
72 assert!(r.contains("foo.proto:10:1"), "{r}");
73 }
74
75 #[test]
76 fn empty_is_ok() {
77 assert_eq!(compress("buf lint", "").unwrap(), "buf: ok");
78 }
79}