diff --git a/src/checkstyle.rs b/src/checkstyle.rs deleted file mode 100644 index 5c2b46583b4..00000000000 --- a/src/checkstyle.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::fmt::{self, Display}; -use std::io::{self, Write}; -use std::path::Path; - -use crate::rustfmt_diff::{DiffLine, Mismatch}; - -/// The checkstyle header - should be emitted before the output of Rustfmt. -/// -/// Note that emitting checkstyle output is not stable and may removed in a -/// future version of Rustfmt. -pub(crate) fn header() -> String { - let mut xml_heading = String::new(); - xml_heading.push_str(r#""#); - xml_heading.push_str("\n"); - xml_heading.push_str(r#""#); - xml_heading -} - -/// The checkstyle footer - should be emitted after the output of Rustfmt. -/// -/// Note that emitting checkstyle output is not stable and may removed in a -/// future version of Rustfmt. -pub(crate) fn footer() -> String { - "\n".to_owned() -} - -pub(crate) fn output_checkstyle_file( - mut writer: T, - filename: &Path, - diff: Vec, -) -> Result<(), io::Error> -where - T: Write, -{ - write!(writer, r#""#, filename.display())?; - for mismatch in diff { - for line in mismatch.lines { - // Do nothing with `DiffLine::Context` and `DiffLine::Resulting`. - if let DiffLine::Expected(message) = line { - write!( - writer, - r#""#, - mismatch.line_number, - XmlEscaped(&message) - )?; - } - } - } - write!(writer, "")?; - Ok(()) -} - -/// Convert special characters into XML entities. -/// This is needed for checkstyle output. -struct XmlEscaped<'a>(&'a str); - -impl<'a> Display for XmlEscaped<'a> { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - for char in self.0.chars() { - match char { - '<' => write!(formatter, "<"), - '>' => write!(formatter, ">"), - '"' => write!(formatter, """), - '\'' => write!(formatter, "'"), - '&' => write!(formatter, "&"), - _ => write!(formatter, "{}", char), - }?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn special_characters_are_escaped() { - assert_eq!( - "<>"'&", - format!("{}", XmlEscaped(r#"<>"'&"#)), - ); - } - - #[test] - fn special_characters_are_escaped_in_string_with_other_characters() { - assert_eq!( - "The quick brown "🦊" jumps <over> the lazy 🐶", - format!( - "{}", - XmlEscaped(r#"The quick brown "🦊" jumps the lazy 🐶"#) - ), - ); - } - - #[test] - fn other_characters_are_not_escaped() { - let string = "The quick brown 🦊 jumps over the lazy 🐶"; - assert_eq!(string, format!("{}", XmlEscaped(string))); - } -} diff --git a/src/emitter.rs b/src/emitter.rs new file mode 100644 index 00000000000..03ca1e35b72 --- /dev/null +++ b/src/emitter.rs @@ -0,0 +1,50 @@ +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::modified_lines::*; +pub(crate) use self::stdout::*; +use crate::FileName; +use std::io::{self, Write}; +use std::path::Path; + +mod checkstyle; +mod diff; +mod files; +mod files_with_backup; +mod modified_lines; +mod stdout; + +pub(crate) struct FormattedFile<'a> { + pub(crate) filename: &'a FileName, + pub(crate) original_text: &'a str, + pub(crate) formatted_text: &'a str, +} + +#[derive(Debug, Default, Clone)] +pub(crate) struct EmitterResult { + pub(crate) has_diff: bool, +} + +pub(crate) trait Emitter { + fn emit_formatted_file( + &self, + output: &mut dyn Write, + formatted_file: FormattedFile<'_>, + ) -> Result; + + fn emit_header(&self, _output: &mut dyn Write) -> Result<(), io::Error> { + Ok(()) + } + + fn emit_footer(&self, _output: &mut dyn Write) -> Result<(), io::Error> { + Ok(()) + } +} + +fn ensure_real_path(filename: &FileName) -> &Path { + match *filename { + FileName::Real(ref path) => path, + _ => panic!("cannot format `{}` and emit to files", filename), + } +} diff --git a/src/emitter/checkstyle.rs b/src/emitter/checkstyle.rs new file mode 100644 index 00000000000..eb1499985fd --- /dev/null +++ b/src/emitter/checkstyle.rs @@ -0,0 +1,64 @@ +use self::xml::XmlEscaped; +use super::*; +use crate::rustfmt_diff::{make_diff, DiffLine, Mismatch}; +use std::io::{self, Write}; +use std::path::Path; + +mod xml; + +#[derive(Debug, Default)] +pub(crate) struct CheckstyleEmitter; + +impl Emitter for CheckstyleEmitter { + fn emit_header(&self, output: &mut dyn Write) -> Result<(), io::Error> { + writeln!(output, r#""#)?; + write!(output, r#""#)?; + Ok(()) + } + + fn emit_footer(&self, output: &mut dyn Write) -> Result<(), io::Error> { + writeln!(output, "") + } + + fn emit_formatted_file( + &self, + output: &mut dyn Write, + FormattedFile { + filename, + original_text, + formatted_text, + }: FormattedFile<'_>, + ) -> Result { + const CONTEXT_SIZE: usize = 3; + let filename = ensure_real_path(filename); + let diff = make_diff(original_text, formatted_text, CONTEXT_SIZE); + output_checkstyle_file(output, filename, diff)?; + Ok(EmitterResult::default()) + } +} + +pub(crate) fn output_checkstyle_file( + mut writer: T, + filename: &Path, + diff: Vec, +) -> Result<(), io::Error> +where + T: Write, +{ + write!(writer, r#""#, filename.display())?; + for mismatch in diff { + for line in mismatch.lines { + // Do nothing with `DiffLine::Context` and `DiffLine::Resulting`. + if let DiffLine::Expected(message) = line { + write!( + writer, + r#""#, + mismatch.line_number, + XmlEscaped(&message) + )?; + } + } + } + write!(writer, "")?; + Ok(()) +} diff --git a/src/emitter/checkstyle/xml.rs b/src/emitter/checkstyle/xml.rs new file mode 100644 index 00000000000..f251aabe878 --- /dev/null +++ b/src/emitter/checkstyle/xml.rs @@ -0,0 +1,52 @@ +use std::fmt::{self, Display}; + +/// Convert special characters into XML entities. +/// This is needed for checkstyle output. +pub(super) struct XmlEscaped<'a>(pub(super) &'a str); + +impl<'a> Display for XmlEscaped<'a> { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + for char in self.0.chars() { + match char { + '<' => write!(formatter, "<"), + '>' => write!(formatter, ">"), + '"' => write!(formatter, """), + '\'' => write!(formatter, "'"), + '&' => write!(formatter, "&"), + _ => write!(formatter, "{}", char), + }?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn special_characters_are_escaped() { + assert_eq!( + "<>"'&", + format!("{}", XmlEscaped(r#"<>"'&"#)), + ); + } + + #[test] + fn special_characters_are_escaped_in_string_with_other_characters() { + assert_eq!( + "The quick brown "🦊" jumps <over> the lazy 🐶", + format!( + "{}", + XmlEscaped(r#"The quick brown "🦊" jumps the lazy 🐶"#) + ), + ); + } + + #[test] + fn other_characters_are_not_escaped() { + let string = "The quick brown 🦊 jumps over the lazy 🐶"; + assert_eq!(string, format!("{}", XmlEscaped(string))); + } +} diff --git a/src/emitter/diff.rs b/src/emitter/diff.rs new file mode 100644 index 00000000000..5e387395b5b --- /dev/null +++ b/src/emitter/diff.rs @@ -0,0 +1,35 @@ +use super::*; +use crate::config::Config; +use crate::rustfmt_diff::{make_diff, print_diff}; + +pub(crate) struct DiffEmitter { + config: Config, +} + +impl DiffEmitter { + pub(crate) fn new(config: Config) -> Self { + Self { config } + } +} + +impl Emitter for DiffEmitter { + fn emit_formatted_file( + &self, + _output: &mut dyn Write, + FormattedFile { + filename, + original_text, + formatted_text, + }: FormattedFile<'_>, + ) -> Result { + const CONTEXT_SIZE: usize = 3; + let mismatch = make_diff(&original_text, formatted_text, CONTEXT_SIZE); + let has_diff = !mismatch.is_empty(); + print_diff( + mismatch, + |line_num| format!("Diff in {} at line {}:", filename, line_num), + &self.config, + ); + return Ok(EmitterResult { has_diff }); + } +} diff --git a/src/emitter/files.rs b/src/emitter/files.rs new file mode 100644 index 00000000000..5b1dbce11c9 --- /dev/null +++ b/src/emitter/files.rs @@ -0,0 +1,24 @@ +use super::*; +use std::fs; + +#[derive(Debug, Default)] +pub(crate) struct FilesEmitter; + +impl Emitter for FilesEmitter { + fn emit_formatted_file( + &self, + _output: &mut dyn Write, + FormattedFile { + filename, + original_text, + formatted_text, + }: FormattedFile<'_>, + ) -> Result { + // Write text directly over original file if there is a diff. + let filename = ensure_real_path(filename); + if original_text != formatted_text { + fs::write(filename, formatted_text)?; + } + Ok(EmitterResult::default()) + } +} diff --git a/src/emitter/files_with_backup.rs b/src/emitter/files_with_backup.rs new file mode 100644 index 00000000000..af3e0e2d91d --- /dev/null +++ b/src/emitter/files_with_backup.rs @@ -0,0 +1,31 @@ +use super::*; +use std::fs; + +#[derive(Debug, Default)] +pub(crate) struct FilesWithBackupEmitter; + +impl Emitter for FilesWithBackupEmitter { + fn emit_formatted_file( + &self, + _output: &mut dyn Write, + FormattedFile { + filename, + original_text, + formatted_text, + }: FormattedFile<'_>, + ) -> Result { + let filename = ensure_real_path(filename); + if original_text != formatted_text { + // Do a little dance to make writing safer - write to a temp file + // rename the original to a .bk, then rename the temp file to the + // original. + let tmp_name = filename.with_extension("tmp"); + let bk_name = filename.with_extension("bk"); + + fs::write(&tmp_name, formatted_text)?; + fs::rename(filename, bk_name)?; + fs::rename(tmp_name, filename)?; + } + Ok(EmitterResult::default()) + } +} diff --git a/src/emitter/modified_lines.rs b/src/emitter/modified_lines.rs new file mode 100644 index 00000000000..83736c47bd6 --- /dev/null +++ b/src/emitter/modified_lines.rs @@ -0,0 +1,24 @@ +use super::*; +use crate::rustfmt_diff::{make_diff, ModifiedLines}; +use std::io::Write; + +#[derive(Debug, Default)] +pub(crate) struct ModifiedLinesEmitter; + +impl Emitter for ModifiedLinesEmitter { + fn emit_formatted_file( + &self, + output: &mut dyn Write, + FormattedFile { + original_text, + formatted_text, + .. + }: FormattedFile<'_>, + ) -> Result { + const CONTEXT_SIZE: usize = 0; + let mismatch = make_diff(original_text, formatted_text, CONTEXT_SIZE); + let has_diff = !mismatch.is_empty(); + write!(output, "{}", ModifiedLines::from(mismatch))?; + Ok(EmitterResult { has_diff }) + } +} diff --git a/src/emitter/stdout.rs b/src/emitter/stdout.rs new file mode 100644 index 00000000000..968de68c741 --- /dev/null +++ b/src/emitter/stdout.rs @@ -0,0 +1,32 @@ +use super::*; +use crate::config::Verbosity; +use std::io::Write; + +#[derive(Debug)] +pub(crate) struct StdoutEmitter { + verbosity: Verbosity, +} + +impl StdoutEmitter { + pub(crate) fn new(verbosity: Verbosity) -> Self { + Self { verbosity } + } +} + +impl Emitter for StdoutEmitter { + fn emit_formatted_file( + &self, + output: &mut dyn Write, + FormattedFile { + filename, + formatted_text, + .. + }: FormattedFile<'_>, + ) -> Result { + if self.verbosity != Verbosity::Quiet { + writeln!(output, "{}:\n", filename)?; + } + write!(output, "{}", formatted_text)?; + Ok(EmitterResult::default()) + } +} diff --git a/src/formatting.rs b/src/formatting.rs index d431dd2679c..522de400840 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -233,8 +233,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.config) { - Ok(has_diff) if has_diff => report.add_diff(), + match source_file::write_file(Some(source_map), &path, &result, out, &*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 let err_msg = format!("{}: {}", path, e); diff --git a/src/lib.rs b/src/lib.rs index 88605079b7e..14e2a6fe6e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ use ignore; use syntax::{ast, parse::DirectoryOwnership}; use crate::comment::LineClasses; +use crate::emitter::Emitter; use crate::formatting::{FormatErrorMap, FormattingError, ReportedErrors, SourceFile}; use crate::issues::Issue; use crate::shape::Indent; @@ -45,10 +46,10 @@ mod release_channel; mod attr; mod chains; -pub(crate) mod checkstyle; mod closures; mod comment; pub(crate) mod config; +mod emitter; mod expr; mod format_report_formatter; pub(crate) mod formatting; @@ -403,17 +404,21 @@ pub struct Session<'b, T: Write> { pub out: Option<&'b mut T>, pub(crate) errors: ReportedErrors, source_file: SourceFile, + emitter: Box, } impl<'b, T: Write + 'b> Session<'b, T> { - pub fn new(config: Config, out: Option<&'b mut T>) -> Session<'b, T> { - if config.emit_mode() == EmitMode::Checkstyle { - println!("{}", checkstyle::header()); + pub fn new(config: Config, mut out: Option<&'b mut T>) -> Session<'b, T> { + let emitter = create_emitter(&config); + + if let Some(ref mut out) = out { + let _ = emitter.emit_header(out); } Session { config, out, + emitter, errors: ReportedErrors::default(), source_file: SourceFile::new(), } @@ -469,10 +474,25 @@ impl<'b, T: Write + 'b> Session<'b, T> { } } +pub(crate) fn create_emitter<'a>(config: &Config) -> Box { + match config.emit_mode() { + EmitMode::Files if config.make_backup() => { + Box::new(emitter::FilesWithBackupEmitter::default()) + } + EmitMode::Files => Box::new(emitter::FilesEmitter::default()), + EmitMode::Stdout | EmitMode::Coverage => { + Box::new(emitter::StdoutEmitter::new(config.verbose())) + } + EmitMode::ModifiedLines => Box::new(emitter::ModifiedLinesEmitter::default()), + EmitMode::Checkstyle => Box::new(emitter::CheckstyleEmitter::default()), + EmitMode::Diff => Box::new(emitter::DiffEmitter::new(config.clone())), + } +} + impl<'b, T: Write + 'b> Drop for Session<'b, T> { fn drop(&mut self) { - if self.config.emit_mode() == EmitMode::Checkstyle { - println!("{}", checkstyle::footer()); + if let Some(ref mut out) = self.out { + let _ = self.emitter.emit_footer(out); } } } diff --git a/src/source_file.rs b/src/source_file.rs index a7e0074cd2f..074b7a7315f 100644 --- a/src/source_file.rs +++ b/src/source_file.rs @@ -4,10 +4,13 @@ use std::path::Path; use syntax::source_map::SourceMap; -use crate::checkstyle::output_checkstyle_file; -use crate::config::{Config, EmitMode, FileName, Verbosity}; -use crate::rustfmt_diff::{make_diff, print_diff, ModifiedLines}; +use crate::config::FileName; +use crate::emitter::{self, Emitter}; +#[cfg(test)] +use crate::config::Config; +#[cfg(test)] +use crate::create_emitter; #[cfg(test)] use crate::formatting::FileRecord; @@ -25,15 +28,13 @@ pub(crate) fn write_all_files( where T: Write, { - if config.emit_mode() == EmitMode::Checkstyle { - write!(out, "{}", crate::checkstyle::header())?; - } + let emitter = create_emitter(config); + + emitter.emit_header(out)?; for &(ref filename, ref text) in source_file { - write_file(None, filename, text, out, config)?; - } - if config.emit_mode() == EmitMode::Checkstyle { - write!(out, "{}", crate::checkstyle::footer())?; + write_file(None, filename, text, out, &*emitter)?; } + emitter.emit_footer(out)?; Ok(()) } @@ -43,8 +44,8 @@ pub(crate) fn write_file( filename: &FileName, formatted_text: &str, out: &mut T, - config: &Config, -) -> Result + emitter: &dyn Emitter, +) -> Result where T: Write, { @@ -75,59 +76,11 @@ where None => fs::read_to_string(ensure_real_path(filename))?, }; - match config.emit_mode() { - EmitMode::Files if config.make_backup() => { - let filename = ensure_real_path(filename); - if original_text != formatted_text { - // Do a little dance to make writing safer - write to a temp file - // rename the original to a .bk, then rename the temp file to the - // original. - let tmp_name = filename.with_extension("tmp"); - let bk_name = filename.with_extension("bk"); + let formatted_file = emitter::FormattedFile { + filename, + original_text: &original_text, + formatted_text, + }; - fs::write(&tmp_name, formatted_text)?; - fs::rename(filename, bk_name)?; - fs::rename(tmp_name, filename)?; - } - } - EmitMode::Files => { - // Write text directly over original file if there is a diff. - let filename = ensure_real_path(filename); - - if original_text != formatted_text { - fs::write(filename, formatted_text)?; - } - } - EmitMode::Stdout | EmitMode::Coverage => { - if config.verbose() != Verbosity::Quiet { - println!("{}:\n", filename); - } - write!(out, "{}", formatted_text)?; - } - EmitMode::ModifiedLines => { - let mismatch = make_diff(&original_text, formatted_text, 0); - let has_diff = !mismatch.is_empty(); - write!(out, "{}", ModifiedLines::from(mismatch))?; - return Ok(has_diff); - } - EmitMode::Checkstyle => { - let filename = ensure_real_path(filename); - - let diff = make_diff(&original_text, formatted_text, 3); - output_checkstyle_file(out, filename, diff)?; - } - EmitMode::Diff => { - let mismatch = make_diff(&original_text, formatted_text, 3); - let has_diff = !mismatch.is_empty(); - print_diff( - mismatch, - |line_num| format!("Diff in {} at line {}:", filename, line_num), - config, - ); - return Ok(has_diff); - } - } - - // when we are not in diff mode, don't indicate differing files - Ok(false) + emitter.emit_formatted_file(out, formatted_file) } diff --git a/src/test/mod.rs b/src/test/mod.rs index 96f6f2ce9ba..2830f9aeff1 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -9,7 +9,7 @@ use std::process::{Command, Stdio}; use std::str::Chars; use std::thread; -use crate::config::{Color, Config, EmitMode, FileName, NewlineStyle, ReportTactic}; +use crate::config::{Color, Config, EmitMode, FileName, NewlineStyle, ReportTactic, Verbosity}; use crate::formatting::{ReportedErrors, SourceFile}; use crate::is_nightly_channel; use crate::rustfmt_diff::{make_diff, print_diff, DiffLine, Mismatch, ModifiedChunk, OutputWriter}; @@ -344,9 +344,9 @@ fn stdin_formatting_smoke_test() { } #[cfg(not(windows))] - assert_eq!(buf, "fn main() {}\n".as_bytes()); + assert_eq!(buf, "stdin:\n\nfn main() {}\n".as_bytes()); #[cfg(windows)] - assert_eq!(buf, "fn main() {}\r\n".as_bytes()); + assert_eq!(buf, "stdin:\n\nfn main() {}\r\n".as_bytes()); } #[test] @@ -838,6 +838,7 @@ impl ConfigCodeBlock { fn get_block_config(&self) -> Config { let mut config = Config::default(); + config.set().verbose(Verbosity::Quiet); if self.config_name.is_some() && self.config_value.is_some() { config.override_value( self.config_name.as_ref().unwrap(),