config_type: add unstable_variant attribute

This commit is contained in:
Tom Milligan 2022-06-10 12:17:29 +01:00 committed by Caleb Cartwright
parent b3d4fb448c
commit 2ae63f0018
9 changed files with 244 additions and 34 deletions

2
Cargo.lock generated
View File

@ -476,7 +476,7 @@ checksum = "fc71d2faa173b74b232dedc235e3ee1696581bb132fc116fa3626d6151a1a8fb"
[[package]]
name = "rustfmt-config_proc_macro"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"proc-macro2",
"quote",

View File

@ -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`

View File

@ -22,7 +22,7 @@ dependencies = [
[[package]]
name = "rustfmt-config_proc_macro"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"proc-macro2",
"quote",

View File

@ -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"

View File

@ -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 {

View File

@ -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 from_str(s: &str) -> Result<Self, Self::Err> {
}
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;

View File

@ -1,6 +1,7 @@
pub mod config {
pub trait ConfigType: Sized {
fn doc_hint() -> String;
fn stable_variant(&self) -> bool;
}
}

View File

@ -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 @@ fn doc_hint() -> String {
}
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 @@ pub fn was_set(&self) -> ConfigWasSet<'_> {
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 @@ pub fn override_value(&mut self, key: &str, val: &str)
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 @@ fn default() -> 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,
}
}

View File

@ -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 mock {
// 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 @@ fn test_was_set() {
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 @@ fn test_print_docs_exclude_unstable() {
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 @@ fn test_print_docs_include_unstable() {
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 @@ fn test_override_single_line_if_else_max_width_exceeds_max_width() {
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
);
}
}
}