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}