From 0318711e47a09280e7a7dcc1d0a29d25a8cc4f95 Mon Sep 17 00:00:00 2001 From: Niklas <71767810+niklass-l@users.noreply.github.com> Date: Sun, 26 May 2024 10:53:16 +0200 Subject: [PATCH 1/4] Add support for pub(crate) visibility on new method implementation --- Cargo.toml | 2 +- src/lib.rs | 134 +++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 100 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d6a7591..90a1975 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ proc-macro = true [dependencies] proc-macro2 = "1" quote = "1" -syn = "2" +syn = {version = "2", features = ["parsing"]} [features] default = ["std"] diff --git a/src/lib.rs b/src/lib.rs index e5e119d..4a229e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,19 +119,24 @@ macro_rules! my_quote { } fn path_to_string(path: &syn::Path) -> String { - path.segments.iter().map(|s| s.ident.to_string()).collect::>().join("::") + path.segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join("::") } use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; -use syn::{Token, punctuated::Punctuated}; +use syn::{punctuated::Punctuated, Attribute, Lit, Token, Visibility}; #[proc_macro_derive(new, attributes(new))] pub fn derive(input: TokenStream) -> TokenStream { let ast: syn::DeriveInput = syn::parse(input).expect("Couldn't parse item"); + let options = NewOptions::from_attributes(&ast.attrs); let result = match ast.data { - syn::Data::Enum(ref e) => new_for_enum(&ast, e), - syn::Data::Struct(ref s) => new_for_struct(&ast, &s.fields, None), + syn::Data::Enum(ref e) => new_for_enum(&ast, e, &options), + syn::Data::Struct(ref s) => new_for_struct(&ast, &s.fields, None, &options), syn::Data::Union(_) => panic!("doesn't work with unions yet"), }; result.into() @@ -141,15 +146,24 @@ fn new_for_struct( ast: &syn::DeriveInput, fields: &syn::Fields, variant: Option<&syn::Ident>, + options: &NewOptions, ) -> proc_macro2::TokenStream { match *fields { - syn::Fields::Named(ref fields) => new_impl(&ast, Some(&fields.named), true, variant), - syn::Fields::Unit => new_impl(&ast, None, false, variant), - syn::Fields::Unnamed(ref fields) => new_impl(&ast, Some(&fields.unnamed), false, variant), + syn::Fields::Named(ref fields) => { + new_impl(ast, Some(&fields.named), true, variant, options) + } + syn::Fields::Unit => new_impl(ast, None, false, variant, options), + syn::Fields::Unnamed(ref fields) => { + new_impl(ast, Some(&fields.unnamed), false, variant, options) + } } } -fn new_for_enum(ast: &syn::DeriveInput, data: &syn::DataEnum) -> proc_macro2::TokenStream { +fn new_for_enum( + ast: &syn::DeriveInput, + data: &syn::DataEnum, + options: &NewOptions, +) -> proc_macro2::TokenStream { if data.variants.is_empty() { panic!("#[derive(new)] cannot be implemented for enums with zero variants"); } @@ -157,7 +171,7 @@ fn new_for_enum(ast: &syn::DeriveInput, data: &syn::DataEnum) -> proc_macro2::To if v.discriminant.is_some() { panic!("#[derive(new)] cannot be implemented for enums with discriminants"); } - new_for_struct(ast, &v.fields, Some(&v.ident)) + new_for_struct(ast, &v.fields, Some(&v.ident), options) }); my_quote!(#(#impls)*) } @@ -167,6 +181,7 @@ fn new_impl( fields: Option<&Punctuated>, named: bool, variant: Option<&syn::Ident>, + options: &NewOptions, ) -> proc_macro2::TokenStream { let name = &ast.ident; let unit = fields.is_none(); @@ -205,11 +220,12 @@ fn new_impl( new.set_span(proc_macro2::Span::call_site()); let lint_attrs = collect_parent_lint_attrs(&ast.attrs); let lint_attrs = my_quote![#(#lint_attrs),*]; + let visibility = &options.visibility; my_quote! { impl #impl_generics #name #ty_generics #where_clause { #[doc = #doc] #lint_attrs - pub fn #new(#(#args),*) -> Self { + #visibility fn #new(#(#args),*) -> Self { #name #qual #inits } } @@ -220,7 +236,10 @@ fn collect_parent_lint_attrs(attrs: &[syn::Attribute]) -> Vec { fn is_lint(item: &syn::Meta) -> bool { if let syn::Meta::List(ref l) = *item { let path = &l.path; - return path.is_ident("allow") || path.is_ident("deny") || path.is_ident("forbid") || path.is_ident("warn") + return path.is_ident("allow") + || path.is_ident("deny") + || path.is_ident("forbid") + || path.is_ident("warn"); } false } @@ -245,6 +264,38 @@ fn collect_parent_lint_attrs(attrs: &[syn::Attribute]) -> Vec { .collect() } +struct NewOptions { + visibility: Option, +} + +impl NewOptions { + fn from_attributes(attrs: &[Attribute]) -> Self { + let mut visibility = None; + + for attr in attrs { + if attr.path().is_ident("new") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("visibility") { + let value: Lit = meta.value()?.parse()?; + if let Lit::Str(lit_str) = value { + // Parse the visibility string into a syn::Visibility type + let parsed_visibility: Visibility = + lit_str.parse().expect("Invalid visibility"); + visibility = Some(parsed_visibility); + } + Ok(()) + } else { + Err(meta.error("unsupported attribute")) + } + }) + .unwrap_or(()); + } + } + + NewOptions { visibility } + } +} + enum FieldAttr { Default, Value(proc_macro2::TokenStream), @@ -270,7 +321,7 @@ impl FieldAttr { .segments .last() .expect("Expected at least one segment where #[segment[::segment*](..)]"); - if (*last_attr_path).ident != "new" { + if last_attr_path.ident != "new" { continue; } let list = match attr.meta { @@ -292,26 +343,38 @@ impl FieldAttr { if path.is_ident("default") { result = Some(FieldAttr::Default); } else { - panic!("Invalid #[new] attribute: #[new({})]", path_to_string(&path)); + panic!( + "Invalid #[new] attribute: #[new({})]", + path_to_string(&path) + ); } } syn::Meta::NameValue(kv) => { - if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(ref s), .. }) = kv.value { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(ref s), + .. + }) = kv.value + { if kv.path.is_ident("value") { - let tokens = lit_str_to_token_stream(s).ok().expect(&format!( - "Invalid expression in #[new]: `{}`", - s.value() - )); + let tokens = lit_str_to_token_stream(s).ok().unwrap_or_else(|| { + panic!("Invalid expression in #[new]: `{}`", s.value()) + }); result = Some(FieldAttr::Value(tokens)); } else { - panic!("Invalid #[new] attribute: #[new({} = ..)]", path_to_string(&kv.path)); + panic!( + "Invalid #[new] attribute: #[new({} = ..)]", + path_to_string(&kv.path) + ); } } else { panic!("Non-string literal value in #[new] attribute"); } } syn::Meta::List(l) => { - panic!("Invalid #[new] attribute: #[new({}(..))]", path_to_string(&l.path)); + panic!( + "Invalid #[new] attribute: #[new({}(..))]", + path_to_string(&l.path) + ); } } } @@ -337,7 +400,7 @@ impl<'a> FieldExt<'a> { } else { syn::Ident::new(&format!("f{}", idx), proc_macro2::Span::call_site()) }, - named: named, + named, } } @@ -394,14 +457,16 @@ fn lit_str_to_token_stream(s: &syn::LitStr) -> Result TokenStream2 { - ts.into_iter().map(|mut tt| { - tt.set_span(span.clone()); - if let proc_macro2::TokenTree::Group(group) = &mut tt { - let stream = set_ts_span_recursive(group.stream(), span); - *group = proc_macro2::Group::new(group.delimiter(), stream); - } - tt - }).collect() + ts.into_iter() + .map(|mut tt| { + tt.set_span(*span); + if let proc_macro2::TokenTree::Group(group) = &mut tt { + let stream = set_ts_span_recursive(group.stream(), span); + *group = proc_macro2::Group::new(group.delimiter(), stream); + } + tt + }) + .collect() } fn to_snake_case(s: &str) -> String { @@ -410,13 +475,12 @@ fn to_snake_case(s: &str) -> String { .fold((None, None, String::new()), |(prev, ch, mut acc), next| { if let Some(ch) = ch { if let Some(prev) = prev { - if ch.is_uppercase() { - if prev.is_lowercase() + if ch.is_uppercase() + && (prev.is_lowercase() || prev.is_numeric() - || (prev.is_uppercase() && next.is_lowercase()) - { - acc.push('_'); - } + || (prev.is_uppercase() && next.is_lowercase())) + { + acc.push('_'); } } acc.extend(ch.to_lowercase()); From 61853b79702bae6d0ffa343718310f20463687a4 Mon Sep 17 00:00:00 2001 From: Niklas <71767810+niklass-l@users.noreply.github.com> Date: Sun, 26 May 2024 10:57:11 +0200 Subject: [PATCH 2/4] Make the default visibility public --- src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4a229e0..088724c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -270,7 +270,10 @@ struct NewOptions { impl NewOptions { fn from_attributes(attrs: &[Attribute]) -> Self { - let mut visibility = None; + // Default visibility is public + let mut visibility = Some(Visibility::Public(syn::token::Pub { + span: proc_macro2::Span::call_site(), + })); for attr in attrs { if attr.path().is_ident("new") { From 3311408f8fc4844e857708bf2a222c939ac1066d Mon Sep 17 00:00:00 2001 From: Niklas <71767810+niklass-l@users.noreply.github.com> Date: Sun, 26 May 2024 16:05:00 +0200 Subject: [PATCH 3/4] Add visibility to test struct to test compilation and correct macro expansion --- tests/test.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test.rs b/tests/test.rs index c47012c..95a27b1 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -27,6 +27,7 @@ fn test_unit_struct() { /// A struct with fields. #[derive(new, PartialEq, Debug)] +#[new(visibility = "pub(crate)")] pub struct Bar { pub x: i32, pub y: String, @@ -170,7 +171,6 @@ fn test_struct_with_defaults() { pub z: T, } - let x = Waldo::>::new(42); assert_eq!( x, @@ -227,7 +227,6 @@ fn test_struct_mixed_defaults() { ); } - #[cfg(feature = "std")] #[test] fn test_struct_phantom_data() { @@ -326,8 +325,8 @@ fn test_enum_unit_variants() { #[cfg(feature = "std")] #[test] fn test_more_involved_enum() { - use std::marker::PhantomData; use std::default::Default; + use std::marker::PhantomData; /// A more involved enum #[derive(new, PartialEq, Debug)] From d11f63160c0829dbf06a1aab30f431c9b58ecc37 Mon Sep 17 00:00:00 2001 From: Niklas <71767810+niklass-l@users.noreply.github.com> Date: Sun, 26 May 2024 16:16:50 +0200 Subject: [PATCH 4/4] Add documentation --- src/lib.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 088724c..699a4c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,6 +105,60 @@ //! let _ = Enum::new_third_variant(42); //! } //! ``` +//! ### Setting Visibility for the Constructor +//! +//! By default, the generated constructor will be `pub`. However, you can control the visibility of the constructor using the `#[new(visibility = "...")]` attribute. +//! +//! #### Public Constructor (default) +//! +//! ```rust +//! use derive_new::new; +//! +//! #[derive(new)] +//! pub struct Bar { +//! a: i32, +//! b: String, +//! } +//! +//! fn main() { +//! let _ = Bar::new(42, "Hello".to_owned()); +//! } +//! ``` +//! +//! #### Crate-Visible Constructor +//! +//! ```rust +//! use derive_new::new; +//! +//! #[derive(new)] +//! #[new(visibility = "pub(crate)")] +//! pub struct Bar { +//! a: i32, +//! b: String, +//! } +//! +//! fn main() { +//! let _ = Bar::new(42, "Hello".to_owned()); +//! } +//! ``` +//! +//! #### Private Constructor +//! +//! ```rust +//! use derive_new::new; +//! +//! #[derive(new)] +//! #[new(visibility = "")] +//! pub struct Bar { +//! a: i32, +//! b: String, +//! } +//! +//! fn main() { +//! // Bar::new is not accessible here as it is private +//! let _ = Bar::new(42, "Hello".to_owned()); // This will cause a compile error +//! } +//! ``` #![crate_type = "proc-macro"] #![recursion_limit = "192"]