Skip to main content

tldr_cli/commands/bugbot/parsers/
cppcheck.rs

1//! Parser for cppcheck output using `--template` format.
2//!
3//! We use `--template='{file}\t{line}\t{column}\t{severity}\t{id}\t{message}'`
4//! to get tab-separated output that's easy to parse without XML dependencies.
5
6use std::path::PathBuf;
7
8use super::super::tools::{L1Finding, ToolCategory};
9use super::ParseError;
10
11pub fn parse_cppcheck_output(stdout: &str) -> Result<Vec<L1Finding>, ParseError> {
12    let mut findings = Vec::new();
13
14    for line in stdout.lines() {
15        let line = line.trim();
16        if line.is_empty() {
17            continue;
18        }
19
20        let parts: Vec<&str> = line.splitn(6, '\t').collect();
21        if parts.len() < 6 {
22            continue; // Skip malformed lines (e.g., "Checking ..." progress output)
23        }
24
25        let file = parts[0];
26        let line_num: u32 = parts[1].parse().unwrap_or(0);
27        let column: u32 = parts[2].parse().unwrap_or(0);
28        let native_sev = parts[3];
29        let id = parts[4];
30        let message = parts[5];
31
32        // Skip "information" severity (file-level notes, not bugs)
33        if native_sev == "information" {
34            continue;
35        }
36
37        let severity = match native_sev {
38            "error" => "high",
39            "warning" => "medium",
40            "style" | "performance" | "portability" => "low",
41            _ => "info",
42        };
43
44        findings.push(L1Finding {
45            tool: String::new(),
46            category: ToolCategory::Linter,
47            file: PathBuf::from(file),
48            line: line_num,
49            column,
50            native_severity: native_sev.to_string(),
51            severity: severity.to_string(),
52            message: message.to_string(),
53            code: if id.is_empty() {
54                None
55            } else {
56                Some(id.to_string())
57            },
58        });
59    }
60
61    Ok(findings)
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn test_parse_empty() {
70        assert!(parse_cppcheck_output("").unwrap().is_empty());
71    }
72
73    #[test]
74    fn test_parse_finding() {
75        let output = "main.c\t10\t5\twarning\tunreadVariable\tVariable 'x' is assigned a value that is never used.";
76        let findings = parse_cppcheck_output(output).unwrap();
77        assert_eq!(findings.len(), 1);
78        assert_eq!(findings[0].file, PathBuf::from("main.c"));
79        assert_eq!(findings[0].line, 10);
80        assert_eq!(findings[0].column, 5);
81        assert_eq!(findings[0].severity, "medium");
82        assert_eq!(
83            findings[0].code,
84            Some("unreadVariable".to_string())
85        );
86    }
87
88    #[test]
89    fn test_parse_skips_information() {
90        let output = "main.c\t0\t0\tinformation\tmissingInclude\tInclude file not found";
91        assert!(parse_cppcheck_output(output).unwrap().is_empty());
92    }
93
94    #[test]
95    fn test_parse_skips_malformed() {
96        let output = "Checking main.c ...\nmain.c\t5\t1\terror\tnullPointer\tNull pointer dereference";
97        let findings = parse_cppcheck_output(output).unwrap();
98        assert_eq!(findings.len(), 1);
99        assert_eq!(findings[0].severity, "high");
100    }
101}