rustfmt_nightly/emitter/
json.rs

1use super::*;
2use crate::rustfmt_diff::{DiffLine, Mismatch, make_diff};
3use serde::Serialize;
4use serde_json::to_string as to_json_string;
5
6#[derive(Debug, Default)]
7pub(crate) struct JsonEmitter {
8    mismatched_files: Vec<MismatchedFile>,
9}
10
11#[derive(Debug, Default, PartialEq, Serialize)]
12struct MismatchedBlock {
13    original_begin_line: u32,
14    original_end_line: u32,
15    expected_begin_line: u32,
16    expected_end_line: u32,
17    original: String,
18    expected: String,
19}
20
21#[derive(Debug, Default, PartialEq, Serialize)]
22struct MismatchedFile {
23    name: String,
24    mismatches: Vec<MismatchedBlock>,
25}
26
27impl Emitter for JsonEmitter {
28    fn emit_footer(&self, output: &mut dyn Write) -> Result<(), io::Error> {
29        writeln!(output, "{}", &to_json_string(&self.mismatched_files)?)
30    }
31
32    fn emit_formatted_file(
33        &mut self,
34        _output: &mut dyn Write,
35        FormattedFile {
36            filename,
37            original_text,
38            formatted_text,
39        }: FormattedFile<'_>,
40    ) -> Result<EmitterResult, io::Error> {
41        const CONTEXT_SIZE: usize = 0;
42        let diff = make_diff(original_text, formatted_text, CONTEXT_SIZE);
43        let has_diff = !diff.is_empty();
44
45        if has_diff {
46            self.add_misformatted_file(filename, diff)?;
47        }
48
49        Ok(EmitterResult { has_diff })
50    }
51}
52
53impl JsonEmitter {
54    fn add_misformatted_file(
55        &mut self,
56        filename: &FileName,
57        diff: Vec<Mismatch>,
58    ) -> Result<(), io::Error> {
59        let mut mismatches = vec![];
60        for mismatch in diff {
61            let original_begin_line = mismatch.line_number_orig;
62            let expected_begin_line = mismatch.line_number;
63            let mut original_end_line = original_begin_line;
64            let mut expected_end_line = expected_begin_line;
65            let mut original_line_counter = 0;
66            let mut expected_line_counter = 0;
67            let mut original = String::new();
68            let mut expected = String::new();
69
70            for line in mismatch.lines {
71                match line {
72                    DiffLine::Expected(msg) => {
73                        expected_end_line = expected_begin_line + expected_line_counter;
74                        expected_line_counter += 1;
75                        expected.push_str(&msg);
76                        expected.push('\n');
77                    }
78                    DiffLine::Resulting(msg) => {
79                        original_end_line = original_begin_line + original_line_counter;
80                        original_line_counter += 1;
81                        original.push_str(&msg);
82                        original.push('\n');
83                    }
84                    DiffLine::Context(_) => continue,
85                }
86            }
87
88            mismatches.push(MismatchedBlock {
89                original_begin_line,
90                original_end_line,
91                expected_begin_line,
92                expected_end_line,
93                original,
94                expected,
95            });
96        }
97        self.mismatched_files.push(MismatchedFile {
98            name: format!("{filename}"),
99            mismatches,
100        });
101        Ok(())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use std::path::PathBuf;
109
110    #[test]
111    fn expected_line_range_correct_when_single_line_split() {
112        let mut emitter = JsonEmitter {
113            mismatched_files: vec![],
114        };
115        let file = "foo/bar.rs";
116        let mismatched_file = MismatchedFile {
117            name: String::from(file),
118            mismatches: vec![MismatchedBlock {
119                original_begin_line: 79,
120                original_end_line: 79,
121                expected_begin_line: 79,
122                expected_end_line: 82,
123                original: String::from("fn Foo<T>() where T: Bar {\n"),
124                expected: String::from("fn Foo<T>()\nwhere\n    T: Bar,\n{\n"),
125            }],
126        };
127        let mismatch = Mismatch {
128            line_number: 79,
129            line_number_orig: 79,
130            lines: vec![
131                DiffLine::Resulting(String::from("fn Foo<T>() where T: Bar {")),
132                DiffLine::Expected(String::from("fn Foo<T>()")),
133                DiffLine::Expected(String::from("where")),
134                DiffLine::Expected(String::from("    T: Bar,")),
135                DiffLine::Expected(String::from("{")),
136            ],
137        };
138
139        let _ = emitter
140            .add_misformatted_file(&FileName::Real(PathBuf::from(file)), vec![mismatch])
141            .unwrap();
142
143        assert_eq!(emitter.mismatched_files.len(), 1);
144        assert_eq!(emitter.mismatched_files[0], mismatched_file);
145    }
146
147    #[test]
148    fn context_lines_ignored() {
149        let mut emitter = JsonEmitter {
150            mismatched_files: vec![],
151        };
152        let file = "src/lib.rs";
153        let mismatched_file = MismatchedFile {
154            name: String::from(file),
155            mismatches: vec![MismatchedBlock {
156                original_begin_line: 5,
157                original_end_line: 5,
158                expected_begin_line: 5,
159                expected_end_line: 5,
160                original: String::from(
161                    "fn foo(_x: &u64) -> Option<&(dyn::std::error::Error + 'static)> {\n",
162                ),
163                expected: String::from(
164                    "fn foo(_x: &u64) -> Option<&(dyn ::std::error::Error + 'static)> {\n",
165                ),
166            }],
167        };
168        let mismatch = Mismatch {
169            line_number: 5,
170            line_number_orig: 5,
171            lines: vec![
172                DiffLine::Context(String::new()),
173                DiffLine::Resulting(String::from(
174                    "fn foo(_x: &u64) -> Option<&(dyn::std::error::Error + 'static)> {",
175                )),
176                DiffLine::Context(String::new()),
177                DiffLine::Expected(String::from(
178                    "fn foo(_x: &u64) -> Option<&(dyn ::std::error::Error + 'static)> {",
179                )),
180                DiffLine::Context(String::new()),
181            ],
182        };
183
184        let _ = emitter
185            .add_misformatted_file(&FileName::Real(PathBuf::from(file)), vec![mismatch])
186            .unwrap();
187
188        assert_eq!(emitter.mismatched_files.len(), 1);
189        assert_eq!(emitter.mismatched_files[0], mismatched_file);
190    }
191
192    #[test]
193    fn emits_empty_array_on_no_diffs() {
194        let mut writer = Vec::new();
195        let mut emitter = JsonEmitter::default();
196        let _ = emitter.emit_header(&mut writer);
197        let result = emitter
198            .emit_formatted_file(
199                &mut writer,
200                FormattedFile {
201                    filename: &FileName::Real(PathBuf::from("src/lib.rs")),
202                    original_text: "fn empty() {}\n",
203                    formatted_text: "fn empty() {}\n",
204                },
205            )
206            .unwrap();
207        let _ = emitter.emit_footer(&mut writer);
208        assert_eq!(result.has_diff, false);
209        assert_eq!(&writer[..], "[]\n".as_bytes());
210    }
211
212    #[test]
213    fn emits_array_with_files_with_diffs() {
214        let file_name = "src/bin.rs";
215        let original = [
216            "fn main() {",
217            "println!(\"Hello, world!\");",
218            "}",
219            "",
220            "#[cfg(test)]",
221            "mod tests {",
222            "#[test]",
223            "fn it_works() {",
224            "    assert_eq!(2 + 2, 4);",
225            "}",
226            "}",
227        ];
228        let formatted = [
229            "fn main() {",
230            "    println!(\"Hello, world!\");",
231            "}",
232            "",
233            "#[cfg(test)]",
234            "mod tests {",
235            "    #[test]",
236            "    fn it_works() {",
237            "        assert_eq!(2 + 2, 4);",
238            "    }",
239            "}",
240        ];
241        let mut writer = Vec::new();
242        let mut emitter = JsonEmitter::default();
243        let _ = emitter.emit_header(&mut writer);
244        let result = emitter
245            .emit_formatted_file(
246                &mut writer,
247                FormattedFile {
248                    filename: &FileName::Real(PathBuf::from(file_name)),
249                    original_text: &original.join("\n"),
250                    formatted_text: &formatted.join("\n"),
251                },
252            )
253            .unwrap();
254        let _ = emitter.emit_footer(&mut writer);
255        let exp_json = to_json_string(&vec![MismatchedFile {
256            name: String::from(file_name),
257            mismatches: vec![
258                MismatchedBlock {
259                    original_begin_line: 2,
260                    original_end_line: 2,
261                    expected_begin_line: 2,
262                    expected_end_line: 2,
263                    original: String::from("println!(\"Hello, world!\");\n"),
264                    expected: String::from("    println!(\"Hello, world!\");\n"),
265                },
266                MismatchedBlock {
267                    original_begin_line: 7,
268                    original_end_line: 10,
269                    expected_begin_line: 7,
270                    expected_end_line: 10,
271                    original: String::from(
272                        "#[test]\nfn it_works() {\n    assert_eq!(2 + 2, 4);\n}\n",
273                    ),
274                    expected: String::from(
275                        "    #[test]\n    fn it_works() {\n        assert_eq!(2 + 2, 4);\n    }\n",
276                    ),
277                },
278            ],
279        }])
280        .unwrap();
281        assert_eq!(result.has_diff, true);
282        assert_eq!(&writer[..], format!("{exp_json}\n").as_bytes());
283    }
284
285    #[test]
286    fn emits_valid_json_with_multiple_files() {
287        let bin_file = "src/bin.rs";
288        let bin_original = ["fn main() {", "println!(\"Hello, world!\");", "}"];
289        let bin_formatted = ["fn main() {", "    println!(\"Hello, world!\");", "}"];
290        let lib_file = "src/lib.rs";
291        let lib_original = ["fn greet() {", "println!(\"Greetings!\");", "}"];
292        let lib_formatted = ["fn greet() {", "    println!(\"Greetings!\");", "}"];
293        let mut writer = Vec::new();
294        let mut emitter = JsonEmitter::default();
295        let _ = emitter.emit_header(&mut writer);
296        let _ = emitter
297            .emit_formatted_file(
298                &mut writer,
299                FormattedFile {
300                    filename: &FileName::Real(PathBuf::from(bin_file)),
301                    original_text: &bin_original.join("\n"),
302                    formatted_text: &bin_formatted.join("\n"),
303                },
304            )
305            .unwrap();
306        let _ = emitter
307            .emit_formatted_file(
308                &mut writer,
309                FormattedFile {
310                    filename: &FileName::Real(PathBuf::from(lib_file)),
311                    original_text: &lib_original.join("\n"),
312                    formatted_text: &lib_formatted.join("\n"),
313                },
314            )
315            .unwrap();
316        let _ = emitter.emit_footer(&mut writer);
317        let exp_bin = MismatchedFile {
318            name: String::from(bin_file),
319            mismatches: vec![MismatchedBlock {
320                original_begin_line: 2,
321                original_end_line: 2,
322                expected_begin_line: 2,
323                expected_end_line: 2,
324                original: String::from("println!(\"Hello, world!\");\n"),
325                expected: String::from("    println!(\"Hello, world!\");\n"),
326            }],
327        };
328
329        let exp_lib = MismatchedFile {
330            name: String::from(lib_file),
331            mismatches: vec![MismatchedBlock {
332                original_begin_line: 2,
333                original_end_line: 2,
334                expected_begin_line: 2,
335                expected_end_line: 2,
336                original: String::from("println!(\"Greetings!\");\n"),
337                expected: String::from("    println!(\"Greetings!\");\n"),
338            }],
339        };
340
341        let exp_json = to_json_string(&vec![exp_bin, exp_lib]).unwrap();
342        assert_eq!(&writer[..], format!("{exp_json}\n").as_bytes());
343    }
344}