about summary refs log tree commit diff
diff options
context:
space:
mode:
authorTom Milligan <tom@reinfer.io>2022-06-10 12:17:29 +0100
committerCaleb Cartwright <calebcartwright@users.noreply.github.com>2022-06-30 22:25:39 -0500
commit2ae63f0018a7e18deb7ac0063379c2350a631fca (patch)
tree4c48d35571dec6283e5032062e49bdb9c12991ae
parentb3d4fb448c79240785d8ccd78b73d40c59fc5071 (diff)
downloadrust-2ae63f0018a7e18deb7ac0063379c2350a631fca.tar.gz
rust-2ae63f0018a7e18deb7ac0063379c2350a631fca.zip
config_type: add unstable_variant attribute
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml2
-rw-r--r--config_proc_macro/Cargo.lock2
-rw-r--r--config_proc_macro/Cargo.toml2
-rw-r--r--config_proc_macro/src/attrs.rs27
-rw-r--r--config_proc_macro/src/item_enum.rs40
-rw-r--r--config_proc_macro/tests/smoke.rs1
-rw-r--r--src/config/config_type.rs90
-rw-r--r--src/config/mod.rs112
9 files changed, 244 insertions, 34 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 311df226da1..e5175528970 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -476,7 +476,7 @@ checksum = "fc71d2faa173b74b232dedc235e3ee1696581bb132fc116fa3626d6151a1a8fb"
 
 [[package]]
 name = "rustfmt-config_proc_macro"
-version = "0.2.0"
+version = "0.3.0"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/Cargo.toml b/Cargo.toml
index 7a4e02d69ed..7438335eaa7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -57,7 +57,7 @@ unicode-segmentation = "1.9"
 unicode-width = "0.1"
 unicode_categories = "0.1"
 
-rustfmt-config_proc_macro = { version = "0.2", path = "config_proc_macro" }
+rustfmt-config_proc_macro = { version = "0.3", path = "config_proc_macro" }
 
 # A noop dependency that changes in the Rust repository, it's a bit of a hack.
 # See the `src/tools/rustc-workspace-hack/README.md` file in `rust-lang/rust`
diff --git a/config_proc_macro/Cargo.lock b/config_proc_macro/Cargo.lock
index ecf561f28fb..49f2f72a8d2 100644
--- a/config_proc_macro/Cargo.lock
+++ b/config_proc_macro/Cargo.lock
@@ -22,7 +22,7 @@ dependencies = [
 
 [[package]]
 name = "rustfmt-config_proc_macro"
-version = "0.2.0"
+version = "0.3.0"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/config_proc_macro/Cargo.toml b/config_proc_macro/Cargo.toml
index a41b3a5e6bf..d10d0469cc4 100644
--- a/config_proc_macro/Cargo.toml
+++ b/config_proc_macro/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "rustfmt-config_proc_macro"
-version = "0.2.0"
+version = "0.3.0"
 edition = "2018"
 description = "A collection of procedural macros for rustfmt"
 license = "Apache-2.0/MIT"
diff --git a/config_proc_macro/src/attrs.rs b/config_proc_macro/src/attrs.rs
index 0baba046f9e..dd18ff572cb 100644
--- a/config_proc_macro/src/attrs.rs
+++ b/config_proc_macro/src/attrs.rs
@@ -1,8 +1,10 @@
 //! This module provides utilities for handling attributes on variants
-//! of `config_type` enum. Currently there are two types of attributes
-//! that could appear on the variants of `config_type` enum: `doc_hint`
-//! and `value`. Both comes in the form of name-value pair whose value
-//! is string literal.
+//! of `config_type` enum. Currently there are the following attributes
+//! that could appear on the variants of `config_type` enum:
+//!
+//! - `doc_hint`: name-value pair whose value is string literal
+//! - `value`: name-value pair whose value is string literal
+//! - `unstable_variant`: name only
 
 /// Returns the value of the first `doc_hint` attribute in the given slice or
 /// `None` if `doc_hint` attribute is not available.
@@ -27,6 +29,11 @@ pub fn find_config_value(attrs: &[syn::Attribute]) -> Option<String> {
     attrs.iter().filter_map(config_value).next()
 }
 
+/// Returns `true` if the there is at least one `unstable` attribute in the given slice.
+pub fn any_unstable_variant(attrs: &[syn::Attribute]) -> bool {
+    attrs.iter().any(is_unstable_variant)
+}
+
 /// Returns a string literal value if the given attribute is `value`
 /// attribute or `None` otherwise.
 pub fn config_value(attr: &syn::Attribute) -> Option<String> {
@@ -38,6 +45,11 @@ pub fn is_config_value(attr: &syn::Attribute) -> bool {
     is_attr_name_value(attr, "value")
 }
 
+/// Returns `true` if the given attribute is an `unstable` attribute.
+pub fn is_unstable_variant(attr: &syn::Attribute) -> bool {
+    is_attr_path(attr, "unstable_variant")
+}
+
 fn is_attr_name_value(attr: &syn::Attribute, name: &str) -> bool {
     attr.parse_meta().ok().map_or(false, |meta| match meta {
         syn::Meta::NameValue(syn::MetaNameValue { ref path, .. }) if path.is_ident(name) => true,
@@ -45,6 +57,13 @@ fn is_attr_name_value(attr: &syn::Attribute, name: &str) -> bool {
     })
 }
 
+fn is_attr_path(attr: &syn::Attribute, name: &str) -> bool {
+    attr.parse_meta().ok().map_or(false, |meta| match meta {
+        syn::Meta::Path(path) if path.is_ident(name) => true,
+        _ => false,
+    })
+}
+
 fn get_name_value_str_lit(attr: &syn::Attribute, name: &str) -> Option<String> {
     attr.parse_meta().ok().and_then(|meta| match meta {
         syn::Meta::NameValue(syn::MetaNameValue {
diff --git a/config_proc_macro/src/item_enum.rs b/config_proc_macro/src/item_enum.rs
index dcee77a8549..731a7ea0607 100644
--- a/config_proc_macro/src/item_enum.rs
+++ b/config_proc_macro/src/item_enum.rs
@@ -1,5 +1,6 @@
 use proc_macro2::TokenStream;
-use quote::quote;
+use quote::{quote, quote_spanned};
+use syn::spanned::Spanned;
 
 use crate::attrs::*;
 use crate::utils::*;
@@ -47,12 +48,23 @@ fn process_variant(variant: &syn::Variant) -> TokenStream {
     let metas = variant
         .attrs
         .iter()
-        .filter(|attr| !is_doc_hint(attr) && !is_config_value(attr));
+        .filter(|attr| !is_doc_hint(attr) && !is_config_value(attr) && !is_unstable_variant(attr));
     let attrs = fold_quote(metas, |meta| quote!(#meta));
     let syn::Variant { ident, fields, .. } = variant;
     quote!(#attrs #ident #fields)
 }
 
+/// Return the correct syntax to pattern match on the enum variant, discarding all
+/// internal field data.
+fn fields_in_variant(variant: &syn::Variant) -> TokenStream {
+    // With thanks to https://stackoverflow.com/a/65182902
+    match &variant.fields {
+        syn::Fields::Unnamed(_) => quote_spanned! { variant.span() => (..) },
+        syn::Fields::Unit => quote_spanned! { variant.span() => },
+        syn::Fields::Named(_) => quote_spanned! { variant.span() => {..} },
+    }
+}
+
 fn impl_doc_hint(ident: &syn::Ident, variants: &Variants) -> TokenStream {
     let doc_hint = variants
         .iter()
@@ -60,12 +72,26 @@ fn impl_doc_hint(ident: &syn::Ident, variants: &Variants) -> TokenStream {
         .collect::<Vec<_>>()
         .join("|");
     let doc_hint = format!("[{}]", doc_hint);
+
+    let variant_stables = variants
+        .iter()
+        .map(|v| (&v.ident, fields_in_variant(&v), !unstable_of_variant(v)));
+    let match_patterns = fold_quote(variant_stables, |(v, fields, stable)| {
+        quote! {
+            #ident::#v #fields => #stable,
+        }
+    });
     quote! {
         use crate::config::ConfigType;
         impl ConfigType for #ident {
             fn doc_hint() -> String {
                 #doc_hint.to_owned()
             }
+            fn stable_variant(&self) -> bool {
+                match self {
+                    #match_patterns
+                }
+            }
         }
     }
 }
@@ -123,13 +149,21 @@ fn impl_from_str(ident: &syn::Ident, variants: &Variants) -> TokenStream {
 }
 
 fn doc_hint_of_variant(variant: &syn::Variant) -> String {
-    find_doc_hint(&variant.attrs).unwrap_or(variant.ident.to_string())
+    let mut text = find_doc_hint(&variant.attrs).unwrap_or(variant.ident.to_string());
+    if unstable_of_variant(&variant) {
+        text.push_str(" (unstable)")
+    };
+    text
 }
 
 fn config_value_of_variant(variant: &syn::Variant) -> String {
     find_config_value(&variant.attrs).unwrap_or(variant.ident.to_string())
 }
 
+fn unstable_of_variant(variant: &syn::Variant) -> bool {
+    any_unstable_variant(&variant.attrs)
+}
+
 fn impl_serde(ident: &syn::Ident, variants: &Variants) -> TokenStream {
     let arms = fold_quote(variants.iter(), |v| {
         let v_ident = &v.ident;
diff --git a/config_proc_macro/tests/smoke.rs b/config_proc_macro/tests/smoke.rs
index 940a8a0c251..c8a83e39c9e 100644
--- a/config_proc_macro/tests/smoke.rs
+++ b/config_proc_macro/tests/smoke.rs
@@ -1,6 +1,7 @@
 pub mod config {
     pub trait ConfigType: Sized {
         fn doc_hint() -> String;
+        fn stable_variant(&self) -> bool;
     }
 }
 
diff --git a/src/config/config_type.rs b/src/config/config_type.rs
index e37ed798cb5..26d57a13791 100644
--- a/src/config/config_type.rs
+++ b/src/config/config_type.rs
@@ -6,6 +6,14 @@ pub(crate) trait ConfigType: Sized {
     /// Returns hint text for use in `Config::print_docs()`. For enum types, this is a
     /// pipe-separated list of variants; for other types it returns "<type>".
     fn doc_hint() -> String;
+
+    /// Return `true` if the variant (i.e. value of this type) is stable.
+    ///
+    /// By default, return true for all values. Enums annotated with `#[config_type]`
+    /// are automatically implemented, based on the `#[unstable_variant]` annotation.
+    fn stable_variant(&self) -> bool {
+        true
+    }
 }
 
 impl ConfigType for bool {
@@ -51,6 +59,13 @@ impl ConfigType for IgnoreList {
 }
 
 macro_rules! create_config {
+    // Options passed in to the macro.
+    //
+    // - $i: the ident name of the option
+    // - $ty: the type of the option value
+    // - $def: the default value of the option
+    // - $stb: true if the option is stable
+    // - $dstring: description of the option
     ($($i:ident: $ty:ty, $def:expr, $stb:expr, $( $dstring:expr ),+ );+ $(;)*) => (
         #[cfg(test)]
         use std::collections::HashSet;
@@ -61,9 +76,12 @@ macro_rules! create_config {
         #[derive(Clone)]
         #[allow(unreachable_pub)]
         pub struct Config {
-            // For each config item, we store a bool indicating whether it has
-            // been accessed and the value, and a bool whether the option was
-            // manually initialised, or taken from the default,
+            // For each config item, we store:
+            //
+            // - 0: true if the value has been access
+            // - 1: true if the option was manually initialized
+            // - 2: the option value
+            // - 3: true if the option is unstable
             $($i: (Cell<bool>, bool, $ty, bool)),+
         }
 
@@ -143,18 +161,13 @@ macro_rules! create_config {
 
             fn fill_from_parsed_config(mut self, parsed: PartialConfig, dir: &Path) -> Config {
             $(
-                if let Some(val) = parsed.$i {
-                    if self.$i.3 {
+                if let Some(option_value) = parsed.$i {
+                    let option_stable = self.$i.3;
+                    if $crate::config::config_type::is_stable_option_and_value(
+                        stringify!($i), option_stable, &option_value
+                    ) {
                         self.$i.1 = true;
-                        self.$i.2 = val;
-                    } else {
-                        if crate::is_nightly_channel!() {
-                            self.$i.1 = true;
-                            self.$i.2 = val;
-                        } else {
-                            eprintln!("Warning: can't set `{} = {:?}`, unstable features are only \
-                                       available in nightly channel.", stringify!($i), val);
-                        }
+                        self.$i.2 = option_value;
                     }
                 }
             )+
@@ -221,12 +234,22 @@ macro_rules! create_config {
                 match key {
                     $(
                         stringify!($i) => {
-                            self.$i.1 = true;
-                            self.$i.2 = val.parse::<$ty>()
+                            let option_value = val.parse::<$ty>()
                                 .expect(&format!("Failed to parse override for {} (\"{}\") as a {}",
                                                  stringify!($i),
                                                  val,
                                                  stringify!($ty)));
+
+                            // Users are currently allowed to set unstable
+                            // options/variants via the `--config` options override.
+                            //
+                            // There is ongoing discussion about how to move forward here:
+                            // https://github.com/rust-lang/rustfmt/pull/5379
+                            //
+                            // For now, do not validate whether the option or value is stable,
+                            // just always set it.
+                            self.$i.1 = true;
+                            self.$i.2 = option_value;
                         }
                     )+
                     _ => panic!("Unknown config key in override: {}", key)
@@ -424,3 +447,38 @@ macro_rules! create_config {
         }
     )
 }
+
+pub(crate) fn is_stable_option_and_value<T>(
+    option_name: &str,
+    option_stable: bool,
+    option_value: &T,
+) -> bool
+where
+    T: PartialEq + std::fmt::Debug + ConfigType,
+{
+    let nightly = crate::is_nightly_channel!();
+    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
+        }
+        // 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
+        }
+        // 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 f49c18d3a46..eaada8db090 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -408,6 +408,15 @@ mod test {
     #[allow(dead_code)]
     mod mock {
         use super::super::*;
+        use rustfmt_config_proc_macro::config_type;
+
+        #[config_type]
+        pub enum PartiallyUnstableOption {
+            V1,
+            V2,
+            #[unstable_variant]
+            V3,
+        }
 
         create_config! {
             // Options that are used by the generated functions
@@ -451,6 +460,63 @@ mod test {
             // Options that are used by the tests
             stable_option: bool, false, true, "A stable option";
             unstable_option: bool, false, false, "An unstable option";
+            partially_unstable_option: PartiallyUnstableOption, PartiallyUnstableOption::V1, true,
+                "A partially unstable option";
+        }
+
+        #[cfg(test)]
+        mod partially_unstable_option {
+            use super::{Config, PartialConfig, PartiallyUnstableOption};
+            use rustfmt_config_proc_macro::{nightly_only_test, stable_only_test};
+            use std::path::Path;
+
+            /// From the config file, we can fill with a stable variant
+            #[test]
+            fn test_from_toml_stable_value() {
+                let toml = r#"
+                    partially_unstable_option = "V2"
+                "#;
+                let partial_config: PartialConfig = toml::from_str(toml).unwrap();
+                let config = Config::default();
+                let config = config.fill_from_parsed_config(partial_config, Path::new(""));
+                assert_eq!(
+                    config.partially_unstable_option(),
+                    PartiallyUnstableOption::V2
+                );
+            }
+
+            /// From the config file, we cannot fill with an unstable variant (stable only)
+            #[stable_only_test]
+            #[test]
+            fn test_from_toml_unstable_value_on_stable() {
+                let toml = r#"
+                    partially_unstable_option = "V3"
+                "#;
+                let partial_config: PartialConfig = toml::from_str(toml).unwrap();
+                let config = Config::default();
+                let config = config.fill_from_parsed_config(partial_config, Path::new(""));
+                assert_eq!(
+                    config.partially_unstable_option(),
+                    // default value from config, i.e. fill failed
+                    PartiallyUnstableOption::V1
+                );
+            }
+
+            /// From the config file, we can fill with an unstable variant (nightly only)
+            #[nightly_only_test]
+            #[test]
+            fn test_from_toml_unstable_value_on_nightly() {
+                let toml = r#"
+                    partially_unstable_option = "V3"
+                "#;
+                let partial_config: PartialConfig = toml::from_str(toml).unwrap();
+                let config = Config::default();
+                let config = config.fill_from_parsed_config(partial_config, Path::new(""));
+                assert_eq!(
+                    config.partially_unstable_option(),
+                    PartiallyUnstableOption::V3
+                );
+            }
         }
     }
 
@@ -489,6 +555,11 @@ mod test {
         assert_eq!(config.was_set().verbose(), false);
     }
 
+    const PRINT_DOCS_STABLE_OPTION: &str = "stable_option <boolean> Default: false";
+    const PRINT_DOCS_UNSTABLE_OPTION: &str = "unstable_option <boolean> Default: false (unstable)";
+    const PRINT_DOCS_PARTIALLY_UNSTABLE_OPTION: &str =
+        "partially_unstable_option [V1|V2|V3 (unstable)] Default: V1";
+
     #[test]
     fn test_print_docs_exclude_unstable() {
         use self::mock::Config;
@@ -497,10 +568,9 @@ mod test {
         Config::print_docs(&mut output, false);
 
         let s = str::from_utf8(&output).unwrap();
-
-        assert_eq!(s.contains("stable_option"), true);
-        assert_eq!(s.contains("unstable_option"), false);
-        assert_eq!(s.contains("(unstable)"), false);
+        assert_eq!(s.contains(PRINT_DOCS_STABLE_OPTION), true);
+        assert_eq!(s.contains(PRINT_DOCS_UNSTABLE_OPTION), false);
+        assert_eq!(s.contains(PRINT_DOCS_PARTIALLY_UNSTABLE_OPTION), true);
     }
 
     #[test]
@@ -511,9 +581,9 @@ mod test {
         Config::print_docs(&mut output, true);
 
         let s = str::from_utf8(&output).unwrap();
-        assert_eq!(s.contains("stable_option"), true);
-        assert_eq!(s.contains("unstable_option"), true);
-        assert_eq!(s.contains("(unstable)"), true);
+        assert_eq!(s.contains(PRINT_DOCS_STABLE_OPTION), true);
+        assert_eq!(s.contains(PRINT_DOCS_UNSTABLE_OPTION), true);
+        assert_eq!(s.contains(PRINT_DOCS_PARTIALLY_UNSTABLE_OPTION), true);
     }
 
     #[test]
@@ -921,4 +991,32 @@ make_backup = false
             assert_eq!(config.single_line_if_else_max_width(), 100);
         }
     }
+
+    #[cfg(test)]
+    mod partially_unstable_option {
+        use super::mock::{Config, PartiallyUnstableOption};
+        use super::*;
+
+        /// From the command line, we can override with a stable variant.
+        #[test]
+        fn test_override_stable_value() {
+            let mut config = Config::default();
+            config.override_value("partially_unstable_option", "V2");
+            assert_eq!(
+                config.partially_unstable_option(),
+                PartiallyUnstableOption::V2
+            );
+        }
+
+        /// From the command line, we can override with an unstable variant.
+        #[test]
+        fn test_override_unstable_value() {
+            let mut config = Config::default();
+            config.override_value("partially_unstable_option", "V3");
+            assert_eq!(
+                config.partially_unstable_option(),
+                PartiallyUnstableOption::V3
+            );
+        }
+    }
 }