diff --git a/Cargo.toml b/Cargo.toml index 4ef42a4..35f5ec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,6 @@ quote = "1" syn = "2" proc-macro2 = { version = "1", default-features = false } proc-macro-error2 = "2" + +[dev-dependencies] +fixture = { path = "tests/fixture" } diff --git a/src/generate.rs b/src/generate.rs index 6ae4a26..2317a17 100644 --- a/src/generate.rs +++ b/src/generate.rs @@ -1,20 +1,23 @@ use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use proc_macro_error2::abort; use syn::{ - self, ext::IdentExt, spanned::Spanned, Expr, Field, Lit, Meta, MetaNameValue, Visibility, + self, ext::IdentExt, spanned::Spanned, Attribute, Expr, Field, Lit, Meta, MetaNameValue, + Visibility, }; -use self::GenMode::{Get, GetCopy, GetMut, Set, SetWith}; +use self::GenMode::{Get, GetClone, GetCopy, GetMut, Set, SetWith}; use super::parse_attr; pub struct GenParams { pub mode: GenMode, pub global_attr: Option, + pub impl_attrs: Vec, } #[derive(PartialEq, Eq, Copy, Clone)] pub enum GenMode { Get, + GetClone, GetCopy, GetMut, Set, @@ -25,6 +28,7 @@ impl GenMode { pub fn name(self) -> &'static str { match self { Get => "get", + GetClone => "get_clone", GetCopy => "get_copy", GetMut => "get_mut", Set => "set", @@ -34,7 +38,7 @@ impl GenMode { pub fn prefix(self) -> &'static str { match self { - Get | GetCopy | GetMut => "", + Get | GetClone | GetCopy | GetMut => "", Set => "set_", SetWith => "with_", } @@ -42,21 +46,21 @@ impl GenMode { pub fn suffix(self) -> &'static str { match self { - Get | GetCopy | Set | SetWith => "", + Get | GetClone | GetCopy | Set | SetWith => "", GetMut => "_mut", } } fn is_get(self) -> bool { match self { - GenMode::Get | GenMode::GetCopy | GenMode::GetMut => true, - GenMode::Set | GenMode::SetWith => false, + Get | GetClone | GetCopy | GetMut => true, + Set | SetWith => false, } } } // Helper function to extract string from Expr -fn expr_to_string(expr: &Expr) -> Option { +pub(crate) fn expr_to_string(expr: &Expr) -> Option { if let Expr::Lit(expr_lit) = expr { if let Lit::Str(s) = &expr_lit.lit { Some(s.value()) @@ -69,7 +73,7 @@ fn expr_to_string(expr: &Expr) -> Option { } // Helper function to parse visibility -fn parse_vis_str(s: &str, span: proc_macro2::Span) -> Visibility { +fn parse_vis_str(s: &str, span: Span) -> Visibility { match syn::parse_str(s) { Ok(vis) => vis, Err(e) => abort!(span, "Invalid visibility found: {}", e), @@ -109,9 +113,10 @@ fn has_prefix_attr(f: &Field, params: &GenParams) -> bool { let field_attr_has_prefix = f .attrs .iter() - .filter_map(|attr| parse_attr(attr, params.mode)) + .filter_map(|attr| parse_attr(attr, params.mode, false).0) .find(|meta| { meta.path().is_ident("get") + || meta.path().is_ident("get_clone") || meta.path().is_ident("get_copy") || meta.path().is_ident("get_mut") }) @@ -158,7 +163,7 @@ pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 { let attr = field .attrs .iter() - .filter_map(|v| parse_attr(v, params.mode)) + .filter_map(|v| parse_attr(v, params.mode, false).0) .last() .or_else(|| params.global_attr.clone()); @@ -167,7 +172,7 @@ pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 { // Generate nothing for skipped field Some(meta) if meta.path().is_ident("skip") => quote! {}, Some(_) => match params.mode { - GenMode::Get => { + Get => { quote! { #(#doc)* #[inline(always)] @@ -176,7 +181,16 @@ pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 { } } } - GenMode::GetCopy => { + GetClone => { + quote! { + #(#doc)* + #[inline(always)] + #visibility fn #fn_name(&self) -> #ty { + self.#field_name.clone() + } + } + } + GetCopy => { quote! { #(#doc)* #[inline(always)] @@ -185,7 +199,7 @@ pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 { } } } - GenMode::Set => { + Set => { quote! { #(#doc)* #[inline(always)] @@ -195,7 +209,7 @@ pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 { } } } - GenMode::GetMut => { + GetMut => { quote! { #(#doc)* #[inline(always)] @@ -204,7 +218,7 @@ pub fn implement(field: &Field, params: &GenParams) -> TokenStream2 { } } } - GenMode::SetWith => { + SetWith => { quote! { #(#doc)* #[inline(always)] @@ -224,7 +238,7 @@ pub fn implement_for_unnamed(field: &Field, params: &GenParams) -> TokenStream2 let attr = field .attrs .iter() - .filter_map(|v| parse_attr(v, params.mode)) + .filter_map(|v| parse_attr(v, params.mode, false).0) .last() .or_else(|| params.global_attr.clone()); let ty = field.ty.clone(); @@ -234,7 +248,7 @@ pub fn implement_for_unnamed(field: &Field, params: &GenParams) -> TokenStream2 // Generate nothing for skipped field Some(meta) if meta.path().is_ident("skip") => quote! {}, Some(_) => match params.mode { - GenMode::Get => { + Get => { let fn_name = Ident::new("get", Span::call_site()); quote! { #(#doc)* @@ -244,7 +258,17 @@ pub fn implement_for_unnamed(field: &Field, params: &GenParams) -> TokenStream2 } } } - GenMode::GetCopy => { + GetClone => { + let fn_name = Ident::new("get", Span::call_site()); + quote! { + #(#doc)* + #[inline(always)] + #visibility fn #fn_name(&self) -> #ty { + self.0.clone() + } + } + } + GetCopy => { let fn_name = Ident::new("get", Span::call_site()); quote! { #(#doc)* @@ -254,7 +278,7 @@ pub fn implement_for_unnamed(field: &Field, params: &GenParams) -> TokenStream2 } } } - GenMode::Set => { + Set => { let fn_name = Ident::new("set", Span::call_site()); quote! { #(#doc)* @@ -265,7 +289,7 @@ pub fn implement_for_unnamed(field: &Field, params: &GenParams) -> TokenStream2 } } } - GenMode::GetMut => { + GetMut => { let fn_name = Ident::new("get_mut", Span::call_site()); quote! { #(#doc)* @@ -275,7 +299,7 @@ pub fn implement_for_unnamed(field: &Field, params: &GenParams) -> TokenStream2 } } } - GenMode::SetWith => { + SetWith => { let fn_name = Ident::new("set_with", Span::call_site()); quote! { #(#doc)* diff --git a/src/lib.rs b/src/lib.rs index 30949e3..10460ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -208,9 +208,12 @@ extern crate quote; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use proc_macro_error2::{abort, abort_call_site, proc_macro_error}; -use syn::{parse_macro_input, spanned::Spanned, DataStruct, DeriveInput, Meta}; +use syn::{ + parse_macro_input, parse_str, punctuated::Punctuated, spanned::Spanned, Attribute, Data, + DataStruct, DeriveInput, Fields, ItemImpl, Meta, Token, +}; -use crate::generate::{GenMode, GenParams}; +use crate::generate::{expr_to_string, GenMode, GenParams}; mod generate; @@ -218,10 +221,16 @@ mod generate; #[proc_macro_error] pub fn getters(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let params = GenParams { - mode: GenMode::Get, - global_attr: parse_global_attr(&ast.attrs, GenMode::Get), - }; + let params = make_params(&ast.attrs, GenMode::Get); + + produce(&ast, ¶ms).into() +} + +#[proc_macro_derive(CloneGetters, attributes(get_clone, with_prefix, getset))] +#[proc_macro_error] +pub fn clone_getters(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let params = make_params(&ast.attrs, GenMode::GetClone); produce(&ast, ¶ms).into() } @@ -230,10 +239,7 @@ pub fn getters(input: TokenStream) -> TokenStream { #[proc_macro_error] pub fn copy_getters(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let params = GenParams { - mode: GenMode::GetCopy, - global_attr: parse_global_attr(&ast.attrs, GenMode::GetCopy), - }; + let params = make_params(&ast.attrs, GenMode::GetCopy); produce(&ast, ¶ms).into() } @@ -242,10 +248,7 @@ pub fn copy_getters(input: TokenStream) -> TokenStream { #[proc_macro_error] pub fn mut_getters(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let params = GenParams { - mode: GenMode::GetMut, - global_attr: parse_global_attr(&ast.attrs, GenMode::GetMut), - }; + let params = make_params(&ast.attrs, GenMode::GetMut); produce(&ast, ¶ms).into() } @@ -254,10 +257,7 @@ pub fn mut_getters(input: TokenStream) -> TokenStream { #[proc_macro_error] pub fn setters(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let params = GenParams { - mode: GenMode::Set, - global_attr: parse_global_attr(&ast.attrs, GenMode::Set), - }; + let params = make_params(&ast.attrs, GenMode::Set); produce(&ast, ¶ms).into() } @@ -266,51 +266,79 @@ pub fn setters(input: TokenStream) -> TokenStream { #[proc_macro_error] pub fn with_setters(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let params = GenParams { - mode: GenMode::SetWith, - global_attr: parse_global_attr(&ast.attrs, GenMode::SetWith), - }; + let params = make_params(&ast.attrs, GenMode::SetWith); produce(&ast, ¶ms).into() } -fn parse_global_attr(attrs: &[syn::Attribute], mode: GenMode) -> Option { - attrs.iter().filter_map(|v| parse_attr(v, mode)).last() +fn make_params(attrs: &[Attribute], mode: GenMode) -> GenParams { + let mut impl_attrs = vec![]; + GenParams { + mode, + global_attr: attrs + .iter() + .filter_map(|v| { + let (attr, impl_attrs_exist) = parse_attr(v, mode, true); + if let Some(Meta::NameValue(code)) = &impl_attrs_exist { + match expr_to_string(&code.value) { + Some(code_str) => { + match parse_str::(&format!("{} impl _ {{}}", code_str)) { + Ok(parsed_impl) => impl_attrs.extend(parsed_impl.attrs), + Err(_) => abort!( + code.value.span(), + "Syntax error, expected attributes like #[..]." + ), + } + } + None => abort!(code.value.span(), "Expected string."), + } + } + attr + }) + .last(), + impl_attrs, + } } -fn parse_attr(attr: &syn::Attribute, mode: GenMode) -> Option { - use syn::{punctuated::Punctuated, Token}; - +fn parse_attr( + attr: &Attribute, + mode: GenMode, + globally_called: bool, +) -> (Option, Option) { if attr.path().is_ident("getset") { - let meta_list = - match attr.parse_args_with(Punctuated::::parse_terminated) { - Ok(list) => list, - Err(e) => abort!(attr.span(), "Failed to parse getset attribute: {}", e), - }; + let meta_list = match attr.parse_args_with(Punctuated::::parse_terminated) + { + Ok(list) => list, + Err(e) => abort!(attr.span(), "Failed to parse getset attribute: {}", e), + }; - let (last, skip, mut collected) = meta_list + let (last, skip, impl_attrs, mut collected) = meta_list .into_iter() .inspect(|meta| { if !(meta.path().is_ident("get") + || meta.path().is_ident("get_clone") || meta.path().is_ident("get_copy") || meta.path().is_ident("get_mut") || meta.path().is_ident("set") || meta.path().is_ident("set_with") - || meta.path().is_ident("skip")) + || meta.path().is_ident("skip") + || (meta.path().is_ident("impl_attrs") && globally_called)) { abort!(meta.path().span(), "unknown setter or getter") } }) .fold( - (None, None, Vec::new()), - |(last, skip, mut collected), meta| { + (None, None, None, Vec::new()), + |(last, skip, impl_attrs, mut collected), meta| { if meta.path().is_ident(mode.name()) { - (Some(meta), skip, collected) + (Some(meta), skip, impl_attrs, collected) } else if meta.path().is_ident("skip") { - (last, Some(meta), collected) + (last, Some(meta), impl_attrs, collected) + } else if meta.path().is_ident("impl_attrs") { + (last, skip, Some(meta), collected) } else { collected.push(meta); - (last, skip, collected) + (last, skip, impl_attrs, collected) } }, ); @@ -319,7 +347,7 @@ fn parse_attr(attr: &syn::Attribute, mode: GenMode) -> Option { // Check if there is any setter or getter used with skip, which is // forbidden. if last.is_none() && collected.is_empty() { - skip + (skip, impl_attrs) } else { abort!( last.or_else(|| collected.pop()).unwrap().path().span(), @@ -327,26 +355,27 @@ fn parse_attr(attr: &syn::Attribute, mode: GenMode) -> Option { ); } } else { - last + (last, impl_attrs) } } else if attr.path().is_ident(mode.name()) { // If skip is not used, return the last occurrence of matching // setter/getter, if there is any. - attr.meta.clone().into() + (attr.meta.clone().into(), None) } else { - None + (None, None) } } fn produce(ast: &DeriveInput, params: &GenParams) -> TokenStream2 { + let impl_attrs = ¶ms.impl_attrs; let name = &ast.ident; let generics = &ast.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); // Is it a struct? - if let syn::Data::Struct(DataStruct { ref fields, .. }) = ast.data { + if let Data::Struct(DataStruct { ref fields, .. }) = ast.data { // Handle unary struct - if matches!(fields, syn::Fields::Unnamed(_)) { + if matches!(fields, Fields::Unnamed(_)) { if fields.len() != 1 { abort_call_site!("Only support unary struct!"); } @@ -355,6 +384,8 @@ fn produce(ast: &DeriveInput, params: &GenParams) -> TokenStream2 { let generated = generate::implement_for_unnamed(field, params); quote! { + #(#impl_attrs)* + #[automatically_derived] impl #impl_generics #name #ty_generics #where_clause { #generated } @@ -363,6 +394,8 @@ fn produce(ast: &DeriveInput, params: &GenParams) -> TokenStream2 { let generated = fields.iter().map(|f| generate::implement(f, params)); quote! { + #(#impl_attrs)* + #[automatically_derived] impl #impl_generics #name #ty_generics #where_clause { #(#generated)* } diff --git a/tests/clone_getters.rs b/tests/clone_getters.rs new file mode 100644 index 0000000..34b1cc9 --- /dev/null +++ b/tests/clone_getters.rs @@ -0,0 +1,149 @@ +#[macro_use] +extern crate getset; + +use crate::submodule::other::{Generic, Plain, Where}; + +// For testing `pub(super)` +mod submodule { + // For testing `pub(super::other)` + pub mod other { + #[derive(CloneGetters)] + #[get_clone] + pub struct Plain { + /// A doc comment. + /// Multiple lines, even. + private_accessible: Box, + + /// A doc comment. + #[get_clone = "pub"] + public_accessible: Box, + // /// A doc comment. + // #[get_clone = "pub(crate)"] + // crate_accessible: Box, + + // /// A doc comment. + // #[get_clone = "pub(super)"] + // super_accessible: Box, + + // /// A doc comment. + // #[get_clone = "pub(super::other)"] + // scope_accessible: Box, + + // Prefixed getter. + #[get_clone = "with_prefix"] + private_prefixed: Box, + + // Prefixed getter. + #[get_clone = "pub with_prefix"] + public_prefixed: Box, + } + + impl Default for Plain { + fn default() -> Plain { + Plain { + private_accessible: Box::new(17), + public_accessible: Box::new(18), + private_prefixed: Box::new(19), + public_prefixed: Box::new(20), + } + } + } + + #[derive(CloneGetters, Default)] + #[get_clone] + pub struct Generic { + /// A doc comment. + /// Multiple lines, even. + private_accessible: T, + + /// A doc comment. + #[get_clone = "pub"] + public_accessible: T, + // /// A doc comment. + // #[get_clone = "pub(crate)"] + // crate_accessible: T, + + // /// A doc comment. + // #[get_clone = "pub(super)"] + // super_accessible: T, + + // /// A doc comment. + // #[get_clone = "pub(super::other)"] + // scope_accessible: T, + } + + #[derive(CloneGetters, Getters, Default)] + #[get_clone] + pub struct Where + where + T: Clone + Default, + { + /// A doc comment. + /// Multiple lines, even. + private_accessible: T, + + /// A doc comment. + #[get_clone = "pub"] + public_accessible: T, + // /// A doc comment. + // #[get_clone = "pub(crate)"] + // crate_accessible: T, + + // /// A doc comment. + // #[get_clone = "pub(super)"] + // super_accessible: T, + + // /// A doc comment. + // #[get_clone = "pub(super::other)"] + // scope_accessible: T, + } + + #[test] + fn test_plain() { + let val = Plain::default(); + val.private_accessible(); + } + + #[test] + fn test_generic() { + let val = Generic::>::default(); + val.private_accessible(); + } + + #[test] + fn test_where() { + let val = Where::>::default(); + val.private_accessible(); + } + + #[test] + fn test_prefixed_plain() { + let val = Plain::default(); + assert_eq!(19, *val.get_private_prefixed()); + } + } +} + +#[test] +fn test_plain() { + let val = Plain::default(); + assert_eq!(18, *val.public_accessible()); +} + +#[test] +fn test_generic() { + let val = Generic::>::default(); + assert_eq!(Box::default(), val.public_accessible()); +} + +#[test] +fn test_where() { + let val = Where::>::default(); + assert_eq!(Box::default(), val.public_accessible()); +} + +#[test] +fn test_prefixed_plain() { + let val = Plain::default(); + assert_eq!(20, *val.get_public_prefixed()); +} diff --git a/tests/fixture/Cargo.toml b/tests/fixture/Cargo.toml new file mode 100644 index 0000000..f73e456 --- /dev/null +++ b/tests/fixture/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "fixture" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +proc-macro-error2 = "2.0.1" +quote = "1.0.40" +syn = { version = "2.0.100", features = ["full"] } diff --git a/tests/fixture/src/lib.rs b/tests/fixture/src/lib.rs new file mode 100644 index 0000000..c169e29 --- /dev/null +++ b/tests/fixture/src/lib.rs @@ -0,0 +1,39 @@ +use proc_macro::TokenStream; +use proc_macro_error2::{abort, proc_macro_error}; +use quote::quote; +use syn::{parse_macro_input, spanned::Spanned as _, ImplItem, ItemImpl}; + +#[proc_macro_attribute] +#[proc_macro_error] +pub fn add_1_to_implementation(_attr: TokenStream, item_impl: TokenStream) -> TokenStream { + let input = parse_macro_input!(item_impl as ItemImpl); + let attrs = &input.attrs; + let name = &input.self_ty; + let (generics, ty_generics, where_clause) = &input.generics.split_for_impl(); + let statements = &input + .items + .iter() + .map(|item| match item { + ImplItem::Fn(function) => { + let func_attrs = &function.attrs; + let sig = &function.sig; + let body = &function.block.stmts[0]; + quote! { + #(#func_attrs)* + #sig { + #body + 1 + } + } + } + _ => abort!(item.span(), "Expected a method."), + }) + .collect::>(); + + quote! { + #(#attrs)* + impl #generics #name #ty_generics #where_clause { + #(#statements)* + } + } + .into() +} diff --git a/tests/impl_attrs.rs b/tests/impl_attrs.rs new file mode 100644 index 0000000..55f2bca --- /dev/null +++ b/tests/impl_attrs.rs @@ -0,0 +1,47 @@ +use fixture::add_1_to_implementation; +use getset::{CloneGetters, CopyGetters}; + +#[derive(CopyGetters, CloneGetters)] +#[getset( + get_clone = "with_prefix", + get_copy, + impl_attrs = r#" + #[add_1_to_implementation] + #[cfg(target_os = "linux")] + #[add_1_to_implementation] + #[allow(unused)] + #[add_1_to_implementation] + "# +)] +struct Wardrobe { + shirts: u8, + pants: u8, +} + +#[test] +fn basic() { + let wardrobe = Wardrobe { + shirts: 2, + pants: 1, + }; + assert_eq!( + wardrobe.shirts(), + 5, + "Function attribute not applied correctly." + ); + assert_eq!( + wardrobe.pants(), + 4, + "Function attribute not applied correctly." + ); + assert_eq!( + wardrobe.get_shirts(), + 5, + "Function attribute not applied correctly." + ); + assert_eq!( + wardrobe.get_pants(), + 4, + "Function attribute not applied correctly." + ); +}