Skip to main content

lean_ctx/core/patterns/
semgrep.rs

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