diff --git a/codegen/src/custom_type.rs b/codegen/src/custom_type.rs index 9517ba740..962c8c2ef 100644 --- a/codegen/src/custom_type.rs +++ b/codegen/src/custom_type.rs @@ -112,11 +112,30 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream { } }; + let register = { + let method = { + quote! { builder.with_name(#display_name) } + }; + + #[cfg(feature = "metadata")] + { + let Ok(docs) = crate::attrs::doc_attributes(&input.attrs) else { + return syn::Error::new(Span::call_site(), "failed to parse doc comments") + .into_compile_error(); + }; + // Not sure how to make a Vec a literal, using a string instead. + let docs = proc_macro2::Literal::string(&docs.join("\n")); + quote! { #method.with_comments(&#docs.lines().collect::>()[..]); } + } + #[cfg(not(feature = "metadata"))] + quote! { #method; } + }; + quote! { impl CustomType for #type_name { fn build(mut builder: TypeBuilder) { #(#errors)* - builder.with_name(#display_name); + #register #(#field_accessors)* #(#extras(&mut builder);)* } @@ -220,6 +239,7 @@ fn scan_fields(fields: &[&Field], accessors: &mut Vec, errors: &mut errors.push(syn::Error::new(path.span(), msg).into_compile_error()); continue; } + // Error _ => { errors.push( @@ -264,10 +284,38 @@ fn scan_fields(fields: &[&Field], accessors: &mut Vec, errors: &mut let set = set_fn.unwrap_or_else(|| quote! { |obj: &mut Self, val| obj.#field_name = val }); let name = map_name.unwrap_or_else(|| quote! { stringify!(#field_name) }); - accessors.push(if readonly { - quote! { builder.with_get(#name, #get); } - } else { - quote! { builder.with_get_set(#name, #get, #set); } + accessors.push({ + let method = if readonly { + quote! { builder.with_get(#name, #get) } + } else { + quote! { builder.with_get_set(#name, #get, #set) } + }; + + #[cfg(feature = "metadata")] + { + match crate::attrs::doc_attributes(&field.attrs) { + Ok(docs) => { + // Not sure how to make a Vec a literal, using a string instead. + let docs = proc_macro2::Literal::string(&docs.join("\n")); + quote! { #method.and_comments(&#docs.lines().collect::>()[..]); } + } + Err(_) => { + errors.push( + syn::Error::new( + Span::call_site(), + format!( + "failed to parse doc comments for field {}", + quote! { #name } + ), + ) + .into_compile_error(), + ); + continue; + } + } + } + #[cfg(not(feature = "metadata"))] + quote! { #method; } }); } } diff --git a/codegen/src/test/custom_type.rs b/codegen/src/test/custom_type.rs index 4853fc70d..e18e36bc0 100644 --- a/codegen/src/test/custom_type.rs +++ b/codegen/src/test/custom_type.rs @@ -1,3 +1,4 @@ +#[cfg(not(feature = "metadata"))] #[cfg(test)] mod custom_type_tests { use crate::test::assert_streams_eq; @@ -85,3 +86,99 @@ mod custom_type_tests { assert_streams_eq(result, expected); } } + +#[cfg(feature = "metadata")] +#[cfg(test)] +mod custom_type_tests { + use crate::test::assert_streams_eq; + use quote::quote; + + #[test] + fn test_custom_type_tuple_struct() { + let input = quote! { + /// Bar comments. + #[derive(Clone, CustomType)] + pub struct Bar( + #[rhai_type(skip)] + #[cfg(not(feature = "no_float"))] + rhai::FLOAT, + INT, + /// boo comments. + #[rhai_type(name = "boo", readonly)] + String, + /// This is a vector. + Vec + ); + }; + + let result = crate::custom_type::derive_custom_type_impl( + syn::parse2::(input).unwrap(), + ); + + let expected = quote! { + impl CustomType for Bar { + fn build(mut builder: TypeBuilder) { + builder.with_name(stringify!(Bar)).with_comments(&"/// Bar comments.".lines().collect::>()[..]); + builder.with_get_set("field1", + |obj: &mut Self| obj.1.clone(), + |obj: &mut Self, val| obj.1 = val + ).and_comments(&"".lines().collect::>()[..]); + builder.with_get("boo", |obj: &mut Self| obj.2.clone()) + .and_comments(&"/// boo comments.".lines().collect::>()[..]); + builder.with_get_set("field3", + |obj: &mut Self| obj.3.clone(), + |obj: &mut Self, val| obj.3 = val + ).and_comments(&"/// This is a vector.".lines().collect::>()[..]); + } + } + }; + + assert_streams_eq(result, expected); + } + + #[test] + fn test_custom_type_struct() { + let input = quote! { + /// Foo comments. + #[derive(CustomType)] + #[rhai_type(skip, name = "MyFoo", extra = Self::build_extra)] + pub struct Foo { + #[cfg(not(feature = "no_float"))] + #[rhai_type(skip)] + _dummy: rhai::FLOAT, + #[rhai_type(get = get_bar)] + pub bar: INT, + /// boo comments. + #[rhai_type(name = "boo", readonly)] + pub(crate) baz: String, + #[rhai_type(set = Self::set_qux)] + pub qux: Vec + } + }; + + let result = crate::custom_type::derive_custom_type_impl( + syn::parse2::(input).unwrap(), + ); + + let expected = quote! { + impl CustomType for Foo { + fn build(mut builder: TypeBuilder) { + builder.with_name("MyFoo").with_comments(&"/// Foo comments.".lines().collect::>()[..]); + builder.with_get_set(stringify!(bar), + |obj: &mut Self| get_bar(&*obj), + |obj: &mut Self, val| obj.bar = val + ).and_comments(&"".lines().collect::>()[..]); + builder.with_get("boo", |obj: &mut Self| obj.baz.clone()) + .and_comments(&"/// boo comments.".lines().collect::>()[..]); + builder.with_get_set(stringify!(qux), + |obj: &mut Self| obj.qux.clone(), + Self::set_qux + ).and_comments(&"".lines().collect::>()[..]); + Self::build_extra(&mut builder); + } + } + }; + + assert_streams_eq(result, expected); + } +} diff --git a/examples/custom_types_and_methods.rs b/examples/custom_types_and_methods.rs index 4dd0f5618..4a98fda01 100644 --- a/examples/custom_types_and_methods.rs +++ b/examples/custom_types_and_methods.rs @@ -9,9 +9,18 @@ use rhai::{CustomType, Engine, EvalAltResult, TypeBuilder}; #[cfg(not(feature = "no_object"))] fn main() -> Result<(), Box> { + /// This is a test structure. If the metadata feature + /// is enabled, this comment will be exported. #[derive(Debug, Clone, CustomType)] #[rhai_type(extra = Self::build_extra)] struct TestStruct { + /// A number. + /// + /// ```js + /// let t = new_ts(); + /// print(t.x); // Get the value of x. + /// t.x = 42; // Set the value of x. + /// ``` x: i64, } @@ -48,6 +57,46 @@ fn main() -> Result<(), Box> { .for_each(|func| println!("{func}")); println!(); + + let docs: serde_json::Value = + serde_json::from_str(&engine.gen_fn_metadata_to_json(false).unwrap()).unwrap(); + + // compare comments from the type. + assert_eq!( + docs["customTypes"][0]["docComments"], + serde_json::json!([ + "/// This is a test structure. If the metadata feature", + "/// is enabled, this comment will be exported." + ]) + ); + + // compare comments from the getter. + assert_eq!( + docs["functions"][1]["docComments"], + serde_json::json!([ + "/// A number.", + "///", + "/// ```js", + "/// let t = new_ts();", + "/// print(t.x); // Get the value of x.", + "/// t.x = 42; // Set the value of x.", + "/// ```" + ]) + ); + + // compare comments from the setter. + assert_eq!( + docs["functions"][3]["docComments"], + serde_json::json!([ + "/// A number.", + "///", + "/// ```js", + "/// let t = new_ts();", + "/// print(t.x); // Get the value of x.", + "/// t.x = 42; // Set the value of x.", + "/// ```" + ]) + ); } let result = engine.eval::( diff --git a/src/api/build_type.rs b/src/api/build_type.rs index 0011bf25d..ecb5a808b 100644 --- a/src/api/build_type.rs +++ b/src/api/build_type.rs @@ -1,8 +1,11 @@ //! Trait to build a custom type for use with [`Engine`]. use crate::func::SendSync; +use crate::module::FuncMetadata; use crate::packages::string_basic::{FUNC_TO_DEBUG, FUNC_TO_STRING}; +use crate::FuncRegistration; use crate::{types::dynamic::Variant, Engine, Identifier, RhaiNativeFunc}; use std::marker::PhantomData; + #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -100,6 +103,8 @@ impl Engine { /// to use [`Engine::register_type_with_name`] instead. pub struct TypeBuilder<'a, T: Variant + Clone> { engine: &'a mut Engine, + /// Keep the latest registered function(s) in cache to add additional metadata. + hashes: Vec, _marker: PhantomData, } @@ -109,6 +114,7 @@ impl<'a, T: Variant + Clone> TypeBuilder<'a, T> { fn new(engine: &'a mut Engine) -> Self { Self { engine, + hashes: vec![], _marker: PhantomData, } } @@ -122,13 +128,34 @@ impl TypeBuilder<'_, T> { self } + /// Set a comments for the type. + /// `TypeBuilder::with_name` must be called before this function, otherwise + /// the comments will not be registered. + /// + /// Available with the metadata feature only. + #[cfg(feature = "metadata")] + #[inline(always)] + pub fn with_comments(&mut self, comments: &[&str]) -> &mut Self { + let namespace = self.engine.global_namespace_mut(); + if let Some(name) = namespace + .get_custom_type_raw::() + .map(|ty| ty.display_name.clone()) + { + namespace.set_custom_type_with_comments::(name.as_str(), comments); + } + + self + } + /// Pretty-print this custom type. #[inline(always)] pub fn on_print( &mut self, on_print: impl Fn(&mut T) -> String + SendSync + 'static, ) -> &mut Self { - self.engine.register_fn(FUNC_TO_STRING, on_print); + let FuncMetadata { hash, .. } = + FuncRegistration::new(FUNC_TO_STRING).register_into_engine(self.engine, on_print); + self.hashes = vec![*hash]; self } @@ -136,9 +163,11 @@ impl TypeBuilder<'_, T> { #[inline(always)] pub fn on_debug( &mut self, - on_print: impl Fn(&mut T) -> String + SendSync + 'static, + on_debug: impl Fn(&mut T) -> String + SendSync + 'static, ) -> &mut Self { - self.engine.register_fn(FUNC_TO_DEBUG, on_print); + let FuncMetadata { hash, .. } = + FuncRegistration::new(FUNC_TO_DEBUG).register_into_engine(self.engine, on_debug); + self.hashes = vec![*hash]; self } @@ -149,7 +178,23 @@ impl TypeBuilder<'_, T> { name: impl AsRef + Into, method: impl RhaiNativeFunc + SendSync + 'static, ) -> &mut Self { - self.engine.register_fn(name, method); + let FuncMetadata { hash, .. } = + FuncRegistration::new(name).register_into_engine(self.engine, method); + self.hashes = vec![*hash]; + self + } + + /// Add comments to the last registered function. + /// Available under the metadata feature only. + #[cfg(feature = "metadata")] + #[inline(always)] + pub fn and_comments(&mut self, comments: &[&str]) -> &mut Self { + let module = self.engine.global_namespace_mut(); + + for hash in &self.hashes { + module.update_fn_comments(*hash, comments); + } + self } } @@ -181,7 +226,10 @@ impl TypeBuilder<'_, T> { name: impl AsRef, get_fn: impl RhaiNativeFunc<(Mut,), 1, X, R, F> + SendSync + 'static, ) -> &mut Self { - self.engine.register_get(name, get_fn); + let FuncMetadata { hash, .. } = + FuncRegistration::new_getter(name).register_into_engine(self.engine, get_fn); + self.hashes = vec![*hash]; + self } @@ -194,7 +242,10 @@ impl TypeBuilder<'_, T> { name: impl AsRef, set_fn: impl RhaiNativeFunc<(Mut, R), 2, X, (), F> + SendSync + 'static, ) -> &mut Self { - self.engine.register_set(name, set_fn); + let FuncMetadata { hash, .. } = + FuncRegistration::new_setter(name).register_into_engine(self.engine, set_fn); + self.hashes = vec![*hash]; + self } @@ -216,7 +267,14 @@ impl TypeBuilder<'_, T> { get_fn: impl RhaiNativeFunc<(Mut,), 1, X1, R, F1> + SendSync + 'static, set_fn: impl RhaiNativeFunc<(Mut, R), 2, X2, (), F2> + SendSync + 'static, ) -> &mut Self { - self.engine.register_get_set(name, get_fn, set_fn); + let hash_1 = FuncRegistration::new_getter(&name) + .register_into_engine(self.engine, get_fn) + .hash; + let hash_2 = FuncRegistration::new_setter(&name) + .register_into_engine(self.engine, set_fn) + .hash; + self.hashes = vec![hash_1, hash_2]; + self } } @@ -238,7 +296,10 @@ impl TypeBuilder<'_, T> { &mut self, get_fn: impl RhaiNativeFunc<(Mut, IDX), 2, X, R, F> + SendSync + 'static, ) -> &mut Self { - self.engine.register_indexer_get(get_fn); + let FuncMetadata { hash, .. } = + FuncRegistration::new_index_getter().register_into_engine(self.engine, get_fn); + self.hashes = vec![*hash]; + self } @@ -255,7 +316,10 @@ impl TypeBuilder<'_, T> { &mut self, set_fn: impl RhaiNativeFunc<(Mut, IDX, R), 3, X, (), F> + SendSync + 'static, ) -> &mut Self { - self.engine.register_indexer_set(set_fn); + let FuncMetadata { hash, .. } = + FuncRegistration::new_index_setter().register_into_engine(self.engine, set_fn); + self.hashes = vec![*hash]; + self } @@ -275,7 +339,14 @@ impl TypeBuilder<'_, T> { get_fn: impl RhaiNativeFunc<(Mut, IDX), 2, X1, R, F1> + SendSync + 'static, set_fn: impl RhaiNativeFunc<(Mut, IDX, R), 3, X2, (), F2> + SendSync + 'static, ) -> &mut Self { - self.engine.register_indexer_get_set(get_fn, set_fn); + let hash_1 = FuncRegistration::new_index_getter() + .register_into_engine(self.engine, get_fn) + .hash; + let hash_2 = FuncRegistration::new_index_setter() + .register_into_engine(self.engine, set_fn) + .hash; + self.hashes = vec![hash_1, hash_2]; + self } } diff --git a/src/api/register.rs b/src/api/register.rs index 7a68988cb..aabe26fdb 100644 --- a/src/api/register.rs +++ b/src/api/register.rs @@ -74,28 +74,7 @@ impl Engine { name: impl AsRef + Into, func: FUNC, ) -> &mut Self { - let reg = FuncRegistration::new(name.into()).in_global_namespace(); - - #[cfg(feature = "metadata")] - let reg = { - let mut param_type_names = FUNC::param_names() - .iter() - .map(|ty| format!("_: {}", self.format_param_type(ty))) - .collect::>(); - - if FUNC::return_type() != TypeId::of::<()>() { - param_type_names.push(self.format_param_type(FUNC::return_type_name()).into()); - } - - let param_type_names = param_type_names - .iter() - .map(String::as_str) - .collect::>(); - - reg.with_params_info(param_type_names) - }; - - reg.set_into_module(self.global_namespace_mut(), func); + FuncRegistration::new(name.into()).register_into_engine(self, func); self } @@ -331,6 +310,7 @@ impl Engine { ) -> &mut Self { self.register_fn(crate::engine::make_getter(name.as_ref()), get_fn) } + /// Register a setter function for a member of a registered type with the [`Engine`]. /// /// Not available under `no_object`. diff --git a/src/module/mod.rs b/src/module/mod.rs index 1478e76d5..44c8c42bd 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -398,6 +398,37 @@ impl FuncRegistration { R: Variant + Clone, FUNC: RhaiNativeFunc + SendSync + 'static, { + #[cfg(feature = "metadata")] + { + // Do not update parameter informations if `with_params_info` was called previously. + if self.metadata.params_info.is_empty() { + let mut param_type_names = FUNC::param_names() + .iter() + .map(|ty| format!("_: {}", engine.format_param_type(ty))) + .collect::>(); + + if FUNC::return_type() != TypeId::of::<()>() { + param_type_names + .push(engine.format_param_type(FUNC::return_type_name()).into()); + } + + let param_type_names = param_type_names + .iter() + .map(String::as_str) + .collect::>(); + + self.with_params_info(param_type_names) + } else { + self + } + // Duplicate of code without metadata feature because it would + // require to set self as mut, which would trigger a warning without + // the metadata feature. + .in_global_namespace() + .set_into_module(engine.global_namespace_mut(), func) + } + + #[cfg(not(feature = "metadata"))] self.in_global_namespace() .set_into_module(engine.global_namespace_mut(), func) } @@ -1452,6 +1483,22 @@ impl Module { self } + /// _(metadata)_ Update the comments of a registered function. + /// Exported under the `metadata` feature only. + #[cfg(feature = "metadata")] + #[inline] + pub(crate) fn update_fn_comments>( + &mut self, + hash_fn: u64, + comments: impl IntoIterator, + ) -> &mut Self { + if let Some((_, f)) = self.functions.as_mut().and_then(|m| m.get_mut(&hash_fn)) { + f.comments = comments.into_iter().map(|s| s.as_ref().into()).collect(); + } + + self + } + /// Remap type ID. #[inline] #[must_use]