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>(
41        &self,
42        result: &LintResult,
43        filename: &str,
44        writer: &mut W,
45    ) -> std::io::Result<()> {
46        writeln!(writer, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
47        writeln!(writer, r#"<checkstyle version="4.3">"#)?;
48
49        if !result.failures.is_empty() {
50            writeln!(writer, r#"  <file name="{}">"#, escape_xml(filename))?;
51
52            for failure in &result.failures {
53                let col_attr = failure
54                    .column
55                    .map(|c| format!(r#" column="{}""#, c))
56                    .unwrap_or_default();
57
58                writeln!(
59                    writer,
60                    r#"    <error line="{}"{}  severity="{}" message="{}" source="{}"/>"#,
61                    failure.line,
62                    col_attr,
63                    severity_to_checkstyle(failure.severity),
64                    escape_xml(&failure.message),
65                    escape_xml(&failure.code.to_string())
66                )?;
67            }
68
69            writeln!(writer, "  </file>")?;
70        }
71
72        writeln!(writer, "</checkstyle>")
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::analyzer::hadolint::types::CheckFailure;
80
81    #[test]
82    fn test_checkstyle_output() {
83        let mut result = LintResult::new();
84        result.failures.push(CheckFailure::new(
85            "DL3008",
86            Severity::Warning,
87            "Pin versions in apt get install",
88            5,
89        ));
90
91        let formatter = CheckstyleFormatter::new();
92        let output = formatter.format_to_string(&result, "Dockerfile");
93
94        assert!(output.contains("<?xml version"));
95        assert!(output.contains("<checkstyle"));
96        assert!(output.contains(r#"<file name="Dockerfile">"#));
97        assert!(output.contains(r#"line="5""#));
98        assert!(output.contains(r#"severity="warning""#));
99        assert!(output.contains("DL3008"));
100    }
101
102    #[test]
103    fn test_xml_escaping() {
104        assert_eq!(escape_xml("a < b"), "a &lt; b");
105        assert_eq!(escape_xml("a & b"), "a &amp; b");
106        assert_eq!(escape_xml(r#"a "b""#), "a &quot;b&quot;");
107    }
108}