Skip to main content

lean_ctx_patterns/
buf.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! buf (protobuf tooling) output compression.
4//!
5//! `buf lint`/`breaking` emit one `path:line:col:message` line per violation.
6//! We prefix a violation count and keep the findings (capped), so large lint
7//! runs collapse to a scannable summary. Clean builds become `buf: ok`.
8
9use 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        // build/generate success or an error message we keep verbatim.
47        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}