Skip to main content

lean_ctx_patterns/
semgrep.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Semgrep output compression.
4//!
5//! Semgrep's text output wraps findings in a banner, scan-progress lines and a
6//! code preview (gutter lines containing `┆`). We drop that noise and keep the
7//! findings (rule id, file, message) plus the final `Ran N rules ... findings`
8//! summary.
9
10use crate::ansi::strip_ansi;
11
12pub fn compress(_cmd: &str, output: &str) -> Option<String> {
13    let trimmed = output.trim();
14    if trimmed.is_empty() {
15        return Some("semgrep: ok".to_string());
16    }
17
18    let mut kept: Vec<String> = Vec::new();
19    for raw in trimmed.lines() {
20        let stripped = strip_ansi(raw);
21        let line = stripped.trim();
22        if line.is_empty() || is_noise(line) {
23            continue;
24        }
25        kept.push(line.to_string());
26    }
27
28    if kept.is_empty() {
29        return Some("semgrep: ok".to_string());
30    }
31    Some(kept.join("\n"))
32}
33
34fn is_noise(line: &str) -> bool {
35    // Code preview gutter: "42┆ subprocess.call(...)".
36    if line.contains('┆') {
37        return true;
38    }
39    // Box-drawing banner.
40    let first = line.chars().next().unwrap_or(' ');
41    if matches!(first, '┌' | '├' | '└' | '│' | '─' | '╷' | '╵') {
42        return true;
43    }
44    const PREFIXES: [&str; 6] = [
45        "Scanning",
46        "Loading rules",
47        "Fetching",
48        "Some files were skipped",
49        "partially analyzed",
50        "For a full list",
51    ];
52    PREFIXES.iter().any(|p| line.starts_with(p)) || line.contains("were skipped")
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    const SCAN: &str = "Scanning 120 files with 450 rules.\n\nFindings:\n\n  src/app.py\n     python.lang.security.audit.dangerous-subprocess-use\n        Detected subprocess function 'call' with user-controlled data.\n\n         42┆ subprocess.call(user_input, shell=True)\n\nSome files were skipped or only partially analyzed.\n\nRan 450 rules on 120 files: 3 findings.\n";
60
61    #[test]
62    fn keeps_findings_and_summary_drops_noise() {
63        let r = compress("semgrep scan", SCAN).unwrap();
64        assert!(r.contains("dangerous-subprocess-use"), "keeps rule id: {r}");
65        assert!(r.contains("src/app.py"), "keeps file: {r}");
66        assert!(r.contains("3 findings"), "keeps summary: {r}");
67        assert!(!r.contains("42┆"), "drops code preview: {r}");
68        assert!(!r.contains("Scanning 120"), "drops scan banner: {r}");
69        assert!(!r.contains("Some files were skipped"), "{r}");
70    }
71
72    #[test]
73    fn empty_is_ok() {
74        assert_eq!(compress("semgrep scan", "").unwrap(), "semgrep: ok");
75    }
76}