use syn::spanned::Spanned;
use crate::export::function::meta::Kind;
pub(crate) mod meta;
pub(super) fn export(meta: meta::Meta, input: syn::ItemFn) -> proc_macro::TokenStream {
let syn::ItemFn {
attrs,
vis,
sig,
block,
} = input;
let name = &sig.ident;
let (context_extract, context_arg) = match context_parse(&meta, &sig) {
Ok(arg) => arg,
Err(err) => return err.into_compile_error().into(),
};
let has_this = check_this(&meta, &sig, context_arg.is_some());
let this_arg = has_this.then(|| quote::quote!(this,));
let this_extract = has_this.then(|| {
quote::quote!(
let this = cx.this()?;
let this = neon::types::extract::TryFromJs::from_js(&mut cx, this)?;
)
});
let num_args = count_args(&sig, context_arg.is_some(), has_this);
let args = (0..num_args).map(|i| quote::format_ident!("a{i}"));
let tuple_fields = args.clone().map(|name| {
meta.json
.then(|| quote::quote!(neon::types::extract::Json(#name)))
.unwrap_or_else(|| quote::quote!(#name))
});
let return_tag = if meta.json {
quote::format_ident!("NeonJsonTag")
} else {
quote::format_ident!("NeonValueTag")
};
let result_extract = quote::quote!({
use neon::macro_internal::{ToNeonMarker, #return_tag as NeonReturnTag};
(&res).to_neon_marker::<NeonReturnTag>().neon_into_js(&mut cx, res)
});
let call_body = match meta.kind {
Kind::Async => quote::quote!(
#context_extract
#this_extract
let (#(#tuple_fields,)*) = cx.args()?;
let fut = #name(#context_arg #this_arg #(#args),*);
let fut = {
use neon::macro_internal::{ToNeonMarker, NeonValueTag};
(&fut).to_neon_marker::<NeonValueTag>().into_neon_result(&mut cx, fut)?
};
neon::macro_internal::spawn(&mut cx, fut, |mut cx, res| #result_extract)
),
Kind::AsyncFn => quote::quote!(
#context_extract
#this_extract
let (#(#tuple_fields,)*) = cx.args()?;
let fut = #name(#context_arg #this_arg #(#args),*);
neon::macro_internal::spawn(&mut cx, fut, |mut cx, res| #result_extract)
),
Kind::Normal => quote::quote!(
#context_extract
#this_extract
let (#(#tuple_fields,)*) = cx.args()?;
let res = #name(#context_arg #this_arg #(#args),*);
#result_extract
),
Kind::Task => quote::quote!(
#context_extract
#this_extract
let (#(#tuple_fields,)*) = cx.args()?;
let promise = neon::context::Context::task(&mut cx, move || #name(#context_arg #this_arg #(#args),*))
.promise(|mut cx, res| #result_extract);
Ok(neon::handle::Handle::upcast(&promise))
),
};
let wrapper_name = quote::format_ident!("__NEON_EXPORT_WRAPPER__{name}");
let wrapper_fn = quote::quote!(
#[doc(hidden)]
fn #wrapper_name(mut cx: neon::context::FunctionContext) -> neon::result::JsResult<neon::types::JsValue> {
#call_body
}
);
let export_name = meta
.name
.map(|name| quote::quote!(#name))
.unwrap_or_else(|| {
let name = to_camel_case(&name.to_string());
quote::quote!(#name)
});
let create_name = quote::format_ident!("__NEON_EXPORT_CREATE__{name}");
let create_fn = quote::quote!({
#[doc(hidden)]
#[neon::macro_internal::linkme::distributed_slice(neon::macro_internal::EXPORTS)]
#[linkme(crate = neon::macro_internal::linkme)]
fn #create_name<'cx>(
cx: &mut neon::context::ModuleContext<'cx>,
) -> neon::result::NeonResult<(&'static str, neon::handle::Handle<'cx, neon::types::JsValue>)> {
static NAME: &str = #export_name;
#wrapper_fn
neon::types::JsFunction::with_name(cx, NAME, #wrapper_name).map(|v| (
NAME,
neon::handle::Handle::upcast(&v),
))
}
});
quote::quote!(
#(#attrs) *
#vis #sig {
#create_fn
#block
}
)
.into()
}
fn count_args(sig: &syn::Signature, has_context: bool, has_this: bool) -> usize {
let n = sig.inputs.len();
match (has_context, has_this) {
(true, true) => n - 2,
(false, false) => n,
_ => n - 1,
}
}
fn context_parse(
opts: &meta::Meta,
sig: &syn::Signature,
) -> syn::Result<(
Option<proc_macro2::TokenStream>,
Option<proc_macro2::TokenStream>,
)> {
match opts.kind {
Kind::Async | Kind::Normal if check_context(opts, sig)? => {
Ok((None, Some(quote::quote!(&mut cx,))))
}
Kind::AsyncFn | Kind::Task if check_channel(opts, sig)? => Ok((
Some(quote::quote!(let ch = neon::context::Context::channel(&mut cx);)),
Some(quote::quote!(ch,)),
)),
_ => Ok((None, None)),
}
}
fn check_context(opts: &meta::Meta, sig: &syn::Signature) -> syn::Result<bool> {
let ty = match first_arg(opts, sig)? {
Some(arg) => arg,
None => return Ok(false),
};
let ty = match &*ty.ty {
syn::Type::Reference(ty) if !opts.context && is_channel_type(&ty.elem) => {
return Err(syn::Error::new(
ty.elem.span(),
"Expected `&mut Cx` instead of a `Channel` reference.",
))
}
syn::Type::Reference(ty) => ty,
_ if opts.context || is_context_type(&ty.ty) => {
return Err(syn::Error::new(
ty.ty.span(),
"Context must be a `&mut` reference.",
))
}
_ if is_channel_type(&ty.ty) => {
return Err(syn::Error::new(
ty.ty.span(),
"Expected `&mut Cx` instead of `Channel`.",
))
}
_ => return Ok(false),
};
if !opts.context && !is_context_type(&ty.elem) {
return Ok(false);
}
if ty.mutability.is_none() {
return Err(syn::Error::new(ty.span(), "Must be a `&mut` reference."));
}
Ok(true)
}
fn check_channel(opts: &meta::Meta, sig: &syn::Signature) -> syn::Result<bool> {
let ty = match first_arg(opts, sig)? {
Some(arg) => arg,
None => return Ok(false),
};
match &*ty.ty {
syn::Type::Reference(ty) if opts.context || is_channel_type(&ty.elem) => {
Err(syn::Error::new(
ty.span(),
"Expected an owned `Channel` instead of a reference.",
))
}
syn::Type::Reference(ty) if is_context_type(&ty.elem) => Err(syn::Error::new(
ty.elem.span(),
"Expected an owned `Channel` instead of a context reference.",
)),
_ if opts.context || is_channel_type(&ty.ty) => Ok(true),
_ if is_context_type(&ty.ty) => Err(syn::Error::new(
ty.ty.span(),
"Context is not available in async functions. Try a `Channel` instead.",
)),
_ => Ok(false),
}
}
fn first_arg<'a>(
opts: &meta::Meta,
sig: &'a syn::Signature,
) -> syn::Result<Option<&'a syn::PatType>> {
let arg = match sig.inputs.first() {
Some(arg) => arg,
None if opts.context => {
return Err(syn::Error::new(
sig.inputs.span(),
"Expected a context argument. Try removing the `context` attribute.",
))
}
None => return Ok(None),
};
match arg {
syn::FnArg::Typed(ty) => Ok(Some(ty)),
syn::FnArg::Receiver(arg) => Err(syn::Error::new(
arg.span(),
"Exported functions cannot receive `self`.",
)),
}
}
fn is_context_type(ty: &syn::Type) -> bool {
let ident = match type_path_ident(ty) {
Some(ident) => ident,
None => return false,
};
ident == "FunctionContext" || ident == "Cx"
}
fn is_channel_type(ty: &syn::Type) -> bool {
let ident = match type_path_ident(ty) {
Some(ident) => ident,
None => return false,
};
ident == "Channel"
}
fn type_path_ident(ty: &syn::Type) -> Option<&syn::Ident> {
let segment = match ty {
syn::Type::Path(ty) => ty.path.segments.last()?,
_ => return None,
};
Some(&segment.ident)
}
fn check_this(opts: &meta::Meta, sig: &syn::Signature, has_context: bool) -> bool {
static THIS: &str = "this";
if opts.this {
return true;
}
let first = if has_context {
sig.inputs.iter().nth(1)
} else {
sig.inputs.first()
};
let first = match first {
Some(first) => first,
None => return false,
};
let ty = match first {
syn::FnArg::Receiver(_) => return false,
syn::FnArg::Typed(ty) => ty,
};
let pat = match &*ty.pat {
syn::Pat::Ident(ident) if ident.ident == THIS => return true,
syn::Pat::TupleStruct(pat) => pat,
_ => return false,
};
let elem = match pat.elems.first() {
Some(elem) if pat.elems.len() == 1 => elem,
_ => return false,
};
match elem {
syn::Pat::Ident(ident) => ident.ident == THIS,
_ => false,
}
}
fn to_camel_case(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut it = name.chars();
let mut next = it.next();
let mut count = 0usize;
while matches!(next, Some('_')) {
out.push('_');
next = it.next();
}
while let Some(c) = next {
match c {
'_' => count += 1,
_ if c.is_uppercase() || count >= 2 => {
return name.to_string();
}
_ if count == 0 => {
out.push(c);
count = 0;
}
_ => {
out.extend(c.to_uppercase());
count = 0;
}
}
next = it.next();
}
for _ in 0..count {
out.push('_');
}
out
}
#[cfg(test)]
mod test {
#[test]
fn to_camel_case() {
use super::to_camel_case;
assert_eq!(to_camel_case(""), "");
assert_eq!(to_camel_case("one"), "one");
assert_eq!(to_camel_case("two_words"), "twoWords");
assert_eq!(to_camel_case("three_word_name"), "threeWordName");
assert_eq!(to_camel_case("extra__underscore"), "extra__underscore");
assert_eq!(to_camel_case("PreserveCase"), "PreserveCase");
assert_eq!(to_camel_case("PreServe_case"), "PreServe_case");
assert_eq!(to_camel_case("_preserve_leading"), "_preserveLeading");
assert_eq!(to_camel_case("__preserve_leading"), "__preserveLeading");
assert_eq!(to_camel_case("preserve_trailing_"), "preserveTrailing_");
assert_eq!(to_camel_case("preserve_trailing__"), "preserveTrailing__");
assert_eq!(to_camel_case("_preserve_both_"), "_preserveBoth_");
assert_eq!(to_camel_case("__preserve_both__"), "__preserveBoth__");
assert_eq!(to_camel_case("_"), "_");
assert_eq!(to_camel_case("__"), "__");
assert_eq!(to_camel_case("___"), "___");
}
}