Skip to main content

ts_gen/codegen/
classes.rs

1//! ClassDecl / ClassLike InterfaceDecl → wasm_bindgen `extern "C"` block generation.
2//!
3//! Generates the standard pattern seen in worker-sys:
4//!
5//! ```rust,ignore
6//! #[wasm_bindgen]
7//! extern "C" {
8//!     #[wasm_bindgen(extends = js_sys::Object, js_name = "MyClass")]
9//!     #[derive(Debug, Clone, PartialEq, Eq)]
10//!     pub type MyClass;
11//!
12//!     #[wasm_bindgen(constructor, catch)]
13//!     pub fn new(arg: &str) -> Result<MyClass, JsValue>;
14//!
15//!     #[wasm_bindgen(method, getter)]
16//!     pub fn name(this: &MyClass) -> String;
17//!
18//!     #[wasm_bindgen(method, js_name = "doThing")]
19//!     pub fn do_thing(this: &MyClass, x: f64);
20//! }
21//! ```
22
23use std::collections::HashSet;
24
25use proc_macro2::TokenStream;
26use quote::quote;
27
28use crate::codegen::signatures::{
29    dedupe_name, expand_signatures, generate_concrete_params, is_void_return, ExpandedSignature,
30    SignatureKind,
31};
32use crate::codegen::typemap::{to_return_type, to_syn_type, CodegenContext, TypePosition};
33use crate::ir::{
34    ClassDecl, GetterMember, InterfaceClassification, InterfaceDecl, Member, ModuleContext,
35    SetterMember, StaticGetterMember, StaticSetterMember, TypeRef,
36};
37use crate::parse::scope::ScopeId;
38use crate::util::naming::to_snake_case;
39
40/// Configuration for generating a class-like extern block.
41struct ClassConfig<'a> {
42    /// Rust type name.
43    rust_name: String,
44    /// JS class name (for `js_name` / `js_class` attributes).
45    js_name: String,
46    /// The `extends` parents (wasm_bindgen supports chained `extends = ...`).
47    extends: Vec<TokenStream>,
48    /// Module specifier for `#[wasm_bindgen(module = "...")]`.
49    module: Option<std::rc::Rc<str>>,
50    /// JS namespace (e.g., `"WebAssembly"`) for types inside a namespace.
51    js_namespace: Option<String>,
52    /// Whether this is an abstract class (skip constructor).
53    is_abstract: bool,
54    /// Members to generate.
55    members: Vec<Member>,
56    /// Codegen context for type resolution.
57    cgctx: Option<&'a CodegenContext<'a>>,
58    /// Scope for type reference resolution.
59    scope: ScopeId,
60}
61
62impl<'a> ClassConfig<'a> {
63    fn from_class(
64        decl: &ClassDecl,
65        ctx: &ModuleContext,
66        cgctx: Option<&'a CodegenContext>,
67        scope: ScopeId,
68    ) -> Self {
69        let extends = match &decl.extends {
70            Some(e) => vec![extends_tokens(e, cgctx, scope)],
71            None => vec![quote! { Object }],
72        };
73        let module = match ctx {
74            ModuleContext::Module(m) => Some(m.clone()),
75            ModuleContext::Global => None,
76        };
77
78        ClassConfig {
79            rust_name: decl.name.clone(),
80            js_name: decl.js_name.clone(),
81            extends,
82            module,
83            js_namespace: None,
84            is_abstract: decl.is_abstract,
85            members: decl.members.clone(),
86            cgctx,
87            scope,
88        }
89    }
90
91    fn from_interface(
92        decl: &InterfaceDecl,
93        ctx: &ModuleContext,
94        cgctx: Option<&'a CodegenContext>,
95        scope: ScopeId,
96    ) -> Self {
97        let extends = if decl.extends.is_empty() {
98            vec![quote! { Object }]
99        } else {
100            decl.extends
101                .iter()
102                .map(|e| extends_tokens(e, cgctx, scope))
103                .collect()
104        };
105        let module = match ctx {
106            ModuleContext::Module(m) => Some(m.clone()),
107            ModuleContext::Global => None,
108        };
109
110        ClassConfig {
111            rust_name: decl.name.clone(),
112            js_name: decl.js_name.clone(),
113            extends,
114            module,
115            js_namespace: None,
116            is_abstract: false,
117            members: decl.members.clone(),
118            cgctx,
119            scope,
120        }
121    }
122}
123
124/// Generate a complete `extern "C"` block for a class-like declaration.
125pub fn generate_class(
126    decl: &ClassDecl,
127    ctx: &ModuleContext,
128    cgctx: Option<&CodegenContext<'_>>,
129    scope: ScopeId,
130) -> TokenStream {
131    let config = ClassConfig::from_class(decl, ctx, cgctx, scope);
132    generate_extern_block(&config)
133}
134
135/// Generate a complete `extern "C"` block for a class-like interface.
136pub fn generate_class_like_interface(
137    decl: &InterfaceDecl,
138    ctx: &ModuleContext,
139    cgctx: Option<&CodegenContext<'_>>,
140    js_namespace: Option<&str>,
141    scope: ScopeId,
142) -> TokenStream {
143    debug_assert!(
144        matches!(
145            decl.classification,
146            InterfaceClassification::ClassLike | InterfaceClassification::Unclassified
147        ),
148        "expected ClassLike or Unclassified, got {:?}",
149        decl.classification
150    );
151    let mut config = ClassConfig::from_interface(decl, ctx, cgctx, scope);
152    config.js_namespace = js_namespace.map(|s| s.to_string());
153    generate_extern_block(&config)
154}
155
156/// Generate a complete `extern "C"` block for a class inside a namespace, with `js_namespace`.
157pub fn generate_class_with_js_namespace(
158    decl: &ClassDecl,
159    ctx: &ModuleContext,
160    js_namespace: &str,
161    cgctx: Option<&CodegenContext<'_>>,
162    scope: ScopeId,
163) -> TokenStream {
164    let mut config = ClassConfig::from_class(decl, ctx, cgctx, scope);
165    config.js_namespace = Some(js_namespace.to_string());
166    generate_extern_block(&config)
167}
168
169/// Generate a simple extern "C" block for a dictionary interface.
170/// Temporary until M5 implements proper dictionary builders.
171pub fn generate_dictionary_extern(
172    decl: &InterfaceDecl,
173    ctx: &ModuleContext,
174    cgctx: Option<&CodegenContext<'_>>,
175    js_namespace: Option<&str>,
176    scope: ScopeId,
177) -> TokenStream {
178    let mut config = ClassConfig::from_interface(decl, ctx, cgctx, scope);
179    config.js_namespace = js_namespace.map(|s| s.to_string());
180
181    let extern_block = generate_extern_block(&config);
182    let factory = generate_dictionary_factory(&config);
183
184    quote! {
185        #extern_block
186        #factory
187    }
188}
189
190/// Generate a Rust `impl` block with factory constructors for a dictionary interface.
191///
192/// Produces `new()` plus expanded variants like `new_with_status(status: f64)`,
193/// `new_with_status_and_status_text(status: f64, status_text: &str)`, etc.
194/// Each factory creates a bare `Object`, sets the provided properties via their
195/// setters, and returns it cast to the dictionary type.
196/// Generate a Rust `impl` block with `new()` and `builder()` for a dictionary interface.
197///
198/// Produces:
199/// ```ignore
200/// impl ResponseInit {
201///     pub fn new() -> Self { ... }
202///     pub fn builder() -> ResponseInitBuilder { ... }
203/// }
204///
205/// pub struct ResponseInitBuilder { inner: ResponseInit }
206/// impl ResponseInitBuilder {
207///     pub fn status(self, val: f64) -> Self { ... }
208///     pub fn headers(self, val: &Headers) -> Self { ... }
209///     pub fn build(self) -> ResponseInit { ... }
210/// }
211/// ```
212fn generate_dictionary_factory(config: &ClassConfig) -> TokenStream {
213    let rust_type = super::typemap::make_ident(&config.rust_name);
214    let builder_name = super::typemap::make_ident(&format!("{}Builder", config.rust_name));
215
216    // If any getter lacks a corresponding setter the type has readonly
217    // properties, which means it is not constructible via setters — skip
218    // the builder entirely and only emit a bare `new()`.
219    let setter_names: std::collections::HashSet<&str> = config
220        .members
221        .iter()
222        .filter_map(|m| {
223            if let Member::Setter(s) = m {
224                Some(s.js_name.as_str())
225            } else {
226                None
227            }
228        })
229        .collect();
230    let has_readonly = config.members.iter().any(|m| {
231        if let Member::Getter(g) = m {
232            !setter_names.contains(g.js_name.as_str())
233        } else {
234            false
235        }
236    });
237    if has_readonly {
238        return quote! {
239            impl #rust_type {
240                #[allow(clippy::new_without_default)]
241                pub fn new() -> Self {
242                    #[allow(unused_unsafe)]
243                    unsafe { JsValue::from(js_sys::Object::new()).unchecked_into() }
244                }
245            }
246        };
247    }
248
249    // Collect getter properties for builder methods
250    let getters: Vec<&crate::ir::GetterMember> = config
251        .members
252        .iter()
253        .filter_map(|m| {
254            if let Member::Getter(g) = m {
255                Some(g)
256            } else {
257                None
258            }
259        })
260        .collect();
261
262    // Identify required properties (non-optional getters) and assign bitmask positions
263    let required_props: Vec<(usize, &str)> = getters
264        .iter()
265        .enumerate()
266        .filter(|(_, g)| !g.optional)
267        .take(64) // u64 bitmask supports up to 64 required properties
268        .map(|(i, g)| (i, g.js_name.as_str()))
269        .collect();
270    let has_required = !required_props.is_empty();
271
272    // Build a map: getter index → bitmask bit (only for required props)
273    let mut required_bit: Vec<Option<u64>> = vec![None; getters.len()];
274    for (bit, &(getter_idx, _)) in required_props.iter().enumerate() {
275        required_bit[getter_idx] = Some(bit as u64);
276    }
277    let full_mask: u64 = if required_props.len() >= 64 {
278        u64::MAX
279    } else {
280        (1u64 << required_props.len()) - 1
281    };
282
283    // Generate builder setter methods
284    let mut builder_methods = Vec::new();
285
286    for (getter_idx, g) in getters.iter().enumerate() {
287        let setter_param = crate::ir::Param {
288            name: "val".to_string(),
289            type_ref: g.type_ref.clone(),
290            optional: false,
291            variadic: false,
292        };
293
294        let mut setter_used = HashSet::new();
295        let setter_sigs = expand_signatures(
296            &g.js_name,
297            &[&[setter_param]],
298            &crate::ir::TypeRef::Void,
299            SignatureKind::Setter,
300            &None,
301            &mut setter_used,
302            config.cgctx,
303            config.scope,
304        );
305
306        let bit_clear = required_bit[getter_idx].map(|bit| {
307            let mask = !(1u64 << bit);
308            quote! { self.required &= #mask; }
309        });
310
311        for sig in &setter_sigs {
312            let builder_method_name = sig.rust_name.strip_prefix("set_").unwrap_or(&sig.rust_name);
313            let method_ident = super::typemap::make_ident(builder_method_name);
314            let setter_ident = super::typemap::make_ident(&sig.rust_name);
315            let params = generate_concrete_params(&sig.params, config.cgctx, config.scope);
316
317            let param_idents: Vec<_> = sig
318                .params
319                .iter()
320                .map(|p| super::typemap::make_ident(&p.name))
321                .collect();
322
323            builder_methods.push(quote! {
324                pub fn #method_ident(mut self, #params) -> Self {
325                    self.inner.#setter_ident(#(#param_idents),*);
326                    #bit_clear
327                    self
328                }
329            });
330        }
331    }
332
333    // Build method: infallible if no required props, Result if there are
334    let build_method = if has_required {
335        let missing_checks: Vec<TokenStream> = required_props
336            .iter()
337            .enumerate()
338            .map(|(bit, (_, name))| {
339                let mask = 1u64 << bit;
340                let msg = format!("missing required property `{name}`");
341                quote! {
342                    if self.required & #mask != 0 {
343                        missing.push(#msg);
344                    }
345                }
346            })
347            .collect();
348
349        quote! {
350            pub fn build(self) -> Result<#rust_type, JsValue> {
351                if self.required != 0 {
352                    let mut missing = Vec::new();
353                    #(#missing_checks)*
354                    return Err(JsValue::from_str(&format!(
355                        "{}: {}", stringify!(#rust_type), missing.join(", ")
356                    )));
357                }
358                Ok(self.inner)
359            }
360        }
361    } else {
362        quote! {
363            pub fn build(self) -> #rust_type {
364                self.inner
365            }
366        }
367    };
368
369    // Builder struct: with or without required bitmask
370    let builder_struct = if has_required {
371        quote! {
372            pub struct #builder_name {
373                inner: #rust_type,
374                required: u64,
375            }
376        }
377    } else {
378        quote! {
379            pub struct #builder_name {
380                inner: #rust_type,
381            }
382        }
383    };
384
385    let builder_init = if has_required {
386        quote! { #builder_name { inner: Self::new(), required: #full_mask } }
387    } else {
388        quote! { #builder_name { inner: Self::new() } }
389    };
390
391    quote! {
392        impl #rust_type {
393            #[allow(clippy::new_without_default)]
394            pub fn new() -> Self {
395                #[allow(unused_imports)]
396                use wasm_bindgen::JsCast;
397                JsCast::unchecked_into(js_sys::Object::new())
398            }
399
400            pub fn builder() -> #builder_name {
401                #builder_init
402            }
403        }
404
405        #builder_struct
406
407        #[allow(unused_mut)]
408        impl #builder_name {
409            #(#builder_methods)*
410
411            #build_method
412        }
413    }
414}
415
416/// Build the full `#[wasm_bindgen] extern "C" { ... }` block.
417///
418/// All naming happens through a single `used_names` set that spans the entire
419/// extern block. Members are processed in declaration order. Methods with the
420/// same `js_name` (TypeScript overloads) are grouped and expanded together as
421/// one unit — overloads feed into the same expansion, producing disambiguated
422/// `_with_`/`_and_` suffixes across all overloads rather than opaque `_1` suffixes.
423///
424/// Each name — including `try_` variants — is assigned via `dedupe_name`, which
425/// guarantees uniqueness by appending numeric suffixes on collision.
426fn generate_extern_block(config: &ClassConfig) -> TokenStream {
427    use crate::ir::{ConstructorMember, MethodMember, Param, StaticMethodMember};
428    use std::collections::HashMap;
429
430    let mut items = Vec::new();
431    let mut used_names: HashSet<String> = HashSet::new();
432
433    // Pre-group methods/statics/constructors by js_name.
434    // We iterate config.members in declaration order, so the first occurrence of
435    // each js_name determines where its expanded signatures appear in the output.
436    let mut method_groups: HashMap<String, Vec<&MethodMember>> = HashMap::new();
437    let mut static_method_groups: HashMap<String, Vec<&StaticMethodMember>> = HashMap::new();
438    let mut constructor_overloads: Vec<&ConstructorMember> = Vec::new();
439
440    for member in &config.members {
441        match member {
442            Member::Constructor(ctor) if !config.is_abstract => {
443                constructor_overloads.push(ctor);
444            }
445            Member::Method(m) => {
446                method_groups.entry(m.js_name.clone()).or_default().push(m);
447            }
448            Member::StaticMethod(m) => {
449                static_method_groups
450                    .entry(m.js_name.clone())
451                    .or_default()
452                    .push(m);
453            }
454            _ => {}
455        }
456    }
457
458    // Track which method groups have been expanded (by js_name).
459    let mut expanded_methods: HashSet<String> = HashSet::new();
460    let mut expanded_static_methods: HashSet<String> = HashSet::new();
461    let mut expanded_constructors = false;
462
463    // 1. Type declaration with attributes
464    items.push(generate_type_decl(config));
465
466    // 2. Process all members in declaration order through the single naming pass.
467    //    When we encounter the first member of a method group, expand all overloads
468    //    of that group together. Skip subsequent members of the same group.
469    for member in &config.members {
470        match member {
471            Member::Constructor(_) if !config.is_abstract => {
472                if expanded_constructors {
473                    continue;
474                }
475                expanded_constructors = true;
476
477                let overloads: Vec<&[Param]> = constructor_overloads
478                    .iter()
479                    .map(|c| c.params.as_slice())
480                    .collect();
481                let doc = constructor_overloads.first().and_then(|c| c.doc.clone());
482                let sigs = expand_signatures(
483                    &config.js_name,
484                    &overloads,
485                    &TypeRef::Named(config.rust_name.clone()),
486                    SignatureKind::Constructor,
487                    &doc,
488                    &mut used_names,
489                    config.cgctx,
490                    config.scope,
491                );
492                for sig in &sigs {
493                    items.push(generate_expanded_constructor(config, sig));
494                }
495            }
496            Member::Method(m) => {
497                if expanded_methods.contains(&m.js_name) {
498                    continue;
499                }
500                expanded_methods.insert(m.js_name.clone());
501
502                let group = &method_groups[&m.js_name];
503                let overloads: Vec<&[Param]> = group.iter().map(|m| m.params.as_slice()).collect();
504                let doc = group.first().and_then(|m| m.doc.clone());
505                let return_type = &group[0].return_type;
506                let sigs = expand_signatures(
507                    &m.js_name,
508                    &overloads,
509                    return_type,
510                    SignatureKind::Method,
511                    &doc,
512                    &mut used_names,
513                    config.cgctx,
514                    config.scope,
515                );
516                for sig in &sigs {
517                    items.push(generate_expanded_method(config, sig));
518                }
519            }
520            Member::StaticMethod(m) => {
521                if expanded_static_methods.contains(&m.js_name) {
522                    continue;
523                }
524                expanded_static_methods.insert(m.js_name.clone());
525
526                let group = &static_method_groups[&m.js_name];
527                let overloads: Vec<&[Param]> = group.iter().map(|m| m.params.as_slice()).collect();
528                let doc = group.first().and_then(|m| m.doc.clone());
529                let return_type = &group[0].return_type;
530                let sigs = expand_signatures(
531                    &m.js_name,
532                    &overloads,
533                    return_type,
534                    SignatureKind::StaticMethod,
535                    &doc,
536                    &mut used_names,
537                    config.cgctx,
538                    config.scope,
539                );
540                for sig in &sigs {
541                    items.push(generate_expanded_static_method(config, sig));
542                }
543            }
544            Member::Getter(g) => {
545                items.push(generate_getter(config, g, &mut used_names));
546            }
547            Member::Setter(s) => {
548                items.extend(generate_setter(config, s, &mut used_names));
549            }
550            Member::StaticGetter(g) => {
551                items.push(generate_static_getter(config, g, &mut used_names));
552            }
553            Member::StaticSetter(s) => {
554                items.extend(generate_static_setter(config, s, &mut used_names));
555            }
556            Member::IndexSignature(_) | Member::Constructor(_) => {
557                // IndexSignature: not yet supported in codegen
558                // Constructor on abstract class: skip
559            }
560        }
561    }
562
563    // Build the extern block with optional module attribute
564    let wb_extern_attr = match &config.module {
565        Some(m) => quote! { #[wasm_bindgen(module = #m)] },
566        None => quote! { #[wasm_bindgen] },
567    };
568
569    quote! {
570        #wb_extern_attr
571        extern "C" {
572            #(#items)*
573        }
574    }
575}
576
577/// Generate the type declaration:
578///
579/// ```rust,ignore
580/// #[wasm_bindgen(extends = ..., js_name = "FooBar")]
581/// #[derive(Debug, Clone, PartialEq, Eq)]
582/// pub type FooBar;
583/// ```
584fn generate_type_decl(config: &ClassConfig) -> TokenStream {
585    let rust_ident = super::typemap::make_ident(&config.rust_name);
586    let js_name = &config.js_name;
587
588    // Build wasm_bindgen attribute parts
589    let mut wb_parts: Vec<TokenStream> = Vec::new();
590
591    let mut has_object = false;
592    for extends in &config.extends {
593        let extends_str = extends.to_string();
594        // Skip extends that resolve to JsValue (implicit, causes conflicting impls)
595        if extends_str == "JsValue" {
596            continue;
597        }
598        if let Some(cgctx) = config.cgctx {
599            let uses = cgctx.external_uses.borrow();
600            if uses.get(&extends_str).is_some_and(|v| v == "JsValue") {
601                continue;
602            }
603        }
604        if extends_str == "Object" {
605            has_object = true;
606        }
607        wb_parts.push(quote! { extends = #extends });
608    }
609    // Every type extends Object at minimum
610    if !has_object {
611        wb_parts.push(quote! { extends = Object });
612    }
613
614    // Only emit js_name if it differs from the Rust name
615    if config.js_name != config.rust_name {
616        wb_parts.push(quote! { js_name = #js_name });
617    }
618
619    // Namespace for types inside a JS namespace (e.g., WebAssembly.Module)
620    if let Some(ns) = &config.js_namespace {
621        wb_parts.push(quote! { js_namespace = #ns });
622    }
623
624    let wb_attr = if wb_parts.is_empty() {
625        quote! {}
626    } else {
627        quote! { #[wasm_bindgen(#(#wb_parts),*)] }
628    };
629
630    quote! {
631        #wb_attr
632        #[derive(Debug, Clone, PartialEq, Eq)]
633        pub type #rust_ident;
634    }
635}
636
637/// Generate a constructor binding from an expanded signature.
638fn generate_expanded_constructor(config: &ClassConfig, sig: &ExpandedSignature) -> TokenStream {
639    let rust_ident = super::typemap::make_ident(&sig.rust_name);
640    let rust_type = super::typemap::make_ident(&config.rust_name);
641    let params = generate_concrete_params(&sig.params, config.cgctx, config.scope);
642    let doc = super::doc_tokens(&sig.doc);
643
644    let ret = if sig.catch {
645        quote! { Result<#rust_type, JsValue> }
646    } else {
647        quote! { #rust_type }
648    };
649
650    let mut wb_parts = vec![quote! { constructor }];
651    if sig.catch {
652        wb_parts.push(quote! { catch });
653    }
654    // For non-"new" overloads, we need js_name so wasm_bindgen maps them
655    // to the same JS constructor.
656    if sig.rust_name != "new" {
657        let js_name = &config.js_name;
658        wb_parts.push(quote! { js_name = #js_name });
659    }
660
661    quote! {
662        #doc
663        #[wasm_bindgen(#(#wb_parts),*)]
664        pub fn #rust_ident(#params) -> #ret;
665    }
666}
667
668/// Generate an instance method binding from an expanded signature.
669fn generate_expanded_method(config: &ClassConfig, sig: &ExpandedSignature) -> TokenStream {
670    let rust_ident = super::typemap::make_ident(&sig.rust_name);
671    let this_type = super::typemap::make_ident(&config.rust_name);
672    let params = generate_concrete_params(&sig.params, config.cgctx, config.scope);
673    let doc = super::doc_tokens(&sig.doc);
674    let has_variadic = sig.params.last().is_some_and(|p| p.variadic);
675
676    let mut wb_parts: Vec<TokenStream> = vec![quote! { method }];
677    if has_variadic {
678        wb_parts.push(quote! { variadic });
679    }
680    if sig.catch {
681        wb_parts.push(quote! { catch });
682    }
683    // Emit js_name when the JS name differs from the Rust name.
684    if sig.rust_name != sig.js_name {
685        let js_name = &sig.js_name;
686        wb_parts.push(quote! { js_name = #js_name });
687    }
688
689    let ret_ty = to_return_type(&sig.return_type, sig.catch, config.cgctx, config.scope);
690    let ret = if is_void_return(&sig.return_type) && !sig.catch {
691        quote! {}
692    } else {
693        quote! { -> #ret_ty }
694    };
695
696    quote! {
697        #doc
698        #[wasm_bindgen(#(#wb_parts),*)]
699        pub fn #rust_ident(this: &#this_type, #params) #ret;
700    }
701}
702
703/// Generate a static method binding from an expanded signature.
704fn generate_expanded_static_method(config: &ClassConfig, sig: &ExpandedSignature) -> TokenStream {
705    let rust_ident = super::typemap::make_ident(&sig.rust_name);
706    let class_ident = super::typemap::make_ident(&config.rust_name);
707    let params = generate_concrete_params(&sig.params, config.cgctx, config.scope);
708    let doc = super::doc_tokens(&sig.doc);
709    let has_variadic = sig.params.last().is_some_and(|p| p.variadic);
710
711    let mut wb_parts: Vec<TokenStream> = vec![quote! { static_method_of = #class_ident }];
712    if has_variadic {
713        wb_parts.push(quote! { variadic });
714    }
715    if sig.catch {
716        wb_parts.push(quote! { catch });
717    }
718    if sig.rust_name != sig.js_name {
719        let js_name = &sig.js_name;
720        wb_parts.push(quote! { js_name = #js_name });
721    }
722
723    let ret_ty = to_return_type(&sig.return_type, sig.catch, config.cgctx, config.scope);
724    let ret = if is_void_return(&sig.return_type) && !sig.catch {
725        quote! {}
726    } else {
727        quote! { -> #ret_ty }
728    };
729
730    quote! {
731        #doc
732        #[wasm_bindgen(#(#wb_parts),*)]
733        pub fn #rust_ident(#params) #ret;
734    }
735}
736
737/// Generate an instance getter binding.
738fn generate_getter(
739    config: &ClassConfig,
740    getter: &GetterMember,
741    used_names: &mut HashSet<String>,
742) -> TokenStream {
743    let this_type = super::typemap::make_ident(&config.rust_name);
744    let doc = super::doc_tokens(&getter.doc);
745
746    let candidate = to_snake_case(&getter.js_name);
747    let rust_name = dedupe_name(&candidate, used_names);
748    let rust_ident = super::typemap::make_ident(&rust_name);
749
750    let getter_type = if getter.optional {
751        // Unwrap Nullable to avoid Option<Option<T>> — the optionality from `?`
752        // already provides the outer Option.
753        let unwrapped = match &getter.type_ref {
754            TypeRef::Nullable(inner) => inner.as_ref(),
755            other => other,
756        };
757        let inner = to_syn_type(unwrapped, TypePosition::RETURN, config.cgctx, config.scope);
758        quote! { Option<#inner> }
759    } else {
760        to_syn_type(
761            &getter.type_ref,
762            TypePosition::RETURN,
763            config.cgctx,
764            config.scope,
765        )
766    };
767
768    let mut wb_parts: Vec<TokenStream> = vec![quote! { method }, quote! { getter }];
769    if rust_name != getter.js_name {
770        let js_name = &getter.js_name;
771        wb_parts.push(quote! { js_name = #js_name });
772    }
773
774    quote! {
775        #doc
776        #[wasm_bindgen(#(#wb_parts),*)]
777        pub fn #rust_ident(this: &#this_type) -> #getter_type;
778    }
779}
780
781/// Generate instance setter bindings, expanding union types into separate overloads.
782fn generate_setter(
783    config: &ClassConfig,
784    setter: &SetterMember,
785    used_names: &mut HashSet<String>,
786) -> Vec<TokenStream> {
787    let this_type = super::typemap::make_ident(&config.rust_name);
788    let doc = setter.doc.clone();
789
790    // Treat the setter as a single-param method and expand through signatures
791    let param = crate::ir::Param {
792        name: "val".to_string(),
793        type_ref: setter.type_ref.clone(),
794        optional: false,
795        variadic: false,
796    };
797
798    let sigs = expand_signatures(
799        &setter.js_name,
800        &[&[param]],
801        &crate::ir::TypeRef::Void,
802        SignatureKind::Setter,
803        &doc,
804        used_names,
805        config.cgctx,
806        config.scope,
807    );
808
809    sigs.iter()
810        .map(|sig| {
811            let rust_ident = super::typemap::make_ident(&sig.rust_name);
812            let params = generate_concrete_params(&sig.params, config.cgctx, config.scope);
813
814            let mut wb_parts: Vec<TokenStream> = vec![quote! { method }, quote! { setter }];
815            if sig.rust_name != format!("set_{}", setter.js_name) {
816                let js_name = &setter.js_name;
817                wb_parts.push(quote! { js_name = #js_name });
818            }
819
820            let doc = super::doc_tokens(&sig.doc);
821            quote! {
822                #doc
823                #[wasm_bindgen(#(#wb_parts),*)]
824                pub fn #rust_ident(this: &#this_type, #params);
825            }
826        })
827        .collect()
828}
829
830/// Generate a static getter binding.
831fn generate_static_getter(
832    config: &ClassConfig,
833    getter: &StaticGetterMember,
834    used_names: &mut HashSet<String>,
835) -> TokenStream {
836    let class_ident = super::typemap::make_ident(&config.rust_name);
837    let doc = super::doc_tokens(&getter.doc);
838
839    let candidate = to_snake_case(&getter.js_name);
840    let rust_name = dedupe_name(&candidate, used_names);
841    let rust_ident = super::typemap::make_ident(&rust_name);
842
843    let getter_type = to_syn_type(
844        &getter.type_ref,
845        TypePosition::RETURN,
846        config.cgctx,
847        config.scope,
848    );
849
850    let mut wb_parts: Vec<TokenStream> = vec![
851        quote! { static_method_of = #class_ident },
852        quote! { getter },
853    ];
854    if rust_name != getter.js_name {
855        let js_name = &getter.js_name;
856        wb_parts.push(quote! { js_name = #js_name });
857    }
858
859    quote! {
860        #doc
861        #[wasm_bindgen(#(#wb_parts),*)]
862        pub fn #rust_ident() -> #getter_type;
863    }
864}
865
866/// Generate static setter bindings, expanding union types into separate overloads.
867fn generate_static_setter(
868    config: &ClassConfig,
869    setter: &StaticSetterMember,
870    used_names: &mut HashSet<String>,
871) -> Vec<TokenStream> {
872    let class_ident = super::typemap::make_ident(&config.rust_name);
873    let doc = setter.doc.clone();
874
875    let param = crate::ir::Param {
876        name: "val".to_string(),
877        type_ref: setter.type_ref.clone(),
878        optional: false,
879        variadic: false,
880    };
881
882    let sigs = expand_signatures(
883        &setter.js_name,
884        &[&[param]],
885        &crate::ir::TypeRef::Void,
886        SignatureKind::StaticSetter,
887        &doc,
888        used_names,
889        config.cgctx,
890        config.scope,
891    );
892
893    sigs.iter()
894        .map(|sig| {
895            let rust_ident = super::typemap::make_ident(&sig.rust_name);
896            let params = generate_concrete_params(&sig.params, config.cgctx, config.scope);
897
898            let mut wb_parts: Vec<TokenStream> = vec![
899                quote! { static_method_of = #class_ident },
900                quote! { setter },
901            ];
902            if sig.rust_name != format!("set_{}", setter.js_name) {
903                let js_name = &setter.js_name;
904                wb_parts.push(quote! { js_name = #js_name });
905            }
906
907            let doc = super::doc_tokens(&sig.doc);
908            quote! {
909                #doc
910                #[wasm_bindgen(#(#wb_parts),*)]
911                pub fn #rust_ident(#params);
912            }
913        })
914        .collect()
915}
916
917// ─── Helpers ─────────────────────────────────────────────────────────
918
919/// Convert concrete params to `fn` parameter token stream.
920/// Convert a `TypeRef` representing an extends target into tokens for
921/// the `extends = ...` attribute.
922///
923/// Falls back to `Object` for unresolved types — `extends = JsValue` is
924/// never useful (it's implicit and causes conflicting trait impls).
925fn extends_tokens(ty: &TypeRef, cgctx: Option<&CodegenContext<'_>>, scope: ScopeId) -> TokenStream {
926    let tokens = match ty {
927        TypeRef::Named(_) | TypeRef::GenericInstantiation(_, _) => {
928            super::typemap::to_syn_type(ty, TypePosition::ARGUMENT.to_inner(), cgctx, scope)
929        }
930        _ => {
931            if let Some(ctx) = cgctx {
932                ctx.warn(format!(
933                    "unsupported extends type `{ty:?}`, falling back to Object"
934                ));
935            }
936            quote! { Object }
937        }
938    };
939    // JsValue is the root of all wasm_bindgen types — extending it is
940    // implicit, so fall back to Object (which is always safe).
941    if tokens.to_string() == "JsValue" {
942        quote! { Object }
943    } else {
944        tokens
945    }
946}