Skip to content

Commit

Permalink
Merge pull request #824 from schungx/master
Browse files Browse the repository at this point in the history
Change derive macro to use key/value options style.
  • Loading branch information
schungx committed Jan 29, 2024
2 parents 3f21292 + fc16251 commit a89212b
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 160 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ New features
* `#[derive(CustomType)]` is now available, driven by procedural macros in `rhai_codegen`.
* A new `FuncRegistration` API is added to assist in registering native Rust functions into modules with various settings. Some of the original `Module::set_fn...` API is now deprecated.
* Functions defined in plugin modules can now be marked as `volatile` which prevents it from being optimized away even under `OptimizationLevel::Full`.
* Added `Engine::max_functions` and `Engine::set_max_functions` to limit the maximum number of functions allowed in a script. This s to guard against DOS attacks -- e.g. a simple closure `||` (two characters) is a function. When `max_function` is exceeded during script compilation, a new parse error, `TooManyFunctions`, is returned.
* Added `Engine::max_functions` and `Engine::set_max_functions` to limit the maximum number of functions allowed in a script. This is to guard against DOS attacks -- e.g. a simple closure `||` (two characters) is a function. When `max_function` is exceeded during script compilation, a new parse error, `ParseErrorType::TooManyFunctions`, is returned.
* `Engine::get_interned_string` is made public instead of gated under `internals`.

Enhancements
Expand Down
261 changes: 173 additions & 88 deletions codegen/src/custom_type.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,71 @@
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{spanned::Spanned, DeriveInput, Fields};
use quote::{quote, ToTokens};
use syn::{
punctuated::Punctuated, spanned::Spanned, DeriveInput, Expr, Fields, MetaNameValue, Path, Token,
};

const ATTR_NAME: &str = "rhai_type_name";
const ATTR_SKIP: &str = "rhai_type_skip";
const ATTR_GET: &str = "rhai_type_get";
const ATTR_GET_MUT: &str = "rhai_type_get_mut";
const ATTR_SET: &str = "rhai_type_set";
const ATTR_READONLY: &str = "rhai_type_readonly";
const ATTR_EXTRA: &str = "rhai_type_extra";
const ATTR_ROOT: &str = "rhai_type";
const ATTR_NAME: &str = "name";
const ATTR_SKIP: &str = "skip";
const ATTR_GET: &str = "get";
const ATTR_GET_MUT: &str = "get_mut";
const ATTR_SET: &str = "set";
const ATTR_READONLY: &str = "readonly";
const ATTR_EXTRA: &str = "extra";

/// Derive the `CustomType` trait for a struct.
pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream {
let type_name = input.ident;
let mut pretty_print_name = quote! { stringify!(#type_name) };
let mut extras = Vec::new();
let mut errors = Vec::new();

for attr in input.attrs.iter() {
if attr.path().is_ident(ATTR_NAME) {
// Type name
match attr.parse_args::<TokenStream>() {
Ok(name) => pretty_print_name = quote! { #name },
Err(e) => return e.into_compile_error(),
for attr in input.attrs.iter().filter(|a| a.path().is_ident(ATTR_ROOT)) {
let config_list: Result<Punctuated<Expr, Token![,]>, _> =
attr.parse_args_with(Punctuated::parse_terminated);

match config_list {
Ok(list) => {
for expr in list {
match expr {
// Key-value
syn::Expr::Assign(..) => {
let MetaNameValue { path, value, .. } =
syn::parse2::<MetaNameValue>(expr.to_token_stream()).unwrap();

if path.is_ident(ATTR_NAME) {
// Type name
pretty_print_name = value.to_token_stream();
} else if path.is_ident(ATTR_EXTRA) {
match syn::parse2::<Path>(value.to_token_stream()) {
Ok(path) => extras.push(path.to_token_stream()),
Err(err) => errors.push(err.into_compile_error()),
}
} else {
let key = path.get_ident().unwrap().to_string();
let msg = format!("invalid option: '{}'", key);
errors.push(syn::Error::new(path.span(), msg).into_compile_error());
}
}
// skip
syn::Expr::Path(path) if path.path.is_ident(ATTR_SKIP) => {
println!("SKIPPED");
}
// any other identifier
syn::Expr::Path(path) if path.path.get_ident().is_some() => {
let key = path.path.get_ident().unwrap().to_string();
let msg = format!("invalid option: '{}'", key);
errors.push(syn::Error::new(path.span(), msg).into_compile_error());
}
// Error
_ => errors.push(
syn::Error::new(expr.span(), "expecting identifier")
.into_compile_error(),
),
}
}
}
} else if attr.path().is_ident(ATTR_EXTRA) {
extras.push(
attr.parse_args()
.unwrap_or_else(syn::Error::into_compile_error),
);
Err(err) => errors.push(err.into_compile_error()),
}
}

Expand All @@ -53,71 +92,118 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream {
let mut readonly = false;
let mut skip = false;

for attr in field.attrs.iter() {
if attr.path().is_ident(ATTR_SKIP) {
skip = true;

if get_fn.is_some()
|| get_mut_fn.is_some()
|| set_fn.is_some()
|| name.is_some()
|| readonly
{
return syn::Error::new(
attr.path().span(),
format!("cannot use '{ATTR_SKIP}' with other attributes"),
)
.into_compile_error();
for attr in field.attrs.iter().filter(|a| a.path().is_ident(ATTR_ROOT)) {
let config_list: Result<Punctuated<Expr, Token![,]>, _> =
attr.parse_args_with(Punctuated::parse_terminated);

let list = match config_list {
Ok(list) => list,
Err(err) => {
errors.push(err.into_compile_error());
continue;
}
};

continue;
}
for expr in list {
let ident = match expr {
// skip
syn::Expr::Path(path) if path.path.is_ident(ATTR_SKIP) => {
skip = true;

if attr.path().is_ident(ATTR_NAME) {
name = Some(
attr.parse_args()
.unwrap_or_else(syn::Error::into_compile_error),
);
} else if attr.path().is_ident(ATTR_GET) {
get_fn = Some(
attr.parse_args()
.unwrap_or_else(syn::Error::into_compile_error),
);
} else if attr.path().is_ident(ATTR_GET_MUT) {
get_mut_fn = Some(
attr.parse_args()
.unwrap_or_else(syn::Error::into_compile_error),
);
} else if attr.path().is_ident(ATTR_SET) {
if readonly {
return syn::Error::new(
attr.path().span(),
format!("cannot use '{ATTR_SET}' with '{ATTR_READONLY}'"),
)
.into_compile_error();
}
set_fn = Some(
attr.parse_args()
.unwrap_or_else(syn::Error::into_compile_error),
);
} else if attr.path().is_ident(ATTR_READONLY) {
if set_fn.is_some() {
return syn::Error::new(
attr.path().span(),
format!("cannot use '{ATTR_READONLY}' with '{ATTR_SET}'"),
)
.into_compile_error();
}
readonly = true;
}
// `skip` cannot be used with any other attributes.
if get_fn.is_some()
|| get_mut_fn.is_some()
|| set_fn.is_some()
|| name.is_some()
|| readonly
{
let msg =
format!("cannot use '{ATTR_SKIP}' with other attributes");
errors.push(
syn::Error::new(path.span(), msg).into_compile_error(),
);
}

continue;
}
// readonly
syn::Expr::Path(path) if path.path.is_ident(ATTR_READONLY) => {
readonly = true;

if set_fn.is_some() {
let msg =
format!("cannot use '{ATTR_READONLY}' with '{ATTR_SET}'");
errors.push(
syn::Error::new(path.path.span(), msg).into_compile_error(),
);
}

if skip {
let attr_name = attr.path().get_ident().unwrap();
return syn::Error::new(
attr.path().span(),
format!("cannot use '{}' with '{ATTR_SKIP}'", attr_name),
)
.into_compile_error();
path.path.get_ident().unwrap().clone()
}
// Key-value
syn::Expr::Assign(..) => {
let MetaNameValue { path, value, .. } =
syn::parse2::<MetaNameValue>(expr.to_token_stream()).unwrap();

if path.is_ident(ATTR_NAME) {
// Type name
name = Some(value.to_token_stream());
} else if path.is_ident(ATTR_GET) {
match syn::parse2::<Path>(value.to_token_stream()) {
Ok(path) => get_fn = Some(path.to_token_stream()),
Err(err) => errors.push(err.into_compile_error()),
}
} else if path.is_ident(ATTR_GET_MUT) {
match syn::parse2::<Path>(value.to_token_stream()) {
Ok(path) => get_mut_fn = Some(path.to_token_stream()),
Err(err) => errors.push(err.into_compile_error()),
}
} else if path.is_ident(ATTR_SET) {
match syn::parse2::<Path>(value.to_token_stream()) {
Ok(path) => set_fn = Some(path.to_token_stream()),
Err(err) => errors.push(err.into_compile_error()),
}
} else if path.is_ident(ATTR_SKIP) || path.is_ident(ATTR_READONLY) {
let key = path.get_ident().unwrap().to_string();
let msg = format!("'{key}' cannot have value");
errors.push(
syn::Error::new(path.span(), msg).into_compile_error(),
);
continue;
} else {
let key = path.get_ident().unwrap().to_string();
let msg = format!("invalid option: '{key}'");
errors.push(
syn::Error::new(path.span(), msg).into_compile_error(),
);
continue;
}

path.get_ident().unwrap().clone()
}
// any other identifier
syn::Expr::Path(path) if path.path.get_ident().is_some() => {
let key = path.path.get_ident().unwrap().to_string();
let msg = format!("invalid option: '{key}'");
errors.push(syn::Error::new(path.span(), msg).into_compile_error());
continue;
}
// Error
_ => {
errors.push(
syn::Error::new(expr.span(), "expecting identifier")
.into_compile_error(),
);
continue;
}
};

if skip {
let msg = format!("cannot use '{ident}' with '{ATTR_SKIP}'");
errors.push(
syn::Error::new(attr.path().span(), msg).into_compile_error(),
);
}
}
}

Expand All @@ -139,7 +225,7 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream {
}
});

quote! { #(#iter ;)* }
quote! { #(#iter;)* }
}

syn::Data::Enum(_) => {
Expand All @@ -155,6 +241,7 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream {
quote! {
impl CustomType for #type_name {
fn build(mut builder: TypeBuilder<Self>) {
#(#errors;)*
builder.with_name(#pretty_print_name);
#accessors;
#(#extras(&mut builder);)*
Expand All @@ -163,6 +250,7 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream {
}
}

/// Generate a `TypeBuilder` accessor function.
fn generate_accessor_fns(
field: TokenStream,
name: Option<TokenStream>,
Expand All @@ -172,16 +260,13 @@ fn generate_accessor_fns(
readonly: bool,
) -> proc_macro2::TokenStream {
let get = match (get_mut, get) {
(Some(func), _) => quote! { #func },
(Some(func), _) => func,
(None, Some(func)) => quote! { |obj: &mut Self| #func(&*obj) },
(None, None) => quote! { |obj: &mut Self| obj.#field.clone() },
};

let set = set.map_or_else(
|| quote! { |obj: &mut Self, val| obj.#field = val },
|func| quote! { #func },
);
let name = name.map_or_else(|| quote! { stringify!(#field) }, |expr| quote! { #expr });
let set = set.unwrap_or_else(|| quote! { |obj: &mut Self, val| obj.#field = val });
let name = name.unwrap_or_else(|| quote! { stringify!(#field) });

if readonly {
quote! { builder.with_get(#name, #get) }
Expand Down
26 changes: 15 additions & 11 deletions codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,17 +410,21 @@ pub fn set_exported_global_fn(args: proc_macro::TokenStream) -> proc_macro::Toke
}
}

#[proc_macro_derive(
CustomType,
attributes(
rhai_type_name,
rhai_type_get,
rhai_type_set,
rhai_type_readonly,
rhai_type_skip,
rhai_type_extra
)
)]
/// Macro to implement the [`CustomType`][rhai::CustomType] trait.
///
/// # Usage
///
/// ```
/// use rhai::{CustomType, TypeBuilder};
///
/// #[derive(Clone, CustomType)]
/// struct MyType {
/// foo: i64,
/// bar: bool,
/// baz: String
/// }
/// ```
#[proc_macro_derive(CustomType, attributes(rhai_type,))]
pub fn derive_custom_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let expanded = custom_type::derive_custom_type_impl(input);
Expand Down

0 comments on commit a89212b

Please sign in to comment.