about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorCaleb Cartwright <calebcartwright@users.noreply.github.com>2019-08-15 21:14:53 -0500
committerSeiichi Uchida <seuchida@gmail.com>2019-08-16 11:14:53 +0900
commitdfd27663dca6b9e0e84c04a4d7319ccf8a879909 (patch)
treedd2146866cd634422e1ec703556dd582c4ed6598 /src
parent541d9a865989459337131f1e130e7d4732acd47f (diff)
add json emit mode (#3735)
Diffstat (limited to 'src')
-rw-r--r--src/bin/main.rs3
-rw-r--r--src/config/options.rs3
-rw-r--r--src/emitter.rs4
-rw-r--r--src/emitter/checkstyle.rs2
-rw-r--r--src/emitter/diff.rs2
-rw-r--r--src/emitter/files.rs2
-rw-r--r--src/emitter/files_with_backup.rs2
-rw-r--r--src/emitter/json.rs349
-rw-r--r--src/emitter/modified_lines.rs2
-rw-r--r--src/emitter/stdout.rs2
-rw-r--r--src/formatting.rs3
-rw-r--r--src/lib.rs1
-rw-r--r--src/source_file.rs6
-rw-r--r--src/test/mod.rs7
14 files changed, 376 insertions, 12 deletions
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<EmitMode, FailureError> {
         "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<EmitterResult, io::Error>;
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<MismatchedBlock>,
+}
+
+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<EmitterResult, io::Error> {
+        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<T>(
+    mut writer: T,
+    filename: &Path,
+    diff: Vec<Mismatch>,
+    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<T>() where T: Bar {"),
+                expected: String::from("fn Foo<T>()\nwhere\n    T: Bar,\n{"),
+            }],
+        };
+        let mismatch = Mismatch {
+            line_number: 79,
+            line_number_orig: 79,
+            lines: vec![
+                DiffLine::Resulting(String::from("fn Foo<T>() where T: Bar {")),
+                DiffLine::Expected(String::from("fn Foo<T>()")),
+                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<dyn Emitter + 'a> {
         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<T>(
 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<T>(
     filename: &FileName,
     formatted_text: &str,
     out: &mut T,
-    emitter: &dyn Emitter,
+    emitter: &mut dyn Emitter,
 ) -> Result<emitter::EmitterResult, io::Error>
 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
@@ -201,6 +201,13 @@ fn checkstyle_test() {
 }
 
 #[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;