From b85a9b3c1cefe88dad3a808ef0f7b43f97aa1f2c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 21 Dec 2021 16:14:07 +0800 Subject: [PATCH] Extract doc-comment on plugin functions. --- CHANGELOG.md | 1 + codegen/Cargo.toml | 4 +- codegen/src/attrs.rs | 48 +++++++++- codegen/src/function.rs | 14 +++ codegen/src/module.rs | 16 ++-- codegen/src/rhai_module.rs | 24 +++-- codegen/src/test/module.rs | 90 +++++++++++++++++++ codegen/ui_tests/rhai_fn_duplicate_attr.rs | 18 ++++ .../ui_tests/rhai_fn_duplicate_attr.stderr | 17 ++++ src/module/mod.rs | 53 ++++++++++- src/serde/metadata.rs | 11 ++- 11 files changed, 275 insertions(+), 21 deletions(-) create mode 100644 codegen/ui_tests/rhai_fn_duplicate_attr.rs create mode 100644 codegen/ui_tests/rhai_fn_duplicate_attr.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index 46cf10f8..f5d9175d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Enhancements * Added `NativeCallContext::call_fn` to easily call a function. * A new syntax is introduced for `def_package!` that will replace the old syntax in future versions. +* Doc-comments on plugin module functions are extracted into the functions' metadata. Deprecated API's ---------------- diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 472c5ca1..8c471813 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai_codegen" -version = "1.2.0" +version = "1.3.0" edition = "2018" authors = ["jhwgh1968", "Stephen Chung"] description = "Procedural macros support package for Rhai, a scripting language and engine for Rust" @@ -16,7 +16,7 @@ default = [] metadata = [] [dev-dependencies] -rhai = { path = "..", version = "1.1" } +rhai = { path = "..", version = "1.4" } trybuild = "1" [dependencies] diff --git a/codegen/src/attrs.rs b/codegen/src/attrs.rs index 977e73de..ede7c88b 100644 --- a/codegen/src/attrs.rs +++ b/codegen/src/attrs.rs @@ -117,18 +117,60 @@ pub fn inner_item_attributes( attrs: &mut Vec, attr_name: &str, ) -> syn::Result { - // Find the #[rhai_fn] attribute which will turn be read for the function parameters. - if let Some(rhai_fn_idx) = attrs + // Find the #[rhai_fn] attribute which will turn be read for function parameters. + if let Some(index) = attrs .iter() .position(|a| a.path.get_ident().map(|i| *i == attr_name).unwrap_or(false)) { - let rhai_fn_attr = attrs.remove(rhai_fn_idx); + let rhai_fn_attr = attrs.remove(index); + + // Cannot have more than one #[rhai_fn] + if let Some(duplicate) = attrs + .iter() + .find(|a| a.path.get_ident().map(|i| *i == attr_name).unwrap_or(false)) + { + return Err(syn::Error::new( + duplicate.span(), + format!("duplicated attribute '{}'", attr_name), + )); + } + rhai_fn_attr.parse_args_with(T::parse_stream) } else { Ok(T::no_attrs()) } } +#[cfg(feature = "metadata")] +pub fn doc_attribute(attrs: &mut Vec) -> syn::Result { + // Find the #[doc] attribute which will turn be read for function documentation. + let mut comments = String::new(); + + while let Some(index) = attrs + .iter() + .position(|attr| attr.path.get_ident().map(|i| *i == "doc").unwrap_or(false)) + { + let attr = attrs.remove(index); + let meta = attr.parse_meta()?; + + match meta { + syn::Meta::NameValue(syn::MetaNameValue { + lit: syn::Lit::Str(s), + .. + }) => { + if !comments.is_empty() { + comments += "\n"; + } + + comments += &s.value(); + } + _ => continue, + } + } + + Ok(comments) +} + pub fn collect_cfg_attr(attrs: &[syn::Attribute]) -> Vec { attrs .iter() diff --git a/codegen/src/function.rs b/codegen/src/function.rs index 5e70c8c8..53d1f95d 100644 --- a/codegen/src/function.rs +++ b/codegen/src/function.rs @@ -282,6 +282,8 @@ pub struct ExportedFn { mut_receiver: bool, params: ExportedFnParams, cfg_attrs: Vec, + #[cfg(feature = "metadata")] + comment: String, } impl Parse for ExportedFn { @@ -404,6 +406,8 @@ impl Parse for ExportedFn { mut_receiver, params: Default::default(), cfg_attrs, + #[cfg(feature = "metadata")] + comment: Default::default(), }) } } @@ -503,6 +507,16 @@ impl ExportedFn { } } + #[cfg(feature = "metadata")] + pub fn comment(&self) -> &str { + &self.comment + } + + #[cfg(feature = "metadata")] + pub fn set_comment(&mut self, comment: String) { + self.comment = comment + } + pub fn set_cfg_attrs(&mut self, cfg_attrs: Vec) { self.cfg_attrs = cfg_attrs } diff --git a/codegen/src/module.rs b/codegen/src/module.rs index 75099218..38b2509f 100644 --- a/codegen/src/module.rs +++ b/codegen/src/module.rs @@ -117,18 +117,22 @@ impl Parse for Module { syn::Item::Fn(f) => Some(f), _ => None, }) - .try_fold(Vec::new(), |mut vec, item_fn| { + .try_fold(Vec::new(), |mut vec, item_fn| -> syn::Result<_> { let params = crate::attrs::inner_item_attributes(&mut item_fn.attrs, "rhai_fn")?; - syn::parse2::(item_fn.to_token_stream()) - .and_then(|mut f| { + let f = + syn::parse2(item_fn.to_token_stream()).and_then(|mut f: ExportedFn| { f.set_params(params)?; f.set_cfg_attrs(crate::attrs::collect_cfg_attr(&item_fn.attrs)); + + #[cfg(feature = "metadata")] + f.set_comment(crate::attrs::doc_attribute(&mut item_fn.attrs)?); Ok(f) - }) - .map(|f| vec.push(f)) - .map(|_| vec) + })?; + + vec.push(f); + Ok(vec) })?; // Gather and parse constants definitions. for item in content.iter() { diff --git a/codegen/src/rhai_module.rs b/codegen/src/rhai_module.rs index a16eab56..bf12102c 100644 --- a/codegen/src/rhai_module.rs +++ b/codegen/src/rhai_module.rs @@ -166,20 +166,30 @@ pub fn generate_body( ); #[cfg(feature = "metadata")] - let param_names = quote! { - Some(#fn_token_name::PARAM_NAMES) - }; + let (param_names, comment) = ( + quote! { Some(#fn_token_name::PARAM_NAMES) }, + function.comment(), + ); #[cfg(not(feature = "metadata"))] - let param_names = quote! { None }; + let (param_names, comment) = (quote! { None }, ""); - set_fn_statements.push( + set_fn_statements.push(if comment.is_empty() { syn::parse2::(quote! { #(#cfg_attrs)* m.set_fn(#fn_literal, FnNamespace::#ns_str, FnAccess::Public, #param_names, &[#(#fn_input_types),*], #fn_token_name().into()); }) - .unwrap(), - ); + .unwrap() + } else { + let comment_literal = syn::LitStr::new(comment, Span::call_site()); + + syn::parse2::(quote! { + #(#cfg_attrs)* + m.set_fn_with_comment(#fn_literal, FnNamespace::#ns_str, FnAccess::Public, + #param_names, &[#(#fn_input_types),*], #comment_literal, #fn_token_name().into()); + }) + .unwrap() + }); } gen_fn_tokens.push(quote! { diff --git a/codegen/src/test/module.rs b/codegen/src/test/module.rs index 3ada9401..597dbb46 100644 --- a/codegen/src/test/module.rs +++ b/codegen/src/test/module.rs @@ -37,6 +37,36 @@ mod module_tests { ); } + #[test] + fn one_factory_fn_with_comment_module() { + let input_tokens: TokenStream = quote! { + pub mod one_fn { + /// This is a doc-comment. + /// Another line. + /** block doc-comment */ + // Regular comment + /// Final line. + /** doc-comment + in multiple lines + */ + pub fn get_mystic_number() -> INT { + 42 + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert!(item_mod.consts().is_empty()); + assert_eq!(item_mod.fns().len(), 1); + assert_eq!(item_mod.fns()[0].name().to_string(), "get_mystic_number"); + assert_eq!(item_mod.fns()[0].comment(), " This is a doc-comment.\n Another line.\n block doc-comment \n Final line.\n doc-comment\n in multiple lines\n "); + assert_eq!(item_mod.fns()[0].arg_count(), 0); + assert_eq!( + item_mod.fns()[0].return_type().unwrap(), + &syn::parse2::(quote! { INT }).unwrap() + ); + } + #[test] fn one_single_arg_fn_module() { let input_tokens: TokenStream = quote! { @@ -323,6 +353,66 @@ mod generate_tests { assert_streams_eq(item_mod.generate(), expected_tokens); } + #[test] + fn one_factory_fn_with_comment_module() { + let input_tokens: TokenStream = quote! { + pub mod one_fn { + /// This is a doc-comment. + /// Another line. + /** block doc-comment */ + // Regular comment + /// Final line. + /** doc-comment + in multiple lines + */ + pub fn get_mystic_number() -> INT { + 42 + } + } + }; + + let expected_tokens = quote! { + pub mod one_fn { + pub fn get_mystic_number() -> INT { + 42 + } + #[allow(unused_imports)] + use super::*; + + pub fn rhai_module_generate() -> Module { + let mut m = Module::new(); + rhai_generate_into_module(&mut m, false); + m.build_index(); + m + } + #[allow(unused_mut)] + pub fn rhai_generate_into_module(m: &mut Module, flatten: bool) { + m.set_fn_with_comment("get_mystic_number", FnNamespace::Internal, FnAccess::Public, + Some(get_mystic_number_token::PARAM_NAMES), &[], " This is a doc-comment.\n Another line.\n block doc-comment \n Final line.\n doc-comment\n in multiple lines\n ", + get_mystic_number_token().into()); + if flatten {} else {} + } + #[allow(non_camel_case_types)] + pub struct get_mystic_number_token(); + impl get_mystic_number_token { + pub const PARAM_NAMES: &'static [&'static str] = &["INT"]; + #[inline(always)] pub fn param_types() -> [TypeId; 0usize] { [] } + } + impl PluginFunction for get_mystic_number_token { + #[inline(always)] + fn call(&self, context: NativeCallContext, args: &mut [&mut Dynamic]) -> RhaiResult { + Ok(Dynamic::from(get_mystic_number())) + } + + #[inline(always)] fn is_method_call(&self) -> bool { false } + } + } + }; + + let item_mod = syn::parse2::(input_tokens).unwrap(); + assert_streams_eq(item_mod.generate(), expected_tokens); + } + #[test] fn one_single_arg_global_fn_module() { let input_tokens: TokenStream = quote! { diff --git a/codegen/ui_tests/rhai_fn_duplicate_attr.rs b/codegen/ui_tests/rhai_fn_duplicate_attr.rs new file mode 100644 index 00000000..b775fe4d --- /dev/null +++ b/codegen/ui_tests/rhai_fn_duplicate_attr.rs @@ -0,0 +1,18 @@ +use rhai::plugin::*; + +#[export_module] +pub mod test_module { + #[rhai_fn(name = "test")] + #[rhai_fn(pure)] + pub fn test_fn(input: Point) -> bool { + input.x > input.y + } +} + +fn main() { + if test_module::test_fn(n) { + println!("yes"); + } else { + println!("no"); + } +} diff --git a/codegen/ui_tests/rhai_fn_duplicate_attr.stderr b/codegen/ui_tests/rhai_fn_duplicate_attr.stderr new file mode 100644 index 00000000..983e5d1d --- /dev/null +++ b/codegen/ui_tests/rhai_fn_duplicate_attr.stderr @@ -0,0 +1,17 @@ +error: duplicated attribute 'rhai_fn' + --> ui_tests/rhai_fn_duplicate_attr.rs:6:5 + | +6 | #[rhai_fn(pure)] + | ^^^^^^^^^^^^^^^^ + +error[E0433]: failed to resolve: use of undeclared crate or module `test_module` + --> ui_tests/rhai_fn_duplicate_attr.rs:13:8 + | +13 | if test_module::test_fn(n) { + | ^^^^^^^^^^^ use of undeclared crate or module `test_module` + +error[E0425]: cannot find value `n` in this scope + --> ui_tests/rhai_fn_duplicate_attr.rs:13:29 + | +13 | if test_module::test_fn(n) { + | ^ not found in this scope diff --git a/src/module/mod.rs b/src/module/mod.rs index 7a4a25ae..e29d22fe 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -10,7 +10,7 @@ use crate::tokenizer::Token; use crate::types::dynamic::Variant; use crate::{ calc_fn_params_hash, calc_qualified_fn_hash, combine_hashes, Dynamic, EvalAltResult, - Identifier, ImmutableString, NativeCallContext, Shared, StaticVec, + Identifier, ImmutableString, NativeCallContext, Shared, SmartString, StaticVec, }; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -53,6 +53,9 @@ pub struct FuncInfo { /// Return type name. #[cfg(feature = "metadata")] pub return_type_name: Identifier, + /// Comments. + #[cfg(feature = "metadata")] + pub comments: SmartString, } impl FuncInfo { @@ -488,6 +491,11 @@ impl Module { param_names_and_types, #[cfg(feature = "metadata")] return_type_name: self.identifiers.get("Dynamic"), + #[cfg(feature = "metadata")] + comments: fn_def + .comments + .as_ref() + .map_or(SmartString::new_const(), |v| v.join("\n").into()), func: Into::::into(fn_def).into(), } .into(), @@ -750,6 +758,8 @@ impl Module { param_names_and_types: param_names, #[cfg(feature = "metadata")] return_type_name, + #[cfg(feature = "metadata")] + comments: SmartString::new_const(), func: func.into(), } .into(), @@ -761,6 +771,47 @@ impl Module { hash_fn } + /// _(metadata)_ Set a Rust function into the [`Module`], returning a non-zero hash key. + /// Exported under the `metadata` feature only. + /// + /// If there is an existing Rust function of the same hash, it is replaced. + /// + /// # WARNING - Low Level API + /// + /// This function is very low level. + /// + /// ## Parameter Names and Types + /// + /// Each parameter name/type pair should be a single string of the format: `var_name: type`. + /// + /// ## Return Type + /// + /// The _last entry_ in the list should be the _return type_ of the function. + /// In other words, the number of entries should be one larger than the number of parameters. + #[cfg(feature = "metadata")] + #[inline] + pub fn set_fn_with_comment( + &mut self, + name: impl AsRef + Into, + namespace: FnNamespace, + access: FnAccess, + arg_names: Option<&[&str]>, + arg_types: &[TypeId], + comment: impl Into, + func: CallableFunction, + ) -> u64 { + let hash = self.set_fn(name, namespace, access, arg_names, arg_types, func); + + let comment = comment.into(); + + if !comment.is_empty() { + let f = self.functions.get_mut(&hash).expect("exists"); + f.comments = comment; + } + + hash + } + /// Set a Rust function taking a reference to the scripting [`Engine`][crate::Engine], /// the current set of functions, plus a list of mutable [`Dynamic`] references /// into the [`Module`], returning a non-zero hash key. diff --git a/src/serde/metadata.rs b/src/serde/metadata.rs index 2aa999b7..d214a701 100644 --- a/src/serde/metadata.rs +++ b/src/serde/metadata.rs @@ -60,7 +60,7 @@ impl PartialOrd for FnParam<'_> { Some(match self.name.partial_cmp(&other.name).expect("succeed") { Ordering::Less => Ordering::Less, Ordering::Greater => Ordering::Greater, - Ordering::Equal => self.typ.partial_cmp(other.typ).expect("succeed"), + Ordering::Equal => self.typ.partial_cmp(&other.typ).expect("succeed"), }) } } @@ -162,7 +162,14 @@ impl<'a> From<&'a crate::module::FuncInfo> for FnMetadata<'a> { .as_ref() .map_or_else(|| Vec::new(), |v| v.iter().map(|s| &**s).collect()) } else { - Vec::new() + #[cfg(not(feature = "metadata"))] + { + Vec::new() + } + #[cfg(feature = "metadata")] + { + info.comments.split("\n").collect() + } }, } }