Skip to content

Commit

Permalink
Merge pull request #67 from Paragit-Solutions/implement-new-method-vi…
Browse files Browse the repository at this point in the history
…sibility

Added support for visibility on the generated new method
  • Loading branch information
nrc authored Aug 29, 2024
2 parents cacbada + d11f631 commit 77cb828
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 39 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = "2"
syn = {version = "2", features = ["parsing"]}

[features]
default = ["std"]
Expand Down
191 changes: 156 additions & 35 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -119,19 +173,24 @@ macro_rules! my_quote {
}

fn path_to_string(path: &syn::Path) -> String {
path.segments.iter().map(|s| s.ident.to_string()).collect::<Vec<String>>().join("::")
path.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<String>>()
.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()
Expand All @@ -141,23 +200,32 @@ 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");
}
let impls = data.variants.iter().map(|v| {
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)*)
}
Expand All @@ -167,6 +235,7 @@ fn new_impl(
fields: Option<&Punctuated<syn::Field, Token![,]>>,
named: bool,
variant: Option<&syn::Ident>,
options: &NewOptions,
) -> proc_macro2::TokenStream {
let name = &ast.ident;
let unit = fields.is_none();
Expand Down Expand Up @@ -205,11 +274,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
}
}
Expand All @@ -220,7 +290,10 @@ fn collect_parent_lint_attrs(attrs: &[syn::Attribute]) -> Vec<syn::Attribute> {
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
}
Expand All @@ -245,6 +318,41 @@ fn collect_parent_lint_attrs(attrs: &[syn::Attribute]) -> Vec<syn::Attribute> {
.collect()
}

struct NewOptions {
visibility: Option<syn::Visibility>,
}

impl NewOptions {
fn from_attributes(attrs: &[Attribute]) -> Self {
// 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") {
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),
Expand All @@ -270,7 +378,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 {
Expand All @@ -292,26 +400,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)
);
}
}
}
Expand All @@ -337,7 +457,7 @@ impl<'a> FieldExt<'a> {
} else {
syn::Ident::new(&format!("f{}", idx), proc_macro2::Span::call_site())
},
named: named,
named,
}
}

Expand Down Expand Up @@ -394,14 +514,16 @@ fn lit_str_to_token_stream(s: &syn::LitStr) -> Result<TokenStream2, proc_macro2:
}

fn set_ts_span_recursive(ts: TokenStream2, span: &proc_macro2::Span) -> 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 {
Expand All @@ -410,13 +532,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());
Expand Down
5 changes: 2 additions & 3 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -170,7 +171,6 @@ fn test_struct_with_defaults() {
pub z: T,
}


let x = Waldo::<Vec<String>>::new(42);
assert_eq!(
x,
Expand Down Expand Up @@ -227,7 +227,6 @@ fn test_struct_mixed_defaults() {
);
}


#[cfg(feature = "std")]
#[test]
fn test_struct_phantom_data() {
Expand Down Expand Up @@ -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)]
Expand Down

0 comments on commit 77cb828

Please sign in to comment.