Skip to content

Commit 3b4c02f

Browse files
committed
Add new configuration option abort_on_unrecognised_options
This option was proposed in issue 5022 and allows rustfmt to exit early in the event that any unstable options are used while using stable rustfmt.
1 parent 34d374e commit 3b4c02f

File tree

9 files changed

+391
-23
lines changed

9 files changed

+391
-23
lines changed

Configurations.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ To enable unstable options, set `unstable_features = true` in `rustfmt.toml` or
1717

1818
Below you find a detailed visual guide on all the supported configuration options of rustfmt:
1919

20+
## `abort_on_unrecognised_options`
21+
22+
Exit early when using nightly only options on the stable channel
23+
24+
- **Default value**: `false`
25+
- **Possible values**: `true`, `false`
26+
- **Stable**: No (tracking issue: [#5022](https://github.com/rust-lang/rustfmt/issues/5022))
27+
28+
2029
## `array_width`
2130

2231
Maximum width of an array literal before falling back to vertical formatting.

src/config/config_type.rs

Lines changed: 114 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::HashMap;
2+
13
use crate::config::file_lines::FileLines;
24
use crate::config::options::{IgnoreList, WidthHeuristics};
35

@@ -50,6 +52,82 @@ impl ConfigType for IgnoreList {
5052
}
5153
}
5254

55+
/// Store a map of all Unstable options used in in the configuration.
56+
#[derive(Clone, Debug)]
57+
pub struct UnstableOptions {
58+
pub(crate) options: HashMap<&'static str, String>,
59+
}
60+
61+
impl std::fmt::Display for UnstableOptions {
62+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63+
if let Some(message) = self.abort_message() {
64+
write!(f, "{}", message)
65+
} else {
66+
write!(f, "No unstable options were used.")
67+
}
68+
}
69+
}
70+
71+
impl UnstableOptions {
72+
/// Create a new UnstableOptions struct
73+
pub(crate) fn new() -> Self {
74+
Self {
75+
options: HashMap::new(),
76+
}
77+
}
78+
79+
/// Insert an unstable option and a user supplied value for that unstable option
80+
pub(crate) fn insert(&mut self, option: &'static str, user_supplied_value: String) {
81+
self.options.insert(option, user_supplied_value);
82+
}
83+
84+
/// Check if any unstable options have been set
85+
pub(crate) fn has_unstable_options(&self) -> bool {
86+
!self.options.is_empty()
87+
}
88+
89+
/// Generate the Warning message
90+
pub(crate) fn warning_message(&self) -> Option<String> {
91+
if self.options.is_empty() {
92+
return None;
93+
}
94+
let mut result = String::new();
95+
96+
for (k, v) in self.options.iter() {
97+
result.push_str(&format!(
98+
"Warning: can't set `{} = {}`, unstable features are only \
99+
available in nightly channel.\n",
100+
k, v,
101+
));
102+
}
103+
104+
let upgrade_to_abort_message = "\nSet `abort_on_unrecognised_options = true` \
105+
to convert this warning into an error\n\n";
106+
107+
result.push_str(upgrade_to_abort_message);
108+
109+
Some(result)
110+
}
111+
112+
/// Generate the Abort message
113+
pub(crate) fn abort_message(&self) -> Option<String> {
114+
if self.options.is_empty() {
115+
return None;
116+
}
117+
118+
let mut result = String::new();
119+
result.push_str("Can't set nightly options when using stable rustfmt\n");
120+
121+
for (k, v) in self.options.iter() {
122+
result.push_str(&format!(" - `{} = {}`\n", k, v));
123+
}
124+
let to_warning_message = "\nSet `abort_on_unrecognised_options = false` \
125+
to convert this error into a warning\n\n";
126+
result.push_str(to_warning_message);
127+
Some(result)
128+
}
129+
}
130+
53131
macro_rules! create_config {
54132
($($i:ident: $ty:ty, $def:expr, $stb:expr, $( $dstring:expr ),+ );+ $(;)*) => (
55133
#[cfg(test)]
@@ -64,6 +142,8 @@ macro_rules! create_config {
64142
// if a license_template_path has been specified, successfully read, parsed and compiled
65143
// into a regex, it will be stored here
66144
pub license_template: Option<Regex>,
145+
// Unstable Options specified on the stable channel
146+
configured_unstable_options: UnstableOptions,
67147
// For each config item, we store a bool indicating whether it has
68148
// been accessed and the value, and a bool whether the option was
69149
// manually initialised, or taken from the default,
@@ -145,21 +225,35 @@ macro_rules! create_config {
145225
ConfigWasSet(self)
146226
}
147227

228+
/// Insert all unstable options and their values into the UnstableOptions struct.
229+
/// The only exception is the "abort_on_unrecognised_options", which helps
230+
/// determine if we should abort or warn when using unstable options on stable rustfmt
231+
#[allow(unreachable_pub)]
232+
pub fn insert_unstable_options(&mut self, option: &'static str, value: String) {
233+
if option == "abort_on_unrecognised_options" {
234+
return
235+
}
236+
237+
match option {
238+
$(
239+
stringify!($i) => {
240+
// If its an unstable option then add it to the unstable list
241+
if !self.$i.3 {
242+
self.configured_unstable_options.insert(option, value);
243+
}
244+
}
245+
)+
246+
_ => panic!("Unknown config key in override: {}", option)
247+
}
248+
249+
}
250+
148251
fn fill_from_parsed_config(mut self, parsed: PartialConfig, dir: &Path) -> Config {
149252
$(
150253
if let Some(val) = parsed.$i {
151-
if self.$i.3 {
152-
self.$i.1 = true;
153-
self.$i.2 = val;
154-
} else {
155-
if crate::is_nightly_channel!() {
156-
self.$i.1 = true;
157-
self.$i.2 = val;
158-
} else {
159-
eprintln!("Warning: can't set `{} = {:?}`, unstable features are only \
160-
available in nightly channel.", stringify!($i), val);
161-
}
162-
}
254+
self.insert_unstable_options(stringify!($i), format!("{:?}", &val));
255+
self.$i.1 = true;
256+
self.$i.2 = val;
163257
}
164258
)+
165259
self.set_heuristics();
@@ -220,6 +314,12 @@ macro_rules! create_config {
220314
}
221315
}
222316

317+
/// Get a reference to the UnstableOptions set on the configuration.
318+
#[allow(unreachable_pub)]
319+
pub fn unstable_options(&self) -> &UnstableOptions {
320+
&self.configured_unstable_options
321+
}
322+
223323
#[allow(unreachable_pub)]
224324
pub fn override_value(&mut self, key: &str, val: &str)
225325
{
@@ -232,6 +332,7 @@ macro_rules! create_config {
232332
stringify!($i),
233333
val,
234334
stringify!($ty)));
335+
self.insert_unstable_options(stringify!($i), val.to_owned());
235336
}
236337
)+
237338
_ => panic!("Unknown config key in override: {}", key)
@@ -438,6 +539,7 @@ macro_rules! create_config {
438539
fn default() -> Config {
439540
Config {
440541
license_template: None,
542+
configured_unstable_options: UnstableOptions::new(),
441543
$(
442544
$i: (Cell::new(false), false, $def, $stb),
443545
)+

src/config/mod.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use thiserror::Error;
1010

1111
use crate::config::config_type::ConfigType;
1212
#[allow(unreachable_pub)]
13+
pub use crate::config::config_type::UnstableOptions;
14+
#[allow(unreachable_pub)]
1315
pub use crate::config::file_lines::{FileLines, FileName, Range};
1416
#[allow(unreachable_pub)]
1517
pub use crate::config::lists::*;
@@ -168,6 +170,8 @@ create_config! {
168170
"Report all, none or unnumbered occurrences of FIXME in source file comments";
169171
ignore: IgnoreList, IgnoreList::default(), false,
170172
"Skip formatting the specified files and directories";
173+
abort_on_unrecognised_options: bool, false, false,
174+
"Exit early when using nightly only options on the stable channel";
171175

172176
// Not user-facing
173177
verbose: Verbosity, Verbosity::Normal, false, "How much to information to emit to the user";
@@ -403,6 +407,7 @@ fn config_path(options: &dyn CliOptions) -> Result<Option<PathBuf>, Error> {
403407
#[cfg(test)]
404408
mod test {
405409
use super::*;
410+
use std::collections::HashMap;
406411
use std::str;
407412

408413
use rustfmt_config_proc_macro::{nightly_only_test, stable_only_test};
@@ -545,6 +550,55 @@ mod test {
545550
assert!(config.license_template.is_none());
546551
}
547552

553+
#[test]
554+
fn test_get_all_unstable_options_set_in_toml() {
555+
let toml = r#"
556+
reorder_impl_items = true
557+
"#;
558+
let config = Config::from_toml(toml, Path::new("")).unwrap();
559+
let mut expected = HashMap::new();
560+
expected.insert("reorder_impl_items", String::from("true"));
561+
assert_eq!(config.unstable_options().options, expected);
562+
}
563+
564+
#[test]
565+
fn test_warning_message_when_using_unstable_options() {
566+
let toml = r#"
567+
reorder_impl_items = true
568+
"#;
569+
let config = Config::from_toml(toml, Path::new("")).unwrap();
570+
let warning = "\
571+
Warning: can't set `reorder_impl_items = true`, unstable features are only available in \
572+
nightly channel.
573+
574+
Set `abort_on_unrecognised_options = true` to convert this warning into an error
575+
576+
";
577+
assert_eq!(
578+
warning,
579+
config.unstable_options().warning_message().unwrap()
580+
)
581+
}
582+
583+
#[test]
584+
fn test_abort_message_when_using_unstable_options() {
585+
let toml = r#"
586+
reorder_impl_items = true
587+
"#;
588+
let config = Config::from_toml(toml, Path::new("")).unwrap();
589+
let abort_message = "\
590+
Can't set nightly options when using stable rustfmt
591+
- `reorder_impl_items = true`
592+
593+
Set `abort_on_unrecognised_options = false` to convert this error into a warning
594+
595+
";
596+
assert_eq!(
597+
abort_message,
598+
config.unstable_options().abort_message().unwrap()
599+
)
600+
}
601+
548602
#[test]
549603
fn test_dump_default_config() {
550604
let default_config = format!(
@@ -623,6 +677,7 @@ error_on_unformatted = false
623677
report_todo = "Never"
624678
report_fixme = "Never"
625679
ignore = []
680+
abort_on_unrecognised_options = false
626681
emit_mode = "Files"
627682
make_backup = false
628683
"#,

src/format_report_formatter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ fn error_kind_to_snippet_annotation_type(error_kind: &ErrorKind) -> AnnotationTy
144144
| ErrorKind::LicenseCheck
145145
| ErrorKind::BadAttr
146146
| ErrorKind::InvalidGlobPattern(_)
147+
| ErrorKind::NightlyOnlyOptions(_)
147148
| ErrorKind::VersionMismatch => AnnotationType::Error,
148149
ErrorKind::BadIssue(_) | ErrorKind::DeprecatedAttr => AnnotationType::Warning,
149150
}

src/formatting.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ impl<'b, T: Write + 'b> Session<'b, T> {
3737
return Err(ErrorKind::VersionMismatch);
3838
}
3939

40+
if !crate::is_nightly_channel!() {
41+
let using_unstalbe_options = self.config.unstable_options().has_unstable_options();
42+
let abort_on_unstable_options = self.config.abort_on_unrecognised_options();
43+
if using_unstalbe_options && abort_on_unstable_options {
44+
return Err(ErrorKind::NightlyOnlyOptions(
45+
self.config.unstable_options().clone(),
46+
));
47+
} else if using_unstalbe_options && !abort_on_unstable_options {
48+
if let Some(warning) = self.config.unstable_options().warning_message() {
49+
eprintln!("{}", warning);
50+
}
51+
}
52+
}
53+
4054
rustc_span::create_session_if_not_set_then(self.config.edition().into(), |_| {
4155
if self.config.disable_all_formatting() {
4256
// When the input is from stdin, echo back the input.

src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ use crate::utils::indent_next_line;
4747

4848
pub use crate::config::{
4949
load_config, CliOptions, Color, Config, Edition, EmitMode, FileLines, FileName, NewlineStyle,
50-
Range, Verbosity,
50+
Range, UnstableOptions, Verbosity,
5151
};
5252

5353
pub use crate::format_report_formatter::{FormatReportFormatter, FormatReportFormatterBuilder};
@@ -141,6 +141,9 @@ pub enum ErrorKind {
141141
/// Invalid glob pattern in `ignore` configuration option.
142142
#[error("Invalid glob pattern found in ignore list: {0}")]
143143
InvalidGlobPattern(ignore::Error),
144+
/// Using unstable, nightly only options on stable rustfmt.
145+
#[error("{0}")]
146+
NightlyOnlyOptions(UnstableOptions),
144147
}
145148

146149
impl ErrorKind {

tests/config/no_unstable_options.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
max_width = 100
2+
hard_tabs = false
3+
tab_spaces = 4
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
wrap_comments = true
2+
unstable_features = true

0 commit comments

Comments
 (0)