Extract doc-comment on plugin functions.

This commit is contained in:
Stephen Chung 2021-12-21 16:14:07 +08:00
parent f74486f904
commit b85a9b3c1c
11 changed files with 275 additions and 21 deletions

View File

@ -17,6 +17,7 @@ Enhancements
* Added `NativeCallContext::call_fn` to easily call a function. * 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. * 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 Deprecated API's
---------------- ----------------

View File

@ -1,6 +1,6 @@
[package] [package]
name = "rhai_codegen" name = "rhai_codegen"
version = "1.2.0" version = "1.3.0"
edition = "2018" edition = "2018"
authors = ["jhwgh1968", "Stephen Chung"] authors = ["jhwgh1968", "Stephen Chung"]
description = "Procedural macros support package for Rhai, a scripting language and engine for Rust" description = "Procedural macros support package for Rhai, a scripting language and engine for Rust"
@ -16,7 +16,7 @@ default = []
metadata = [] metadata = []
[dev-dependencies] [dev-dependencies]
rhai = { path = "..", version = "1.1" } rhai = { path = "..", version = "1.4" }
trybuild = "1" trybuild = "1"
[dependencies] [dependencies]

View File

@ -117,18 +117,60 @@ pub fn inner_item_attributes<T: ExportedParams>(
attrs: &mut Vec<syn::Attribute>, attrs: &mut Vec<syn::Attribute>,
attr_name: &str, attr_name: &str,
) -> syn::Result<T> { ) -> syn::Result<T> {
// Find the #[rhai_fn] attribute which will turn be read for the function parameters. // Find the #[rhai_fn] attribute which will turn be read for function parameters.
if let Some(rhai_fn_idx) = attrs if let Some(index) = attrs
.iter() .iter()
.position(|a| a.path.get_ident().map(|i| *i == attr_name).unwrap_or(false)) .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) rhai_fn_attr.parse_args_with(T::parse_stream)
} else { } else {
Ok(T::no_attrs()) Ok(T::no_attrs())
} }
} }
#[cfg(feature = "metadata")]
pub fn doc_attribute(attrs: &mut Vec<syn::Attribute>) -> syn::Result<String> {
// 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<syn::Attribute> { pub fn collect_cfg_attr(attrs: &[syn::Attribute]) -> Vec<syn::Attribute> {
attrs attrs
.iter() .iter()

View File

@ -282,6 +282,8 @@ pub struct ExportedFn {
mut_receiver: bool, mut_receiver: bool,
params: ExportedFnParams, params: ExportedFnParams,
cfg_attrs: Vec<syn::Attribute>, cfg_attrs: Vec<syn::Attribute>,
#[cfg(feature = "metadata")]
comment: String,
} }
impl Parse for ExportedFn { impl Parse for ExportedFn {
@ -404,6 +406,8 @@ impl Parse for ExportedFn {
mut_receiver, mut_receiver,
params: Default::default(), params: Default::default(),
cfg_attrs, 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<syn::Attribute>) { pub fn set_cfg_attrs(&mut self, cfg_attrs: Vec<syn::Attribute>) {
self.cfg_attrs = cfg_attrs self.cfg_attrs = cfg_attrs
} }

View File

@ -117,18 +117,22 @@ impl Parse for Module {
syn::Item::Fn(f) => Some(f), syn::Item::Fn(f) => Some(f),
_ => None, _ => None,
}) })
.try_fold(Vec::new(), |mut vec, item_fn| { .try_fold(Vec::new(), |mut vec, item_fn| -> syn::Result<_> {
let params = let params =
crate::attrs::inner_item_attributes(&mut item_fn.attrs, "rhai_fn")?; crate::attrs::inner_item_attributes(&mut item_fn.attrs, "rhai_fn")?;
syn::parse2::<ExportedFn>(item_fn.to_token_stream()) let f =
.and_then(|mut f| { syn::parse2(item_fn.to_token_stream()).and_then(|mut f: ExportedFn| {
f.set_params(params)?; f.set_params(params)?;
f.set_cfg_attrs(crate::attrs::collect_cfg_attr(&item_fn.attrs)); 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) Ok(f)
}) })?;
.map(|f| vec.push(f))
.map(|_| vec) vec.push(f);
Ok(vec)
})?; })?;
// Gather and parse constants definitions. // Gather and parse constants definitions.
for item in content.iter() { for item in content.iter() {

View File

@ -166,20 +166,30 @@ pub fn generate_body(
); );
#[cfg(feature = "metadata")] #[cfg(feature = "metadata")]
let param_names = quote! { let (param_names, comment) = (
Some(#fn_token_name::PARAM_NAMES) quote! { Some(#fn_token_name::PARAM_NAMES) },
}; function.comment(),
);
#[cfg(not(feature = "metadata"))] #[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::<syn::Stmt>(quote! { syn::parse2::<syn::Stmt>(quote! {
#(#cfg_attrs)* #(#cfg_attrs)*
m.set_fn(#fn_literal, FnNamespace::#ns_str, FnAccess::Public, m.set_fn(#fn_literal, FnNamespace::#ns_str, FnAccess::Public,
#param_names, &[#(#fn_input_types),*], #fn_token_name().into()); #param_names, &[#(#fn_input_types),*], #fn_token_name().into());
}) })
.unwrap(), .unwrap()
); } else {
let comment_literal = syn::LitStr::new(comment, Span::call_site());
syn::parse2::<syn::Stmt>(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! { gen_fn_tokens.push(quote! {

View File

@ -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::<Module>(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::<syn::Type>(quote! { INT }).unwrap()
);
}
#[test] #[test]
fn one_single_arg_fn_module() { fn one_single_arg_fn_module() {
let input_tokens: TokenStream = quote! { let input_tokens: TokenStream = quote! {
@ -323,6 +353,66 @@ mod generate_tests {
assert_streams_eq(item_mod.generate(), expected_tokens); 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::<Module>(input_tokens).unwrap();
assert_streams_eq(item_mod.generate(), expected_tokens);
}
#[test] #[test]
fn one_single_arg_global_fn_module() { fn one_single_arg_global_fn_module() {
let input_tokens: TokenStream = quote! { let input_tokens: TokenStream = quote! {

View File

@ -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");
}
}

View File

@ -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

View File

@ -10,7 +10,7 @@ use crate::tokenizer::Token;
use crate::types::dynamic::Variant; use crate::types::dynamic::Variant;
use crate::{ use crate::{
calc_fn_params_hash, calc_qualified_fn_hash, combine_hashes, Dynamic, EvalAltResult, 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")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
@ -53,6 +53,9 @@ pub struct FuncInfo {
/// Return type name. /// Return type name.
#[cfg(feature = "metadata")] #[cfg(feature = "metadata")]
pub return_type_name: Identifier, pub return_type_name: Identifier,
/// Comments.
#[cfg(feature = "metadata")]
pub comments: SmartString,
} }
impl FuncInfo { impl FuncInfo {
@ -488,6 +491,11 @@ impl Module {
param_names_and_types, param_names_and_types,
#[cfg(feature = "metadata")] #[cfg(feature = "metadata")]
return_type_name: self.identifiers.get("Dynamic"), 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::<CallableFunction>::into(fn_def).into(), func: Into::<CallableFunction>::into(fn_def).into(),
} }
.into(), .into(),
@ -750,6 +758,8 @@ impl Module {
param_names_and_types: param_names, param_names_and_types: param_names,
#[cfg(feature = "metadata")] #[cfg(feature = "metadata")]
return_type_name, return_type_name,
#[cfg(feature = "metadata")]
comments: SmartString::new_const(),
func: func.into(), func: func.into(),
} }
.into(), .into(),
@ -761,6 +771,47 @@ impl Module {
hash_fn 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<str> + Into<Identifier>,
namespace: FnNamespace,
access: FnAccess,
arg_names: Option<&[&str]>,
arg_types: &[TypeId],
comment: impl Into<SmartString>,
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], /// 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 /// the current set of functions, plus a list of mutable [`Dynamic`] references
/// into the [`Module`], returning a non-zero hash key. /// into the [`Module`], returning a non-zero hash key.

View File

@ -60,7 +60,7 @@ impl PartialOrd for FnParam<'_> {
Some(match self.name.partial_cmp(&other.name).expect("succeed") { Some(match self.name.partial_cmp(&other.name).expect("succeed") {
Ordering::Less => Ordering::Less, Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater, 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() .as_ref()
.map_or_else(|| Vec::new(), |v| v.iter().map(|s| &**s).collect()) .map_or_else(|| Vec::new(), |v| v.iter().map(|s| &**s).collect())
} else { } else {
#[cfg(not(feature = "metadata"))]
{
Vec::new() Vec::new()
}
#[cfg(feature = "metadata")]
{
info.comments.split("\n").collect()
}
}, },
} }
} }