diff --git a/README.md b/README.md index 142e1f1d272..7a97d31bab9 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,7 @@ needs to be specified in `rustfmt.toml`, e.g., with `edition = "2018"`. | stdout | writes output to stdout | No | | coverage | displays how much of the input file was processed | Yes | | checkstyle | emits in a checkstyle format | Yes | + | json | emits diffs in a json format | Yes | ## License diff --git a/src/bin/main.rs b/src/bin/main.rs index 70990aa798d..9d3be60ac8f 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -98,7 +98,7 @@ fn make_opts() -> Options { ); let is_nightly = is_nightly(); let emit_opts = if is_nightly { - "[files|stdout|coverage|checkstyle]" + "[files|stdout|coverage|checkstyle|json]" } else { "[files|stdout]" }; @@ -631,6 +631,7 @@ fn emit_mode_from_emit_str(emit_str: &str) -> Result { "stdout" => Ok(EmitMode::Stdout), "coverage" => Ok(EmitMode::Coverage), "checkstyle" => Ok(EmitMode::Checkstyle), + "json" => Ok(EmitMode::Json), _ => Err(format_err!("Invalid value for `--emit`")), } } diff --git a/src/config/options.rs b/src/config/options.rs index cf1a328e9cd..d2063650ccb 100644 --- a/src/config/options.rs +++ b/src/config/options.rs @@ -119,6 +119,9 @@ pub enum EmitMode { Coverage, /// Unfancy stdout Checkstyle, + /// Writes the resulting diffs in a JSON format. Returns an empty array + /// `[]` if there were no diffs. + Json, /// Output the changed lines (for internal value only) ModifiedLines, /// Checks if a diff can be generated. If so, rustfmt outputs a diff and diff --git a/src/emitter.rs b/src/emitter.rs index 03ca1e35b72..dc2c99a301e 100644 --- a/src/emitter.rs +++ b/src/emitter.rs @@ -2,6 +2,7 @@ pub(crate) use self::checkstyle::*; pub(crate) use self::diff::*; pub(crate) use self::files::*; pub(crate) use self::files_with_backup::*; +pub(crate) use self::json::*; pub(crate) use self::modified_lines::*; pub(crate) use self::stdout::*; use crate::FileName; @@ -12,6 +13,7 @@ mod checkstyle; mod diff; mod files; mod files_with_backup; +mod json; mod modified_lines; mod stdout; @@ -28,7 +30,7 @@ pub(crate) struct EmitterResult { pub(crate) trait Emitter { fn emit_formatted_file( - &self, + &mut self, output: &mut dyn Write, formatted_file: FormattedFile<'_>, ) -> Result; diff --git a/src/emitter/checkstyle.rs b/src/emitter/checkstyle.rs index eb1499985fd..e8f8ec86cf1 100644 --- a/src/emitter/checkstyle.rs +++ b/src/emitter/checkstyle.rs @@ -21,7 +21,7 @@ impl Emitter for CheckstyleEmitter { } fn emit_formatted_file( - &self, + &mut self, output: &mut dyn Write, FormattedFile { filename, diff --git a/src/emitter/diff.rs b/src/emitter/diff.rs index 5e387395b5b..08a42b4679d 100644 --- a/src/emitter/diff.rs +++ b/src/emitter/diff.rs @@ -14,7 +14,7 @@ impl DiffEmitter { impl Emitter for DiffEmitter { fn emit_formatted_file( - &self, + &mut self, _output: &mut dyn Write, FormattedFile { filename, diff --git a/src/emitter/files.rs b/src/emitter/files.rs index 5b1dbce11c9..4538a8f4c49 100644 --- a/src/emitter/files.rs +++ b/src/emitter/files.rs @@ -6,7 +6,7 @@ pub(crate) struct FilesEmitter; impl Emitter for FilesEmitter { fn emit_formatted_file( - &self, + &mut self, _output: &mut dyn Write, FormattedFile { filename, diff --git a/src/emitter/files_with_backup.rs b/src/emitter/files_with_backup.rs index af3e0e2d91d..4c15f6fa5ec 100644 --- a/src/emitter/files_with_backup.rs +++ b/src/emitter/files_with_backup.rs @@ -6,7 +6,7 @@ pub(crate) struct FilesWithBackupEmitter; impl Emitter for FilesWithBackupEmitter { fn emit_formatted_file( - &self, + &mut self, _output: &mut dyn Write, FormattedFile { filename, diff --git a/src/emitter/json.rs b/src/emitter/json.rs new file mode 100644 index 00000000000..269dd2d4daf --- /dev/null +++ b/src/emitter/json.rs @@ -0,0 +1,349 @@ +use super::*; +use crate::rustfmt_diff::{make_diff, DiffLine, Mismatch}; +use serde::Serialize; +use serde_json::to_string as to_json_string; +use std::io::{self, Write}; +use std::path::Path; + +#[derive(Debug, Default)] +pub(crate) struct JsonEmitter { + num_files: u32, +} + +#[derive(Debug, Default, Serialize)] +struct MismatchedBlock { + original_begin_line: u32, + original_end_line: u32, + expected_begin_line: u32, + expected_end_line: u32, + original: String, + expected: String, +} + +#[derive(Debug, Default, Serialize)] +struct MismatchedFile { + name: String, + mismatches: Vec, +} + +impl Emitter for JsonEmitter { + fn emit_header(&self, output: &mut dyn Write) -> Result<(), io::Error> { + write!(output, "[")?; + Ok(()) + } + + fn emit_footer(&self, output: &mut dyn Write) -> Result<(), io::Error> { + write!(output, "]")?; + Ok(()) + } + + fn emit_formatted_file( + &mut self, + output: &mut dyn Write, + FormattedFile { + filename, + original_text, + formatted_text, + }: FormattedFile<'_>, + ) -> Result { + const CONTEXT_SIZE: usize = 0; + let filename = ensure_real_path(filename); + let diff = make_diff(original_text, formatted_text, CONTEXT_SIZE); + let has_diff = !diff.is_empty(); + + if has_diff { + output_json_file(output, filename, diff, self.num_files)?; + self.num_files += 1; + } + + Ok(EmitterResult { has_diff }) + } +} + +fn output_json_file( + mut writer: T, + filename: &Path, + diff: Vec, + num_emitted_files: u32, +) -> Result<(), io::Error> +where + T: Write, +{ + let mut mismatches = vec![]; + for mismatch in diff { + let original_begin_line = mismatch.line_number_orig; + let expected_begin_line = mismatch.line_number; + let mut original_end_line = original_begin_line; + let mut expected_end_line = expected_begin_line; + let mut original_line_counter = 0; + let mut expected_line_counter = 0; + let mut original_lines = vec![]; + let mut expected_lines = vec![]; + + for line in mismatch.lines { + match line { + DiffLine::Expected(msg) => { + expected_end_line = expected_begin_line + expected_line_counter; + expected_line_counter += 1; + expected_lines.push(msg) + } + DiffLine::Resulting(msg) => { + original_end_line = original_begin_line + original_line_counter; + original_line_counter += 1; + original_lines.push(msg) + } + DiffLine::Context(_) => continue, + } + } + + mismatches.push(MismatchedBlock { + original_begin_line, + original_end_line, + expected_begin_line, + expected_end_line, + original: original_lines.join("\n"), + expected: expected_lines.join("\n"), + }); + } + let json = to_json_string(&MismatchedFile { + name: String::from(filename.to_str().unwrap()), + mismatches, + })?; + let prefix = if num_emitted_files > 0 { "," } else { "" }; + write!(writer, "{}{}", prefix, &json)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::FileName; + use std::path::PathBuf; + + #[test] + fn expected_line_range_correct_when_single_line_split() { + let file = "foo/bar.rs"; + let mismatched_file = MismatchedFile { + name: String::from(file), + mismatches: vec![MismatchedBlock { + original_begin_line: 79, + original_end_line: 79, + expected_begin_line: 79, + expected_end_line: 82, + original: String::from("fn Foo() where T: Bar {"), + expected: String::from("fn Foo()\nwhere\n T: Bar,\n{"), + }], + }; + let mismatch = Mismatch { + line_number: 79, + line_number_orig: 79, + lines: vec![ + DiffLine::Resulting(String::from("fn Foo() where T: Bar {")), + DiffLine::Expected(String::from("fn Foo()")), + DiffLine::Expected(String::from("where")), + DiffLine::Expected(String::from(" T: Bar,")), + DiffLine::Expected(String::from("{")), + ], + }; + + let mut writer = Vec::new(); + let exp_json = to_json_string(&mismatched_file).unwrap(); + let _ = output_json_file(&mut writer, &PathBuf::from(file), vec![mismatch], 0); + assert_eq!(&writer[..], format!("{}", exp_json).as_bytes()); + } + + #[test] + fn context_lines_ignored() { + let file = "src/lib.rs"; + let mismatched_file = MismatchedFile { + name: String::from(file), + mismatches: vec![MismatchedBlock { + original_begin_line: 5, + original_end_line: 5, + expected_begin_line: 5, + expected_end_line: 5, + original: String::from( + "fn foo(_x: &u64) -> Option<&(dyn::std::error::Error + 'static)> {", + ), + expected: String::from( + "fn foo(_x: &u64) -> Option<&(dyn ::std::error::Error + 'static)> {", + ), + }], + }; + let mismatch = Mismatch { + line_number: 5, + line_number_orig: 5, + lines: vec![ + DiffLine::Context(String::new()), + DiffLine::Resulting(String::from( + "fn foo(_x: &u64) -> Option<&(dyn::std::error::Error + 'static)> {", + )), + DiffLine::Context(String::new()), + DiffLine::Expected(String::from( + "fn foo(_x: &u64) -> Option<&(dyn ::std::error::Error + 'static)> {", + )), + DiffLine::Context(String::new()), + ], + }; + + let mut writer = Vec::new(); + let exp_json = to_json_string(&mismatched_file).unwrap(); + let _ = output_json_file(&mut writer, &PathBuf::from(file), vec![mismatch], 0); + assert_eq!(&writer[..], format!("{}", exp_json).as_bytes()); + } + + #[test] + fn emits_empty_array_on_no_diffs() { + let mut writer = Vec::new(); + let mut emitter = JsonEmitter::default(); + let _ = emitter.emit_header(&mut writer); + let result = emitter + .emit_formatted_file( + &mut writer, + FormattedFile { + filename: &FileName::Real(PathBuf::from("src/lib.rs")), + original_text: "fn empty() {}\n", + formatted_text: "fn empty() {}\n", + }, + ) + .unwrap(); + let _ = emitter.emit_footer(&mut writer); + assert_eq!(result.has_diff, false); + assert_eq!(&writer[..], "[]".as_bytes()); + } + + #[test] + fn emits_array_with_files_with_diffs() { + let file_name = "src/bin.rs"; + let original = vec![ + "fn main() {", + "println!(\"Hello, world!\");", + "}", + "", + "#[cfg(test)]", + "mod tests {", + "#[test]", + "fn it_works() {", + " assert_eq!(2 + 2, 4);", + "}", + "}", + ]; + let formatted = vec![ + "fn main() {", + " println!(\"Hello, world!\");", + "}", + "", + "#[cfg(test)]", + "mod tests {", + " #[test]", + " fn it_works() {", + " assert_eq!(2 + 2, 4);", + " }", + "}", + ]; + let mut writer = Vec::new(); + let mut emitter = JsonEmitter::default(); + let _ = emitter.emit_header(&mut writer); + let result = emitter + .emit_formatted_file( + &mut writer, + FormattedFile { + filename: &FileName::Real(PathBuf::from(file_name)), + original_text: &original.join("\n"), + formatted_text: &formatted.join("\n"), + }, + ) + .unwrap(); + let _ = emitter.emit_footer(&mut writer); + let exp_json = to_json_string(&MismatchedFile { + name: String::from(file_name), + mismatches: vec![ + MismatchedBlock { + original_begin_line: 2, + original_end_line: 2, + expected_begin_line: 2, + expected_end_line: 2, + original: String::from("println!(\"Hello, world!\");"), + expected: String::from(" println!(\"Hello, world!\");"), + }, + MismatchedBlock { + original_begin_line: 7, + original_end_line: 10, + expected_begin_line: 7, + expected_end_line: 10, + original: String::from( + "#[test]\nfn it_works() {\n assert_eq!(2 + 2, 4);\n}", + ), + expected: String::from( + " #[test]\n fn it_works() {\n assert_eq!(2 + 2, 4);\n }", + ), + }, + ], + }) + .unwrap(); + assert_eq!(result.has_diff, true); + assert_eq!(&writer[..], format!("[{}]", exp_json).as_bytes()); + } + + #[test] + fn emits_valid_json_with_multiple_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 = JsonEmitter::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_json = to_json_string(&MismatchedFile { + name: String::from(bin_file), + mismatches: vec![MismatchedBlock { + original_begin_line: 2, + original_end_line: 2, + expected_begin_line: 2, + expected_end_line: 2, + original: String::from("println!(\"Hello, world!\");"), + expected: String::from(" println!(\"Hello, world!\");"), + }], + }) + .unwrap(); + let exp_lib_json = to_json_string(&MismatchedFile { + name: String::from(lib_file), + mismatches: vec![MismatchedBlock { + original_begin_line: 2, + original_end_line: 2, + expected_begin_line: 2, + expected_end_line: 2, + original: String::from("println!(\"Greetings!\");"), + expected: String::from(" println!(\"Greetings!\");"), + }], + }) + .unwrap(); + assert_eq!( + &writer[..], + format!("[{},{}]", exp_bin_json, exp_lib_json).as_bytes() + ); + } +} diff --git a/src/emitter/modified_lines.rs b/src/emitter/modified_lines.rs index 83736c47bd6..94ff570a8a9 100644 --- a/src/emitter/modified_lines.rs +++ b/src/emitter/modified_lines.rs @@ -7,7 +7,7 @@ pub(crate) struct ModifiedLinesEmitter; impl Emitter for ModifiedLinesEmitter { fn emit_formatted_file( - &self, + &mut self, output: &mut dyn Write, FormattedFile { original_text, diff --git a/src/emitter/stdout.rs b/src/emitter/stdout.rs index 968de68c741..9fddd515e49 100644 --- a/src/emitter/stdout.rs +++ b/src/emitter/stdout.rs @@ -15,7 +15,7 @@ impl StdoutEmitter { impl Emitter for StdoutEmitter { fn emit_formatted_file( - &self, + &mut self, output: &mut dyn Write, FormattedFile { filename, diff --git a/src/formatting.rs b/src/formatting.rs index 879ddc61b13..ee44549b2bc 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -235,7 +235,8 @@ impl<'b, T: Write + 'b> FormatHandler for Session<'b, T> { report: &mut FormatReport, ) -> Result<(), ErrorKind> { if let Some(ref mut out) = self.out { - match source_file::write_file(Some(source_map), &path, &result, out, &*self.emitter) { + match source_file::write_file(Some(source_map), &path, &result, out, &mut *self.emitter) + { Ok(ref result) if result.has_diff => report.add_diff(), Err(e) => { // Create a new error with path_str to help users see which files failed diff --git a/src/lib.rs b/src/lib.rs index e3e7f7f9244..dbb7d2e0e17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -486,6 +486,7 @@ pub(crate) fn create_emitter<'a>(config: &Config) -> Box { EmitMode::Stdout | EmitMode::Coverage => { Box::new(emitter::StdoutEmitter::new(config.verbose())) } + EmitMode::Json => Box::new(emitter::JsonEmitter::default()), EmitMode::ModifiedLines => Box::new(emitter::ModifiedLinesEmitter::default()), EmitMode::Checkstyle => Box::new(emitter::CheckstyleEmitter::default()), EmitMode::Diff => Box::new(emitter::DiffEmitter::new(config.clone())), diff --git a/src/source_file.rs b/src/source_file.rs index 074b7a7315f..28366ca2485 100644 --- a/src/source_file.rs +++ b/src/source_file.rs @@ -28,11 +28,11 @@ pub(crate) fn write_all_files( where T: Write, { - let emitter = create_emitter(config); + let mut emitter = create_emitter(config); emitter.emit_header(out)?; for &(ref filename, ref text) in source_file { - write_file(None, filename, text, out, &*emitter)?; + write_file(None, filename, text, out, &mut *emitter)?; } emitter.emit_footer(out)?; @@ -44,7 +44,7 @@ pub(crate) fn write_file( filename: &FileName, formatted_text: &str, out: &mut T, - emitter: &dyn Emitter, + emitter: &mut dyn Emitter, ) -> Result where T: Write, diff --git a/src/test/mod.rs b/src/test/mod.rs index a7063c76f28..bd6613e1488 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -200,6 +200,13 @@ fn checkstyle_test() { assert_output(Path::new(filename), Path::new(expected_filename)); } +#[test] +fn json_test() { + let filename = "tests/writemode/source/json.rs"; + let expected_filename = "tests/writemode/target/output.json"; + assert_output(Path::new(filename), Path::new(expected_filename)); +} + #[test] fn modified_test() { use std::io::BufRead; diff --git a/tests/writemode/source/json.rs b/tests/writemode/source/json.rs new file mode 100644 index 00000000000..89dcf694183 --- /dev/null +++ b/tests/writemode/source/json.rs @@ -0,0 +1,80 @@ +// rustfmt-fn_single_line: true +// rustfmt-emit_mode: json +// Test single-line functions. + +fn foo_expr() { + 1 +} + +fn foo_stmt() { + foo(); +} + +fn foo_decl_local() { + let z = 5; + } + +fn foo_decl_item(x: &mut i32) { + x = 3; +} + + fn empty() { + +} + +fn foo_return() -> String { + "yay" +} + +fn foo_where() -> T where T: Sync { + let x = 2; +} + +fn fooblock() { + { + "inner-block" + } +} + +fn fooblock2(x: i32) { + let z = match x { + _ => 2, + }; +} + +fn comment() { + // this is a test comment + 1 +} + +fn comment2() { + // multi-line comment + let z = 2; + 1 +} + +fn only_comment() { + // Keep this here +} + +fn aaaaaaaaaaaaaaaaa_looooooooooooooooooooooong_name() { + let z = "aaaaaaawwwwwwwwwwwwwwwwwwwwwwwwwwww"; +} + +fn lots_of_space () { + 1 +} + +fn mac() -> Vec { vec![] } + +trait CoolTypes { + fn dummy(&self) { + } +} + +trait CoolerTypes { fn dummy(&self) { +} +} + +fn Foo() where T: Bar { +} diff --git a/tests/writemode/target/output.json b/tests/writemode/target/output.json new file mode 100644 index 00000000000..b5f327b0a1c --- /dev/null +++ b/tests/writemode/target/output.json @@ -0,0 +1 @@ +[{"name":"tests/writemode/source/json.rs","mismatches":[{"original_begin_line":5,"original_end_line":7,"expected_begin_line":5,"expected_end_line":5,"original":"fn foo_expr() {\n 1\n}","expected":"fn foo_expr() { 1 }"},{"original_begin_line":9,"original_end_line":11,"expected_begin_line":7,"expected_end_line":7,"original":"fn foo_stmt() {\n foo();\n}","expected":"fn foo_stmt() { foo(); }"},{"original_begin_line":13,"original_end_line":15,"expected_begin_line":9,"expected_end_line":9,"original":"fn foo_decl_local() {\n let z = 5;\n }","expected":"fn foo_decl_local() { let z = 5; }"},{"original_begin_line":17,"original_end_line":19,"expected_begin_line":11,"expected_end_line":11,"original":"fn foo_decl_item(x: &mut i32) {\n x = 3;\n}","expected":"fn foo_decl_item(x: &mut i32) { x = 3; }"},{"original_begin_line":21,"original_end_line":21,"expected_begin_line":13,"expected_end_line":13,"original":" fn empty() {","expected":"fn empty() {}"},{"original_begin_line":23,"original_end_line":23,"expected_begin_line":15,"expected_end_line":15,"original":"}","expected":"fn foo_return() -> String { \"yay\" }"},{"original_begin_line":25,"original_end_line":29,"expected_begin_line":17,"expected_end_line":20,"original":"fn foo_return() -> String {\n \"yay\"\n}\n\nfn foo_where() -> T where T: Sync {","expected":"fn foo_where() -> T\nwhere\n T: Sync,\n{"},{"original_begin_line":64,"original_end_line":66,"expected_begin_line":55,"expected_end_line":55,"original":"fn lots_of_space () {\n 1 \n}","expected":"fn lots_of_space() { 1 }"},{"original_begin_line":71,"original_end_line":72,"expected_begin_line":60,"expected_end_line":60,"original":" fn dummy(&self) {\n }","expected":" fn dummy(&self) {}"},{"original_begin_line":75,"original_end_line":75,"expected_begin_line":63,"expected_end_line":64,"original":"trait CoolerTypes { fn dummy(&self) { ","expected":"trait CoolerTypes {\n fn dummy(&self) {}"},{"original_begin_line":77,"original_end_line":77,"expected_begin_line":66,"expected_end_line":66,"original":"}","expected":""},{"original_begin_line":79,"original_end_line":79,"expected_begin_line":67,"expected_end_line":70,"original":"fn Foo() where T: Bar {","expected":"fn Foo()\nwhere\n T: Bar,\n{"}]}] \ No newline at end of file