diff --git a/quork-proc/Cargo.toml b/quork-proc/Cargo.toml index e13aacb..ee007c5 100644 --- a/quork-proc/Cargo.toml +++ b/quork-proc/Cargo.toml @@ -20,3 +20,6 @@ proc-macro-error2 = "2.0" proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["full"] } + +[dev-dependencies] +strum = { version = "0.26", features = ["derive"] } diff --git a/quork-proc/src/lib.rs b/quork-proc/src/lib.rs index 76a69de..2771774 100644 --- a/quork-proc/src/lib.rs +++ b/quork-proc/src/lib.rs @@ -3,6 +3,7 @@ #![warn(clippy::pedantic)] #![warn(missing_docs)] +use proc_macro::TokenStream; use proc_macro_error2::proc_macro_error; use syn::{parse_macro_input, DeriveInput, LitStr}; @@ -10,16 +11,26 @@ mod const_str; mod enum_list; mod from_tuple; mod new; +mod strip_enum; mod time_fn; mod trim_lines; #[macro_use] extern crate quote; +/// Create an additional enum with all values stripped +#[proc_macro_derive(Strip, attributes(stripped_meta, stripped))] +#[proc_macro_error] +pub fn strip_enum(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + + strip_enum::strip_enum(&mut ast).into() +} + /// Implement [`quork::ListVariants`] for enums #[proc_macro_derive(ListVariants)] #[proc_macro_error] -pub fn derive_enum_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +pub fn derive_enum_list(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); enum_list::enum_list(&ast).into() } @@ -29,7 +40,7 @@ pub fn derive_enum_list(input: proc_macro::TokenStream) -> proc_macro::TokenStre /// /// Converts an enum variant to a string literal, within a constant context. #[proc_macro_derive(ConstStr)] -pub fn derive_const_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +pub fn derive_const_str(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); const_str::derive(&ast).into() } @@ -39,7 +50,7 @@ pub fn derive_const_str(input: proc_macro::TokenStream) -> proc_macro::TokenStre /// Will follow the form of `new(field: Type, ...) -> Self`, where all fields are required. #[proc_macro_derive(New)] #[proc_macro_error] -pub fn derive_new(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +pub fn derive_new(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); new::derive(&ast).into() } @@ -47,7 +58,7 @@ pub fn derive_new(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// Implement the [`std::convert::From`] trait for converting tuples into tuple structs #[proc_macro_derive(FromTuple)] #[proc_macro_error] -pub fn derive_from_tuple(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +pub fn derive_from_tuple(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); from_tuple::derive(&ast).into() } @@ -59,10 +70,7 @@ pub fn derive_from_tuple(input: proc_macro::TokenStream) -> proc_macro::TokenStr /// You can pass "s", "ms", "ns" #[proc_macro_attribute] #[proc_macro_error] -pub fn time( - args: proc_macro::TokenStream, - input: proc_macro::TokenStream, -) -> proc_macro::TokenStream { +pub fn time(args: TokenStream, input: TokenStream) -> TokenStream { let args_str = args.to_string(); let fmt = match args_str.as_str() { "ms" | "milliseconds" => time_fn::TimeFormat::Milliseconds, @@ -98,7 +106,6 @@ pub fn ltrim_lines(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// Trim whitespace from the right of a string literal on each line #[proc_macro] -#[deprecated = "Use trim_lines (renamed to avoid confusion)"] pub fn strip_lines(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let literal = parse_macro_input!(input as LitStr); @@ -108,7 +115,7 @@ pub fn strip_lines(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// Trim whitespace from the left and right of a string literal on each line #[proc_macro] #[deprecated = "Use rtrim_lines (renamed to avoid confusion)"] -pub fn rstrip_lines(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +pub fn rstrip_lines(input: TokenStream) -> proc_macro::TokenStream { let literal = parse_macro_input!(input as LitStr); trim_lines::trim_lines(&literal, &trim_lines::Alignment::Right).into() @@ -117,7 +124,7 @@ pub fn rstrip_lines(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// Trim whitespace from the left of a string literal on each line #[proc_macro] #[deprecated = "Use ltrim_lines (renamed to avoid confusion)"] -pub fn lstrip_lines(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +pub fn lstrip_lines(input: TokenStream) -> proc_macro::TokenStream { let literal = parse_macro_input!(input as LitStr); trim_lines::trim_lines(&literal, &trim_lines::Alignment::Left).into() diff --git a/quork-proc/src/strip_enum.rs b/quork-proc/src/strip_enum.rs new file mode 100644 index 0000000..f31b634 --- /dev/null +++ b/quork-proc/src/strip_enum.rs @@ -0,0 +1,172 @@ +use proc_macro2::{Ident, TokenStream}; +use proc_macro_error2::{abort, abort_call_site}; +use quote::{quote, ToTokens}; +use syn::{spanned::Spanned, DeriveInput, Meta, Variant, Visibility}; + +fn ignore_variant(variant: &Variant) -> bool { + variant.attrs.iter().any(|attr| match attr.meta { + syn::Meta::List(ref list) if list.path.is_ident("stripped") => { + let mut ignored = false; + + let list_parser = syn::meta::parser(|meta| { + if meta.path.is_ident("ignore") { + ignored = true; + Ok(()) + } else { + Err(meta.error("unsupported property")) + } + }); + + if let Err(err) = list.parse_args_with(list_parser) { + abort! { + err.span(), + "Failed to parse attribute: {}", err; + help = "Only supported properties on enum variants are `ignore`" + } + } + + ignored + } + _ => abort!( + attr.span(), + "Expected list-style (i.e #[stripped(...)]), found other style attribute macro" + ), + }) +} + +struct StrippedData { + ident: Ident, + variants: Vec, + meta: Vec, + vis: Visibility, +} + +// struct MetaArgs { +// meta: Vec, +// } + +// impl Parse for MetaArgs { +// fn parse(input: syn::parse::ParseStream) -> syn::Result { +// input.peek3(token) +// } +// } + +pub fn strip_enum(ast: &mut DeriveInput) -> TokenStream { + let data = &ast.data; + let attrs = &mut ast.attrs; + + let info: StrippedData = match data { + syn::Data::Enum(ref e) => { + let variants = e + .variants + .iter() + .filter_map(|variant| { + if ignore_variant(variant) { + None + } else { + Some(variant.ident.to_token_stream()) + } + }) + .collect::>(); + + let default_ident = { + let ident = ast.ident.clone(); + let span = ident.span(); + move || Ident::new(&format!("{ident}Stripped"), span) + }; + + let new_ident = if let Some(info_attr_pos) = attrs + .iter() + .position(|attr| attr.path().is_ident("stripped")) + { + let info_attr = attrs.remove(info_attr_pos); + + let mut new_ident: Option = None; + + let ident_parser = syn::meta::parser(|meta| { + if meta.path.is_ident("ident") { + new_ident = Some(meta.value()?.parse()?); + Ok(()) + } else { + Err(meta.error("unsupported property")) + } + }); + + if let Err(err) = info_attr.parse_args_with(ident_parser) { + abort! { + err.span(), + "Failed to parse attribute: {}", err; + help = "Only supported properties on enum definitions are `ident`" + } + } + + new_ident.unwrap_or_else(default_ident) + } else { + default_ident() + }; + + let meta_list: Vec = attrs + .iter() + .filter(|attr| attr.path().is_ident("stripped_meta")) + .flat_map(|meta_attr| match &meta_attr.meta { + Meta::List(meta_data) => match meta_data.parse_args::() { + Ok(meta) => vec![meta], + Err(err) => { + abort! { + err.span(), + "Failed to parse specified metadata: {}", err; + help = "Make sure the provided arguments are in the form of Rust metadata. (i.e the tokens contained within `#[...]`)" + } + } + }, + // Meta::NameValue(MetaNameValue { + // value: + // syn::Expr::Lit(syn::ExprLit { + // lit: syn::Lit::Str(path), + // .. + // }), + // .. + // }) => { + // if &path.value() == "inherit" { + // attrs + // .iter() + // .filter(|attr| !attr.path().is_ident("stripped_meta")) + // .map(|attr| attr.meta.clone()) + // .collect() + // } else { + // abort!(path.span(), "Expected `inherit`"); + // } + // } + _ => abort!( + meta_attr.span(), + "Expected #[stripped_meta(...)]. Found other style attribute." + ), + }) + .collect(); + + StrippedData { + ident: new_ident, + variants, + meta: meta_list, + vis: ast.vis.clone(), + } + } + _ => abort_call_site!("`Strip` can only be derived for enums"), + }; + + let StrippedData { + ident, + variants, + meta, + vis, + } = info; + + // panic!("{:?}", meta); + + quote! { + #(#[#meta])* + #vis enum #ident { + #(#variants),* + } + } +} diff --git a/quork-proc/tests/strip_enum.rs b/quork-proc/tests/strip_enum.rs new file mode 100644 index 0000000..be4f4ee --- /dev/null +++ b/quork-proc/tests/strip_enum.rs @@ -0,0 +1,60 @@ +#![allow(dead_code)] + +use std::fmt::Display; + +use strum::{Display, EnumIter, IntoEnumIterator}; + +use quork_proc::Strip; + +pub fn enum_to_string() -> String { + T::iter().map(|v| v.to_string()).collect::() +} + +struct DummyStruct; + +#[derive(Strip)] +#[stripped_meta(derive(EnumIter, Display))] +#[stripped_meta(strum(serialize_all = "kebab-case"))] +enum EnumWithData { + Test1(DummyStruct), + Test2(DummyStruct), +} + +#[test] +fn has_all_variants() { + let variants = enum_to_string::(); + + assert_eq!(variants, "test1test2"); +} + +#[derive(Strip)] +#[stripped_meta(derive(EnumIter, Display))] +#[stripped_meta(strum(serialize_all = "kebab-case"))] +enum EnumExclude { + Test1(DummyStruct), + #[stripped(ignore)] + Test2(DummyStruct), + Test3(DummyStruct), +} + +#[derive(Strip)] +#[stripped_meta(derive(EnumIter))] +#[stripped_meta(strum(serialize_all = "kebab-case"))] +enum EnumWithInherit { + Test1(DummyStruct), +} + +#[derive(Strip)] +#[stripped_meta(derive(EnumIter))] +#[stripped_meta(strum(serialize_all = "kebab-case"))] +#[stripped(ident = IChoseThisIdent)] +enum EnumWithCustomIdent { + Test1(DummyStruct), +} + +#[test] +fn excludes_no_hook_variant() { + let variants = enum_to_string::(); + + assert_eq!(variants, "test1test3"); +}