diff --git a/Cargo.lock b/Cargo.lock index 7465a4f9521..0aaae84b5eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -547,6 +547,7 @@ dependencies = [ "serde", "serde_json", "term", + "termcolor", "thiserror", "toml", "tracing", @@ -669,6 +670,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.40" diff --git a/Cargo.toml b/Cargo.toml index bcd3b420acb..bd11dd22225 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ regex = "1.7" serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0" term = "0.7" +termcolor = "1.2.0" thiserror = "1.0.40" toml = "0.7.4" tracing = "0.1.37" diff --git a/src/attr.rs b/src/attr.rs index 4d83547d664..ebe32c97de6 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -147,7 +147,7 @@ fn format_derive( .tactic(tactic) .trailing_separator(trailing_separator) .ends_with_newline(false); - let item_str = write_list(&all_items, &fmt)?; + let item_str = write_list(&all_items, &fmt, context.printer)?; debug!("item_str: '{}'", item_str); @@ -235,6 +235,7 @@ fn rewrite_initial_doc_comments( &snippet, shape.comment(context.config), context.config, + context.printer, )?), )); } @@ -318,7 +319,12 @@ impl Rewrite for ast::Attribute { fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option { let snippet = context.snippet(self.span); if self.is_doc_comment() { - rewrite_doc_comment(snippet, shape.comment(context.config), context.config) + rewrite_doc_comment( + snippet, + shape.comment(context.config), + context.config, + context.printer, + ) } else { let should_skip = self .ident() @@ -347,6 +353,7 @@ impl Rewrite for ast::Attribute { &doc_comment, shape.comment(context.config), context.config, + context.printer, ); } } diff --git a/src/bin/main.rs b/src/bin/main.rs index 88281d296be..cce29a7840f 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -9,18 +9,19 @@ use rustfmt_nightly as rustfmt; use tracing_subscriber::EnvFilter; use std::collections::HashMap; -use std::env; use std::fs::File; use std::io::{self, stdout, Read, Write}; +use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::str::FromStr; - -use getopts::{Matches, Options}; +use std::thread::JoinHandle; +use std::{env, panic}; use crate::rustfmt::{ - load_config, CliOptions, Color, Config, Edition, EmitMode, FileLines, FileName, - FormatReportFormatterBuilder, Input, Session, Verbosity, + buf_eprintln, buf_println, load_config, print::Printer, CliOptions, Color, Config, Edition, + EmitMode, FileLines, FileName, FormatReportFormatterBuilder, Input, Session, Verbosity, }; +use getopts::{Matches, Options}; const BUG_REPORT_URL: &str = "https://github.com/rust-lang/rustfmt/issues/new?labels=bug"; @@ -289,8 +290,10 @@ fn format_string(input: String, options: GetOptsOptions) -> Result { } let out = &mut stdout(); - let mut session = Session::new(config, Some(out)); + let printer = Printer::new(config.color()); + let mut session = Session::new(config, Some(out), &printer); format_and_emit_report(&mut session, Input::Text(input)); + printer.write_to_outputs()?; let exit_code = if session.has_operational_errors() || session.has_parsing_errors() { 1 @@ -300,6 +303,13 @@ fn format_string(input: String, options: GetOptsOptions) -> Result { Ok(exit_code) } +struct ThreadedFileOutput { + session_result: Result, std::io::Error>, + id: i32, + exit_code: i32, + printer: Printer, +} + fn format( files: Vec, minimal_config_path: Option, @@ -307,6 +317,7 @@ fn format( ) -> Result { options.verify_file_lines(&files); let (config, config_path) = load_config(None, Some(options.clone()))?; + let cfg_path_is_none = config_path.is_none(); if config.verbose() == Verbosity::Verbose { if let Some(path) = config_path.as_ref() { @@ -314,36 +325,131 @@ fn format( } } - let out = &mut stdout(); - let mut session = Session::new(config, Some(out)); - + let parallelism = std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN); + // Use a channel + map to get 'next-completed' thread, rather than + // waiting on the chronologically first handle to join, impactful if there are more files + // than available parallelism. + let (send, recv) = std::sync::mpsc::channel(); + let mut handles: HashMap> = HashMap::new(); + + let mut exit_code = 0; + let mut outstanding = 0; + // If the thread panics, the channel will just be dropped, + // so keep track of the spinning threads to get a stacktrace from it later + let mut id = 0; + let check = options.check; + let color = config.color(); for file in files { - if !file.exists() { - eprintln!("Error: file `{}` does not exist", file.to_str().unwrap()); - session.add_operational_error(); - } else if file.is_dir() { - eprintln!("Error: `{}` is a directory", file.to_str().unwrap()); - session.add_operational_error(); - } else { - // Check the file directory if the config-path could not be read or not provided - if config_path.is_none() { - let (local_config, config_path) = - load_config(Some(file.parent().unwrap()), Some(options.clone()))?; - if local_config.verbose() == Verbosity::Verbose { - if let Some(path) = config_path { - println!( - "Using rustfmt config file {} for {}", - path.display(), - file.display() - ); + let cfg = config.clone(); + let opts = options.clone(); + let s = send.clone(); + let handle = std::thread::spawn(move || { + let my_id = id; + let mut session_out = Vec::new(); + let printer = Printer::new(color); + let mut session = Session::new(cfg, Some(&mut session_out), &printer); + if !file.exists() { + buf_eprintln!( + printer, + "Error: file `{}` does not exist", + file.to_str().unwrap() + ); + session.add_operational_error(); + } else if file.is_dir() { + buf_eprintln!( + printer, + "Error: `{}` is a directory", + file.to_str().unwrap() + ); + session.add_operational_error(); + } else { + // Check the file directory if the config-path could not be read or not provided + if cfg_path_is_none { + let (local_config, config_path) = + match load_config(Some(file.parent().unwrap()), Some(opts)) { + Ok((lc, cf)) => (lc, cf), + Err(e) => { + drop(session); + let _ = s.send(ThreadedFileOutput { + session_result: Err(e), + id: my_id, + exit_code: 1, + printer, + }); + return Ok::<_, std::io::Error>(()); + } + }; + if local_config.verbose() == Verbosity::Verbose { + if let Some(path) = config_path { + buf_println!( + printer, + "Using rustfmt config file {} for {}", + path.display(), + file.display() + ); + } } - } - session.override_config(local_config, |sess| { - format_and_emit_report(sess, Input::File(file)) - }); + session.override_config(local_config, |sess| { + format_and_emit_report(sess, Input::File(file)) + }); + } else { + format_and_emit_report(&mut session, Input::File(file)); + } + } + let exit_code = if session.has_operational_errors() + || session.has_parsing_errors() + || ((session.has_diff() || session.has_check_errors()) && check) + { + 1 + } else { + 0 + }; + drop(session); + let _ = s.send(ThreadedFileOutput { + session_result: Ok(session_out), + id: my_id, + exit_code, + printer, + }); + Ok(()) + }); + handles.insert(id, handle); + id += 1; + outstanding += 1; + if outstanding >= parallelism.get() { + if let Ok(thread_out) = recv.recv() { + let exit = join_thread_reporting_back(&mut handles, thread_out)?; + if exit != 0 { + exit_code = exit; + } + outstanding -= 1; } else { - format_and_emit_report(&mut session, Input::File(file)); + break; + } + } + } + // Drop sender, or this will deadlock + drop(send); + + // Drain running threads + while let Ok(thread_out) = recv.recv() { + let exit = join_thread_reporting_back(&mut handles, thread_out)?; + if exit != 0 { + exit_code = exit; + } + } + + // All successful threads have been removed from `handles` only errors are left + for (_id, jh) in handles { + match jh.join() { + Ok(res) => { + res?; + } + Err(panicked) => { + // Propagate the thread's panic, not much to do here + // if the error should be preserved (which it should) + panic::resume_unwind(panicked) } } } @@ -352,26 +458,43 @@ fn format( // that were used during formatting as TOML. if let Some(path) = minimal_config_path { let mut file = File::create(path)?; - let toml = session.config.used_options().to_toml()?; + let toml = config.used_options().to_toml()?; file.write_all(toml.as_bytes())?; } - let exit_code = if session.has_operational_errors() - || session.has_parsing_errors() - || ((session.has_diff() || session.has_check_errors()) && options.check) - { - 1 - } else { - 0 - }; Ok(exit_code) } +fn join_thread_reporting_back( + handles: &mut HashMap>>, + threaded_file_output: ThreadedFileOutput, +) -> Result { + let handle = handles + .remove(&threaded_file_output.id) + .expect("Join thread not found by id"); + match handle.join() { + Ok(result) => { + result?; + } + Err(panicked) => { + // Should never end up here logically, since the thread reported back + panic::resume_unwind(panicked) + } + } + let output = threaded_file_output.session_result?; + if !output.is_empty() { + stdout().write_all(&output).unwrap(); + } + threaded_file_output.printer.write_to_outputs()?; + Ok(threaded_file_output.exit_code) +} + fn format_and_emit_report(session: &mut Session<'_, T>, input: Input) { match session.format(input) { Ok(report) => { if report.has_warnings() { - eprintln!( + buf_eprintln!( + session.printer, "{}", FormatReportFormatterBuilder::new(&report) .enable_colors(should_print_with_colors(session)) @@ -380,7 +503,7 @@ fn format_and_emit_report(session: &mut Session<'_, T>, input: Input) } } Err(msg) => { - eprintln!("Error writing files: {msg}"); + buf_eprintln!(session.printer, "Error writing files: {msg}"); session.add_operational_error(); } } diff --git a/src/chains.rs b/src/chains.rs index ea23690caed..73a8ab9bcb7 100644 --- a/src/chains.rs +++ b/src/chains.rs @@ -293,7 +293,7 @@ impl Rewrite for ChainItem { ), ChainItemKind::Await => ".await".to_owned(), ChainItemKind::Comment(ref comment, _) => { - rewrite_comment(comment, false, shape, context.config)? + rewrite_comment(comment, false, shape, context.config, context.printer)? } }; Some(format!("{rewrite}{}", "?".repeat(self.tries))) diff --git a/src/closures.rs b/src/closures.rs index 5bf29441b54..50b2427f392 100644 --- a/src/closures.rs +++ b/src/closures.rs @@ -323,7 +323,7 @@ fn rewrite_closure_fn_decl( let fmt = ListFormatting::new(param_shape, context.config) .tactic(tactic) .preserve_newline(true); - let list_str = write_list(&item_vec, &fmt)?; + let list_str = write_list(&item_vec, &fmt, context.printer)?; let mut prefix = format!("{binder}{const_}{immovable}{coro}{mover}|{list_str}|"); if !ret_str.is_empty() { diff --git a/src/comment.rs b/src/comment.rs index 55e710bd27d..bed45cc0feb 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -8,6 +8,7 @@ use regex::Regex; use rustc_span::Span; use crate::config::Config; +use crate::print::Printer; use crate::rewrite::RewriteContext; use crate::shape::{Indent, Shape}; use crate::string::{rewrite_string, StringFormat}; @@ -248,8 +249,13 @@ pub(crate) fn combine_strs_with_missing_comments( Some(result) } -pub(crate) fn rewrite_doc_comment(orig: &str, shape: Shape, config: &Config) -> Option { - identify_comment(orig, false, shape, config, true) +pub(crate) fn rewrite_doc_comment( + orig: &str, + shape: Shape, + config: &Config, + printer: &Printer, +) -> Option { + identify_comment(orig, false, shape, config, true, printer) } pub(crate) fn rewrite_comment( @@ -257,8 +263,9 @@ pub(crate) fn rewrite_comment( block_style: bool, shape: Shape, config: &Config, + printer: &Printer, ) -> Option { - identify_comment(orig, block_style, shape, config, false) + identify_comment(orig, block_style, shape, config, false, printer) } fn identify_comment( @@ -267,6 +274,7 @@ fn identify_comment( shape: Shape, config: &Config, is_doc_comment: bool, + printer: &Printer, ) -> Option { let style = comment_style(orig, false); @@ -377,6 +385,7 @@ fn identify_comment( shape, config, is_doc_comment || style.is_doc_comment(), + printer, )? }; if rest.is_empty() { @@ -388,6 +397,7 @@ fn identify_comment( shape, config, is_doc_comment, + printer, ) .map(|rest_str| { format!( @@ -725,6 +735,7 @@ impl<'a> CommentRewrite<'a> { line: &'a str, has_leading_whitespace: bool, is_doc_comment: bool, + printer: &Printer, ) -> bool { let num_newlines = count_newlines(orig); let is_last = i == num_newlines; @@ -775,9 +786,12 @@ impl<'a> CommentRewrite<'a> { .doc_comment_code_block_width() .min(config.max_width()); config.set().max_width(comment_max_width); - if let Some(s) = - crate::format_code_block(&self.code_block_buffer, &config, false) - { + if let Some(s) = crate::format_code_block( + &self.code_block_buffer, + &config, + false, + printer, + ) { trim_custom_comment_prefix(&s.snippet) } else { trim_custom_comment_prefix(&self.code_block_buffer) @@ -912,6 +926,7 @@ fn rewrite_comment_inner( shape: Shape, config: &Config, is_doc_comment: bool, + printer: &Printer, ) -> Option { let mut rewriter = CommentRewrite::new(orig, block_style, shape, config); @@ -941,7 +956,14 @@ fn rewrite_comment_inner( }); for (i, (line, has_leading_whitespace)) in lines.enumerate() { - if rewriter.handle_line(orig, i, line, has_leading_whitespace, is_doc_comment) { + if rewriter.handle_line( + orig, + i, + line, + has_leading_whitespace, + is_doc_comment, + printer, + ) { break; } } @@ -1008,7 +1030,13 @@ pub(crate) fn rewrite_missing_comment( // check the span starts with a comment let pos = trimmed_snippet.find('/'); if !trimmed_snippet.is_empty() && pos.is_some() { - rewrite_comment(trimmed_snippet, false, shape, context.config) + rewrite_comment( + trimmed_snippet, + false, + shape, + context.config, + context.printer, + ) } else { Some(String::new()) } @@ -1914,19 +1942,19 @@ mod test { let comment = rewrite_comment(" //test", true, Shape::legacy(100, Indent::new(0, 100)), - &wrap_normalize_config).unwrap(); + &wrap_normalize_config, &Printer::no_color()).unwrap(); assert_eq!("/* test */", comment); let comment = rewrite_comment("// comment on a", false, Shape::legacy(10, Indent::empty()), - &wrap_normalize_config).unwrap(); + &wrap_normalize_config, &Printer::no_color()).unwrap(); assert_eq!("// comment\n// on a", comment); let comment = rewrite_comment("// A multi line comment\n // between args.", false, Shape::legacy(60, Indent::new(0, 12)), - &wrap_normalize_config).unwrap(); + &wrap_normalize_config, &Printer::no_color()).unwrap(); assert_eq!("// A multi line comment\n // between args.", comment); let input = "// comment"; @@ -1935,13 +1963,13 @@ mod test { let comment = rewrite_comment(input, true, Shape::legacy(9, Indent::new(0, 69)), - &wrap_normalize_config).unwrap(); + &wrap_normalize_config, &Printer::no_color()).unwrap(); assert_eq!(expected, comment); let comment = rewrite_comment("/* trimmed */", true, Shape::legacy(100, Indent::new(0, 100)), - &wrap_normalize_config).unwrap(); + &wrap_normalize_config, &Printer::no_color()).unwrap(); assert_eq!("/* trimmed */", comment); // Check that different comment style are properly recognised. @@ -1952,7 +1980,7 @@ mod test { */"#, false, Shape::legacy(100, Indent::new(0, 0)), - &wrap_normalize_config).unwrap(); + &wrap_normalize_config, &Printer::no_color()).unwrap(); assert_eq!("/// test1\n/// test2\n// test3", comment); // Check that the blank line marks the end of a commented paragraph. @@ -1961,7 +1989,7 @@ mod test { // test2"#, false, Shape::legacy(100, Indent::new(0, 0)), - &wrap_normalize_config).unwrap(); + &wrap_normalize_config, &Printer::no_color()).unwrap(); assert_eq!("// test1\n\n// test2", comment); // Check that the blank line marks the end of a custom-commented paragraph. @@ -1970,7 +1998,7 @@ mod test { //@ test2"#, false, Shape::legacy(100, Indent::new(0, 0)), - &wrap_normalize_config).unwrap(); + &wrap_normalize_config, &Printer::no_color()).unwrap(); assert_eq!("//@ test1\n\n//@ test2", comment); // Check that bare lines are just indented but otherwise left unchanged. @@ -1982,7 +2010,7 @@ mod test { */"#, false, Shape::legacy(100, Indent::new(0, 0)), - &wrap_config).unwrap(); + &wrap_config, &Printer::no_color()).unwrap(); assert_eq!("// test1\n/*\n a bare line!\n\n another bare line!\n*/", comment); } diff --git a/src/emitter.rs b/src/emitter.rs index 9c335314d75..720c3c64323 100644 --- a/src/emitter.rs +++ b/src/emitter.rs @@ -5,6 +5,7 @@ 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::print::Printer; use crate::FileName; use std::io::{self, Write}; use std::path::Path; diff --git a/src/emitter/diff.rs b/src/emitter/diff.rs index 4e48c59ea29..64917317ced 100644 --- a/src/emitter/diff.rs +++ b/src/emitter/diff.rs @@ -2,17 +2,18 @@ use super::*; use crate::config::Config; use crate::rustfmt_diff::{make_diff, print_diff}; -pub(crate) struct DiffEmitter { +pub(crate) struct DiffEmitter<'a> { config: Config, + printer: &'a Printer, } -impl DiffEmitter { - pub(crate) fn new(config: Config) -> Self { - Self { config } +impl<'a> DiffEmitter<'a> { + pub(crate) fn new(config: Config, printer: &'a Printer) -> Self { + Self { config, printer } } } -impl Emitter for DiffEmitter { +impl<'a> Emitter for DiffEmitter<'a> { fn emit_formatted_file( &mut self, output: &mut dyn Write, @@ -34,6 +35,7 @@ impl Emitter for DiffEmitter { mismatch, |line_num| format!("Diff in {}:{}:", filename, line_num), &self.config, + self.printer, ); } } else if original_text != formatted_text { @@ -57,7 +59,8 @@ mod tests { fn does_not_print_when_no_files_reformatted() { let mut writer = Vec::new(); let config = Config::default(); - let mut emitter = DiffEmitter::new(config); + let printer = Printer::no_color(); + let mut emitter = DiffEmitter::new(config, &printer); let result = emitter .emit_formatted_file( &mut writer, @@ -84,7 +87,8 @@ mod tests { let mut writer = Vec::new(); let mut config = Config::default(); config.set().print_misformatted_file_names(true); - let mut emitter = DiffEmitter::new(config); + let printer = Printer::no_color(); + let mut emitter = DiffEmitter::new(config, &printer); let _ = emitter .emit_formatted_file( &mut writer, @@ -116,7 +120,8 @@ mod tests { fn prints_newline_message_with_only_newline_style_diff() { let mut writer = Vec::new(); let config = Config::default(); - let mut emitter = DiffEmitter::new(config); + let printer = Printer::no_color(); + let mut emitter = DiffEmitter::new(config, &printer); let _ = emitter .emit_formatted_file( &mut writer, diff --git a/src/emitter/stdout.rs b/src/emitter/stdout.rs index 0b635a28bf2..6c467e7f897 100644 --- a/src/emitter/stdout.rs +++ b/src/emitter/stdout.rs @@ -2,17 +2,17 @@ use super::*; use crate::config::Verbosity; #[derive(Debug)] -pub(crate) struct StdoutEmitter { +pub(crate) struct IntoOutputEmitter { verbosity: Verbosity, } -impl StdoutEmitter { +impl IntoOutputEmitter { pub(crate) fn new(verbosity: Verbosity) -> Self { Self { verbosity } } } -impl Emitter for StdoutEmitter { +impl Emitter for IntoOutputEmitter { fn emit_formatted_file( &mut self, output: &mut dyn Write, diff --git a/src/expr.rs b/src/expr.rs index 7808f891336..1bcd4deac17 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -496,6 +496,7 @@ fn block_prefix(context: &RewriteContext<'_>, block: &ast::Block, shape: Shape) true, Shape::legacy(budget, shape.indent + 7), context.config, + context.printer, )? ) } else { @@ -1688,7 +1689,7 @@ fn rewrite_struct_lit<'a>( force_no_trailing_comma || has_base_or_rest || !context.use_block_indent(), ); - write_list(&item_vec, &fmt)? + write_list(&item_vec, &fmt, context.printer)? }; let fields_str = @@ -1835,7 +1836,7 @@ fn rewrite_tuple_in_visual_indent_style<'a, T: 'a + IntoOverflowableItem<'a>>( let fmt = ListFormatting::new(nested_shape, context.config) .tactic(tactic) .ends_with_newline(false); - let list_str = write_list(&item_vec, &fmt)?; + let list_str = write_list(&item_vec, &fmt, context.printer)?; Some(format!("({list_str})")) } diff --git a/src/formatting.rs b/src/formatting.rs index 60361505a3f..44777c289bc 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -14,9 +14,13 @@ use crate::formatting::generated::is_generated_file; use crate::modules::Module; use crate::parse::parser::{DirectoryOwnership, Parser, ParserError}; use crate::parse::session::ParseSess; +use crate::print::Printer; use crate::utils::{contains_skip, count_newlines}; use crate::visitor::FmtVisitor; -use crate::{modules, source_file, ErrorKind, FormatReport, Input, Session}; +use crate::{ + buf_eprintln, buf_print, buf_println, modules, source_file, ErrorKind, FormatReport, Input, + Session, +}; mod generated; mod newline_style; @@ -39,13 +43,17 @@ impl<'b, T: Write + 'b> Session<'b, T> { if self.config.disable_all_formatting() { // When the input is from stdin, echo back the input. return match input { - Input::Text(ref buf) => echo_back_stdin(buf), + Input::Text(ref buf) => { + // Echo back stdin + buf_print!(self.printer, "{buf}"); + Ok(FormatReport::new()) + } _ => Ok(FormatReport::new()), }; } let config = &self.config.clone(); - let format_result = format_project(input, config, self, is_macro_def); + let format_result = format_project(input, config, self, is_macro_def, self.printer); format_result.map(|report| { self.errors.add(&report.internal.borrow().1); @@ -90,26 +98,20 @@ fn should_skip_module( false } -fn echo_back_stdin(input: &str) -> Result { - if let Err(e) = io::stdout().write_all(input.as_bytes()) { - return Err(From::from(e)); - } - Ok(FormatReport::new()) -} - // Format an entire crate (or subset of the module tree). fn format_project( input: Input, config: &Config, handler: &mut T, is_macro_def: bool, + printer: &Printer, ) -> Result { let mut timer = Timer::start(); let main_file = input.file_name(); let input_is_stdin = main_file == FileName::Stdin; - let parse_session = ParseSess::new(config)?; + let parse_session = ParseSess::new(config, printer)?; if config.skip_children() && parse_session.ignore_file(&main_file) { return Ok(FormatReport::new()); } @@ -123,14 +125,14 @@ fn format_project( Err(e) => { let forbid_verbose = input_is_stdin || e != ParserError::ParsePanicError; should_emit_verbose(forbid_verbose, config, || { - eprintln!("The Rust parser panicked"); + buf_eprintln!(printer, "The Rust parser panicked"); }); report.add_parsing_error(); return Ok(report); } }; - let mut context = FormatContext::new(&krate, report, parse_session, config, handler); + let mut context = FormatContext::new(&krate, report, parse_session, config, handler, printer); let files = modules::ModResolver::new( &context.parse_session, directory_ownership.unwrap_or(DirectoryOwnership::UnownedViaBlock), @@ -151,20 +153,27 @@ fn format_project( for (path, module) in files { if input_is_stdin && contains_skip(module.attrs()) { - return echo_back_stdin( + // Echo back stdin + buf_print!( + printer, + "{}", context .parse_session .snippet_provider(module.span) - .entire_snippet(), + .entire_snippet() ); + return Ok(FormatReport::new()); } - should_emit_verbose(input_is_stdin, config, || println!("Formatting {}", path)); + should_emit_verbose(input_is_stdin, config, || { + buf_println!(printer, "Formatting {}", path) + }); context.format_file(path, &module, is_macro_def)?; } timer = timer.done_formatting(); should_emit_verbose(input_is_stdin, config, || { - println!( + buf_println!( + printer, "Spent {0:.3} secs in the parsing phase, and {1:.3} secs in the formatting phase", timer.get_parse_time(), timer.get_format_time(), @@ -181,6 +190,7 @@ struct FormatContext<'a, T: FormatHandler> { parse_session: ParseSess, config: &'a Config, handler: &'a mut T, + printer: &'a Printer, } impl<'a, T: FormatHandler + 'a> FormatContext<'a, T> { @@ -190,6 +200,7 @@ impl<'a, T: FormatHandler + 'a> FormatContext<'a, T> { parse_session: ParseSess, config: &'a Config, handler: &'a mut T, + printer: &'a Printer, ) -> Self { FormatContext { krate, @@ -197,6 +208,7 @@ impl<'a, T: FormatHandler + 'a> FormatContext<'a, T> { parse_session, config, handler, + printer, } } @@ -216,6 +228,7 @@ impl<'a, T: FormatHandler + 'a> FormatContext<'a, T> { &self.parse_session, self.config, &snippet_provider, + &self.printer, self.report.clone(), ); visitor.skip_context.update_with_attrs(&self.krate.attrs); diff --git a/src/git-rustfmt/main.rs b/src/git-rustfmt/main.rs index 3059d917c6b..ff29d6b41ea 100644 --- a/src/git-rustfmt/main.rs +++ b/src/git-rustfmt/main.rs @@ -11,7 +11,9 @@ use getopts::{Matches, Options}; use rustfmt_nightly as rustfmt; use tracing_subscriber::EnvFilter; -use crate::rustfmt::{load_config, CliOptions, FormatReportFormatterBuilder, Input, Session}; +use crate::rustfmt::{ + load_config, print::Printer, CliOptions, FormatReportFormatterBuilder, Input, Session, +}; fn prune_files(files: Vec<&str>) -> Vec<&str> { let prefixes: Vec<_> = files @@ -63,7 +65,8 @@ fn fmt_files(files: &[&str]) -> i32 { let mut exit_code = 0; let mut out = stdout(); - let mut session = Session::new(config, Some(&mut out)); + let printer = Printer::new(config.color()); + let mut session = Session::new(config, Some(&mut out), &printer); for file in files { let report = session.format(Input::File(PathBuf::from(file))).unwrap(); if report.has_warnings() { diff --git a/src/imports.rs b/src/imports.rs index 05195553c08..00bbf5243d5 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -1030,7 +1030,7 @@ fn rewrite_nested_use_tree( .preserve_newline(true) .nested(has_nested_list); - let list_str = write_list(&list_items, &fmt)?; + let list_str = write_list(&list_items, &fmt, context.printer)?; let result = if (list_str.contains('\n') || list_str.len() > remaining_width diff --git a/src/items.rs b/src/items.rs index e7ff5ff818b..f581fdc9657 100644 --- a/src/items.rs +++ b/src/items.rs @@ -635,7 +635,7 @@ impl<'a> FmtVisitor<'a> { .trailing_separator(self.config.trailing_comma()) .preserve_newline(true); - let list = write_list(&items, &fmt)?; + let list = write_list(&items, &fmt, self.printer)?; result.push_str(&list); result.push_str(&original_offset.to_string_with_newline(self.config)); result.push('}'); @@ -2769,7 +2769,7 @@ fn rewrite_params( .trailing_separator(trailing_separator) .ends_with_newline(tactic.ends_with_newline(context.config.indent_style())) .preserve_newline(true); - write_list(¶m_items, &fmt) + write_list(¶m_items, &fmt, context.printer) } fn compute_budgets_for_params( @@ -3026,7 +3026,7 @@ fn rewrite_bounds_on_where_clause( .tactic(shape_tactic) .trailing_separator(comma_tactic) .preserve_newline(preserve_newline); - write_list(&items.collect::>(), &fmt) + write_list(&items.collect::>(), &fmt, context.printer) } fn rewrite_where_clause( @@ -3102,7 +3102,7 @@ fn rewrite_where_clause( .trailing_separator(comma_tactic) .ends_with_newline(tactic.ends_with_newline(context.config.indent_style())) .preserve_newline(true); - let preds_str = write_list(&item_vec, &fmt)?; + let preds_str = write_list(&item_vec, &fmt, context.printer)?; let end_length = if terminator == "{" { // If the brace is on the next line we don't need to count it otherwise it needs two diff --git a/src/lib.rs b/src/lib.rs index a67adb1478f..123b6e6642f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,7 @@ pub use crate::config::{ }; pub use crate::format_report_formatter::{FormatReportFormatter, FormatReportFormatterBuilder}; +use crate::print::Printer; pub use crate::rustfmt_diff::{ModifiedChunk, ModifiedLines}; @@ -83,6 +84,7 @@ mod overflow; mod pairs; mod parse; mod patterns; +pub mod print; mod release_channel; mod reorder; mod rewrite; @@ -298,7 +300,12 @@ impl fmt::Display for FormatReport { /// Format the given snippet. The snippet is expected to be *complete* code. /// When we cannot parse the given snippet, this function returns `None`. -fn format_snippet(snippet: &str, config: &Config, is_macro_def: bool) -> Option { +fn format_snippet( + snippet: &str, + config: &Config, + is_macro_def: bool, + printer: &Printer, +) -> Option { let mut config = config.clone(); panic::catch_unwind(|| { let mut out: Vec = Vec::with_capacity(snippet.len() * 2); @@ -311,7 +318,7 @@ fn format_snippet(snippet: &str, config: &Config, is_macro_def: bool) -> Option< let (formatting_error, result) = { let input = Input::Text(snippet.into()); - let mut session = Session::new(config, Some(&mut out)); + let mut session = Session::new(config, Some(&mut out), printer); let result = session.format_input_inner(input, is_macro_def); ( session.errors.has_macro_format_failure @@ -343,6 +350,7 @@ fn format_code_block( code_snippet: &str, config: &Config, is_macro_def: bool, + printer: &Printer, ) -> Option { const FN_MAIN_PREFIX: &str = "fn main() {\n"; @@ -376,7 +384,7 @@ fn format_code_block( config_with_unix_newline .set() .newline_style(NewlineStyle::Unix); - let mut formatted = format_snippet(&snippet, &config_with_unix_newline, is_macro_def)?; + let mut formatted = format_snippet(&snippet, &config_with_unix_newline, is_macro_def, printer)?; // Remove wrapping main block formatted.unwrap_code_block(); @@ -436,14 +444,15 @@ fn format_code_block( pub struct Session<'b, T: Write> { pub config: Config, pub out: Option<&'b mut T>, + pub printer: &'b Printer, pub(crate) errors: ReportedErrors, source_file: SourceFile, emitter: Box, } impl<'b, T: Write + 'b> Session<'b, T> { - pub fn new(config: Config, mut out: Option<&'b mut T>) -> Session<'b, T> { - let emitter = create_emitter(&config); + pub fn new(config: Config, mut out: Option<&'b mut T>, printer: &'b Printer) -> Session<'b, T> { + let emitter = create_emitter(&config, printer); if let Some(ref mut out) = out { let _ = emitter.emit_header(out); @@ -455,6 +464,7 @@ impl<'b, T: Write + 'b> Session<'b, T> { emitter, errors: ReportedErrors::default(), source_file: SourceFile::new(), + printer, } } @@ -513,7 +523,7 @@ impl<'b, T: Write + 'b> Session<'b, T> { } } -pub(crate) fn create_emitter<'a>(config: &Config) -> Box { +pub(crate) fn create_emitter<'a>(config: &Config, printer: &'a Printer) -> Box { match config.emit_mode() { EmitMode::Files if config.make_backup() => { Box::new(emitter::FilesWithBackupEmitter::default()) @@ -522,12 +532,12 @@ pub(crate) fn create_emitter<'a>(config: &Config) -> Box { config.print_misformatted_file_names(), )), EmitMode::Stdout | EmitMode::Coverage => { - Box::new(emitter::StdoutEmitter::new(config.verbose())) + Box::new(emitter::IntoOutputEmitter::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())), + EmitMode::Diff => Box::new(emitter::DiffEmitter::new(config.clone(), printer)), } } @@ -581,15 +591,17 @@ mod unit_tests { // `format_snippet()` and `format_code_block()` should not panic // even when we cannot parse the given snippet. let snippet = "let"; - assert!(format_snippet(snippet, &Config::default(), false).is_none()); - assert!(format_code_block(snippet, &Config::default(), false).is_none()); + assert!(format_snippet(snippet, &Config::default(), false, &Printer::no_color()).is_none()); + assert!( + format_code_block(snippet, &Config::default(), false, &Printer::no_color()).is_none() + ); } fn test_format_inner(formatter: F, input: &str, expected: &str) -> bool where - F: Fn(&str, &Config, bool) -> Option, + F: Fn(&str, &Config, bool, &Printer) -> Option, { - let output = formatter(input, &Config::default(), false); + let output = formatter(input, &Config::default(), false, &Printer::no_color()); output.is_some() && output.unwrap().snippet == expected } @@ -611,7 +623,10 @@ mod unit_tests { fn test_format_code_block_fail() { #[rustfmt::skip] let code_block = "this_line_is_100_characters_long_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(x, y, z);"; - assert!(format_code_block(code_block, &Config::default(), false).is_none()); + assert!( + format_code_block(code_block, &Config::default(), false, &Printer::no_color()) + .is_none() + ); } #[test] diff --git a/src/lists.rs b/src/lists.rs index 41afef279e9..a5d40b4cac9 100644 --- a/src/lists.rs +++ b/src/lists.rs @@ -8,6 +8,7 @@ use rustc_span::BytePos; use crate::comment::{find_comment_end, rewrite_comment, FindUncommented}; use crate::config::lists::*; use crate::config::{Config, IndentStyle}; +use crate::print::Printer; use crate::rewrite::RewriteContext; use crate::shape::{Indent, Shape}; use crate::utils::{ @@ -257,7 +258,11 @@ where } // Format a list of commented items into a string. -pub(crate) fn write_list(items: I, formatting: &ListFormatting<'_>) -> Option +pub(crate) fn write_list( + items: I, + formatting: &ListFormatting<'_>, + printer: &Printer, +) -> Option where I: IntoIterator + Clone, T: AsRef, @@ -362,8 +367,13 @@ where // Block style in non-vertical mode. let block_mode = tactic == DefinitiveListTactic::Horizontal; // Width restriction is only relevant in vertical mode. - let comment = - rewrite_comment(comment, block_mode, formatting.shape, formatting.config)?; + let comment = rewrite_comment( + comment, + block_mode, + formatting.shape, + formatting.config, + printer, + )?; result.push_str(&comment); if !inner_item.is_empty() { @@ -409,6 +419,7 @@ where true, Shape::legacy(formatting.shape.width, Indent::empty()), formatting.config, + printer, )?; result.push(' '); @@ -457,6 +468,7 @@ where block_style, comment_shape, formatting.config, + printer, ) }; diff --git a/src/macros.rs b/src/macros.rs index 6e114c76f26..290778ce037 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -476,7 +476,7 @@ pub(crate) fn rewrite_macro_def( result += &arm_shape.indent.to_string_with_newline(context.config); } - match write_list(&branch_items, &fmt) { + match write_list(&branch_items, &fmt, context.printer) { Some(ref s) => result += s, None => return snippet, } @@ -1289,17 +1289,18 @@ impl MacroBranch { config.set().max_width(new_width); // First try to format as items, then as statements. - let new_body_snippet = match crate::format_snippet(&body_str, &config, true) { - Some(new_body) => new_body, - None => { - let new_width = new_width + config.tab_spaces(); - config.set().max_width(new_width); - match crate::format_code_block(&body_str, &config, true) { - Some(new_body) => new_body, - None => return None, + let new_body_snippet = + match crate::format_snippet(&body_str, &config, true, context.printer) { + Some(new_body) => new_body, + None => { + let new_width = new_width + config.tab_spaces(); + config.set().max_width(new_width); + match crate::format_code_block(&body_str, &config, true, context.printer) { + Some(new_body) => new_body, + None => return None, + } } - } - }; + }; if !filtered_str_fits(&new_body_snippet.snippet, config.max_width(), shape) { return None; diff --git a/src/matches.rs b/src/matches.rs index 5e36d9e857d..25bb928d89e 100644 --- a/src/matches.rs +++ b/src/matches.rs @@ -211,7 +211,7 @@ fn rewrite_match_arms( .separator("") .preserve_newline(true); - write_list(&arms_vec, &fmt) + write_list(&arms_vec, &fmt, context.printer) } fn rewrite_match_arm( @@ -408,7 +408,7 @@ fn rewrite_match_body( if comment_str.is_empty() { String::new() } else { - rewrite_comment(comment_str, false, shape, context.config)? + rewrite_comment(comment_str, false, shape, context.config, context.printer)? } }; diff --git a/src/missed_spans.rs b/src/missed_spans.rs index 28edcb784b4..c47bb18b16b 100644 --- a/src/missed_spans.rs +++ b/src/missed_spans.rs @@ -280,15 +280,21 @@ impl<'a> FmtVisitor<'a> { self.push_str(&comment_indent.to_string_with_newline(self.config)); let other_lines = &subslice[offset + 1..]; - let comment_str = - rewrite_comment(other_lines, false, comment_shape, self.config) - .unwrap_or_else(|| String::from(other_lines)); + let comment_str = rewrite_comment( + other_lines, + false, + comment_shape, + self.config, + self.printer, + ) + .unwrap_or_else(|| String::from(other_lines)); self.push_str(&comment_str); } } } else { - let comment_str = rewrite_comment(subslice, false, comment_shape, self.config) - .unwrap_or_else(|| String::from(subslice)); + let comment_str = + rewrite_comment(subslice, false, comment_shape, self.config, self.printer) + .unwrap_or_else(|| String::from(subslice)); self.push_str(&comment_str); } diff --git a/src/overflow.rs b/src/overflow.rs index c44f3788d97..a2fb2d881b3 100644 --- a/src/overflow.rs +++ b/src/overflow.rs @@ -642,7 +642,7 @@ impl<'a> Context<'a> { .trailing_separator(trailing_separator) .ends_with_newline(ends_with_newline); - write_list(&list_items, &fmt) + write_list(&list_items, &fmt, self.context.printer) .map(|items_str| (tactic == DefinitiveListTactic::Horizontal, items_str)) } diff --git a/src/parse/session.rs b/src/parse/session.rs index 5de6ee49acf..4e1a41a14a1 100644 --- a/src/parse/session.rs +++ b/src/parse/session.rs @@ -15,6 +15,7 @@ use crate::config::file_lines::LineRange; use crate::config::options::Color; use crate::ignore_path::IgnorePathSet; use crate::parse::parser::{ModError, ModulePathSuccess}; +use crate::print::Printer; use crate::source_map::LineRangeUtils; use crate::utils::starts_with_newline; use crate::visitor::SnippetProvider; @@ -123,15 +124,8 @@ fn default_dcx( ignore_path_set: Lrc, can_reset: Lrc, show_parse_errors: bool, - color: Color, + printer: &Printer, ) -> DiagCtxt { - let supports_color = term::stderr().map_or(false, |term| term.supports_color()); - let emit_color = if supports_color { - ColorConfig::from(color) - } else { - ColorConfig::Never - }; - let emitter = if !show_parse_errors { silent_emitter() } else { @@ -139,7 +133,10 @@ fn default_dcx( rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(), false, ); - Box::new(EmitterWriter::stderr(emit_color, fallback_bundle).sm(Some(source_map.clone()))) + Box::new( + EmitterWriter::new(Box::new(printer.clone()), fallback_bundle) + .sm(Some(source_map.clone())), + ) }; DiagCtxt::with_emitter(Box::new(SilentOnIgnoredFilesEmitter { has_non_ignorable_parser_errors: false, @@ -151,7 +148,7 @@ fn default_dcx( } impl ParseSess { - pub(crate) fn new(config: &Config) -> Result { + pub(crate) fn new(config: &Config, printer: &Printer) -> Result { let ignore_path_set = match IgnorePathSet::from_ignore_list(&config.ignore()) { Ok(ignore_path_set) => Lrc::new(ignore_path_set), Err(e) => return Err(ErrorKind::InvalidGlobPattern(e)), @@ -164,7 +161,7 @@ impl ParseSess { Lrc::clone(&ignore_path_set), Lrc::clone(&can_reset_errors), config.show_parse_errors(), - config.color(), + printer, ); let parse_sess = RawParseSess::with_dcx(dcx, source_map); diff --git a/src/patterns.rs b/src/patterns.rs index 0fa6edaa5d7..4ffecd8836e 100644 --- a/src/patterns.rs +++ b/src/patterns.rs @@ -100,7 +100,7 @@ impl Rewrite for Pat { .separator(" |") .separator_place(context.config.binop_separator()) .ends_with_newline(false); - write_list(&items, &fmt) + write_list(&items, &fmt, context.printer) } PatKind::Box(ref pat) => rewrite_unary_prefix(context, "box ", &**pat, shape), PatKind::Ident(BindingAnnotation(by_ref, mutability), ident, ref sub_pat) => { @@ -325,7 +325,7 @@ fn rewrite_struct_pat( let nested_shape = shape_for_tactic(tactic, h_shape, v_shape); let fmt = struct_lit_formatting(nested_shape, tactic, context, false); - let mut fields_str = write_list(&item_vec, &fmt)?; + let mut fields_str = write_list(&item_vec, &fmt, context.printer)?; let one_line_width = h_shape.map_or(0, |shape| shape.width); let has_trailing_comma = fmt.needs_trailing_separator(); diff --git a/src/print.rs b/src/print.rs new file mode 100644 index 00000000000..e857f51f64c --- /dev/null +++ b/src/print.rs @@ -0,0 +1,252 @@ +use crate::Color; +use rustc_errors::{Color as RustColor, ColorSpec, WriteColor}; +use std::io::{stderr, stdout, Write}; +use std::sync::{Arc, Mutex}; +use termcolor::{ColorChoice, StandardStream, WriteColor as _}; + +#[derive(Clone)] +pub struct Printer { + // Needs `Mutex` to be UnwindSafe, although, this should be + // safe to `Unwind` without it. + // Needs `Arc` to pass the boundary over to `rustc_error`. + inner: Arc>, +} + +struct PrinterInner { + color_setting: Color, + current_color: Option, + messages: Vec, + supports_color: bool, +} + +impl Printer { + pub fn new(term_output_color: Color) -> Self { + Self { + inner: Arc::new(Mutex::new(PrinterInner { + color_setting: term_output_color, + current_color: None, + messages: vec![], + supports_color: true, // Todo: Actually check + })), + } + } + + pub fn no_color() -> Self { + Self::new(Color::Never) + } + + #[inline] + pub fn push_msg(&self, msg: PrintMessage) { + self.inner.lock().unwrap().messages.push(msg); + } + + /// Writes stored messages to respective outputs in order. + pub fn write_to_outputs(&self) -> Result<(), std::io::Error> { + let inner = self.inner.lock().unwrap(); + // Pretty common case, early exit + if inner.messages.is_empty() { + return Ok(()); + } + let (mut diff_term_stdout, mut rustc_term_stderr) = inner + .color_setting + .use_colored_tty() + .then(|| { + ( + term::stdout().filter(|t| t.supports_color()), + term::stderr().and_then(|t| { + t.supports_color() + .then_some(StandardStream::stderr(ColorChoice::Always)) + }), + ) + }) + .unwrap_or_default(); + for msg in &inner.messages { + match msg { + PrintMessage::Stdout(out) => { + stdout().write_all(out)?; + } + PrintMessage::StdErr(err) => { + stderr().write_all(err)?; + } + PrintMessage::Term(t_msg) => { + if let Some(t) = &mut diff_term_stdout { + if let Some(col) = t_msg.color { + t.fg(col).unwrap() + } + t.write_all(&t_msg.message)?; + if t_msg.color.is_some() { + t.reset().unwrap(); + } + } else { + stdout().write_all(&t_msg.message)?; + } + } + PrintMessage::RustcErrTerm(msg) => { + if let Some(t) = &mut rustc_term_stderr { + if let Some(col) = msg.color.as_ref().map(rustc_colorspec_compat) { + t.set_color(&col)?; + } + t.write_all(&msg.message)?; + if msg.color.as_ref().map(|cs| cs.reset()).unwrap_or_default() { + t.reset().unwrap(); + } + } else { + stderr().write_all(&msg.message)?; + } + } + } + } + stdout().flush()?; + stderr().flush()?; + + Ok(()) + } +} + +/// Trait evoked by `rustc_error`s `EmitterWriter` (`HumanEmitter` on main) to print +/// compilation errors and diffs. +impl Write for Printer { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let mut inner = self.inner.lock().unwrap(); + let col = inner.current_color.clone(); + inner + .messages + .push(PrintMessage::RustcErrTerm(RustcErrTermMessage::new( + buf.to_vec(), + col, + ))); + Ok(buf.len()) + } + + #[inline] + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + + #[inline] + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + self.write(buf)?; + Ok(()) + } +} + +impl WriteColor for Printer { + #[inline] + fn supports_color(&self) -> bool { + self.inner.lock().unwrap().supports_color + } + + #[inline] + fn set_color(&mut self, spec: &ColorSpec) -> std::io::Result<()> { + self.inner.lock().unwrap().current_color = Some(spec.clone()); + Ok(()) + } + + #[inline] + fn reset(&mut self) -> std::io::Result<()> { + self.inner.lock().unwrap().current_color.take(); + Ok(()) + } +} + +// Rustc vendors termcolor, but not everything needed to use it, +// as far as I can tell +fn rustc_colorspec_compat(rustc: &ColorSpec) -> termcolor::ColorSpec { + let mut cs = termcolor::ColorSpec::new(); + let fg = rustc.fg().and_then(rustc_color_compat); + cs.set_fg(fg); + let bg = rustc.bg().and_then(rustc_color_compat); + cs.set_bg(bg); + cs.set_bold(rustc.bold()); + cs.set_intense(rustc.intense()); + cs.set_underline(rustc.underline()); + cs.set_dimmed(rustc.dimmed()); + cs.set_italic(rustc.italic()); + cs.set_reset(rustc.reset()); + cs.set_strikethrough(rustc.strikethrough()); + cs +} + +fn rustc_color_compat(rustc: &RustColor) -> Option { + let col = match rustc { + RustColor::Black => termcolor::Color::Black, + RustColor::Blue => termcolor::Color::Blue, + RustColor::Green => termcolor::Color::Green, + RustColor::Red => termcolor::Color::Red, + RustColor::Cyan => termcolor::Color::Cyan, + RustColor::Magenta => termcolor::Color::Magenta, + RustColor::Yellow => termcolor::Color::Yellow, + RustColor::White => termcolor::Color::White, + RustColor::Ansi256(c) => termcolor::Color::Ansi256(*c), + RustColor::Rgb(r, g, b) => termcolor::Color::Rgb(*r, *g, *b), + _ => return None, + }; + Some(col) +} + +#[macro_export] +macro_rules! buf_print { + ($pb: expr, $($arg:tt)*) => {{ + let mut msg_buf = Vec::new(); + let _ = write!(&mut msg_buf, $($arg)*); + $pb.push_msg($crate::print::PrintMessage::Stdout(msg_buf)); + }}; +} + +#[macro_export] +macro_rules! buf_println { + ($pb: expr, $($arg:tt)*) => {{ + let mut msg_buf = Vec::new(); + let _ = writeln!(&mut msg_buf, $($arg)*); + $pb.push_msg($crate::print::PrintMessage::Stdout(msg_buf)); + }}; +} + +#[macro_export] +macro_rules! buf_eprintln { + ($pb: expr, $($arg:tt)*) => {{ + let mut msg_buf = Vec::new(); + let _ = writeln!(&mut msg_buf, $($arg)*); + $pb.push_msg($crate::print::PrintMessage::StdErr(msg_buf)); + }}; +} + +#[macro_export] +macro_rules! buf_term_println { + ($pb: expr, $col:expr, $($arg:tt)*) => {{ + let mut msg_buf = Vec::new(); + let _ = writeln!(&mut msg_buf, $($arg)*); + $pb.push_msg( + $crate::print::PrintMessage::Term($crate::print::TermMessage::new(msg_buf, $col)) + ); + }}; +} + +pub enum PrintMessage { + Stdout(Vec), + StdErr(Vec), + Term(TermMessage), + RustcErrTerm(RustcErrTermMessage), +} + +pub struct TermMessage { + message: Vec, + color: Option, +} + +impl TermMessage { + pub fn new(message: Vec, color: Option) -> Self { + Self { message, color } + } +} + +pub struct RustcErrTermMessage { + message: Vec, + color: Option, +} + +impl RustcErrTermMessage { + pub fn new(message: Vec, color: Option) -> Self { + Self { message, color } + } +} diff --git a/src/reorder.rs b/src/reorder.rs index 3e14f9f1272..c6d999d9ce3 100644 --- a/src/reorder.rs +++ b/src/reorder.rs @@ -59,7 +59,7 @@ fn wrap_reorderable_items( let fmt = ListFormatting::new(shape, context.config) .separator("") .align_comments(false); - write_list(list_items, &fmt) + write_list(list_items, &fmt, context.printer) } fn rewrite_reorderable_item( diff --git a/src/rewrite.rs b/src/rewrite.rs index 4a3bd129d16..2a25c556c80 100644 --- a/src/rewrite.rs +++ b/src/rewrite.rs @@ -8,6 +8,7 @@ use rustc_span::Span; use crate::config::{Config, IndentStyle}; use crate::parse::session::ParseSess; +use crate::print::Printer; use crate::shape::Shape; use crate::skip::SkipContext; use crate::visitor::SnippetProvider; @@ -43,6 +44,7 @@ pub(crate) struct RewriteContext<'a> { pub(crate) report: FormatReport, pub(crate) skip_context: SkipContext, pub(crate) skipped_range: Rc>>, + pub(crate) printer: &'a Printer, } pub(crate) struct InsideMacroGuard { diff --git a/src/rustfmt_diff.rs b/src/rustfmt_diff.rs index c9883452185..1515febdf3a 100644 --- a/src/rustfmt_diff.rs +++ b/src/rustfmt_diff.rs @@ -1,9 +1,10 @@ +use crate::buf_term_println; use std::collections::VecDeque; use std::fmt; -use std::io; use std::io::Write; -use crate::config::{Color, Config, Verbosity}; +use crate::config::{Config, Verbosity}; +use crate::print::Printer; #[derive(Debug, PartialEq)] pub(crate) enum DiffLine { @@ -139,43 +140,6 @@ impl std::str::FromStr for ModifiedLines { } } -// This struct handles writing output to stdout and abstracts away the logic -// of printing in color, if it's possible in the executing environment. -pub(crate) struct OutputWriter { - terminal: Option>>, -} - -impl OutputWriter { - // Create a new OutputWriter instance based on the caller's preference - // for colorized output and the capabilities of the terminal. - pub(crate) fn new(color: Color) -> Self { - if let Some(t) = term::stdout() { - if color.use_colored_tty() && t.supports_color() { - return OutputWriter { terminal: Some(t) }; - } - } - OutputWriter { terminal: None } - } - - // Write output in the optionally specified color. The output is written - // in the specified color if this OutputWriter instance contains a - // Terminal in its `terminal` field. - pub(crate) fn writeln(&mut self, msg: &str, color: Option) { - match &mut self.terminal { - Some(ref mut t) => { - if let Some(color) = color { - t.fg(color).unwrap(); - } - writeln!(t, "{msg}").unwrap(); - if color.is_some() { - t.reset().unwrap(); - } - } - None => println!("{msg}"), - } - } -} - // Produces a diff between the expected output and actual output of rustfmt. pub(crate) fn make_diff(expected: &str, actual: &str, context_size: usize) -> Vec { let mut line_number = 1; @@ -245,34 +209,34 @@ pub(crate) fn make_diff(expected: &str, actual: &str, context_size: usize) -> Ve results } -pub(crate) fn print_diff(diff: Vec, get_section_title: F, config: &Config) -where +pub(crate) fn print_diff( + diff: Vec, + get_section_title: F, + config: &Config, + printer: &Printer, +) where F: Fn(u32) -> String, { - let color = config.color(); let line_terminator = if config.verbose() == Verbosity::Verbose { "⏎" } else { "" }; - let mut writer = OutputWriter::new(color); - for mismatch in diff { let title = get_section_title(mismatch.line_number_orig); - writer.writeln(&title, None); + buf_term_println!(printer, None, "{title}"); for line in mismatch.lines { match line { DiffLine::Context(ref str) => { - writer.writeln(&format!(" {str}{line_terminator}"), None) + buf_term_println!(printer, None, " {str}{line_terminator}"); + } + DiffLine::Expected(ref str) => { + buf_term_println!(printer, Some(term::color::GREEN), "+{str}{line_terminator}"); } - DiffLine::Expected(ref str) => writer.writeln( - &format!("+{str}{line_terminator}"), - Some(term::color::GREEN), - ), DiffLine::Resulting(ref str) => { - writer.writeln(&format!("-{str}{line_terminator}"), Some(term::color::RED)) + buf_term_println!(printer, Some(term::color::RED), "-{str}{line_terminator}"); } } } diff --git a/src/source_file.rs b/src/source_file.rs index 8f66def3df3..f0da963752d 100644 --- a/src/source_file.rs +++ b/src/source_file.rs @@ -30,7 +30,8 @@ pub(crate) fn write_all_files( where T: Write, { - let mut emitter = create_emitter(config); + let printer = crate::print::Printer::no_color(); + let mut emitter = create_emitter(config, &printer); emitter.emit_header(out)?; for (filename, text) in source_file { diff --git a/src/test/configuration_snippet.rs b/src/test/configuration_snippet.rs index 857fff5cf6b..d8b170bf338 100644 --- a/src/test/configuration_snippet.rs +++ b/src/test/configuration_snippet.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use super::{print_mismatches, write_message, DIFF_CONTEXT_SIZE}; use crate::config::{Config, EmitMode, Verbosity}; +use crate::print::Printer; use crate::rustfmt_diff::{make_diff, Mismatch}; use crate::{Input, Session}; @@ -197,7 +198,8 @@ impl ConfigCodeBlock { let mut buf: Vec = vec![]; { - let mut session = Session::new(config, Some(&mut buf)); + let printer = Printer::no_color(); + let mut session = Session::new(config, Some(&mut buf), &printer); session.format(input).unwrap(); if self.has_parsing_errors(&session) { return false; diff --git a/src/test/mod.rs b/src/test/mod.rs index b2b3df8f4ad..c6252168270 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -11,10 +11,11 @@ use std::thread; use crate::config::{Color, Config, EmitMode, FileName, NewlineStyle}; use crate::formatting::{ReportedErrors, SourceFile}; -use crate::rustfmt_diff::{make_diff, print_diff, DiffLine, Mismatch, ModifiedChunk, OutputWriter}; -use crate::source_file; +use crate::rustfmt_diff::{make_diff, print_diff, DiffLine, Mismatch, ModifiedChunk}; +use crate::{buf_term_println, source_file}; use crate::{is_nightly_channel, FormatReport, FormatReportFormatterBuilder, Input, Session}; +use crate::print::Printer; use rustfmt_config_proc_macro::nightly_only_test; mod configuration_snippet; @@ -168,8 +169,9 @@ fn verify_config_test_names() { // using only one or the other will cause the output order to differ when // `print_diff` selects the approach not used. fn write_message(msg: &str) { - let mut writer = OutputWriter::new(Color::Auto); - writer.writeln(msg, None); + let print = Printer::new(Color::Auto); + buf_term_println!(print, None, "{msg}"); + print.write_to_outputs().unwrap(); } // Integration tests. The files in `tests/source` are formatted and compared @@ -237,7 +239,8 @@ fn modified_test() { .emit_mode(crate::config::EmitMode::ModifiedLines); { - let mut session = Session::new(config, Some(&mut data)); + let printer = Printer::no_color(); + let mut session = Session::new(config, Some(&mut data), &printer); session.format(Input::File(filename.into())).unwrap(); } @@ -329,7 +332,8 @@ fn assert_stdin_output( // Populate output by writing to a vec. let mut buf: Vec = vec![]; { - let mut session = Session::new(config, Some(&mut buf)); + let printer = Printer::no_color(); + let mut session = Session::new(config, Some(&mut buf), &printer); session.format(input).unwrap(); let errors = ReportedErrors { has_diff, @@ -422,7 +426,8 @@ fn format_files_find_new_files_via_cfg_if() { ]; let config = Config::default(); - let mut session = Session::::new(config, None); + let printer = Printer::no_color(); + let mut session = Session::::new(config, None, &printer); let mut write_result = HashMap::new(); for file in files { @@ -456,7 +461,8 @@ fn stdin_formatting_smoke_test() { config.set().emit_mode(EmitMode::Stdout); let mut buf: Vec = vec![]; { - let mut session = Session::new(config, Some(&mut buf)); + let printer = Printer::no_color(); + let mut session = Session::new(config, Some(&mut buf), &printer); session.format(input).unwrap(); assert!(session.has_no_errors()); } @@ -473,7 +479,8 @@ fn stdin_parser_panic_caught() { // See issue #3239. for text in ["{", "}"].iter().cloned().map(String::from) { let mut buf = vec![]; - let mut session = Session::new(Default::default(), Some(&mut buf)); + let printer = Printer::no_color(); + let mut session = Session::new(Default::default(), Some(&mut buf), &printer); let _ = session.format(Input::Text(text)); assert!(session.has_parsing_errors()); @@ -494,7 +501,8 @@ fn stdin_works_with_modified_lines() { config.set().emit_mode(EmitMode::ModifiedLines); let mut buf: Vec = vec![]; { - let mut session = Session::new(config, Some(&mut buf)); + let printer = Printer::no_color(); + let mut session = Session::new(config, Some(&mut buf), &printer); session.format(input).unwrap(); let errors = ReportedErrors { has_diff: true, @@ -563,7 +571,8 @@ fn stdin_generated_files_issue_5172() { config.set().newline_style(NewlineStyle::Unix); let mut buf: Vec = vec![]; { - let mut session = Session::new(config, Some(&mut buf)); + let printer = Printer::no_color(); + let mut session = Session::new(config, Some(&mut buf), &printer); session.format(input).unwrap(); assert!(session.has_no_errors()); } @@ -605,7 +614,8 @@ fn format_lines_errors_are_reported() { let input = Input::Text(format!("fn {long_identifier}() {{}}")); let mut config = Config::default(); config.set().error_on_line_overflow(true); - let mut session = Session::::new(config, None); + let printer = Printer::no_color(); + let mut session = Session::::new(config, None, &printer); session.format(input).unwrap(); assert!(session.has_formatting_errors()); } @@ -618,7 +628,8 @@ fn format_lines_errors_are_reported_with_tabs() { let mut config = Config::default(); config.set().error_on_line_overflow(true); config.set().hard_tabs(true); - let mut session = Session::::new(config, None); + let printer = Printer::no_color(); + let mut session = Session::::new(config, None, &printer); session.format(input).unwrap(); assert!(session.has_formatting_errors()); } @@ -667,7 +678,12 @@ fn print_mismatches_default_message(result: HashMap>) { for (file_name, diff) in result { let mismatch_msg_formatter = |line_num| format!("\nMismatch at {}:{}:", file_name.display(), line_num); - print_diff(diff, &mismatch_msg_formatter, &Default::default()); + print_diff( + diff, + &mismatch_msg_formatter, + &Default::default(), + &Printer::no_color(), + ); } if let Some(mut t) = term::stdout() { @@ -680,7 +696,12 @@ fn print_mismatches String>( mismatch_msg_formatter: T, ) { for (_file_name, diff) in result { - print_diff(diff, &mismatch_msg_formatter, &Default::default()); + print_diff( + diff, + &mismatch_msg_formatter, + &Default::default(), + &Printer::no_color(), + ); } if let Some(mut t) = term::stdout() { @@ -714,7 +735,8 @@ fn read_config(filename: &Path) -> Config { fn format_file>(filepath: P, config: Config) -> (bool, SourceFile, FormatReport) { let filepath = filepath.into(); let input = Input::File(filepath); - let mut session = Session::::new(config, None); + let printer = Printer::no_color(); + let mut session = Session::::new(config, None, &printer); let result = session.format(input).unwrap(); let parsing_errors = session.has_parsing_errors(); let mut source_file = SourceFile::new(); diff --git a/src/test/mod_resolver.rs b/src/test/mod_resolver.rs index aacb2acc684..1e0d831012a 100644 --- a/src/test/mod_resolver.rs +++ b/src/test/mod_resolver.rs @@ -3,12 +3,14 @@ use std::path::PathBuf; use super::read_config; +use crate::print::Printer; use crate::{FileName, Input, Session}; fn verify_mod_resolution(input_file_name: &str, exp_misformatted_files: &[&str]) { let input_file = PathBuf::from(input_file_name); let config = read_config(&input_file); - let mut session = Session::::new(config, None); + let printer = Printer::no_color(); + let mut session = Session::::new(config, None, &printer); let report = session .format(Input::File(input_file_name.into())) .expect("Should not have had any execution errors"); diff --git a/src/test/parser.rs b/src/test/parser.rs index ae4a4f94d92..167c27f6df3 100644 --- a/src/test/parser.rs +++ b/src/test/parser.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use super::read_config; use crate::modules::{ModuleResolutionError, ModuleResolutionErrorKind}; +use crate::print::Printer; use crate::{ErrorKind, Input, Session}; #[test] @@ -13,7 +14,8 @@ fn parser_errors_in_submods_are_surfaced() { let input_file = PathBuf::from(filename); let exp_mod_name = "invalid"; let config = read_config(&input_file); - let mut session = Session::::new(config, None); + let printer = Printer::no_color(); + let mut session = Session::::new(config, None, &printer); if let Err(ErrorKind::ModuleResolutionError(ModuleResolutionError { module, kind })) = session.format(Input::File(filename.into())) { @@ -37,7 +39,8 @@ fn parser_errors_in_submods_are_surfaced() { fn assert_parser_error(filename: &str) { let file = PathBuf::from(filename); let config = read_config(&file); - let mut session = Session::::new(config, None); + let printer = Printer::no_color(); + let mut session = Session::::new(config, None, &printer); let _ = session.format(Input::File(filename.into())).unwrap(); assert!(session.has_parsing_errors()); } diff --git a/src/types.rs b/src/types.rs index cd2582e66be..cec07936e3e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -365,7 +365,7 @@ where .trailing_separator(trailing_separator) .ends_with_newline(tactic.ends_with_newline(context.config.indent_style())) .preserve_newline(true); - (write_list(&item_vec, &fmt)?, tactic) + (write_list(&item_vec, &fmt, context.printer)?, tactic) }; let args = if tactic == DefinitiveListTactic::Horizontal diff --git a/src/vertical.rs b/src/vertical.rs index a06bc995aa5..f3c20c9e806 100644 --- a/src/vertical.rs +++ b/src/vertical.rs @@ -267,7 +267,7 @@ fn rewrite_aligned_items_inner( .tactic(tactic) .trailing_separator(separator_tactic) .preserve_newline(true); - write_list(&items, &fmt) + write_list(&items, &fmt, context.printer) } /// Returns the index in `fields` up to which a field belongs to the current group. diff --git a/src/visitor.rs b/src/visitor.rs index 61e147ed8f5..c1731b000c2 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -17,6 +17,7 @@ use crate::items::{ use crate::macros::{macro_style, rewrite_macro, rewrite_macro_def, MacroPosition}; use crate::modules::Module; use crate::parse::session::ParseSess; +use crate::print::Printer; use crate::rewrite::{Rewrite, RewriteContext}; use crate::shape::{Indent, Shape}; use crate::skip::{is_skip_attr, SkipContext}; @@ -87,6 +88,7 @@ pub(crate) struct FmtVisitor<'a> { pub(crate) report: FormatReport, pub(crate) skip_context: SkipContext, pub(crate) is_macro_def: bool, + pub(crate) printer: &'a Printer, } impl<'a> Drop for FmtVisitor<'a> { @@ -314,8 +316,13 @@ impl<'b, 'a: 'b> FmtVisitor<'a> { // put the other lines below it, shaping it as needed let other_lines = &sub_slice[offset + 1..]; - let comment_str = - rewrite_comment(other_lines, false, comment_shape, config); + let comment_str = rewrite_comment( + other_lines, + false, + comment_shape, + config, + self.printer, + ); match comment_str { Some(ref s) => self.push_str(s), None => self.push_str(other_lines), @@ -345,7 +352,8 @@ impl<'b, 'a: 'b> FmtVisitor<'a> { self.push_str(&self.block_indent.to_string_with_newline(config)); } - let comment_str = rewrite_comment(&sub_slice, false, comment_shape, config); + let comment_str = + rewrite_comment(&sub_slice, false, comment_shape, config, self.printer); match comment_str { Some(ref s) => self.push_str(s), None => self.push_str(&sub_slice), @@ -757,6 +765,7 @@ impl<'b, 'a: 'b> FmtVisitor<'a> { ctx.parse_sess, ctx.config, ctx.snippet_provider, + ctx.printer, ctx.report.clone(), ); visitor.skip_context.update(ctx.skip_context.clone()); @@ -768,6 +777,7 @@ impl<'b, 'a: 'b> FmtVisitor<'a> { parse_session: &'a ParseSess, config: &'a Config, snippet_provider: &'a SnippetProvider, + printer: &'a Printer, report: FormatReport, ) -> FmtVisitor<'a> { let mut skip_context = SkipContext::default(); @@ -794,6 +804,7 @@ impl<'b, 'a: 'b> FmtVisitor<'a> { macro_rewrite_failure: false, report, skip_context, + printer, } } @@ -1014,6 +1025,7 @@ impl<'b, 'a: 'b> FmtVisitor<'a> { report: self.report.clone(), skip_context: self.skip_context.clone(), skipped_range: self.skipped_range.clone(), + printer: self.printer, } } } diff --git a/tests/rustfmt/main.rs b/tests/rustfmt/main.rs index 11fb4786e82..76214b8196e 100644 --- a/tests/rustfmt/main.rs +++ b/tests/rustfmt/main.rs @@ -176,7 +176,11 @@ fn rustfmt_emits_error_on_line_overflow_true() { #[test] #[allow(non_snake_case)] fn dont_emit_ICE() { - let files = ["tests/target/issue_5728.rs", "tests/target/issue_5729.rs", "tests/target/issue_6069.rs"]; + let files = [ + "tests/target/issue_5728.rs", + "tests/target/issue_5729.rs", + "tests/target/issue_6069.rs", + ]; for file in files { let args = [file];