diff --git a/Configurations.md b/Configurations.md index 8b96b9d3689..bc6d891bc92 100644 --- a/Configurations.md +++ b/Configurations.md @@ -17,6 +17,15 @@ To enable unstable options, set `unstable_features = true` in `rustfmt.toml` or Below you find a detailed visual guide on all the supported configuration options of rustfmt: +## `abort_on_unrecognised_options` + +Exit early when using nightly only options on the stable channel + +- **Default value**: `false` +- **Possible values**: `true`, `false` +- **Stable**: No (tracking issue: [#5022](https://github.com/rust-lang/rustfmt/issues/5022)) + + ## `array_width` Maximum width of an array literal before falling back to vertical formatting. diff --git a/src/config/config_type.rs b/src/config/config_type.rs index 26d57a13791..81efab305cc 100644 --- a/src/config/config_type.rs +++ b/src/config/config_type.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::config::file_lines::FileLines; use crate::config::options::{IgnoreList, WidthHeuristics}; @@ -58,6 +60,82 @@ impl ConfigType for IgnoreList { } } +/// Store a map of all Unstable options used in in the configuration. +#[derive(Clone, Debug)] +pub struct UnstableOptions { + pub(crate) options: HashMap<&'static str, String>, +} + +impl std::fmt::Display for UnstableOptions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(message) = self.abort_message() { + write!(f, "{}", message) + } else { + write!(f, "No unstable options were used.") + } + } +} + +impl UnstableOptions { + /// Create a new UnstableOptions struct + pub(crate) fn new() -> Self { + Self { + options: HashMap::new(), + } + } + + /// Insert an unstable option and a user supplied value for that unstable option + pub(crate) fn insert(&mut self, option: &'static str, user_supplied_value: String) { + self.options.insert(option, user_supplied_value); + } + + /// Check if any unstable options have been set + pub(crate) fn has_unstable_options(&self) -> bool { + !self.options.is_empty() + } + + /// Generate the Warning message + pub(crate) fn warning_message(&self) -> Option { + if self.options.is_empty() { + return None; + } + let mut result = String::new(); + + for (k, v) in self.options.iter() { + result.push_str(&format!( + "Warning: can't set `{} = {}`, unstable features are only \ + available in nightly channel.\n", + k, v, + )); + } + + let upgrade_to_abort_message = "\nSet `abort_on_unrecognised_options = true` \ + to convert this warning into an error\n\n"; + + result.push_str(upgrade_to_abort_message); + + Some(result) + } + + /// Generate the Abort message + pub(crate) fn abort_message(&self) -> Option { + if self.options.is_empty() { + return None; + } + + let mut result = String::new(); + result.push_str("Can't set nightly options when using stable rustfmt\n"); + + for (k, v) in self.options.iter() { + result.push_str(&format!(" - `{} = {}`\n", k, v)); + } + let to_warning_message = "\nSet `abort_on_unrecognised_options = false` \ + to convert this error into a warning\n\n"; + result.push_str(to_warning_message); + Some(result) + } +} + macro_rules! create_config { // Options passed in to the macro. // @@ -76,6 +154,8 @@ macro_rules! create_config { #[derive(Clone)] #[allow(unreachable_pub)] pub struct Config { + // Unstable Options specified on the stable channel + configured_unstable_options: UnstableOptions, // For each config item, we store: // // - 0: true if the value has been access @@ -159,12 +239,41 @@ macro_rules! create_config { ConfigWasSet(self) } + /// Insert all unstable options and their values into the UnstableOptions struct. + /// The only exception is the "abort_on_unrecognised_options", which helps + /// determine if we should abort or warn when using unstable options on stable rustfmt + #[allow(unreachable_pub)] + pub fn insert_unstable_options(&mut self, option: &'static str, value: String) { + if option == "abort_on_unrecognised_options" { + return + } + + match option { + $( + stringify!($i) => { + // If its an unstable option then add it to the unstable list + if !self.$i.3 { + self.configured_unstable_options.insert(option, value); + } + } + )+ + _ => panic!("Unknown config key in override: {}", option) + } + + } + fn fill_from_parsed_config(mut self, parsed: PartialConfig, dir: &Path) -> Config { $( if let Some(option_value) = parsed.$i { let option_stable = self.$i.3; + if !option_stable || !option_value.stable_variant() { + self.insert_unstable_options( + stringify!($i), format!("{:?}", &option_value) + ); + } + if $crate::config::config_type::is_stable_option_and_value( - stringify!($i), option_stable, &option_value + option_stable, &option_value ) { self.$i.1 = true; self.$i.2 = option_value; @@ -228,6 +337,12 @@ macro_rules! create_config { } } + /// Get a reference to the UnstableOptions set on the configuration. + #[allow(unreachable_pub)] + pub fn unstable_options(&self) -> &UnstableOptions { + &self.configured_unstable_options + } + #[allow(unreachable_pub)] pub fn override_value(&mut self, key: &str, val: &str) { @@ -439,6 +554,7 @@ macro_rules! create_config { impl Default for Config { fn default() -> Config { Config { + configured_unstable_options: UnstableOptions::new(), $( $i: (Cell::new(false), false, $def, $stb), )+ @@ -448,11 +564,7 @@ macro_rules! create_config { ) } -pub(crate) fn is_stable_option_and_value( - option_name: &str, - option_stable: bool, - option_value: &T, -) -> bool +pub(crate) fn is_stable_option_and_value(option_stable: bool, option_value: &T) -> bool where T: PartialEq + std::fmt::Debug + ConfigType, { @@ -460,23 +572,9 @@ where let variant_stable = option_value.stable_variant(); match (nightly, option_stable, variant_stable) { // Stable with an unstable option - (false, false, _) => { - eprintln!( - "Warning: can't set `{} = {:?}`, unstable features are only \ - available in nightly channel.", - option_name, option_value - ); - false - } + (false, false, _) => false, // Stable with a stable option, but an unstable variant - (false, true, false) => { - eprintln!( - "Warning: can't set `{} = {:?}`, unstable variants are only \ - available in nightly channel.", - option_name, option_value - ); - false - } + (false, true, false) => false, // Nightly: everything allowed // Stable with stable option and variant: allowed (true, _, _) | (false, true, true) => true, diff --git a/src/config/mod.rs b/src/config/mod.rs index eaada8db090..b156615cdfb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -9,6 +9,8 @@ use thiserror::Error; use crate::config::config_type::ConfigType; #[allow(unreachable_pub)] +pub use crate::config::config_type::UnstableOptions; +#[allow(unreachable_pub)] pub use crate::config::file_lines::{FileLines, FileName, Range}; #[allow(unreachable_pub)] pub use crate::config::lists::*; @@ -164,6 +166,8 @@ create_config! { or they are left with trailing whitespaces"; ignore: IgnoreList, IgnoreList::default(), false, "Skip formatting the specified files and directories"; + abort_on_unrecognised_options: bool, false, false, + "Exit early when using nightly only options on the stable channel"; // Not user-facing verbose: Verbosity, Verbosity::Normal, false, "How much to information to emit to the user"; @@ -586,6 +590,59 @@ mod test { assert_eq!(s.contains(PRINT_DOCS_PARTIALLY_UNSTABLE_OPTION), true); } + #[stable_only_test] + #[test] + fn test_get_all_unstable_options_set_in_toml() { + use std::collections::HashMap; + let toml = r#" + reorder_impl_items = true + "#; + let config = Config::from_toml(toml, Path::new("")).unwrap(); + let mut expected = HashMap::new(); + expected.insert("reorder_impl_items", String::from("true")); + assert_eq!(config.unstable_options().options, expected); + } + + #[stable_only_test] + #[test] + fn test_warning_message_when_using_unstable_options() { + let toml = r#" + reorder_impl_items = true + "#; + let config = Config::from_toml(toml, Path::new("")).unwrap(); + let warning = "\ +Warning: can't set `reorder_impl_items = true`, unstable features are only available in \ +nightly channel. + +Set `abort_on_unrecognised_options = true` to convert this warning into an error + +"; + assert_eq!( + warning, + config.unstable_options().warning_message().unwrap() + ) + } + + #[stable_only_test] + #[test] + fn test_abort_message_when_using_unstable_options() { + let toml = r#" + reorder_impl_items = true + "#; + let config = Config::from_toml(toml, Path::new("")).unwrap(); + let abort_message = "\ +Can't set nightly options when using stable rustfmt + - `reorder_impl_items = true` + +Set `abort_on_unrecognised_options = false` to convert this error into a warning + +"; + assert_eq!( + abort_message, + config.unstable_options().abort_message().unwrap() + ) + } + #[test] fn test_dump_default_config() { let default_config = format!( @@ -663,6 +720,7 @@ hide_parse_errors = false error_on_line_overflow = false error_on_unformatted = false ignore = [] +abort_on_unrecognised_options = false emit_mode = "Files" make_backup = false "#, diff --git a/src/format_report_formatter.rs b/src/format_report_formatter.rs index fd536d4df41..078d184215e 100644 --- a/src/format_report_formatter.rs +++ b/src/format_report_formatter.rs @@ -144,6 +144,7 @@ fn error_kind_to_snippet_annotation_type(error_kind: &ErrorKind) -> AnnotationTy | ErrorKind::LostComment | ErrorKind::BadAttr | ErrorKind::InvalidGlobPattern(_) + | ErrorKind::NightlyOnlyOptions(_) | ErrorKind::VersionMismatch => AnnotationType::Error, ErrorKind::DeprecatedAttr => AnnotationType::Warning, } diff --git a/src/formatting.rs b/src/formatting.rs index 1dfd8a514f0..4faa7226293 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -35,6 +35,20 @@ impl<'b, T: Write + 'b> Session<'b, T> { return Err(ErrorKind::VersionMismatch); } + if !crate::is_nightly_channel!() { + let using_unstalbe_options = self.config.unstable_options().has_unstable_options(); + let abort_on_unstable_options = self.config.abort_on_unrecognised_options(); + if using_unstalbe_options && abort_on_unstable_options { + return Err(ErrorKind::NightlyOnlyOptions( + self.config.unstable_options().clone(), + )); + } else if using_unstalbe_options && !abort_on_unstable_options { + if let Some(warning) = self.config.unstable_options().warning_message() { + eprintln!("{}", warning); + } + } + } + rustc_span::create_session_if_not_set_then(self.config.edition().into(), |_| { if self.config.disable_all_formatting() { // When the input is from stdin, echo back the input. diff --git a/src/lib.rs b/src/lib.rs index 495010a297d..54b1fd28b56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,7 @@ use crate::utils::indent_next_line; pub use crate::config::{ load_config, CliOptions, Color, Config, Edition, EmitMode, FileLines, FileName, NewlineStyle, - Range, Verbosity, + Range, UnstableOptions, Verbosity, }; pub use crate::format_report_formatter::{FormatReportFormatter, FormatReportFormatterBuilder}; @@ -133,6 +133,9 @@ pub enum ErrorKind { /// Invalid glob pattern in `ignore` configuration option. #[error("Invalid glob pattern found in ignore list: {0}")] InvalidGlobPattern(ignore::Error), + /// Using unstable, nightly only options on stable rustfmt. + #[error("{0}")] + NightlyOnlyOptions(UnstableOptions), } impl ErrorKind { diff --git a/tests/config/no_unstable_options.toml b/tests/config/no_unstable_options.toml new file mode 100644 index 00000000000..98268382207 --- /dev/null +++ b/tests/config/no_unstable_options.toml @@ -0,0 +1,3 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 4 \ No newline at end of file diff --git a/tests/config/some_unstable_options.toml b/tests/config/some_unstable_options.toml new file mode 100644 index 00000000000..39a4475a12f --- /dev/null +++ b/tests/config/some_unstable_options.toml @@ -0,0 +1,2 @@ +wrap_comments = true +unstable_features = true \ No newline at end of file diff --git a/tests/rustfmt/main.rs b/tests/rustfmt/main.rs index 636e3053e0f..5e00632ec9f 100644 --- a/tests/rustfmt/main.rs +++ b/tests/rustfmt/main.rs @@ -5,10 +5,10 @@ use std::fs::remove_file; use std::path::Path; use std::process::Command; -use rustfmt_config_proc_macro::rustfmt_only_ci_test; +use rustfmt_config_proc_macro::{nightly_only_test, rustfmt_only_ci_test, stable_only_test}; /// Run the rustfmt executable and return its output. -fn rustfmt(args: &[&str]) -> (String, String) { +fn rustfmt(args: &[&str]) -> (String, String, i32) { let mut bin_dir = env::current_exe().unwrap(); bin_dir.pop(); // chop off test exe name if bin_dir.ends_with("deps") { @@ -26,14 +26,15 @@ fn rustfmt(args: &[&str]) -> (String, String) { Ok(output) => ( String::from_utf8(output.stdout).expect("utf-8"), String::from_utf8(output.stderr).expect("utf-8"), + output.status.code().expect("should have exit status code"), ), Err(e) => panic!("failed to run `{:?} {:?}`: {}", cmd, args, e), } } macro_rules! assert_that { - ($args:expr, $($check:ident $check_args:tt)&&+) => { - let (stdout, stderr) = rustfmt($args); + ($args:expr, $($check:ident $check_args:tt)&&+, $exit_code:expr) => { + let (stdout, stderr, exit_code) = rustfmt($args); if $(!stdout.$check$check_args && !stderr.$check$check_args)||* { panic!( "Output not expected for rustfmt {:?}\n\ @@ -46,6 +47,13 @@ macro_rules! assert_that { stderr ); } + if $exit_code != exit_code { + panic!( + "rustfmt exited with a status code of {}. The expected status code was {}", + exit_code, + $exit_code, + ); + } }; } @@ -54,16 +62,22 @@ macro_rules! assert_that { fn print_config() { assert_that!( &["--print-config", "unknown"], - starts_with("Unknown print-config option") + starts_with("Unknown print-config option"), + 1 + ); + assert_that!( + &["--print-config", "default"], + contains("max_width = 100"), + 0 ); - assert_that!(&["--print-config", "default"], contains("max_width = 100")); - assert_that!(&["--print-config", "minimal"], contains("PATH required")); + assert_that!(&["--print-config", "minimal"], contains("PATH required"), 0); assert_that!( &["--print-config", "minimal", "minimal-config"], - contains("doesn't work with standard input.") + contains("doesn't work with standard input."), + 1 ); - let (stdout, stderr) = rustfmt(&[ + let (stdout, stderr, exit_code) = rustfmt(&[ "--print-config", "minimal", "minimal-config", @@ -76,6 +90,7 @@ fn print_config() { stderr ); remove_file("minimal-config").unwrap(); + assert_eq!(exit_code, 0); } #[rustfmt_only_ci_test] @@ -89,7 +104,8 @@ fn inline_config() { ".", "--config=color=Never,edition=2018" ], - contains("color = \"Never\"") && contains("edition = \"2018\"") + contains("color = \"Never\"") && contains("edition = \"2018\""), + 0 ); // multiple overriding invocations @@ -105,15 +121,182 @@ fn inline_config() { ], contains("color = \"Always\"") && contains("edition = \"2018\"") - && contains("format_strings = true") + && contains("format_strings = true"), + 0 + ); +} + +// `abort_on_unrecognized_options = true` causes stable rustfmt to exit early when +// unstable options are set in the configuration toml file +#[stable_only_test] +#[test] +fn abort_when_using_unstable_options_in_toml_config_while_on_stable_rustfmt() { + assert_that!( + &[ + "--check", + "--config-path", + "tests/config/some_unstable_options.toml", + "--config", + "abort_on_unrecognised_options=true", + "src/bin/main.rs", + ], + contains("Can't set nightly options when using stable rustfmt") + && contains("`wrap_comments = true`") + && contains("`unstable_features = true`"), + 1 + ); +} + +// `abort_on_unrecognized_options = true` has no impact when unstable options are passed +// via the comand line. The option only applies to toml configuration. +#[stable_only_test] +#[test] +fn do_not_abort_when_using_unstable_options_from_command_line_when_on_stable_rustfmt() { + assert_that!( + &[ + "--check", + "--config-path", + "tests/config/no_unstable_options.toml", + "--config", + "abort_on_unrecognised_options=true,wrap_comments=true", + "src/bin/main.rs", + ], + is_empty(), + 0 + ); +} + +// `abort_on_unrecognized_options = false` causes stable rustfmt to display warnings when +// unstable options are set in the configuration toml file +#[stable_only_test] +#[test] +fn warn_when_using_unstable_options_in_toml_file_when_on_stable_rustfmt() { + assert_that!( + &[ + "--check", + "--config-path", + "tests/config/some_unstable_options.toml", + "--config", + "abort_on_unrecognised_options=false", + "src/bin/main.rs", + ], + contains("Warning: can't set `wrap_comments = true`") + && contains("Warning: can't set `unstable_features = true`"), + 0 + ); +} + +// `abort_on_unrecognized_options = false` has no impact when unstable options are passed +// via the comand line. The option only applies to toml configuration. +#[stable_only_test] +#[test] +fn do_not_warn_when_using_unstable_options_from_command_line_when_on_stable_rustfmt() { + assert_that!( + &[ + "--check", + "--config-path", + "tests/config/no_unstable_options.toml", + "--config", + "abort_on_unrecognised_options=false,wrap_comments=true", + "src/bin/main.rs", + ], + is_empty(), + 0 + ); +} + +// `abort_on_unrecognized_options = true` does nothing when no unstable options are used +#[stable_only_test] +#[test] +fn do_not_abort_when_only_using_stable_options_in_toml_file_and_cli_when_on_stable_rustfmt() { + assert_that!( + &[ + "--check", + "--config-path", + "tests/config/no_unstable_options.toml", + "--config", + "abort_on_unrecognised_options=true,max_width=100", + "src/bin/main.rs", + ], + is_empty(), + 0 + ); +} + +// `abort_on_unrecognized_options = true` doesn't affect nightly rustfmt +#[nightly_only_test] +#[test] +fn ignore_abort_option_when_using_unstable_options_in_toml_file_while_on_nightly_rustfmt() { + assert_that!( + &[ + "--check", + "--config-path", + "tests/config/some_unstable_options.toml", + "--config", + "abort_on_unrecognised_options=true", + "src/bin/main.rs", + ], + is_empty(), + 0 + ); +} + +// `abort_on_unrecognized_options = true` doesn't affect nightly rustfmt +#[nightly_only_test] +#[test] +fn ignore_abort_option_when_using_unstable_options_from_command_line_when_on_nightly_rustfmt() { + assert_that!( + &[ + "--check", + "--config", + "abort_on_unrecognised_options=true,wrap_comments=true", + "src/bin/main.rs", + ], + is_empty(), + 0 + ); +} + +// `abort_on_unrecognized_options = false` doesn't affect nightly rustfmt +#[nightly_only_test] +#[test] +fn warn_when_using_unstable_options_in_toml_file_when_on_nightly_rustfmt() { + assert_that!( + &[ + "--check", + "--config-path", + "tests/config/some_unstable_options.toml", + "--config", + "abort_on_unrecognised_options=false", + "src/bin/main.rs", + ], + is_empty(), + 0 + ); +} + +// `abort_on_unrecognized_options = false` doesn't affect nightly rustfmt +#[nightly_only_test] +#[test] +fn warn_when_using_unstable_options_from_command_line_when_on_nightly_rustfmt() { + assert_that!( + &[ + "--check", + "--config", + "abort_on_unrecognised_options=false,wrap_comments=true", + "src/bin/main.rs", + ], + is_empty(), + 0 ); } #[test] fn rustfmt_usage_text() { let args = ["--help"]; - let (stdout, _) = rustfmt(&args); + let (stdout, _stderr, code) = rustfmt(&args); assert!(stdout.contains("Format Rust code\n\nusage: rustfmt [options] ...")); + assert_eq!(code, 0); } #[test] @@ -128,34 +311,38 @@ fn mod_resolution_error_multiple_candidate_files() { ); let args = ["tests/mod-resolver/issue-5167/src/lib.rs"]; - let (_stdout, stderr) = rustfmt(&args); - assert!(stderr.contains(&error_message)) + let (_stdout, stderr, code) = rustfmt(&args); + assert!(stderr.contains(&error_message)); + assert_eq!(code, 1); } #[test] fn mod_resolution_error_sibling_module_not_found() { let args = ["tests/mod-resolver/module-not-found/sibling_module/lib.rs"]; - let (_stdout, stderr) = rustfmt(&args); + let (_stdout, stderr, code) = rustfmt(&args); // Module resolution fails because we're unable to find `a.rs` in the same directory as lib.rs - assert!(stderr.contains("a.rs does not exist")) + assert!(stderr.contains("a.rs does not exist")); + assert_eq!(code, 1); } #[test] fn mod_resolution_error_relative_module_not_found() { let args = ["tests/mod-resolver/module-not-found/relative_module/lib.rs"]; - let (_stdout, stderr) = rustfmt(&args); + let (_stdout, stderr, code) = rustfmt(&args); // The file `./a.rs` and directory `./a` both exist. // Module resolution fails because we're unable to find `./a/b.rs` #[cfg(not(windows))] assert!(stderr.contains("a/b.rs does not exist")); #[cfg(windows)] assert!(stderr.contains("a\\b.rs does not exist")); + assert_eq!(code, 1); } #[test] fn mod_resolution_error_path_attribute_does_not_exist() { let args = ["tests/mod-resolver/module-not-found/bad_path_attribute/lib.rs"]; - let (_stdout, stderr) = rustfmt(&args); + let (_stdout, stderr, code) = rustfmt(&args); // The path attribute points to a file that does not exist assert!(stderr.contains("does_not_exist.rs does not exist")); + assert_eq!(code, 1); }