diff --git a/Cargo.toml b/Cargo.toml index a16620ed99b..80a31205cbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,10 @@ edition = "2021" name = "rustfmt" path = "src/bin/main.rs" +[[bin]] +name = "quiet-test" +path = "src/bin/quiet.rs" + [[bin]] name = "cargo-fmt" path = "src/cargo-fmt/main.rs" diff --git a/src/bin/quiet.rs b/src/bin/quiet.rs new file mode 100644 index 00000000000..d63f5133ac4 --- /dev/null +++ b/src/bin/quiet.rs @@ -0,0 +1,117 @@ +use std::collections::HashMap; +use std::fs; +use std::io::{BufRead, BufReader, Write}; +use std::path::{Path, PathBuf}; + +use rustfmt_nightly::{load_config, CliOptions, Config, Input, Session}; + +fn main() { + let mut args = std::env::args(); + let Some(_arg0) = args.next() else { + std::process::exit(1); + }; + let Some(filename) = args.next() else { + std::process::exit(1); + }; + let filename: PathBuf = filename.into(); + let opt_config = args.next().map(PathBuf::from); + + let config = if let Some(ref config_file_path) = opt_config { + load_config(Some(config_file_path), None::) + .expect("`rustfmt.toml` not found") + .0 + } else { + read_config(&filename) + }; + + let input = Input::File(filename); + let mut session = Session::::new(config, None); + let _ = session.format(input).unwrap(); +} + +struct Blackhole; +impl Write for Blackhole { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +struct NullOptions; + +impl CliOptions for NullOptions { + fn apply_to(self, _: &mut Config) { + unreachable!(); + } + fn config_path(&self) -> Option<&Path> { + unreachable!(); + } +} + +fn read_config(filename: &Path) -> Config { + let sig_comments = read_significant_comments(filename); + // Look for a config file. If there is a 'config' property in the significant comments, use + // that. Otherwise, if there are no significant comments at all, look for a config file with + // the same name as the test file. + let mut config = if !sig_comments.is_empty() { + load_config( + sig_comments.get("config").map(Path::new), + None::, + ) + .map(|(config, _)| config) + .unwrap_or_default() + } else { + load_config( + filename.with_extension("toml").file_name().map(Path::new), + None::, + ) + .map(|(config, _)| config) + .unwrap_or_default() + }; + + for (key, val) in &sig_comments { + if key != "target" && key != "config" && key != "unstable" { + config.override_value(key, val); + } + } + + config +} + +// Reads significant comments of the form: `// rustfmt-key: value` into a hash map. +fn read_significant_comments(file_name: &Path) -> HashMap { + let file = fs::File::open(file_name) + .unwrap_or_else(|_| panic!("couldn't read file {}", file_name.display())); + let reader = BufReader::new(file); + let pattern = r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)"; + let regex = regex::Regex::new(pattern).expect("failed creating pattern 1"); + + // Matches lines containing significant comments or whitespace. + let line_regex = regex::Regex::new(r"(^\s*$)|(^\s*//\s*rustfmt-[^:]+:\s*\S+)") + .expect("failed creating pattern 2"); + + reader + .lines() + .map(|line| line.expect("failed getting line")) + .filter(|line| line_regex.is_match(line)) + .filter_map(|line| { + regex.captures_iter(&line).next().map(|capture| { + ( + capture + .get(1) + .expect("couldn't unwrap capture") + .as_str() + .to_owned(), + capture + .get(2) + .expect("couldn't unwrap capture") + .as_str() + .to_owned(), + ) + }) + }) + .collect() +} diff --git a/src/test/mod.rs b/src/test/mod.rs index 7c563801c32..918d230b9ff 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -661,12 +661,15 @@ fn check_files(files: Vec, opt_config: &Option) -> (Vec { print!("{}", FormatReportFormatterBuilder::new(report).build()); fails += 1; + continue; } Ok(report) => reports.push(report), Err(err) => { @@ -674,15 +677,65 @@ fn check_files(files: Vec, opt_config: &Option) -> (Vec {} + Err(QuietError::Fail { output }) => { + println!( + "Unexpected error from rustfmt when formatting `{file_name}`:\n{output}\n", + file_name = file_name.display(), + ); + fails += 1; + continue; + } + Err(QuietError::Erroneous { output }) => { + println!( + "Erroneous output from rustfmt when formatting `{file_name}`:\n{output}\n", + file_name = file_name.display(), + ); + fails += 1; + continue; + } + } } (reports, count, fails) } +enum QuietError { + Erroneous { output: String }, + Fail { output: String }, +} + +fn quiet_test(file_name: &Path, opt_config: Option<&Path>) -> Result<(), QuietError> { + let cargo = PathBuf::from(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); + let cmd = Command::new(cargo) + .args(["run", "--bin", "quiet-test"]) + .arg(file_name) + .args(opt_config) + .output(); + match cmd { + Ok(cmd) => { + let output = String::from_utf8_lossy(&cmd.stdout).into_owned(); + if !cmd.status.success() { + Err(QuietError::Fail { + output: format!("non-success error code"), + }) + } else if !output.is_empty() { + Err(QuietError::Erroneous { output }) + } else { + Ok(()) + } + } + Err(e) => Err(QuietError::Fail { + output: e.to_string(), + }), + } +} + fn print_mismatches_default_message(result: HashMap>) { for (file_name, diff) in result { let mismatch_msg_formatter = @@ -708,7 +761,7 @@ fn print_mismatches String>( } } -fn read_config(filename: &Path) -> Config { +pub fn read_config(filename: &Path) -> Config { let sig_comments = read_significant_comments(filename); // Look for a config file. If there is a 'config' property in the significant comments, use // that. Otherwise, if there are no significant comments at all, look for a config file with