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