From 48bc4c31ae5665720befaa2ad3fccbe649b630b4 Mon Sep 17 00:00:00 2001 From: Mees Delzenne Date: Mon, 11 Sep 2023 18:22:48 +0200 Subject: [PATCH] Implemented initial version of module macro --- core/src/value/module.rs | 12 +- macro/src/class.rs | 2 +- macro/src/common.rs | 18 ++ macro/src/function.rs | 35 ++- macro/src/lib.rs | 14 +- macro/src/module.rs | 72 ------ macro/src/module/config.rs | 274 +++++++++++++++++++++ macro/src/module/declare.rs | 32 +++ macro/src/module/evaluate.rs | 29 +++ macro/src/module/mod.rs | 455 +++++++++++++++++++++++++++++++++++ tests/macros/pass_module.rs | 123 ++++++++++ 11 files changed, 983 insertions(+), 83 deletions(-) delete mode 100644 macro/src/module.rs create mode 100644 macro/src/module/config.rs create mode 100644 macro/src/module/declare.rs create mode 100644 macro/src/module/evaluate.rs create mode 100644 macro/src/module/mod.rs create mode 100644 tests/macros/pass_module.rs diff --git a/core/src/value/module.rs b/core/src/value/module.rs index d9deb9a61..692c409b3 100644 --- a/core/src/value/module.rs +++ b/core/src/value/module.rs @@ -320,6 +320,11 @@ impl Declarations { } Ok(()) } + + /// Returns an iterator over existing declarations. + pub fn iter(&self) -> impl Iterator> { + self.declarations.iter() + } } struct Export<'js> { @@ -387,6 +392,11 @@ impl<'js> Exports<'js> { } Ok(()) } + + /// Returns an iterator over existing imports. + pub fn iter(&self) -> impl Iterator)> { + self.exports.iter().map(|x| (x.name.as_c_str(), &x.value)) + } } /// A JavaScript module. @@ -932,7 +942,7 @@ mod test { } fn evaluate<'js>(ctx: &Ctx<'js>, _exports: &mut Exports<'js>) -> Result<()> { - let _ = ctx.eval(r#"throw new Error("kaboom")"#)?; + ctx.eval::<(), _>(r#"throw new Error("kaboom")"#)?; Ok(()) } } diff --git a/macro/src/class.rs b/macro/src/class.rs index bcb5ae0a4..9a093c9c7 100644 --- a/macro/src/class.rs +++ b/macro/src/class.rs @@ -14,7 +14,7 @@ use crate::{ fields::Fields, }; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub(crate) struct ClassConfig { pub frozen: bool, pub crate_: Option, diff --git a/macro/src/common.rs b/macro/src/common.rs index c0f75c72d..57eda067a 100644 --- a/macro/src/common.rs +++ b/macro/src/common.rs @@ -2,6 +2,7 @@ use convert_case::Case as ConvertCase; use proc_macro2::Span; use proc_macro_crate::FoundCrate; use proc_macro_error::{abort, abort_call_site}; +use quote::{ToTokens, TokenStreamExt}; use syn::{ fold::Fold, parse::{Parse, ParseStream}, @@ -46,6 +47,19 @@ impl Parse for Case { } } +impl ToTokens for Case { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Case::Lower => tokens.append_all(["lowercase"]), + Case::Upper => tokens.append_all(["UPPERCASE"]), + Case::Camel => tokens.append_all(["camelCase"]), + Case::Pascal => tokens.append_all(["PascalCase"]), + Case::Snake => tokens.append_all(["snake_case"]), + Case::ScreamingSnake => tokens.append_all(["SCREAMING_SNAKE"]), + } + } +} + pub(crate) trait AbortResultExt { type Ouput; fn unwrap_or_abort(self) -> Self::Ouput; @@ -132,6 +146,8 @@ pub(crate) mod kw { syn::custom_keyword!(skip_trace); syn::custom_keyword!(rename); syn::custom_keyword!(rename_all); + syn::custom_keyword!(rename_vars); + syn::custom_keyword!(rename_types); syn::custom_keyword!(get); syn::custom_keyword!(set); syn::custom_keyword!(constructor); @@ -139,4 +155,6 @@ pub(crate) mod kw { syn::custom_keyword!(configurable); syn::custom_keyword!(enumerable); syn::custom_keyword!(prefix); + syn::custom_keyword!(declare); + syn::custom_keyword!(evaluate); } diff --git a/macro/src/function.rs b/macro/src/function.rs index 3c8c917f9..8a0fc198a 100644 --- a/macro/src/function.rs +++ b/macro/src/function.rs @@ -1,3 +1,4 @@ +use convert_case::Casing; use proc_macro2::{Ident, TokenStream}; use proc_macro_error::abort; use quote::{format_ident, quote}; @@ -11,7 +12,7 @@ use syn::{ use crate::{ attrs::{take_attributes, OptionList, ValueOption}, - common::{crate_ident, kw, AbortResultExt, SelfReplacer, BASE_PREFIX}, + common::{crate_ident, kw, AbortResultExt, Case, SelfReplacer, BASE_PREFIX}, }; #[derive(Debug, Default)] @@ -51,14 +52,35 @@ impl FunctionConfig { self.rename = Some(x.value.value()); } FunctionOption::Prefix(ref x) => { - self.rename = Some(x.value.value()); + self.prefix = Some(x.value.value()); } } } + /// Returns a name under which we can access the rquickjs crate. pub fn crate_name(&self) -> String { self.crate_.clone().unwrap_or_else(crate_ident) } + + /// Returns the name of the carry type for which JsFunction will be implemented + pub fn carry_name(&self, name: &Ident) -> Ident { + Ident::new( + &format!("{}{}", self.prefix.as_deref().unwrap_or("js_"), name), + name.span(), + ) + } + + /// The name for the javascript side + pub fn js_name(&self, rust_name: &Ident, case: Option) -> String { + if let Some(x) = self.rename.as_ref() { + return x.clone(); + } + let name = rust_name.to_string(); + if let Some(case) = case { + return name.to_case(case.to_convert_case()); + } + name + } } pub(crate) fn expand(options: OptionList, mut item: syn::ItemFn) -> TokenStream { @@ -82,13 +104,14 @@ pub(crate) fn expand(options: OptionList, mut item: syn::ItemFn) .unwrap_or_abort(); let crate_name = format_ident!("{}", config.crate_name()); - let prefix = config.prefix.unwrap_or_else(|| BASE_PREFIX.to_string()); + let prefix = config.prefix.as_deref().unwrap_or(BASE_PREFIX); let func = JsFunction::new(item.vis.clone(), &item.sig, None); - let carry_type = func.expand_carry_type(&prefix); - let impl_ = func.expand_to_js_function_impl(&prefix, &crate_name); - let into_js = func.expand_into_js_impl(&prefix, &crate_name); + let carry_type = func.expand_carry_type(prefix); + let impl_ = func.expand_to_js_function_impl(prefix, &crate_name); + let into_js = func.expand_into_js_impl(prefix, &crate_name); + let _js_name = config.js_name(&item.sig.ident, None); quote! { #item diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 8c9d13834..948bb7b53 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -2,6 +2,7 @@ use attrs::OptionList; use class::ClassOption; use function::FunctionOption; use methods::ImplOption; +use module::ModuleOption; use proc_macro::TokenStream as TokenStream1; use proc_macro_error::{abort, proc_macro_error}; use syn::{parse_macro_input, DeriveInput, Item}; @@ -22,7 +23,7 @@ mod embed; mod fields; mod function; mod methods; -//mod module; +mod module; mod trace; #[proc_macro_attribute] @@ -61,8 +62,15 @@ pub fn methods(attr: TokenStream1, item: TokenStream1) -> TokenStream1 { #[proc_macro_attribute] #[proc_macro_error] -pub fn module(_attr: TokenStream1, _item: TokenStream1) -> TokenStream1 { - todo!() +pub fn module(attr: TokenStream1, item: TokenStream1) -> TokenStream1 { + let options = parse_macro_input!(attr as OptionList); + let item = parse_macro_input!(item as Item); + match item { + Item::Mod(item) => module::expand(options, item).into(), + item => { + abort!(item, "#[module] macro can only be used on modules") + } + } } #[proc_macro_derive(Trace, attributes(qjs))] diff --git a/macro/src/module.rs b/macro/src/module.rs deleted file mode 100644 index ac186762e..000000000 --- a/macro/src/module.rs +++ /dev/null @@ -1,72 +0,0 @@ -use darling::FromMeta; -use proc_macro2::{Ident, TokenStream}; -use proc_macro_error::{abort, abort_call_site}; -use syn::{parse::Parse, ItemMod}; - -use crate::{common::Case, class}; - - -pub struct JsModule { - name: Ident, -} - -// - exports -// -// static / const: Variable -// fn: Function / Calculated variable, -// struct / enum: Class, -// type: Class -// -// impl: class body, -// -// - missing -// -// - unused -// -// use: Rexport? -// -// - not allowed -// -// union,extern -// -// -pub(crate) fn expand(item: ItemMod) -> TokenStream { - let ItemMod { - content, unsafety, .. - } = item; - - if let Some(unsafe_) = unsafety { - abort!(unsafe_, "unsafe modules are not supported"); - } - - let Some((_, items)) = content else { - abort_call_site!( - "The `module` macro can only be applied to modules with a definition in the same file." - ) - }; - - for item in items { - match item { - syn::Item::Const(_) => todo!(), - syn::Item::Enum(_) => todo!(), - syn::Item::ExternCrate(_) => todo!(), - syn::Item::Fn(_) => todo!(), - syn::Item::ForeignMod(_) => todo!(), - syn::Item::Impl(_) => todo!(), - syn::Item::Macro(_) => todo!(), - syn::Item::Mod(_) => todo!(), - syn::Item::Static(_) => todo!(), - syn::Item::Struct(s) => { - class::AttrItem::from_meta( s.attrs - } - syn::Item::Trait(_) - | syn::Item::TraitAlias(_) - | syn::Item::Type(_) - | syn::Item::Union(_) - | syn::Item::Use(_) - | syn::Item::Verbatim(_) => {} - } - } - - abort_call_site!("Not yet implemented") -} diff --git a/macro/src/module/config.rs b/macro/src/module/config.rs new file mode 100644 index 000000000..535de87d2 --- /dev/null +++ b/macro/src/module/config.rs @@ -0,0 +1,274 @@ +use convert_case::Casing; +use proc_macro2::Span; +use proc_macro_error::abort; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + parse_quote, Attribute, Ident, LitStr, Path, Token, +}; + +use crate::{ + attrs::{FlagOption, ValueOption}, + class::{ClassConfig, ClassOption}, + common::{crate_ident, kw, Case}, + function::{FunctionConfig, FunctionOption}, +}; + +#[derive(Debug, Default)] +pub(crate) struct ModuleConfig { + pub crate_: Option, + pub prefix: Option, + pub rename: Option, + pub rename_vars: Option, + pub rename_types: Option, +} + +impl ModuleConfig { + pub fn apply(&mut self, option: &ModuleOption) { + match option { + ModuleOption::Crate(ref x) => { + self.crate_ = Some(x.value.value()); + } + ModuleOption::RenameVars(ref x) => { + self.rename_vars = Some(x.value); + } + ModuleOption::RenameTypes(ref x) => { + self.rename_types = Some(x.value); + } + ModuleOption::Rename(ref x) => { + self.rename = Some(x.value.value()); + } + ModuleOption::Prefix(ref x) => { + self.prefix = Some(x.value.value()); + } + } + } + + pub fn crate_name(&self) -> String { + self.crate_.clone().unwrap_or_else(crate_ident) + } + + pub fn carry_name(&self, name: &Ident) -> Ident { + Ident::new( + &format!("{}{}", self.prefix.as_deref().unwrap_or("js_"), name), + name.span(), + ) + } +} + +pub(crate) enum ModuleOption { + Prefix(ValueOption), + Crate(ValueOption), + RenameVars(ValueOption), + RenameTypes(ValueOption), + Rename(ValueOption), +} + +impl Parse for ModuleOption { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(kw::prefix) { + input.parse().map(Self::Prefix) + } else if input.peek(Token![crate]) { + input.parse().map(Self::Crate) + } else if input.peek(kw::rename_vars) { + input.parse().map(Self::RenameVars) + } else if input.peek(kw::rename_types) { + input.parse().map(Self::RenameTypes) + } else if input.peek(kw::rename) { + input.parse().map(Self::Rename) + } else { + Err(syn::Error::new(input.span(), "invalid module attribute")) + } + } +} + +#[derive(Default, Debug)] +pub(crate) struct ModuleFunctionConfig { + pub declare: bool, + pub evaluate: bool, + pub skip: bool, + pub function: FunctionConfig, +} + +impl ModuleFunctionConfig { + pub fn apply(&mut self, option: &ModuleFunctionOption) { + match option { + ModuleFunctionOption::Declare(x) => { + self.declare = x.is_true(); + } + ModuleFunctionOption::Evaluate(x) => { + self.evaluate = x.is_true(); + } + ModuleFunctionOption::Function(x) => { + self.function.apply(x); + } + ModuleFunctionOption::Skip(x) => { + self.skip = x.is_true(); + } + } + } + + pub fn reexpand(&self) -> Option { + let mut attrs = Vec::new(); + if let Some(x) = self.function.crate_.as_deref() { + attrs.push(quote!(crate = #x)); + } + if let Some(x) = self.function.prefix.as_deref() { + attrs.push(quote!(prefix = #x)); + } + if let Some(x) = self.function.rename.as_deref() { + attrs.push(quote!(rename = #x)); + } + if attrs.is_empty() { + None + } else { + Some(parse_quote! { + #[qjs(#(#attrs),*)] + }) + } + } + + pub fn validate(&self, span: Span) { + if self.skip && self.evaluate { + abort!(span, "Can't skip the module evaluate function"); + } + if self.skip && self.declare { + abort!(span, "Can't skip the module declare function"); + } + if self.declare && self.evaluate { + abort!( + span, + "A function can be either declare or evaluate but not both." + ); + } + } +} + +pub(crate) enum ModuleFunctionOption { + Declare(FlagOption), + Evaluate(FlagOption), + Skip(FlagOption), + Function(FunctionOption), +} + +impl Parse for ModuleFunctionOption { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(kw::declare) { + input.parse().map(Self::Declare) + } else if input.peek(kw::evaluate) { + input.parse().map(Self::Evaluate) + } else if input.peek(kw::skip) { + input.parse().map(Self::Skip) + } else { + input.parse().map(Self::Function) + } + } +} + +#[derive(Default, Debug)] +pub(crate) struct ModuleTypeConfig { + pub skip: bool, + pub class: ClassConfig, + pub class_macro_name: Option, +} + +impl ModuleTypeConfig { + pub fn apply(&mut self, option: &ModuleTypeOption) { + match option { + ModuleTypeOption::Skip(x) => { + self.skip = x.is_true(); + } + ModuleTypeOption::Class(x) => self.class.apply(x), + } + } + + pub fn reexpand(&self) -> Option { + let mut attrs = Vec::new(); + if self.class.frozen { + attrs.push(quote!(frozen)); + } + if let Some(x) = self.class.crate_.as_ref() { + attrs.push(quote!(crate = #x)); + } + if let Some(x) = self.class.rename.as_ref() { + attrs.push(quote!(rename = #x)); + } + if let Some(x) = self.class.rename_all { + attrs.push(quote!(rename = #x)); + } + + if attrs.is_empty() { + None + } else if let Some(x) = self.class_macro_name.as_ref() { + Some(parse_quote!(#![#x( #(#attrs,)* )])) + } else { + Some(parse_quote!(#![qjs( #(#attrs,)* )])) + } + } +} + +pub(crate) enum ModuleTypeOption { + Skip(FlagOption), + Class(ClassOption), +} + +impl Parse for ModuleTypeOption { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(kw::skip) { + input.parse().map(Self::Skip) + } else { + input.parse().map(Self::Class) + } + } +} + +#[derive(Default, Debug)] +pub(crate) struct ModuleItemConfig { + pub skip: bool, + pub rename: Option, +} + +impl ModuleItemConfig { + pub fn apply(&mut self, option: &ModuleItemOption) { + match option { + ModuleItemOption::Skip(x) => { + self.skip = x.is_true(); + } + ModuleItemOption::Rename(x) => { + self.rename = Some(x.value.value()); + } + } + } + + pub fn js_name(&self, name: &Ident, case: Option) -> String { + if let Some(x) = self.rename.clone() { + return x; + } + + let name = name.to_string(); + if let Some(case) = case { + return name.to_case(case.to_convert_case()); + } + name + } +} + +pub(crate) enum ModuleItemOption { + Skip(FlagOption), + Rename(ValueOption), +} + +impl Parse for ModuleItemOption { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(kw::skip) { + input.parse().map(Self::Skip) + } else if input.peek(kw::rename) { + input.parse().map(Self::Rename) + } else { + Err(syn::Error::new( + input.span(), + "invalid module item attribute", + )) + } + } +} diff --git a/macro/src/module/declare.rs b/macro/src/module/declare.rs new file mode 100644 index 000000000..6e84bfb35 --- /dev/null +++ b/macro/src/module/declare.rs @@ -0,0 +1,32 @@ +use proc_macro2::TokenStream; +use proc_macro_error::abort; +use quote::quote; +use syn::{Ident, ItemFn, ReturnType, Visibility}; + +/// make sure the declare function has the right type. +pub fn validate(func: &ItemFn) { + let sig = &func.sig; + if !matches!(func.vis, Visibility::Public(_)) { + abort!(func.sig.ident, "A module declare function must be public."); + } + if let Some(x) = sig.asyncness.as_ref() { + abort!(x, "A module declare function can't be async."); + } + if let Some(x) = sig.unsafety.as_ref() { + abort!(x, "A module declare function can't be unsafe."); + } + if let Some(x) = sig.abi.as_ref() { + abort!(x, "A module declare function can't have an abi."); + } + if sig.inputs.len() != 1 || sig.output == ReturnType::Default { + abort!(func, "Invalid module declaration function."; + note = "Function should implement `fn(&mut rquickjs::module::Declarations) -> rquickjs::result<()>`."); + } +} + +pub fn expand_use(module_name: &Ident, func: &ItemFn) -> TokenStream { + let func_name = &func.sig.ident; + quote! { + #module_name::#func_name(_declare)?; + } +} diff --git a/macro/src/module/evaluate.rs b/macro/src/module/evaluate.rs new file mode 100644 index 000000000..fe4502c0e --- /dev/null +++ b/macro/src/module/evaluate.rs @@ -0,0 +1,29 @@ +use proc_macro2::TokenStream; +use proc_macro_error::abort; +use quote::quote; +use syn::{Ident, ItemFn, ReturnType}; + +/// make sure the declare function has the right type. +pub fn validate(func: &ItemFn) { + let sig = &func.sig; + if let Some(x) = sig.asyncness.as_ref() { + abort!(x, "A module evaluation function can't be async."); + } + if let Some(x) = sig.unsafety.as_ref() { + abort!(x, "A module evaluation function can't be unsafe."); + } + if let Some(x) = sig.abi.as_ref() { + abort!(x, "A module evaluation function can't have an abi."); + } + if sig.inputs.len() != 2 || sig.output == ReturnType::Default { + abort!(func, "Invalid module evaluation function."; + note = "Function should implement `fn(rquickjs::Ctx,&mut rquickjs::module::Exports) -> rquickjs::result<()>`."); + } +} + +pub(crate) fn expand_use(module_name: &Ident, func: &ItemFn) -> TokenStream { + let func_name = &func.sig.ident; + quote! { + #module_name::#func_name(_ctx,_exports)?; + } +} diff --git a/macro/src/module/mod.rs b/macro/src/module/mod.rs new file mode 100644 index 000000000..0c220c2aa --- /dev/null +++ b/macro/src/module/mod.rs @@ -0,0 +1,455 @@ +use std::collections::{hash_map::Entry, HashMap}; + +use crate::{ + attrs::{take_attributes, OptionList}, + class::Class, + common::AbortResultExt, +}; +use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro_error::{abort, abort_call_site, emit_warning}; +use quote::quote; +use syn::{spanned::Spanned, Attribute, ItemFn, ItemMod, UseTree}; + +mod config; +mod declare; +mod evaluate; +pub(crate) use config::*; + +#[derive(Debug)] +pub(crate) struct JsModule { + pub config: ModuleConfig, + pub name: Ident, + pub declaration: HashMap, +} + +impl JsModule { + pub fn new(config: ModuleConfig, item: &ItemMod) -> Self { + JsModule { + config, + name: item.ident.clone(), + declaration: HashMap::new(), + } + } + + pub fn export(&mut self, name: String, span: Span, tokens: TokenStream) { + match self.declaration.entry(name) { + Entry::Occupied(mut x) => { + let first_span = x.get().0; + emit_warning!( + span,"Module export with name `{}` already exists.", x.key(); + note = first_span => "First declared here."; + info = "Exporting two values with the same name will cause the second value to overwrite the first" + ); + *x.get_mut() = (span, tokens); + } + Entry::Vacant(x) => { + x.insert((span, tokens)); + } + } + } + + pub fn expand_declarations(&mut self) -> TokenStream { + let keys = self.declaration.keys(); + + quote! { + #(_declare.declare(#keys)?;)* + } + } + + pub fn expand_exports(&mut self) -> TokenStream { + let values = self.declaration.values().map(|x| &x.1); + quote! { + #(#values)* + } + } +} + +fn parse_item_attrs(attrs: &mut Vec) -> ModuleItemConfig { + let mut config = ModuleItemConfig::default(); + + take_attributes(attrs, |attr| { + if !attr.path().is_ident("qjs") { + return Ok(false); + } + + let options: OptionList = attr.parse_args()?; + for option in options.0.iter() { + config.apply(option) + } + + Ok(true) + }) + .unwrap_or_abort(); + + config +} + +fn parse_type_attrs(attrs: &mut Vec) -> ModuleTypeConfig { + let mut config = ModuleTypeConfig::default(); + + take_attributes(attrs, |attr| { + if attr.path().is_ident("qjs") { + let options: OptionList = attr.parse_args()?; + for option in options.0.iter() { + config.apply(option) + } + + return Ok(true); + } + + if attr + .path() + .segments + .last() + .map(|x| x.ident == "class") + .unwrap_or(false) + { + config.class_macro_name = Some(attr.path().clone()); + let options: OptionList = attr.parse_args()?; + for option in options.0.iter() { + config.apply(option) + } + } + + Ok(false) + }) + .unwrap_or_abort(); + + config +} + +fn export_use(use_: &UseTree, module: &mut JsModule, config: &ModuleItemConfig) { + match use_ { + UseTree::Path(x) => { + export_use(&x.tree, module, config); + } + UseTree::Name(x) => { + let ident = &x.ident; + let js_name = config.js_name(ident, module.config.rename_types); + let crate_name = Ident::new(&module.config.crate_name(), ident.span()); + let mod_name = module.name.clone(); + module.export( + js_name.clone(), + ident.span(), + quote! { + let _constr = #crate_name::Class::<#mod_name::#ident>::create_constructor(&_ctx)? + .expect(concat!("Tried to export type `" + ,stringify!(#ident), + "` which did not define a constructor." + )); + _exports.export(#js_name,_constr)?; + }, + ) + } + UseTree::Rename(x) => { + let ident = &x.rename; + let js_name = config.js_name(ident, module.config.rename_types); + let crate_name = Ident::new(&module.config.crate_name(), ident.span()); + let mod_name = module.name.clone(); + module.export( + js_name.clone(), + ident.span(), + quote! { + let _constr = #crate_name::Class::<#mod_name::#ident>::create_constructor(&_ctx)? + .expect("Tried to export type which did not define a constructor."); + _exports.export(#js_name,_constr)?; + }, + ) + } + UseTree::Glob(x) => { + emit_warning!( + x.star_token,"Using a glob export does not export the items to javascript"; + note = "Please specify each item to be exported individially." + ) + } + UseTree::Group(x) => { + for i in x.items.iter() { + export_use(i, module, config); + } + } + } +} + +pub(crate) fn expand(options: OptionList, mut item: ItemMod) -> TokenStream { + let mut config = ModuleConfig::default(); + for option in options.0.iter() { + config.apply(option) + } + + let ItemMod { ref mut attrs, .. } = item; + + take_attributes(attrs, |attr| { + if !attr.path().is_ident("qjs") { + return Ok(false); + } + + let options: OptionList = attr.parse_args()?; + for option in options.0.iter() { + config.apply(option) + } + + Ok(true) + }) + .unwrap_or_abort(); + let mut module = JsModule::new(config, &item); + + let ItemMod { + ref mut content, + ref unsafety, + .. + } = item; + + if let Some(unsafe_) = unsafety { + abort!(unsafe_, "unsafe modules are not supported"); + } + + let Some((_, ref mut items)) = content else { + abort_call_site!( + "The `module` macro can only be applied to modules with a definition in the same file." + ) + }; + + let mut _consts = Vec::new(); + let mut _statics = Vec::new(); + let mut _enums = Vec::new(); + let mut _structs = Vec::new(); + let mut _uses = Vec::new(); + let mut _functions = Vec::new(); + + let mut declare: Option<(&ItemFn, Span, ModuleFunctionConfig)> = None; + let mut evaluate: Option<(&ItemFn, Span, ModuleFunctionConfig)> = None; + + for item in items.iter_mut() { + match item { + syn::Item::Use(i) => { + let config = parse_item_attrs(&mut i.attrs); + if config.skip { + continue; + } + + if let syn::Visibility::Public(_) = i.vis { + _uses.push((i, config)) + } + } + syn::Item::Const(i) => { + let config = parse_item_attrs(&mut i.attrs); + if config.skip { + continue; + } + if let syn::Visibility::Public(_) = i.vis { + _consts.push((i, config)) + } + } + syn::Item::Static(i) => { + let config = parse_item_attrs(&mut i.attrs); + if config.skip { + continue; + } + if let syn::Visibility::Public(_) = i.vis { + _statics.push((i, config)) + } + } + syn::Item::Enum(i) => { + let config = parse_type_attrs(&mut i.attrs); + if config.skip { + continue; + } + + if let Some(reexport) = config.reexpand() { + i.attrs.push(reexport); + } + + if let syn::Visibility::Public(_) = i.vis { + _enums.push((i, config)) + } + } + syn::Item::Fn(ref mut i) => { + let mut config = ModuleFunctionConfig::default(); + let mut span = None; + + take_attributes(&mut i.attrs, |attr| { + if !attr.path().is_ident("qjs") { + return Ok(false); + } + + span = Some(attr.span()); + + let options: OptionList = attr.parse_args()?; + for option in options.0.iter() { + config.apply(option) + } + + Ok(true) + }) + .unwrap_or_abort(); + + config.validate(span.unwrap_or_else(Span::call_site)); + + if config.skip { + continue; + } else if config.declare { + let span = span.unwrap(); + if let Some((_, prev_span, _)) = declare { + abort!(span,"Found a second declaration function in module."; + note = prev_span => "First declaration function here."); + } + declare::validate(i); + declare = Some((i, span, config)); + } else if config.evaluate { + let span = span.unwrap(); + if let Some((_, prev_span, _)) = evaluate { + abort!(span,"Found a second evaluation function in module."; + note = prev_span => "First evaluation function here."); + } + evaluate::validate(i); + evaluate = Some((i, span, config)); + } else { + if let Some(reexport) = config.reexpand() { + i.attrs.push(reexport); + } + + if let syn::Visibility::Public(_) = i.vis { + _functions.push((i, config)) + } + } + } + syn::Item::Struct(i) => { + let config = parse_type_attrs(&mut i.attrs); + if config.skip { + continue; + } + + if let Some(reexport) = config.reexpand() { + i.attrs.push(reexport); + } + + if let syn::Visibility::Public(_) = i.vis { + _structs.push((i, config)) + } + } + syn::Item::Trait(_) + | syn::Item::TraitAlias(_) + | syn::Item::Type(_) + | syn::Item::Union(_) + | syn::Item::Verbatim(_) + | syn::Item::ExternCrate(_) + | syn::Item::Impl(_) + | syn::Item::Macro(_) + | syn::Item::ForeignMod(_) + | syn::Item::Mod(_) => {} + _ => {} + } + } + + let mod_name = &item.ident; + let crate_name = Ident::new(&module.config.crate_name(), Span::call_site()); + let name = module.config.carry_name(&item.ident); + let vis = item.vis.clone(); + + let declare = declare.map(|x| declare::expand_use(mod_name, x.0)); + let evaluate = evaluate.map(|x| evaluate::expand_use(mod_name, x.0)); + + for (f, function_config) in _functions { + let ident = function_config.function.carry_name(&f.sig.ident); + let name = format!("{}", f.sig.ident); + let js_name = function_config + .function + .js_name(&f.sig.ident, module.config.rename_types); + + let mod_name = module.name.clone(); + + module.export( + name, + f.sig.ident.span(), + quote! { + _exports.export(#js_name,#mod_name::#ident)?; + }, + ) + } + + for (c, config) in _consts { + let ident = &c.ident; + let js_name = config.js_name(ident, module.config.rename_vars); + module.export( + js_name.clone(), + ident.span(), + quote! { + _exports.export(#js_name,#mod_name::#ident)?; + }, + ) + } + + for (s, config) in _statics { + let ident = &s.ident; + let js_name = config.js_name(ident, module.config.rename_vars); + module.export( + js_name.clone(), + ident.span(), + quote! { + _exports.export(#js_name,#mod_name::#ident)?; + }, + ) + } + + for (s, config) in _structs { + let ident = &s.ident; + let name = Class::from_struct(config.class.clone(), s.clone()).javascript_name(); + + module.export( + name.clone(), + ident.span(), + quote! { + let _constr = #crate_name::Class::<#mod_name::#ident>::create_constructor(&_ctx)? + .expect(concat!("Tried to export type `" + ,stringify!(#ident), + "` which did not define a constructor." + )); + _exports.export(#name,_constr)?; + }, + ) + } + for (e, config) in _enums { + let ident = &e.ident; + + let name = Class::from_enum(config.class.clone(), e.clone()).javascript_name(); + + module.export( + name.clone(), + ident.span(), + quote! { + let _constr = #crate_name::Class::<#mod_name::#ident>::create_constructor(&_ctx)? + .expect(concat!("Tried to export type `" + ,stringify!(#ident), + "` which did not define a constructor." + )); + _exports.export(#name,_constr)?; + }, + ) + } + + for (u, config) in _uses { + export_use(&u.tree, &mut module, &config) + } + + let declarations = module.expand_declarations(); + let exports = module.expand_exports(); + quote! { + #[allow(non_camel_case_types)] + #vis struct #name; + + impl #crate_name::module::ModuleDef for #name{ + fn declare(_declare: &mut #crate_name::module::Declarations) -> #crate_name::Result<()>{ + #declarations + #declare + Ok(()) + } + fn evaluate<'js>(_ctx: &#crate_name::Ctx<'js>, _exports: &mut #crate_name::module::Exports<'js>) -> #crate_name::Result<()>{ + #exports + #evaluate + Ok(()) + } + } + + #item + } +} diff --git a/tests/macros/pass_module.rs b/tests/macros/pass_module.rs new file mode 100644 index 000000000..aa0049047 --- /dev/null +++ b/tests/macros/pass_module.rs @@ -0,0 +1,123 @@ +use rquickjs::{CatchResultExt, Context, Module, Runtime}; + +#[derive(rquickjs::class::Trace)] +#[rquickjs::class] +pub struct Test { + foo: u32, +} + +#[rquickjs::methods] +impl Test { + #[qjs(constructor)] + pub fn new() -> Test { + Test { foo: 3 } + } +} + +impl Default for Test { + fn default() -> Self { + Self::new() + } +} + +#[rquickjs::module(rename_vars = "camelCase")] +mod test_mod { + /// Imports and other declarations which aren't `pub` won't be exported. + use rquickjs::Ctx; + + /// You can even use `use` to export types from outside. + /// + /// Note that this tries to export the type, not the value, + /// So this won't work for functions. + pub use super::Test; + + #[derive(rquickjs::class::Trace)] + #[rquickjs::class(rename = "FooBar")] + pub struct Test2 { + bar: u32, + } + + /// Implement methods for the class like normal. + #[rquickjs::methods] + impl Test2 { + /// A constructor is required for exporting types. + #[qjs(constructor)] + pub fn new() -> Test2 { + Test2 { bar: 3 } + } + } + + impl Default for Test2 { + fn default() -> Self { + Self::new() + } + } + + /// Two variables exported as `aConstValue` and `aStaticValue` because of the `rename_all` attr. + pub const A_CONST_VALUE: f32 = 2.0; + pub static A_STATIC_VALUE: f32 = 2.0; + + /// If our module doesn't quite fit with how this macro exports you can manually export from + /// the declare and evaluate functions. + #[qjs(declare)] + pub fn declare(declare: &mut rquickjs::module::Declarations) -> rquickjs::Result<()> { + declare.declare("aManuallyExportedValue")?; + Ok(()) + } + + #[qjs(evaluate)] + pub fn evaluate<'js>( + _ctx: &Ctx<'js>, + exports: &mut rquickjs::module::Exports<'js>, + ) -> rquickjs::Result<()> { + exports.export("aManuallyExportedValue", "Some Value")?; + Ok(()) + } + + /// You can also export functions. + #[rquickjs::function] + pub fn foo() -> u32 { + 1 + 1 + } + + /// You can make items public but not export them to javascript by adding the skip attribute. + #[qjs(skip)] + pub fn ignore_function() -> u32 { + 2 + 2 + } +} + +fn main() { + assert_eq!(test_mod::ignore_function(), 4); + let rt = Runtime::new().unwrap(); + let ctx = Context::full(&rt).unwrap(); + + ctx.with(|ctx| { + Module::declare_def::(ctx.clone(), "test").unwrap(); + let _ = Module::evaluate( + ctx.clone(), + "test2", + r" + import { foo,aManuallyExportedValue, aConstValue, aStaticValue, FooBar } from 'test'; + if (foo() !== 2){ + throw new Error(1); + } + if (aManuallyExportedValue !== 'Some Value'){ + throw new Error(2); + } + if(aConstValue !== 2){ + throw new Error(3); + } + if(aStaticValue !== 2){ + throw new Error(4); + } + if(aStaticValue !== 2){ + throw new Error(4); + } + let test = new FooBar(); + ", + ) + .catch(&ctx) + .unwrap(); + }) +}