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."
+ );
+}