1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use self::xml::XmlEscaped;
use super::*;
use crate::rustfmt_diff::{make_diff, DiffLine, Mismatch};

mod xml;

#[derive(Debug, Default)]
pub(crate) struct CheckstyleEmitter;

impl Emitter for CheckstyleEmitter {
    fn emit_header(&self, output: &mut dyn Write) -> Result<(), io::Error> {
        writeln!(output, r#"<?xml version="1.0" encoding="utf-8"?>"#)?;
        write!(output, r#"<checkstyle version="4.3">"#)?;
        Ok(())
    }

    fn emit_footer(&self, output: &mut dyn Write) -> Result<(), io::Error> {
        writeln!(output, "</checkstyle>")
    }

    fn emit_formatted_file(
        &mut self,
        output: &mut dyn Write,
        FormattedFile {
            filename,
            original_text,
            formatted_text,
        }: FormattedFile<'_>,
    ) -> Result<EmitterResult, io::Error> {
        const CONTEXT_SIZE: usize = 0;
        let diff = make_diff(original_text, formatted_text, CONTEXT_SIZE);
        output_checkstyle_file(output, filename, diff)?;
        Ok(EmitterResult::default())
    }
}

pub(crate) fn output_checkstyle_file<T>(
    mut writer: T,
    filename: &FileName,
    diff: Vec<Mismatch>,
) -> Result<(), io::Error>
where
    T: Write,
{
    write!(writer, r#"<file name="{filename}">"#)?;
    for mismatch in diff {
        let begin_line = mismatch.line_number;
        let mut current_line;
        let mut line_counter = 0;
        for line in mismatch.lines {
            // Do nothing with `DiffLine::Context` and `DiffLine::Resulting`.
            if let DiffLine::Expected(message) = line {
                current_line = begin_line + line_counter;
                line_counter += 1;
                write!(
                    writer,
                    r#"<error line="{}" severity="warning" message="Should be `{}`" />"#,
                    current_line,
                    XmlEscaped(&message)
                )?;
            }
        }
    }
    write!(writer, "</file>")?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn emits_empty_record_on_file_with_no_mismatches() {
        let file_name = "src/well_formatted.rs";
        let mut writer = Vec::new();
        let _ = output_checkstyle_file(
            &mut writer,
            &FileName::Real(PathBuf::from(file_name)),
            vec![],
        );
        assert_eq!(
            &writer[..],
            format!(r#"<file name="{file_name}"></file>"#).as_bytes()
        );
    }

    // https://github.com/rust-lang/rustfmt/issues/1636
    #[test]
    fn emits_single_xml_tree_containing_all_files() {
        let bin_file = "src/bin.rs";
        let bin_original = vec!["fn main() {", "println!(\"Hello, world!\");", "}"];
        let bin_formatted = vec!["fn main() {", "    println!(\"Hello, world!\");", "}"];
        let lib_file = "src/lib.rs";
        let lib_original = vec!["fn greet() {", "println!(\"Greetings!\");", "}"];
        let lib_formatted = vec!["fn greet() {", "    println!(\"Greetings!\");", "}"];
        let mut writer = Vec::new();
        let mut emitter = CheckstyleEmitter::default();
        let _ = emitter.emit_header(&mut writer);
        let _ = emitter
            .emit_formatted_file(
                &mut writer,
                FormattedFile {
                    filename: &FileName::Real(PathBuf::from(bin_file)),
                    original_text: &bin_original.join("\n"),
                    formatted_text: &bin_formatted.join("\n"),
                },
            )
            .unwrap();
        let _ = emitter
            .emit_formatted_file(
                &mut writer,
                FormattedFile {
                    filename: &FileName::Real(PathBuf::from(lib_file)),
                    original_text: &lib_original.join("\n"),
                    formatted_text: &lib_formatted.join("\n"),
                },
            )
            .unwrap();
        let _ = emitter.emit_footer(&mut writer);
        let exp_bin_xml = vec![
            format!(r#"<file name="{}">"#, bin_file),
            format!(
                r#"<error line="2" severity="warning" message="Should be `{}`" />"#,
                XmlEscaped(r#"    println!("Hello, world!");"#),
            ),
            String::from("</file>"),
        ];
        let exp_lib_xml = vec![
            format!(r#"<file name="{}">"#, lib_file),
            format!(
                r#"<error line="2" severity="warning" message="Should be `{}`" />"#,
                XmlEscaped(r#"    println!("Greetings!");"#),
            ),
            String::from("</file>"),
        ];
        assert_eq!(
            String::from_utf8(writer).unwrap(),
            vec![
                r#"<?xml version="1.0" encoding="utf-8"?>"#,
                "\n",
                r#"<checkstyle version="4.3">"#,
                &format!("{}{}", exp_bin_xml.join(""), exp_lib_xml.join("")),
                "</checkstyle>\n",
            ]
            .join(""),
        );
    }
}