1pub mod classes;
7pub mod enums;
8pub mod functions;
9pub mod signatures;
10pub mod typemap;
11
12use proc_macro2::TokenStream;
13use quote::quote;
14
15use crate::ir::{InterfaceClassification, Module, TypeDeclaration, TypeKind};
16use crate::parse::scope::ScopeId;
17
18use typemap::CodegenContext;
19
20pub(crate) fn doc_tokens(doc: &Option<String>) -> TokenStream {
24 match doc {
25 Some(text) => {
26 let lines: Vec<TokenStream> = text
27 .lines()
28 .map(|line| {
29 let line = format!(" {line}");
30 quote! { #[doc = #line] }
31 })
32 .collect();
33 quote! { #(#lines)* }
34 }
35 None => quote! {},
36 }
37}
38
39#[derive(Debug, Clone, Default)]
41pub struct GenerateOptions {
42 pub skip_promise_ext: bool,
46}
47
48pub fn generate(module: &Module, gctx: &crate::context::GlobalContext) -> anyhow::Result<String> {
52 generate_with_options(module, gctx, &GenerateOptions::default())
53}
54
55pub fn generate_with_options(
57 module: &Module,
58 gctx: &crate::context::GlobalContext,
59 options: &GenerateOptions,
60) -> anyhow::Result<String> {
61 let tokens = generate_tokens(module, gctx, options);
62 let file = syn::parse2::<syn::File>(tokens.clone()).map_err(|e| {
63 anyhow::anyhow!("generated tokens are not valid syn:\n{e}\n\nTokens:\n{tokens}")
64 })?;
65 Ok(prettyplease::unparse(&file))
66}
67
68fn generate_tokens(
70 module: &Module,
71 gctx: &crate::context::GlobalContext,
72 options: &GenerateOptions,
73) -> TokenStream {
74 let cgctx = CodegenContext::from_module(module, gctx);
75
76 let promise_ext = if options.skip_promise_ext {
77 quote! {}
78 } else {
79 quote! {
80 pub trait PromiseExt {
89 type Output;
90 fn into_future(self) -> wasm_bindgen_futures::JsFuture<Self::Output>;
91 }
92
93 impl<T: 'static + wasm_bindgen::convert::FromWasmAbi> PromiseExt for js_sys::Promise<T> {
94 type Output = T;
95 fn into_future(self) -> wasm_bindgen_futures::JsFuture<T> {
96 wasm_bindgen_futures::JsFuture::from(self)
97 }
98 }
99 }
100 };
101
102 let preamble = quote! {
103 #[allow(unused_imports)]
106 use wasm_bindgen::prelude::*;
107 #[allow(unused_imports)]
108 use js_sys::*;
109
110 #promise_ext
111 };
112
113 let mut global_items = Vec::new();
117 let mut module_items: std::collections::BTreeMap<std::rc::Rc<str>, Vec<TokenStream>> =
118 std::collections::BTreeMap::new();
119
120 for &type_id in &module.types {
121 let decl = gctx.get_type(type_id);
122 if let Some(tokens) = generate_declaration(decl, &cgctx) {
123 match &decl.module_context {
124 crate::ir::ModuleContext::Global => {
125 global_items.push(tokens);
126 }
127 crate::ir::ModuleContext::Module(m) => {
128 module_items.entry(m.clone()).or_default().push(tokens);
129 }
130 }
131 }
132 }
133
134 let mod_blocks: Vec<TokenStream> = module_items
136 .into_iter()
137 .map(|(mod_specifier, items)| {
138 let short_name = mod_specifier
140 .rsplit_once(':')
141 .map(|(_, rest)| rest)
142 .unwrap_or(&mod_specifier);
143 let mod_name = typemap::make_ident(&crate::util::naming::to_snake_case(
144 &short_name.replace('/', "_").replace('*', "star"),
145 ));
146 quote! {
147 pub mod #mod_name {
148 use wasm_bindgen::prelude::*;
149 use js_sys::*;
150 use super::*;
151 #(#items)*
152 }
153 }
154 })
155 .collect();
156
157 let external_uses = cgctx.external_use_tokens();
159
160 cgctx.take_diagnostics().emit();
162
163 quote! {
164 #preamble
165 #external_uses
166 #(#global_items)*
167 #(#mod_blocks)*
168 }
169}
170
171fn generate_declaration(decl: &TypeDeclaration, cgctx: &CodegenContext) -> Option<TokenStream> {
173 match &decl.kind {
174 TypeKind::Class(c) => Some(classes::generate_class(
175 c,
176 &decl.module_context,
177 Some(cgctx),
178 decl.scope_id,
179 )),
180 TypeKind::Interface(i) => match i.classification {
181 InterfaceClassification::ClassLike | InterfaceClassification::Unclassified => {
182 Some(classes::generate_class_like_interface(
183 i,
184 &decl.module_context,
185 Some(cgctx),
186 None,
187 decl.scope_id,
188 ))
189 }
190 InterfaceClassification::Dictionary => Some(classes::generate_dictionary_extern(
191 i,
192 &decl.module_context,
193 Some(cgctx),
194 None,
195 decl.scope_id,
196 )),
197 },
198 TypeKind::StringEnum(e) => Some(enums::generate_string_enum(e)),
199 TypeKind::NumericEnum(e) => Some(enums::generate_numeric_enum(e)),
200 TypeKind::Function(f) => Some(functions::generate_function(
201 f,
202 &decl.module_context,
203 Some(cgctx),
204 &decl.doc,
205 decl.scope_id,
206 )),
207 TypeKind::Variable(v) => Some(functions::generate_variable(
208 v,
209 &decl.module_context,
210 Some(cgctx),
211 &decl.doc,
212 None,
213 decl.scope_id,
214 )),
215 TypeKind::TypeAlias(alias) => Some(generate_type_alias(alias, cgctx, decl.scope_id)),
216 TypeKind::Namespace(ns) => Some(generate_namespace(ns, &decl.module_context, cgctx)),
217 }
218}
219
220fn generate_type_alias(
225 alias: &crate::ir::TypeAliasDecl,
226 cgctx: &CodegenContext,
227 scope: ScopeId,
228) -> TokenStream {
229 if let Some(ref module) = alias.from_module {
230 let type_name = match &alias.target {
232 crate::ir::TypeRef::Named(n) => n.as_str(),
233 _ => &alias.name,
234 };
235 if let Some(rust_path) = cgctx.resolve_external(type_name, module) {
236 let path: syn::Path = syn::parse_str(&rust_path.path).unwrap_or_else(|_| {
237 syn::Path::from(syn::Ident::new("JsValue", proc_macro2::Span::call_site()))
238 });
239 let name = typemap::make_ident(&alias.name);
240 if alias.name == type_name {
241 return quote! { pub use #path; };
242 } else {
243 return quote! { pub use #path as #name; };
244 }
245 }
246 cgctx.warn(format!(
247 "No external mapping for `{}` from \"{module}\" — emitting JsValue alias",
248 type_name,
249 ));
250 let name = typemap::make_ident(&alias.name);
251 return quote! {
252 #[allow(dead_code)]
253 pub type #name = JsValue;
254 };
255 }
256
257 if let crate::ir::TypeRef::Named(ref target_name) = alias.target {
259 if !cgctx.local_types.contains(target_name)
260 && !crate::codegen::typemap::JS_SYS_RESERVED.contains(&target_name.as_str())
261 {
262 cgctx.warn(format!(
263 "Type alias `{}` targets unknown type `{target_name}`, skipping",
264 alias.name
265 ));
266 return quote! {};
267 }
268 }
269
270 let target = typemap::to_syn_type(
271 &alias.target,
272 typemap::TypePosition::ARGUMENT.to_inner(),
273 Some(cgctx),
274 scope,
275 );
276 let name = typemap::make_ident(&alias.name);
277
278 if target.to_string() == alias.name {
280 return quote! {};
281 }
282
283 quote! {
284 #[allow(dead_code)]
285 pub type #name = #target;
286 }
287}
288
289fn generate_namespace(
291 ns: &crate::ir::NamespaceDecl,
292 _parent_ctx: &crate::ir::ModuleContext,
293 cgctx: &CodegenContext,
294) -> TokenStream {
295 let mod_name = typemap::make_ident(&crate::util::naming::to_snake_case(&ns.name));
296 let js_name = &ns.name;
297
298 let items: Vec<TokenStream> = ns
299 .declarations
300 .iter()
301 .filter_map(|decl| generate_ns_declaration(decl, js_name, cgctx))
302 .collect();
303
304 quote! {
305 pub mod #mod_name {
306 use wasm_bindgen::prelude::*;
307 #(#items)*
308 }
309 }
310}
311
312fn generate_ns_declaration(
316 decl: &TypeDeclaration,
317 ns_js_name: &str,
318 cgctx: &CodegenContext,
319) -> Option<TokenStream> {
320 match &decl.kind {
321 TypeKind::Class(c) => Some(classes::generate_class_with_js_namespace(
322 c,
323 &decl.module_context,
324 ns_js_name,
325 Some(cgctx),
326 decl.scope_id,
327 )),
328 TypeKind::Interface(i) => match i.classification {
329 InterfaceClassification::ClassLike | InterfaceClassification::Unclassified => {
330 Some(classes::generate_class_like_interface(
331 i,
332 &decl.module_context,
333 Some(cgctx),
334 Some(ns_js_name),
335 decl.scope_id,
336 ))
337 }
338 InterfaceClassification::Dictionary => Some(classes::generate_dictionary_extern(
339 i,
340 &decl.module_context,
341 Some(cgctx),
342 Some(ns_js_name),
343 decl.scope_id,
344 )),
345 },
346 TypeKind::Function(f) => Some(functions::generate_function_with_js_namespace(
347 f,
348 &decl.module_context,
349 ns_js_name,
350 Some(cgctx),
351 &decl.doc,
352 decl.scope_id,
353 )),
354 TypeKind::StringEnum(e) => Some(enums::generate_string_enum(e)),
355 TypeKind::NumericEnum(e) => Some(enums::generate_numeric_enum(e)),
356 TypeKind::Variable(v) => Some(functions::generate_variable(
357 v,
358 &decl.module_context,
359 Some(cgctx),
360 &decl.doc,
361 Some(ns_js_name),
362 decl.scope_id,
363 )),
364 TypeKind::TypeAlias(_) => None,
365 TypeKind::Namespace(ns) => Some(generate_namespace(ns, &decl.module_context, cgctx)),
366 }
367}