pub mod classes;
pub mod enums;
pub mod functions;
pub mod signatures;
pub mod typemap;
use proc_macro2::TokenStream;
use quote::quote;
use crate::ir::{InterfaceClassification, Module, TypeDeclaration, TypeKind};
use crate::parse::scope::ScopeId;
use typemap::CodegenContext;
pub(crate) fn doc_tokens(doc: &Option<String>) -> TokenStream {
match doc {
Some(text) => {
let lines: Vec<TokenStream> = text
.lines()
.map(|line| {
let line = format!(" {line}");
quote! { #[doc = #line] }
})
.collect();
quote! { #(#lines)* }
}
None => quote! {},
}
}
#[derive(Debug, Clone, Default)]
pub struct GenerateOptions {
pub skip_promise_ext: bool,
}
pub fn generate(module: &Module, gctx: &crate::context::GlobalContext) -> anyhow::Result<String> {
generate_with_options(module, gctx, &GenerateOptions::default())
}
pub fn generate_with_options(
module: &Module,
gctx: &crate::context::GlobalContext,
options: &GenerateOptions,
) -> anyhow::Result<String> {
let tokens = generate_tokens(module, gctx, options);
let file = syn::parse2::<syn::File>(tokens.clone()).map_err(|e| {
anyhow::anyhow!("generated tokens are not valid syn:\n{e}\n\nTokens:\n{tokens}")
})?;
Ok(prettyplease::unparse(&file))
}
fn generate_tokens(
module: &Module,
gctx: &crate::context::GlobalContext,
options: &GenerateOptions,
) -> TokenStream {
let cgctx = CodegenContext::from_module(module, gctx);
let promise_ext = if options.skip_promise_ext {
quote! {}
} else {
quote! {
pub trait PromiseExt {
type Output;
fn into_future(self) -> wasm_bindgen_futures::JsFuture<Self::Output>;
}
impl<T: 'static + wasm_bindgen::convert::FromWasmAbi> PromiseExt for js_sys::Promise<T> {
type Output = T;
fn into_future(self) -> wasm_bindgen_futures::JsFuture<T> {
wasm_bindgen_futures::JsFuture::from(self)
}
}
}
};
let preamble = quote! {
#[allow(unused_imports)]
use wasm_bindgen::prelude::*;
#[allow(unused_imports)]
use js_sys::*;
#promise_ext
};
let mut global_items = Vec::new();
let mut module_items: std::collections::BTreeMap<std::rc::Rc<str>, Vec<TokenStream>> =
std::collections::BTreeMap::new();
for &type_id in &module.types {
let decl = gctx.get_type(type_id);
if let Some(tokens) = generate_declaration(decl, &cgctx) {
match &decl.module_context {
crate::ir::ModuleContext::Global => {
global_items.push(tokens);
}
crate::ir::ModuleContext::Module(m) => {
module_items.entry(m.clone()).or_default().push(tokens);
}
}
}
}
let mod_blocks: Vec<TokenStream> = module_items
.into_iter()
.map(|(mod_specifier, items)| {
let short_name = mod_specifier
.rsplit_once(':')
.map(|(_, rest)| rest)
.unwrap_or(&mod_specifier);
let mod_name = typemap::make_ident(&crate::util::naming::to_snake_case(
&short_name.replace('/', "_").replace('*', "star"),
));
quote! {
pub mod #mod_name {
use wasm_bindgen::prelude::*;
use js_sys::*;
use super::*;
#(#items)*
}
}
})
.collect();
let external_uses = cgctx.external_use_tokens();
cgctx.take_diagnostics().emit();
quote! {
#preamble
#external_uses
#(#global_items)*
#(#mod_blocks)*
}
}
fn generate_declaration(decl: &TypeDeclaration, cgctx: &CodegenContext) -> Option<TokenStream> {
match &decl.kind {
TypeKind::Class(c) => Some(classes::generate_class(
c,
&decl.module_context,
Some(cgctx),
decl.scope_id,
)),
TypeKind::Interface(i) => match i.classification {
InterfaceClassification::ClassLike | InterfaceClassification::Unclassified => {
Some(classes::generate_class_like_interface(
i,
&decl.module_context,
Some(cgctx),
None,
decl.scope_id,
))
}
InterfaceClassification::Dictionary => Some(classes::generate_dictionary_extern(
i,
&decl.module_context,
Some(cgctx),
None,
decl.scope_id,
)),
},
TypeKind::StringEnum(e) => Some(enums::generate_string_enum(e)),
TypeKind::NumericEnum(e) => Some(enums::generate_numeric_enum(e)),
TypeKind::Function(f) => Some(functions::generate_function(
f,
&decl.module_context,
Some(cgctx),
&decl.doc,
decl.scope_id,
)),
TypeKind::Variable(v) => Some(functions::generate_variable(
v,
&decl.module_context,
Some(cgctx),
&decl.doc,
None,
decl.scope_id,
)),
TypeKind::TypeAlias(alias) => Some(generate_type_alias(alias, cgctx, decl.scope_id)),
TypeKind::Namespace(ns) => Some(generate_namespace(ns, &decl.module_context, cgctx)),
}
}
fn generate_type_alias(
alias: &crate::ir::TypeAliasDecl,
cgctx: &CodegenContext,
scope: ScopeId,
) -> TokenStream {
if let Some(ref module) = alias.from_module {
let type_name = match &alias.target {
crate::ir::TypeRef::Named(n) => n.as_str(),
_ => &alias.name,
};
if let Some(rust_path) = cgctx.resolve_external(type_name, module) {
let path: syn::Path = syn::parse_str(&rust_path.path).unwrap_or_else(|_| {
syn::Path::from(syn::Ident::new("JsValue", proc_macro2::Span::call_site()))
});
let name = typemap::make_ident(&alias.name);
if alias.name == type_name {
return quote! { pub use #path; };
} else {
return quote! { pub use #path as #name; };
}
}
cgctx.warn(format!(
"No external mapping for `{}` from \"{module}\" — emitting JsValue alias",
type_name,
));
let name = typemap::make_ident(&alias.name);
return quote! {
#[allow(dead_code)]
pub type #name = JsValue;
};
}
if let crate::ir::TypeRef::Named(ref target_name) = alias.target {
if !cgctx.local_types.contains(target_name)
&& !crate::codegen::typemap::JS_SYS_RESERVED.contains(&target_name.as_str())
{
cgctx.warn(format!(
"Type alias `{}` targets unknown type `{target_name}`, skipping",
alias.name
));
return quote! {};
}
}
let target = typemap::to_syn_type(
&alias.target,
typemap::TypePosition::ARGUMENT.to_inner(),
Some(cgctx),
scope,
);
let name = typemap::make_ident(&alias.name);
if target.to_string() == alias.name {
return quote! {};
}
quote! {
#[allow(dead_code)]
pub type #name = #target;
}
}
fn generate_namespace(
ns: &crate::ir::NamespaceDecl,
_parent_ctx: &crate::ir::ModuleContext,
cgctx: &CodegenContext,
) -> TokenStream {
let mod_name = typemap::make_ident(&crate::util::naming::to_snake_case(&ns.name));
let js_name = &ns.name;
let items: Vec<TokenStream> = ns
.declarations
.iter()
.filter_map(|decl| generate_ns_declaration(decl, js_name, cgctx))
.collect();
quote! {
pub mod #mod_name {
use wasm_bindgen::prelude::*;
#(#items)*
}
}
}
fn generate_ns_declaration(
decl: &TypeDeclaration,
ns_js_name: &str,
cgctx: &CodegenContext,
) -> Option<TokenStream> {
match &decl.kind {
TypeKind::Class(c) => Some(classes::generate_class_with_js_namespace(
c,
&decl.module_context,
ns_js_name,
Some(cgctx),
decl.scope_id,
)),
TypeKind::Interface(i) => match i.classification {
InterfaceClassification::ClassLike | InterfaceClassification::Unclassified => {
Some(classes::generate_class_like_interface(
i,
&decl.module_context,
Some(cgctx),
Some(ns_js_name),
decl.scope_id,
))
}
InterfaceClassification::Dictionary => Some(classes::generate_dictionary_extern(
i,
&decl.module_context,
Some(cgctx),
Some(ns_js_name),
decl.scope_id,
)),
},
TypeKind::Function(f) => Some(functions::generate_function_with_js_namespace(
f,
&decl.module_context,
ns_js_name,
Some(cgctx),
&decl.doc,
decl.scope_id,
)),
TypeKind::StringEnum(e) => Some(enums::generate_string_enum(e)),
TypeKind::NumericEnum(e) => Some(enums::generate_numeric_enum(e)),
TypeKind::Variable(v) => Some(functions::generate_variable(
v,
&decl.module_context,
Some(cgctx),
&decl.doc,
Some(ns_js_name),
decl.scope_id,
)),
TypeKind::TypeAlias(_) => None,
TypeKind::Namespace(ns) => Some(generate_namespace(ns, &decl.module_context, cgctx)),
}
}