diff options
Diffstat (limited to 'src/tools/rustfmt/config_proc_macro')
-rw-r--r-- | src/tools/rustfmt/config_proc_macro/Cargo.lock | 68 | ||||
-rw-r--r-- | src/tools/rustfmt/config_proc_macro/Cargo.toml | 23 | ||||
-rw-r--r-- | src/tools/rustfmt/config_proc_macro/src/attrs.rs | 57 | ||||
-rw-r--r-- | src/tools/rustfmt/config_proc_macro/src/config_type.rs | 15 | ||||
-rw-r--r-- | src/tools/rustfmt/config_proc_macro/src/item_enum.rs | 208 | ||||
-rw-r--r-- | src/tools/rustfmt/config_proc_macro/src/item_struct.rs | 5 | ||||
-rw-r--r-- | src/tools/rustfmt/config_proc_macro/src/lib.rs | 71 | ||||
-rw-r--r-- | src/tools/rustfmt/config_proc_macro/src/utils.rs | 52 | ||||
-rw-r--r-- | src/tools/rustfmt/config_proc_macro/tests/smoke.rs | 20 |
9 files changed, 519 insertions, 0 deletions
diff --git a/src/tools/rustfmt/config_proc_macro/Cargo.lock b/src/tools/rustfmt/config_proc_macro/Cargo.lock new file mode 100644 index 000000000..ecf561f28 --- /dev/null +++ b/src/tools/rustfmt/config_proc_macro/Cargo.lock @@ -0,0 +1,68 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "proc-macro2" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e98a83a9f9b331f54b924e68a66acb1bb35cb01fb0a23645139967abefb697e8" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustfmt-config_proc_macro" +version = "0.2.0" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec2851eb56d010dc9a21b89ca53ee75e6528bab60c11e89d38390904982da9f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4dc18c61206b08dc98216c98faa0232f4337e1e1b8574551d5bad29ea1b425" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" diff --git a/src/tools/rustfmt/config_proc_macro/Cargo.toml b/src/tools/rustfmt/config_proc_macro/Cargo.toml new file mode 100644 index 000000000..a41b3a5e6 --- /dev/null +++ b/src/tools/rustfmt/config_proc_macro/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rustfmt-config_proc_macro" +version = "0.2.0" +edition = "2018" +description = "A collection of procedural macros for rustfmt" +license = "Apache-2.0/MIT" +categories = ["development-tools::procedural-macro-helpers"] +repository = "https://github.com/rust-lang/rustfmt" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "1.0", features = ["full", "visit"] } + +[dev-dependencies] +serde = { version = "1.0", features = ["derive"] } + +[features] +default = [] +debug-with-rustfmt = [] diff --git a/src/tools/rustfmt/config_proc_macro/src/attrs.rs b/src/tools/rustfmt/config_proc_macro/src/attrs.rs new file mode 100644 index 000000000..0baba046f --- /dev/null +++ b/src/tools/rustfmt/config_proc_macro/src/attrs.rs @@ -0,0 +1,57 @@ +//! 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. + +/// Returns the value of the first `doc_hint` attribute in the given slice or +/// `None` if `doc_hint` attribute is not available. +pub fn find_doc_hint(attrs: &[syn::Attribute]) -> Option<String> { + attrs.iter().filter_map(doc_hint).next() +} + +/// Returns `true` if the given attribute is a `doc_hint` attribute. +pub fn is_doc_hint(attr: &syn::Attribute) -> bool { + is_attr_name_value(attr, "doc_hint") +} + +/// Returns a string literal value if the given attribute is `doc_hint` +/// attribute or `None` otherwise. +pub fn doc_hint(attr: &syn::Attribute) -> Option<String> { + get_name_value_str_lit(attr, "doc_hint") +} + +/// Returns the value of the first `value` attribute in the given slice or +/// `None` if `value` attribute is not available. +pub fn find_config_value(attrs: &[syn::Attribute]) -> Option<String> { + attrs.iter().filter_map(config_value).next() +} + +/// Returns a string literal value if the given attribute is `value` +/// attribute or `None` otherwise. +pub fn config_value(attr: &syn::Attribute) -> Option<String> { + get_name_value_str_lit(attr, "value") +} + +/// Returns `true` if the given attribute is a `value` attribute. +pub fn is_config_value(attr: &syn::Attribute) -> bool { + is_attr_name_value(attr, "value") +} + +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, + _ => 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 { + ref path, + lit: syn::Lit::Str(ref lit_str), + .. + }) if path.is_ident(name) => Some(lit_str.value()), + _ => None, + }) +} diff --git a/src/tools/rustfmt/config_proc_macro/src/config_type.rs b/src/tools/rustfmt/config_proc_macro/src/config_type.rs new file mode 100644 index 000000000..93a78b846 --- /dev/null +++ b/src/tools/rustfmt/config_proc_macro/src/config_type.rs @@ -0,0 +1,15 @@ +use proc_macro2::TokenStream; + +use crate::item_enum::define_config_type_on_enum; +use crate::item_struct::define_config_type_on_struct; + +/// Defines `config_type` on enum or struct. +// FIXME: Implement this on struct. +pub fn define_config_type(input: &syn::Item) -> TokenStream { + match input { + syn::Item::Struct(st) => define_config_type_on_struct(st), + syn::Item::Enum(en) => define_config_type_on_enum(en), + _ => panic!("Expected enum or struct"), + } + .unwrap() +} diff --git a/src/tools/rustfmt/config_proc_macro/src/item_enum.rs b/src/tools/rustfmt/config_proc_macro/src/item_enum.rs new file mode 100644 index 000000000..dcee77a85 --- /dev/null +++ b/src/tools/rustfmt/config_proc_macro/src/item_enum.rs @@ -0,0 +1,208 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use crate::attrs::*; +use crate::utils::*; + +type Variants = syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>; + +/// Defines and implements `config_type` enum. +pub fn define_config_type_on_enum(em: &syn::ItemEnum) -> syn::Result<TokenStream> { + let syn::ItemEnum { + vis, + enum_token, + ident, + generics, + variants, + .. + } = em; + + let mod_name_str = format!("__define_config_type_on_enum_{}", ident); + let mod_name = syn::Ident::new(&mod_name_str, ident.span()); + let variants = fold_quote(variants.iter().map(process_variant), |meta| quote!(#meta,)); + + let impl_doc_hint = impl_doc_hint(&em.ident, &em.variants); + let impl_from_str = impl_from_str(&em.ident, &em.variants); + let impl_display = impl_display(&em.ident, &em.variants); + let impl_serde = impl_serde(&em.ident, &em.variants); + let impl_deserialize = impl_deserialize(&em.ident, &em.variants); + + Ok(quote! { + #[allow(non_snake_case)] + mod #mod_name { + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + pub #enum_token #ident #generics { #variants } + #impl_display + #impl_doc_hint + #impl_from_str + #impl_serde + #impl_deserialize + } + #vis use #mod_name::#ident; + }) +} + +/// Remove attributes specific to `config_proc_macro` from enum variant fields. +fn process_variant(variant: &syn::Variant) -> TokenStream { + let metas = variant + .attrs + .iter() + .filter(|attr| !is_doc_hint(attr) && !is_config_value(attr)); + let attrs = fold_quote(metas, |meta| quote!(#meta)); + let syn::Variant { ident, fields, .. } = variant; + quote!(#attrs #ident #fields) +} + +fn impl_doc_hint(ident: &syn::Ident, variants: &Variants) -> TokenStream { + let doc_hint = variants + .iter() + .map(doc_hint_of_variant) + .collect::<Vec<_>>() + .join("|"); + let doc_hint = format!("[{}]", doc_hint); + quote! { + use crate::config::ConfigType; + impl ConfigType for #ident { + fn doc_hint() -> String { + #doc_hint.to_owned() + } + } + } +} + +fn impl_display(ident: &syn::Ident, variants: &Variants) -> TokenStream { + let vs = variants + .iter() + .filter(|v| is_unit(v)) + .map(|v| (config_value_of_variant(v), &v.ident)); + let match_patterns = fold_quote(vs, |(s, v)| { + quote! { + #ident::#v => write!(f, "{}", #s), + } + }); + quote! { + use std::fmt; + impl fmt::Display for #ident { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + #match_patterns + _ => unimplemented!(), + } + } + } + } +} + +fn impl_from_str(ident: &syn::Ident, variants: &Variants) -> TokenStream { + let vs = variants + .iter() + .filter(|v| is_unit(v)) + .map(|v| (config_value_of_variant(v), &v.ident)); + let if_patterns = fold_quote(vs, |(s, v)| { + quote! { + if #s.eq_ignore_ascii_case(s) { + return Ok(#ident::#v); + } + } + }); + let mut err_msg = String::from("Bad variant, expected one of:"); + for v in variants.iter().filter(|v| is_unit(v)) { + err_msg.push_str(&format!(" `{}`", v.ident)); + } + + quote! { + impl ::std::str::FromStr for #ident { + type Err = &'static str; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + #if_patterns + return Err(#err_msg); + } + } + } +} + +fn doc_hint_of_variant(variant: &syn::Variant) -> String { + find_doc_hint(&variant.attrs).unwrap_or(variant.ident.to_string()) +} + +fn config_value_of_variant(variant: &syn::Variant) -> String { + find_config_value(&variant.attrs).unwrap_or(variant.ident.to_string()) +} + +fn impl_serde(ident: &syn::Ident, variants: &Variants) -> TokenStream { + let arms = fold_quote(variants.iter(), |v| { + let v_ident = &v.ident; + let pattern = match v.fields { + syn::Fields::Named(..) => quote!(#ident::v_ident{..}), + syn::Fields::Unnamed(..) => quote!(#ident::#v_ident(..)), + syn::Fields::Unit => quote!(#ident::#v_ident), + }; + let option_value = config_value_of_variant(v); + quote! { + #pattern => serializer.serialize_str(&#option_value), + } + }); + + quote! { + impl ::serde::ser::Serialize for #ident { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: ::serde::ser::Serializer, + { + use serde::ser::Error; + match self { + #arms + _ => Err(S::Error::custom(format!("Cannot serialize {:?}", self))), + } + } + } + } +} + +// Currently only unit variants are supported. +fn impl_deserialize(ident: &syn::Ident, variants: &Variants) -> TokenStream { + let supported_vs = variants.iter().filter(|v| is_unit(v)); + let if_patterns = fold_quote(supported_vs, |v| { + let config_value = config_value_of_variant(v); + let variant_ident = &v.ident; + quote! { + if #config_value.eq_ignore_ascii_case(s) { + return Ok(#ident::#variant_ident); + } + } + }); + + let supported_vs = variants.iter().filter(|v| is_unit(v)); + let allowed = fold_quote(supported_vs.map(config_value_of_variant), |s| quote!(#s,)); + + quote! { + impl<'de> serde::de::Deserialize<'de> for #ident { + fn deserialize<D>(d: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + use serde::de::{Error, Visitor}; + use std::marker::PhantomData; + use std::fmt; + struct StringOnly<T>(PhantomData<T>); + impl<'de, T> Visitor<'de> for StringOnly<T> + where T: serde::Deserializer<'de> { + type Value = String; + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("string") + } + fn visit_str<E>(self, value: &str) -> Result<String, E> { + Ok(String::from(value)) + } + } + let s = &d.deserialize_string(StringOnly::<D>(PhantomData))?; + + #if_patterns + + static ALLOWED: &'static[&str] = &[#allowed]; + Err(D::Error::unknown_variant(&s, ALLOWED)) + } + } + } +} diff --git a/src/tools/rustfmt/config_proc_macro/src/item_struct.rs b/src/tools/rustfmt/config_proc_macro/src/item_struct.rs new file mode 100644 index 000000000..f03ff7e30 --- /dev/null +++ b/src/tools/rustfmt/config_proc_macro/src/item_struct.rs @@ -0,0 +1,5 @@ +use proc_macro2::TokenStream; + +pub fn define_config_type_on_struct(_st: &syn::ItemStruct) -> syn::Result<TokenStream> { + unimplemented!() +} diff --git a/src/tools/rustfmt/config_proc_macro/src/lib.rs b/src/tools/rustfmt/config_proc_macro/src/lib.rs new file mode 100644 index 000000000..e772c53f4 --- /dev/null +++ b/src/tools/rustfmt/config_proc_macro/src/lib.rs @@ -0,0 +1,71 @@ +//! This crate provides a derive macro for `ConfigType`. + +#![recursion_limit = "256"] + +mod attrs; +mod config_type; +mod item_enum; +mod item_struct; +mod utils; + +use std::str::FromStr; + +use proc_macro::TokenStream; +use syn::parse_macro_input; + +#[proc_macro_attribute] +pub fn config_type(_args: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as syn::Item); + let output = config_type::define_config_type(&input); + + #[cfg(feature = "debug-with-rustfmt")] + { + utils::debug_with_rustfmt(&output); + } + + TokenStream::from(output) +} + +/// Used to conditionally output the TokenStream for tests that need to be run on nightly only. +/// +/// ```rust +/// # use rustfmt_config_proc_macro::nightly_only_test; +/// +/// #[nightly_only_test] +/// #[test] +/// fn test_needs_nightly_rustfmt() { +/// assert!(true); +/// } +/// ``` +#[proc_macro_attribute] +pub fn nightly_only_test(_args: TokenStream, input: TokenStream) -> TokenStream { + // if CFG_RELEASE_CHANNEL is not set we default to nightly, hence why the default is true + if option_env!("CFG_RELEASE_CHANNEL").map_or(true, |c| c == "nightly" || c == "dev") { + input + } else { + // output an empty token stream if CFG_RELEASE_CHANNEL is not set to "nightly" or "dev" + TokenStream::from_str("").unwrap() + } +} + +/// Used to conditionally output the TokenStream for tests that need to be run on stable only. +/// +/// ```rust +/// # use rustfmt_config_proc_macro::stable_only_test; +/// +/// #[stable_only_test] +/// #[test] +/// fn test_needs_stable_rustfmt() { +/// assert!(true); +/// } +/// ``` +#[proc_macro_attribute] +pub fn stable_only_test(_args: TokenStream, input: TokenStream) -> TokenStream { + // if CFG_RELEASE_CHANNEL is not set we default to nightly, hence why the default is false + if option_env!("CFG_RELEASE_CHANNEL").map_or(false, |c| c == "stable") { + input + } else { + // output an empty token stream if CFG_RELEASE_CHANNEL is not set or is not 'stable' + TokenStream::from_str("").unwrap() + } +} diff --git a/src/tools/rustfmt/config_proc_macro/src/utils.rs b/src/tools/rustfmt/config_proc_macro/src/utils.rs new file mode 100644 index 000000000..f5cba87b0 --- /dev/null +++ b/src/tools/rustfmt/config_proc_macro/src/utils.rs @@ -0,0 +1,52 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +pub fn fold_quote<F, I, T>(input: impl Iterator<Item = I>, f: F) -> TokenStream +where + F: Fn(I) -> T, + T: ToTokens, +{ + input.fold(quote! {}, |acc, x| { + let y = f(x); + quote! { #acc #y } + }) +} + +pub fn is_unit(v: &syn::Variant) -> bool { + match v.fields { + syn::Fields::Unit => true, + _ => false, + } +} + +#[cfg(feature = "debug-with-rustfmt")] +/// Pretty-print the output of proc macro using rustfmt. +pub fn debug_with_rustfmt(input: &TokenStream) { + use std::env; + use std::ffi::OsStr; + use std::io::Write; + use std::process::{Command, Stdio}; + + let rustfmt_var = env::var_os("RUSTFMT"); + let rustfmt = match &rustfmt_var { + Some(rustfmt) => rustfmt, + None => OsStr::new("rustfmt"), + }; + let mut child = Command::new(rustfmt) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to spawn rustfmt in stdio mode"); + { + let stdin = child.stdin.as_mut().expect("Failed to get stdin"); + stdin + .write_all(format!("{}", input).as_bytes()) + .expect("Failed to write to stdin"); + } + let rustfmt_output = child.wait_with_output().expect("rustfmt has failed"); + + eprintln!( + "{}", + String::from_utf8(rustfmt_output.stdout).expect("rustfmt returned non-UTF8 string") + ); +} diff --git a/src/tools/rustfmt/config_proc_macro/tests/smoke.rs b/src/tools/rustfmt/config_proc_macro/tests/smoke.rs new file mode 100644 index 000000000..940a8a0c2 --- /dev/null +++ b/src/tools/rustfmt/config_proc_macro/tests/smoke.rs @@ -0,0 +1,20 @@ +pub mod config { + pub trait ConfigType: Sized { + fn doc_hint() -> String; + } +} + +#[allow(dead_code)] +#[allow(unused_imports)] +mod tests { + use rustfmt_config_proc_macro::config_type; + + #[config_type] + enum Bar { + Foo, + Bar, + #[doc_hint = "foo_bar"] + FooBar, + FooFoo(i32), + } +} |