syncable_cli/analyzer/hadolint/formatter/
checkstyle.rs

1//! Checkstyle XML formatter for hadolint-rs.
2//!
3//! Outputs lint results in Checkstyle XML format for Jenkins and other CI tools.
4
5use crate::analyzer::hadolint::formatter::Formatter;
6use crate::analyzer::hadolint::lint::LintResult;
7use crate::analyzer::hadolint::types::Severity;
8use std::io::Write;
9
10/// Checkstyle XML output formatter for Jenkins.
11#[derive(Debug, Clone, Default)]
12pub struct CheckstyleFormatter;
13
14impl CheckstyleFormatter {
15    /// Create a new Checkstyle formatter.
16    pub fn new() -> Self {
17        Self
18    }
19}
20
21fn escape_xml(s: &str) -> String {
22    s.replace('&', "&")
23        .replace('<', "&lt;")
24        .replace('>', "&gt;")
25        .replace('"', "&quot;")
26        .replace('\'', "&apos;")
27}
28
29fn severity_to_checkstyle(severity: Severity) -> &'static str {
30    match severity {
31        Severity::Error => "error",
32        Severity::Warning => "warning",
33        Severity::Info => "info",
34        Severity::Style => "info",
35        Severity::Ignore => "info",
36    }
37}
38
39impl Formatter for CheckstyleFormatter {
40    fn format<W: Write>(&self, result: &LintResult, filename: &str, writer: &mut W) -> std::io::Result<()> {
41        writeln!(writer, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
42        writeln!(writer, r#"<checkstyle version="4.3">"#)?;
43
44        if !result.failures.is_empty() {
45            writeln!(writer, r#"  <file name="{}">"#, escape_xml(filename))?;
46
47            for failure in &result.failures {
48                let col_attr = failure
49                    .column
50                    .map(|c| format!(r#" column="{}""#, c))
51                    .unwrap_or_default();
52
53                writeln!(
54                    writer,
55                    r#"    <error line="{}"{}  severity="{}" message="{}" source="{}"/>"#,
56                    failure.line,
57                    col_attr,
58                    severity_to_checkstyle(failure.severity),
59                    escape_xml(&failure.message),
60                    escape_xml(&failure.code.to_string())
61                )?;
62            }
63
64            writeln!(writer, "  </file>")?;
65        }
66
67        writeln!(writer, "</checkstyle>")
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::analyzer::hadolint::types::CheckFailure;
75
76    #[test]
77    fn test_checkstyle_output() {
78        let mut result = LintResult::new();
79        result.failures.push(CheckFailure::new(
80            "DL3008",
81            Severity::Warning,
82            "Pin versions in apt get install",
83            5,
84        ));
85
86        let formatter = CheckstyleFormatter::new();
87        let output = formatter.format_to_string(&result, "Dockerfile");
88
89        assert!(output.contains("<?xml version"));
90        assert!(output.contains("<checkstyle"));
91        assert!(output.contains(r#"<file name="Dockerfile">"#));
92        assert!(output.contains(r#"line="5""#));
93        assert!(output.contains(r#"severity="warning""#));
94        assert!(output.contains("DL3008"));
95    }
96
97    #[test]
98    fn test_xml_escaping() {
99        assert_eq!(escape_xml("a < b"), "a &lt; b");
100        assert_eq!(escape_xml("a & b"), "a &amp; b");
101        assert_eq!(escape_xml(r#"a "b""#), "a &quot;b&quot;");
102    }
103}