Skip to main content

ts_gen/codegen/
signatures.rs

1//! Signature expansion: expand TypeScript overloads (with optional/variadic/union
2//! params) into multiple concrete Rust signatures with computed names.
3//!
4//! This implements the core overload expansion algorithm, following the same
5//! pattern as wasm-bindgen's webidl codegen (`util.rs:create_imports`).
6//!
7//! # Overview
8//!
9//! TypeScript overloads describe multiple ways to call the same runtime function.
10//! They are semantically equivalent to unions spread across declarations.
11//! `expand_signatures` takes ALL overloads of a single JS function and produces
12//! the complete set of Rust bindings for it.
13//!
14//! The algorithm has three phases:
15//!
16//! 1. **Per-overload expansion**: For each overload, generate optional truncation
17//!    variants and cartesian-product union type alternatives.
18//! 2. **Cross-overload dedup**: Remove expanded signatures with identical concrete
19//!    param lists (e.g. two overloads both truncate to `(callback)`).
20//! 3. **Naming**: Compute `_with_`/`_and_` suffixes across all surviving signatures
21//!    as one cohort, then assign final unique names via the shared `used_names` set.
22//!
23//! # Expansion Rules
24//!
25//! Given `f(a, b?, c?)`:
26//! - `f(a)` — base signature
27//! - `f_with_b(a, b)` — first optional included
28//! - `f_with_b_and_c(a, b, c)` — all params included
29//!
30//! # Catch Rules
31//!
32//! - **Constructors**: always `catch` (JS constructors can always throw)
33//! - **Methods/Functions**: no `catch` by default; a `try_` prefixed variant
34//!   is generated with `catch` for each expanded signature
35//!
36//! # Variadic
37//!
38//! A variadic param uses `#[wasm_bindgen(variadic)]` with a `&[JsValue]` type.
39//! Variadic params participate in `_with_`/`_and_` suffix computation — if a
40//! signature differs from its siblings only by having a trailing variadic param,
41//! the param name is used as a suffix (e.g. `_with_args`).
42
43use std::collections::HashSet;
44
45use proc_macro2::TokenStream;
46use quote::quote;
47
48use crate::codegen::typemap::{self, CodegenContext, TypePosition};
49use crate::ir::{Param, TypeRef};
50use crate::parse::scope::ScopeId;
51use crate::util::naming::to_snake_case;
52
53/// What kind of callable we're expanding.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum SignatureKind {
56    Constructor,
57    Method,
58    StaticMethod,
59    Function,
60    Setter,
61    StaticSetter,
62}
63
64/// A single concrete parameter in an expanded signature.
65#[derive(Clone, Debug, PartialEq)]
66pub struct ConcreteParam {
67    pub name: String,
68    pub type_ref: TypeRef,
69    /// Whether this param is variadic (only the last param can be).
70    pub variadic: bool,
71}
72
73/// A single expanded, ready-to-codegen signature.
74#[derive(Clone, Debug)]
75pub struct ExpandedSignature {
76    /// Rust function name — unique within the extern block.
77    pub rust_name: String,
78    /// JS function name (the real name on the JS side)
79    pub js_name: String,
80    /// Concrete params (no optional flags — those have been resolved by truncation)
81    pub params: Vec<ConcreteParam>,
82    /// Whether to apply `catch` (wrap return in `Result<T, JsValue>`)
83    pub catch: bool,
84    /// Return type
85    pub return_type: TypeRef,
86    /// Doc comment (only on the primary/shortest signature)
87    pub doc: Option<String>,
88    /// What kind of callable this is
89    pub kind: SignatureKind,
90}
91
92/// Assign a unique name within the extern block.
93///
94/// If `candidate` is already taken, appends `_1`, `_2`, etc. until a unique
95/// name is found. The chosen name is inserted into `used_names`.
96pub fn dedupe_name(candidate: &str, used_names: &mut HashSet<String>) -> String {
97    let mut name = candidate.to_string();
98    if !used_names.contains(&name) {
99        used_names.insert(name.clone());
100        return name;
101    }
102    let base = name.clone();
103    let mut counter = 1u32;
104    loop {
105        name = format!("{base}_{counter}");
106        if !used_names.contains(&name) {
107            used_names.insert(name.clone());
108            return name;
109        }
110        counter += 1;
111    }
112}
113
114/// Expand all overloads of a single JS function into concrete Rust signatures.
115///
116/// Takes ALL overloads (param lists) sharing the same `js_name` and produces
117/// the complete set of bindings. The algorithm:
118///
119/// 1. For each overload: generate optional truncation variants, then expand
120///    union types via cartesian product.
121/// 2. Dedup: remove expanded signatures with identical concrete param lists
122///    across overloads.
123/// 3. Name: compute `_with_`/`_and_` suffixes across all surviving signatures,
124///    then assign final unique names via the shared `used_names` set.
125/// 4. Generate `try_` variants (non-constructors only).
126#[allow(clippy::too_many_arguments)]
127pub fn expand_signatures(
128    js_name: &str,
129    overloads: &[&[Param]],
130    return_type: &TypeRef,
131    kind: SignatureKind,
132    doc: &Option<String>,
133    used_names: &mut HashSet<String>,
134    cgctx: Option<&CodegenContext<'_>>,
135    scope: ScopeId,
136) -> Vec<ExpandedSignature> {
137    let base_rust_name = match kind {
138        SignatureKind::Constructor => "new".to_string(),
139        SignatureKind::Setter | SignatureKind::StaticSetter => {
140            format!("set_{}", to_snake_case(js_name))
141        }
142        _ => to_snake_case(js_name),
143    };
144
145    // Phase 1: Per-overload expansion — optional truncation + union cartesian product.
146    let mut all_sigs: Vec<Vec<ConcreteParam>> = Vec::new();
147
148    for params in overloads {
149        let expanded = expand_single_overload(params, cgctx, scope);
150        all_sigs.extend(expanded);
151    }
152
153    // Phase 2: Cross-overload dedup — remove identical expanded signatures.
154    let mut seen: Vec<&Vec<ConcreteParam>> = Vec::new();
155    let mut deduped: Vec<Vec<ConcreteParam>> = Vec::new();
156    for sig in &all_sigs {
157        if !seen.iter().any(|s| concrete_params_eq(s, sig)) {
158            seen.push(sig);
159            deduped.push(sig.clone());
160        }
161    }
162
163    // Phase 3: Naming — compute candidate names, then assign final unique names.
164    let candidate_names = compute_rust_names(&base_rust_name, &deduped);
165    let is_constructor = kind == SignatureKind::Constructor;
166    let mut result = Vec::new();
167
168    for (i, (candidate, concrete_params)) in candidate_names.into_iter().zip(deduped).enumerate() {
169        let is_first = i == 0;
170        let rust_name = dedupe_name(&candidate, used_names);
171
172        result.push(ExpandedSignature {
173            rust_name: rust_name.clone(),
174            js_name: js_name.to_string(),
175            params: concrete_params.clone(),
176            catch: is_constructor,
177            return_type: return_type.clone(),
178            doc: if is_first { doc.clone() } else { None },
179            kind,
180        });
181
182        // try_ variant (not for constructors or setters)
183        let emit_try = !matches!(
184            kind,
185            SignatureKind::Constructor | SignatureKind::Setter | SignatureKind::StaticSetter
186        );
187        if emit_try {
188            let try_candidate = format!("try_{rust_name}");
189            let try_name = dedupe_name(&try_candidate, used_names);
190            result.push(ExpandedSignature {
191                rust_name: try_name,
192                js_name: js_name.to_string(),
193                params: concrete_params,
194                catch: true,
195                return_type: return_type.clone(),
196                doc: None,
197                kind,
198            });
199        }
200    }
201
202    result
203}
204
205/// Check if two expanded concrete param lists are identical.
206fn concrete_params_eq(a: &[ConcreteParam], b: &[ConcreteParam]) -> bool {
207    a.len() == b.len()
208        && a.iter().zip(b.iter()).all(|(pa, pb)| {
209            pa.name == pb.name && pa.type_ref == pb.type_ref && pa.variadic == pb.variadic
210        })
211}
212
213/// Expand a single overload's params into all concrete signature variants.
214///
215/// Handles optional truncation, union flattening (cartesian product), and
216/// variadic param appending.
217fn expand_single_overload(
218    params: &[Param],
219    cgctx: Option<&CodegenContext<'_>>,
220    scope: ScopeId,
221) -> Vec<Vec<ConcreteParam>> {
222    // Separate trailing variadic param.
223    let (non_variadic, variadic_param) = if params.last().is_some_and(|p| p.variadic) {
224        (&params[..params.len() - 1], Some(&params[params.len() - 1]))
225    } else {
226        (params, None)
227    };
228
229    // Build expanded signatures via cartesian product.
230    // Start with one empty signature, iterate params left to right.
231    let mut sigs: Vec<Vec<ConcreteParam>> = vec![vec![]];
232
233    for (i, param) in non_variadic.iter().enumerate() {
234        let type_alternatives = flatten_type(&param.type_ref, cgctx, scope);
235
236        if param.optional {
237            // Only extend sigs that are "full" up to this point (len == i).
238            // Shorter sigs are from earlier optional truncation — they stay frozen.
239            let frozen: Vec<Vec<ConcreteParam>> =
240                sigs.iter().filter(|s| s.len() < i).cloned().collect();
241            let mut extendable: Vec<Vec<ConcreteParam>> =
242                sigs.into_iter().filter(|s| s.len() == i).collect();
243            let snapshot = extendable.clone(); // before extension (absent variants)
244
245            let cur = extendable.len();
246            for (j, alt) in type_alternatives.into_iter().enumerate() {
247                let concrete = ConcreteParam {
248                    name: param.name.clone(),
249                    type_ref: alt,
250                    variadic: false,
251                };
252                if j == 0 {
253                    for sig in extendable.iter_mut().take(cur) {
254                        sig.push(concrete.clone());
255                    }
256                } else {
257                    for item in snapshot.iter().take(cur) {
258                        let mut sig = item.clone();
259                        sig.push(concrete.clone());
260                        extendable.push(sig);
261                    }
262                }
263            }
264
265            // Reassemble: frozen + absent (snapshot) + extended
266            sigs = frozen;
267            sigs.extend(snapshot);
268            sigs.extend(extendable);
269        } else {
270            // Required param: flatten and multiply.
271            let cur = sigs.len();
272            for (j, alt) in type_alternatives.into_iter().enumerate() {
273                let concrete = ConcreteParam {
274                    name: param.name.clone(),
275                    type_ref: alt,
276                    variadic: false,
277                };
278                if j == 0 {
279                    for sig in sigs.iter_mut().take(cur) {
280                        sig.push(concrete.clone());
281                    }
282                } else {
283                    for k in 0..cur {
284                        let mut sig = sigs[k].clone();
285                        sig.truncate(i);
286                        sig.push(concrete.clone());
287                        sigs.push(sig);
288                    }
289                }
290            }
291        }
292    }
293
294    // Append variadic param to every signature.
295    if let Some(vp) = variadic_param {
296        for sig in &mut sigs {
297            sig.push(ConcreteParam {
298                name: vp.name.clone(),
299                type_ref: vp.type_ref.clone(),
300                variadic: true,
301            });
302        }
303    }
304
305    sigs
306}
307
308/// Recursively flatten a type into its concrete alternatives.
309///
310/// - `Union([A, B])` → flatten(A) ++ flatten(B)
311/// - `Nullable(T)` → flatten(T) wrapped in Nullable
312/// - `Named("Foo")` → resolve alias; if alias is a union, flatten it
313/// - `Promise(T)` → for each flatten(T), wrap in Promise
314/// - `Array(T)` → for each flatten(T), wrap in Array
315/// - Everything else → single leaf
316fn flatten_type(ty: &TypeRef, cgctx: Option<&CodegenContext<'_>>, scope: ScopeId) -> Vec<TypeRef> {
317    match ty {
318        // Unions fan out into each member, recursively
319        TypeRef::Union(members) => members
320            .iter()
321            .flat_map(|m| flatten_type(m, cgctx, scope))
322            .collect(),
323
324        // Named types: resolve through aliases, then re-flatten
325        TypeRef::Named(name) => {
326            if let Some(c) = cgctx {
327                if let Some(target) = c.resolve_alias(name, scope) {
328                    let target = target.clone();
329                    return flatten_type(&target, cgctx, scope);
330                }
331            }
332            vec![ty.clone()]
333        }
334
335        // Nullable: flatten inner, wrap each in Nullable
336        TypeRef::Nullable(inner) => flatten_type(inner, cgctx, scope)
337            .into_iter()
338            .map(|t| TypeRef::Nullable(Box::new(t)))
339            .collect(),
340
341        // Generic containers: flatten inner, wrap each
342        TypeRef::Promise(inner) => flatten_type(inner, cgctx, scope)
343            .into_iter()
344            .map(|t| TypeRef::Promise(Box::new(t)))
345            .collect(),
346        TypeRef::Array(inner) => flatten_type(inner, cgctx, scope)
347            .into_iter()
348            .map(|t| TypeRef::Array(Box::new(t)))
349            .collect(),
350        TypeRef::Set(inner) => flatten_type(inner, cgctx, scope)
351            .into_iter()
352            .map(|t| TypeRef::Set(Box::new(t)))
353            .collect(),
354        // Two-arg containers: cartesian product
355        TypeRef::Record(k, v) => {
356            let ks = flatten_type(k, cgctx, scope);
357            let vs = flatten_type(v, cgctx, scope);
358            let mut result = Vec::new();
359            for k in &ks {
360                for v in &vs {
361                    result.push(TypeRef::Record(Box::new(k.clone()), Box::new(v.clone())));
362                }
363            }
364            result
365        }
366        TypeRef::Map(k, v) => {
367            let ks = flatten_type(k, cgctx, scope);
368            let vs = flatten_type(v, cgctx, scope);
369            let mut result = Vec::new();
370            for k in &ks {
371                for v in &vs {
372                    result.push(TypeRef::Map(Box::new(k.clone()), Box::new(v.clone())));
373                }
374            }
375            result
376        }
377
378        // Leaf types: no expansion
379        _ => vec![ty.clone()],
380    }
381}
382
383/// Compute candidate Rust names for a set of signatures sharing the same JS name.
384///
385/// Follows the wasm-bindgen webidl naming convention:
386/// - The first signature gets the base name
387/// - Other signatures get `_with_` / `_and_` suffixes
388/// - When two sigs differ at a param position:
389///   - If they have different param names (optional expansion / overload), use the param name
390///   - If they have the same param name but different types (union expansion),
391///     use the type's snake_case name
392///
393/// Variadic params participate in naming — if a signature has a trailing variadic
394/// that others lack, the param name is used as a suffix.
395///
396/// These are candidate names — the caller runs them through `dedupe_name` for
397/// final uniqueness within the extern block.
398fn compute_rust_names(base_name: &str, signatures: &[Vec<ConcreteParam>]) -> Vec<String> {
399    if signatures.len() == 1 {
400        return vec![base_name.to_string()];
401    }
402
403    // Compute the number of params to trim from each end — params that are
404    // identical across ALL signatures at the same offset don't disambiguate.
405    //
406    // This handles two cases that pure positional comparison misses:
407    // - Variadic params anchored at the end (e.g. `(data)` vs `(label, data)`)
408    // - Shared leading params (e.g. `(callback)` vs `(callback, msDelay)`)
409    //
410    // Only the "middle" params that differ contribute to naming suffixes.
411    let (trim_start, trim_end) = compute_trim(signatures);
412
413    let mut names = Vec::new();
414
415    for (sig_idx, sig) in signatures.iter().enumerate() {
416        // The first signature (shortest / most basic) gets the base name
417        // without any suffix. This matches the convention that the most
418        // common calling pattern uses the simplest name.
419        if sig_idx == 0 {
420            names.push(base_name.to_string());
421            continue;
422        }
423
424        let mut name = base_name.to_string();
425        let mut first_suffix = true;
426
427        let end = if sig.len() >= trim_end {
428            sig.len() - trim_end
429        } else {
430            // Signature is shorter than the shared suffix — don't trim
431            sig.len()
432        };
433        let start = trim_start.min(end);
434
435        for (param_idx, param) in sig[start..end].iter().enumerate() {
436            let abs_idx = start + param_idx;
437
438            // Check if this param position differs from any other signature
439            // we need to disambiguate against (using original absolute indices).
440            let mut any_different = false;
441            let mut any_same_name_different_type = false;
442
443            for (other_idx, other) in signatures.iter().enumerate() {
444                if other_idx == sig_idx {
445                    continue;
446                }
447                match other.get(abs_idx) {
448                    Some(other_param) => {
449                        if other_param.name == param.name && other_param.type_ref != param.type_ref
450                        {
451                            any_same_name_different_type = true;
452                            any_different = true;
453                        } else if other_param.name != param.name {
454                            any_different = true;
455                        }
456                    }
457                    None => {
458                        // Other sig doesn't have this param
459                        any_different = true;
460                    }
461                }
462            }
463
464            if !any_different {
465                continue;
466            }
467
468            if first_suffix {
469                name.push_str("_with_");
470                first_suffix = false;
471            } else {
472                name.push_str("_and_");
473            }
474
475            if any_same_name_different_type {
476                // Union expansion: use the type name
477                name.push_str(&type_snake_name(&param.type_ref));
478            } else {
479                // Optional expansion or overload: use the param name
480                name.push_str(&to_snake_case(&param.name));
481            }
482        }
483
484        names.push(name);
485    }
486
487    names
488}
489
490/// Compute how many params to trim from the start and end of all signatures.
491///
492/// A param at offset `i` from the start is "shared" if ALL signatures have
493/// length > i and the param at position `i` is identical (same name, type,
494/// variadic) across all signatures. Similarly from the end.
495fn compute_trim(signatures: &[Vec<ConcreteParam>]) -> (usize, usize) {
496    let min_len = signatures.iter().map(|s| s.len()).min().unwrap_or(0);
497
498    // Trim from start: count matching prefix across all signatures
499    let mut trim_start = 0;
500    for i in 0..min_len {
501        let first = &signatures[0][i];
502        if signatures[1..].iter().all(|sig| sig[i] == *first) {
503            trim_start += 1;
504        } else {
505            break;
506        }
507    }
508
509    // Trim from end: count matching suffix across all signatures
510    let mut trim_end = 0;
511    for i in 0..min_len {
512        let first = &signatures[0][signatures[0].len() - 1 - i];
513        if signatures[1..]
514            .iter()
515            .all(|sig| sig[sig.len() - 1 - i] == *first)
516        {
517            trim_end += 1;
518        } else {
519            break;
520        }
521    }
522
523    // Don't let trims overlap
524    if trim_start + trim_end > min_len {
525        trim_end = min_len - trim_start;
526    }
527
528    (trim_start, trim_end)
529}
530
531/// Get a short snake_case name for a TypeRef, used in `_with_` suffixes.
532fn type_snake_name(ty: &TypeRef) -> String {
533    match ty {
534        TypeRef::String => "str".to_string(),
535        TypeRef::Number => "f64".to_string(),
536        TypeRef::Boolean => "bool".to_string(),
537        TypeRef::BigInt => "big_int".to_string(),
538        TypeRef::Void | TypeRef::Undefined => "undefined".to_string(),
539        TypeRef::Null => "null".to_string(),
540        TypeRef::Any | TypeRef::Unknown => "js_value".to_string(),
541        TypeRef::Object => "object".to_string(),
542        TypeRef::Named(n) => to_snake_case(n),
543        TypeRef::ArrayBuffer => "array_buffer".to_string(),
544        TypeRef::Uint8Array => "uint8_array".to_string(),
545        TypeRef::Int8Array => "int8_array".to_string(),
546        TypeRef::Float32Array => "float32_array".to_string(),
547        TypeRef::Float64Array => "float64_array".to_string(),
548        TypeRef::Array(_) => "array".to_string(),
549        TypeRef::Promise(_) => "promise".to_string(),
550        TypeRef::Nullable(inner) => type_snake_name(inner),
551
552        TypeRef::Function(_) => "function".to_string(),
553        TypeRef::Date => "date".to_string(),
554        TypeRef::RegExp => "reg_exp".to_string(),
555        TypeRef::Error => "error".to_string(),
556        TypeRef::Map(_, _) => "map".to_string(),
557        TypeRef::Set(_) => "set".to_string(),
558        TypeRef::Record(_, _) => "record".to_string(),
559        _ => "js_value".to_string(),
560    }
561}
562
563// ─── Shared codegen helpers ─────────────────────────────────────────
564
565/// Convert concrete params to a `fn` parameter token stream.
566///
567/// Handles variadic params with `&[JsValue]`.
568pub fn generate_concrete_params(
569    params: &[ConcreteParam],
570    cgctx: Option<&CodegenContext<'_>>,
571    scope: ScopeId,
572) -> TokenStream {
573    let items: Vec<_> = params
574        .iter()
575        .map(|p| {
576            let name = typemap::make_ident(&p.name);
577            let ty = if p.variadic {
578                quote! { &[JsValue] }
579            } else {
580                typemap::to_syn_type(&p.type_ref, TypePosition::ARGUMENT, cgctx, scope)
581            };
582            quote! { #name: #ty }
583        })
584        .collect();
585
586    quote! { #(#items),* }
587}
588
589/// Returns true if the return type is void (no return value in Rust).
590pub fn is_void_return(ty: &TypeRef) -> bool {
591    matches!(ty, TypeRef::Void | TypeRef::Undefined)
592}
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597    use crate::codegen::typemap::CodegenContext;
598    use crate::context::GlobalContext;
599    use crate::ir::TypeRef;
600
601    fn no_used() -> HashSet<String> {
602        HashSet::new()
603    }
604
605    /// Create a GlobalContext + scope + CodegenContext for tests.
606    fn test_ctx() -> (GlobalContext, ScopeId) {
607        let mut gctx = GlobalContext::new();
608        let scope = gctx.create_root_scope();
609        (gctx, scope)
610    }
611
612    /// Shorthand: expand a single overload.
613    fn expand(
614        js: &str,
615        params: &[Param],
616        ret: &TypeRef,
617        kind: SignatureKind,
618        doc: &Option<String>,
619        used: &mut HashSet<String>,
620    ) -> Vec<ExpandedSignature> {
621        let (gctx, scope) = test_ctx();
622        let cgctx = CodegenContext::empty(&gctx, scope);
623        expand_signatures(js, &[params], ret, kind, doc, used, Some(&cgctx), scope)
624    }
625
626    /// Shorthand: expand multiple overloads.
627    fn expand_overloads(
628        js: &str,
629        overloads: &[&[Param]],
630        ret: &TypeRef,
631        kind: SignatureKind,
632        doc: &Option<String>,
633        used: &mut HashSet<String>,
634    ) -> Vec<ExpandedSignature> {
635        let (gctx, scope) = test_ctx();
636        let cgctx = CodegenContext::empty(&gctx, scope);
637        expand_signatures(js, overloads, ret, kind, doc, used, Some(&cgctx), scope)
638    }
639
640    fn param(name: &str) -> Param {
641        Param {
642            name: name.to_string(),
643            type_ref: TypeRef::Any,
644            optional: false,
645            variadic: false,
646        }
647    }
648
649    fn typed_param(name: &str, ty: TypeRef) -> Param {
650        Param {
651            name: name.to_string(),
652            type_ref: ty,
653            optional: false,
654            variadic: false,
655        }
656    }
657
658    fn opt_param(name: &str) -> Param {
659        Param {
660            name: name.to_string(),
661            type_ref: TypeRef::Any,
662            optional: true,
663            variadic: false,
664        }
665    }
666
667    fn opt_typed_param(name: &str, ty: TypeRef) -> Param {
668        Param {
669            name: name.to_string(),
670            type_ref: ty,
671            optional: true,
672            variadic: false,
673        }
674    }
675
676    fn variadic_param(name: &str) -> Param {
677        Param {
678            name: name.to_string(),
679            type_ref: TypeRef::Any,
680            optional: false,
681            variadic: true,
682        }
683    }
684
685    #[test]
686    fn test_no_optional_params() {
687        let mut used = no_used();
688        let sigs = expand(
689            "foo",
690            &[param("a"), param("b")],
691            &TypeRef::Void,
692            SignatureKind::Method,
693            &None,
694            &mut used,
695        );
696        // Should produce 2: foo (no catch) + try_foo (catch)
697        assert_eq!(sigs.len(), 2);
698        assert_eq!(sigs[0].rust_name, "foo");
699        assert!(!sigs[0].catch);
700        assert_eq!(sigs[0].params.len(), 2);
701        assert_eq!(sigs[1].rust_name, "try_foo");
702        assert!(sigs[1].catch);
703    }
704
705    #[test]
706    fn test_constructor_no_try_variant() {
707        let mut used = no_used();
708        let sigs = expand(
709            "Console",
710            &[param("stdout")],
711            &TypeRef::Named("Console".into()),
712            SignatureKind::Constructor,
713            &None,
714            &mut used,
715        );
716        // Constructor: only 1 signature, always catch, no try_ variant
717        assert_eq!(sigs.len(), 1);
718        assert_eq!(sigs[0].rust_name, "new");
719        assert!(sigs[0].catch);
720    }
721
722    #[test]
723    fn test_optional_expansion() {
724        let mut used = no_used();
725        let sigs = expand(
726            "Console",
727            &[
728                param("stdout"),
729                opt_param("stderr"),
730                opt_param("ignoreErrors"),
731            ],
732            &TypeRef::Named("Console".into()),
733            SignatureKind::Constructor,
734            &None,
735            &mut used,
736        );
737        // 3 constructor signatures (no try_ variants)
738        assert_eq!(sigs.len(), 3);
739        assert_eq!(sigs[0].rust_name, "new");
740        assert_eq!(sigs[0].params.len(), 1);
741        assert_eq!(sigs[1].rust_name, "new_with_stderr");
742        assert_eq!(sigs[1].params.len(), 2);
743        assert_eq!(sigs[2].rust_name, "new_with_stderr_and_ignore_errors");
744        assert_eq!(sigs[2].params.len(), 3);
745    }
746
747    #[test]
748    fn test_optional_method_expansion() {
749        let mut used = no_used();
750        let sigs = expand(
751            "count",
752            &[opt_param("label")],
753            &TypeRef::Void,
754            SignatureKind::Method,
755            &None,
756            &mut used,
757        );
758        // 2 expansions × 2 (normal + try_) = 4
759        assert_eq!(sigs.len(), 4);
760        assert_eq!(sigs[0].rust_name, "count");
761        assert_eq!(sigs[0].params.len(), 0);
762        assert!(!sigs[0].catch);
763        assert_eq!(sigs[1].rust_name, "try_count");
764        assert!(sigs[1].catch);
765        assert_eq!(sigs[2].rust_name, "count_with_label");
766        assert_eq!(sigs[2].params.len(), 1);
767        assert_eq!(sigs[3].rust_name, "try_count_with_label");
768    }
769
770    #[test]
771    fn test_variadic_param() {
772        let mut used = no_used();
773        let sigs = expand(
774            "log",
775            &[variadic_param("data")],
776            &TypeRef::Void,
777            SignatureKind::Method,
778            &None,
779            &mut used,
780        );
781        // Variadic is always present — 1 signature × 2 (normal + try_) = 2
782        assert_eq!(sigs.len(), 2);
783        assert_eq!(sigs[0].rust_name, "log");
784        assert_eq!(sigs[0].params.len(), 1);
785        assert!(sigs[0].params[0].variadic);
786        assert_eq!(sigs[1].rust_name, "try_log");
787    }
788
789    #[test]
790    fn test_optional_then_variadic() {
791        let mut used = no_used();
792        let sigs = expand(
793            "timeLog",
794            &[opt_param("label"), variadic_param("data")],
795            &TypeRef::Void,
796            SignatureKind::Method,
797            &None,
798            &mut used,
799        );
800        // Variadic always present. Optional label creates 2 truncation points.
801        // 2 expansions × 2 (normal + try_) = 4
802        assert_eq!(sigs.len(), 4);
803        assert_eq!(sigs[0].rust_name, "time_log");
804        assert_eq!(sigs[0].params.len(), 1); // just variadic data
805        assert!(sigs[0].params[0].variadic);
806        assert_eq!(sigs[1].rust_name, "try_time_log");
807        // Variadic params participate in naming — data is present in both sigs,
808        // but label differs, so suffix uses the label param name.
809        assert_eq!(sigs[2].rust_name, "time_log_with_label");
810        assert_eq!(sigs[2].params.len(), 2); // label + variadic data
811        assert!(!sigs[2].params[0].variadic);
812        assert!(sigs[2].params[1].variadic);
813        assert_eq!(sigs[3].rust_name, "try_time_log_with_label");
814    }
815
816    #[test]
817    fn test_doc_only_on_first() {
818        let doc = Some("Hello".to_string());
819        let mut used = no_used();
820        let sigs = expand(
821            "count",
822            &[opt_param("label")],
823            &TypeRef::Void,
824            SignatureKind::Method,
825            &doc,
826            &mut used,
827        );
828        assert_eq!(sigs[0].doc, Some("Hello".to_string()));
829        assert_eq!(sigs[1].doc, None); // try_count
830        assert_eq!(sigs[2].doc, None); // count_with_label
831        assert_eq!(sigs[3].doc, None); // try_count_with_label
832    }
833
834    #[test]
835    fn test_try_collision_deduped() {
836        // If "try_count" is already taken, the try_ variant gets a numeric suffix.
837        let mut used: HashSet<String> = ["try_count".to_string()].into_iter().collect();
838        let sigs = expand(
839            "count",
840            &[param("x")],
841            &TypeRef::Void,
842            SignatureKind::Method,
843            &None,
844            &mut used,
845        );
846        assert_eq!(sigs.len(), 2);
847        assert_eq!(sigs[0].rust_name, "count");
848        assert!(!sigs[0].catch);
849        assert_eq!(sigs[1].rust_name, "try_count_1");
850        assert!(sigs[1].catch);
851    }
852
853    #[test]
854    fn test_name_collision_deduped() {
855        // Two separate expand calls with the same JS name — second gets numeric suffix.
856        let mut used = no_used();
857        let sigs1 = expand(
858            "foo",
859            &[param("a")],
860            &TypeRef::Void,
861            SignatureKind::Method,
862            &None,
863            &mut used,
864        );
865        let sigs2 = expand(
866            "foo",
867            &[param("a"), param("b")],
868            &TypeRef::Void,
869            SignatureKind::Method,
870            &None,
871            &mut used,
872        );
873        assert_eq!(sigs1[0].rust_name, "foo");
874        assert_eq!(sigs2[0].rust_name, "foo_1");
875    }
876
877    #[test]
878    fn test_overloads_with_variadic() {
879        // setTimeout pattern:
880        //   overload 1: (callback: Function, msDelay?: number)
881        //   overload 2: (callback: Function, msDelay?: number, ...args: any[])
882        let mut used = no_used();
883        let overload1 = [
884            typed_param("callback", TypeRef::Any),
885            opt_typed_param("msDelay", TypeRef::Number),
886        ];
887        let overload2 = [
888            typed_param("callback", TypeRef::Any),
889            opt_typed_param("msDelay", TypeRef::Number),
890            variadic_param("args"),
891        ];
892        let sigs = expand_overloads(
893            "setTimeout",
894            &[&overload1, &overload2],
895            &TypeRef::Number,
896            SignatureKind::Method,
897            &None,
898            &mut used,
899        );
900
901        // Expected (non-try_ only):
902        //   set_timeout(callback)                        — from overload 1 truncation
903        //   set_timeout_with_ms_delay(callback, msDelay) — from overload 1 full
904        //   set_timeout_with_args(callback, args)        — from overload 2 truncation + variadic
905        //   set_timeout_with_ms_delay_and_args(callback, msDelay, args) — from overload 2 full
906        // Note: overload 2's truncated (callback) is deduped against overload 1's.
907        let non_try: Vec<_> = sigs.iter().filter(|s| !s.catch).collect();
908        assert_eq!(non_try.len(), 4);
909        assert_eq!(non_try[0].rust_name, "set_timeout");
910        assert_eq!(non_try[0].params.len(), 1);
911        assert_eq!(non_try[1].rust_name, "set_timeout_with_ms_delay");
912        assert_eq!(non_try[1].params.len(), 2);
913        assert_eq!(non_try[2].rust_name, "set_timeout_with_args");
914        assert_eq!(non_try[2].params.len(), 2);
915        assert!(non_try[2].params[1].variadic);
916        assert_eq!(non_try[3].rust_name, "set_timeout_with_ms_delay_and_args");
917        assert_eq!(non_try[3].params.len(), 3);
918        assert!(non_try[3].params[2].variadic);
919    }
920
921    #[test]
922    fn test_overloads_with_different_types() {
923        // foo(x: string) and foo(x: Promise<string>) should expand as
924        // foo_with_str and foo_with_promise
925        let mut used = no_used();
926        let overload1 = [typed_param("x", TypeRef::String)];
927        let overload2 = [typed_param(
928            "x",
929            TypeRef::Promise(Box::new(TypeRef::String)),
930        )];
931        let sigs = expand_overloads(
932            "foo",
933            &[&overload1, &overload2],
934            &TypeRef::Void,
935            SignatureKind::Method,
936            &None,
937            &mut used,
938        );
939
940        let non_try: Vec<_> = sigs.iter().filter(|s| !s.catch).collect();
941        assert_eq!(non_try.len(), 2);
942        // First overload gets base name, second gets type suffix
943        assert_eq!(non_try[0].rust_name, "foo");
944        assert_eq!(non_try[1].rust_name, "foo_with_promise");
945    }
946
947    #[test]
948    fn test_overloads_shared_truncation_deduped() {
949        // Two overloads that share a truncation: both truncate to (a)
950        //   overload 1: (a: any, b?: any)
951        //   overload 2: (a: any, c?: any)
952        let mut used = no_used();
953        let overload1 = [param("a"), opt_param("b")];
954        let overload2 = [param("a"), opt_param("c")];
955        let sigs = expand_overloads(
956            "foo",
957            &[&overload1, &overload2],
958            &TypeRef::Void,
959            SignatureKind::Method,
960            &None,
961            &mut used,
962        );
963
964        // Expected: foo(a), foo_with_b(a, b), foo_with_c(a, c)
965        // The two (a) truncations are deduped.
966        let non_try: Vec<_> = sigs.iter().filter(|s| !s.catch).collect();
967        assert_eq!(non_try.len(), 3);
968        assert_eq!(non_try[0].rust_name, "foo");
969        assert_eq!(non_try[0].params.len(), 1);
970        assert_eq!(non_try[1].rust_name, "foo_with_b");
971        assert_eq!(non_try[1].params.len(), 2);
972        assert_eq!(non_try[2].rust_name, "foo_with_c");
973        assert_eq!(non_try[2].params.len(), 2);
974    }
975}