Skip to main content

alef_backend_napi/
gen_bindings.rs

1use crate::type_map::NapiMapper;
2use ahash::AHashSet;
3use alef_codegen::builder::{ImplBuilder, RustFileBuilder, StructBuilder};
4use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
5use alef_codegen::naming::to_node_name;
6use alef_codegen::shared::{can_auto_delegate, function_params, partition_methods};
7use alef_codegen::type_mapper::TypeMapper;
8use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
9use alef_core::config::{AlefConfig, Language, resolve_output_dir};
10use alef_core::hash::{self, CommentStyle};
11use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, MethodDef, ParamDef, TypeDef, TypeRef};
12use std::path::PathBuf;
13
14pub struct NapiBackend;
15
16impl NapiBackend {
17    fn binding_config<'a>(core_import: &'a str, prefix: &'a str, has_serde: bool) -> RustBindingConfig<'a> {
18        RustBindingConfig {
19            struct_attrs: &["napi"],
20            field_attrs: &[],
21            struct_derives: &["Clone"],
22            method_block_attr: Some("napi"),
23            constructor_attr: "#[napi(constructor)]",
24            static_attr: None,
25            function_attr: "#[napi]",
26            enum_attrs: &["napi(string_enum)"],
27            enum_derives: &["Clone"],
28            needs_signature: false,
29            signature_prefix: "",
30            signature_suffix: "",
31            core_import,
32            async_pattern: AsyncPattern::NapiNativeAsync,
33            has_serde,
34            // NAPI napi(object) structs don't derive Serialize — disable serde bridge
35            type_name_prefix: prefix,
36            option_duration_on_defaults: true,
37            opaque_type_names: &[],
38        }
39    }
40}
41
42impl Backend for NapiBackend {
43    fn name(&self) -> &str {
44        "napi"
45    }
46
47    fn language(&self) -> Language {
48        Language::Node
49    }
50
51    fn capabilities(&self) -> Capabilities {
52        Capabilities {
53            supports_async: true,
54            supports_classes: true,
55            supports_enums: true,
56            supports_option: true,
57            supports_result: true,
58            ..Capabilities::default()
59        }
60    }
61
62    fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
63        let prefix = config.node_type_prefix();
64        let mapper = NapiMapper::new(prefix.clone());
65        let core_import = config.core_import();
66
67        // Detect serde availability from the output crate's Cargo.toml
68        let output_dir = resolve_output_dir(
69            config.output.node.as_ref(),
70            &config.crate_config.name,
71            "crates/{name}-node/src/",
72        );
73        let has_serde = alef_core::config::detect_serde_available(&output_dir);
74        let cfg = Self::binding_config(&core_import, &prefix, has_serde);
75
76        let mut builder = RustFileBuilder::new().with_generated_header();
77        builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
78        builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions)");
79        builder.add_import("napi::*");
80        builder.add_import("napi_derive::napi");
81
82        // Always import serde_json for type conversion in From/Into impls,
83        // even if the binding crate doesn't explicitly list it as a dependency.
84        // serde_json is needed for conversions of types with serde-serializable fields.
85        builder.add_import("serde_json");
86
87        // Import traits needed for trait method dispatch
88        for trait_path in generators::collect_trait_imports(api) {
89            builder.add_import(&trait_path);
90        }
91
92        // Only import HashMap when Map-typed fields or returns are present
93        let has_maps = api
94            .types
95            .iter()
96            .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
97            || api
98                .functions
99                .iter()
100                .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
101        if has_maps {
102            builder.add_import("std::collections::HashMap");
103        }
104
105        // Note: custom_modules for Node are TypeScript-only re-exports
106        // (used in generate_public_api), not Rust module declarations.
107
108        // Check if any function or method is async
109        let has_async =
110            api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
111
112        if has_async {
113            builder.add_item(&gen_tokio_runtime());
114        }
115
116        // Check if we have opaque types and add Arc import if needed
117        let opaque_types: AHashSet<String> = api
118            .types
119            .iter()
120            .filter(|t| t.is_opaque)
121            .map(|t| t.name.clone())
122            .collect();
123        if !opaque_types.is_empty() {
124            builder.add_import("std::sync::Arc");
125        }
126
127        let exclude_types: ahash::AHashSet<String> = config
128            .node
129            .as_ref()
130            .map(|c| c.exclude_types.iter().cloned().collect())
131            .unwrap_or_default();
132
133        // Build adapter body map before type iteration so bodies are available for method generation.
134        let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
135
136        // Emit adapter-generated standalone items (streaming iterators, callback bridges).
137        for adapter in &config.adapters {
138            match adapter.pattern {
139                alef_core::config::AdapterPattern::Streaming => {
140                    let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
141                    if let Some(struct_code) = adapter_bodies.get(&key) {
142                        builder.add_item(struct_code);
143                    }
144                }
145                alef_core::config::AdapterPattern::CallbackBridge => {
146                    let struct_key = format!("{}.__bridge_struct__", adapter.name);
147                    let impl_key = format!("{}.__bridge_impl__", adapter.name);
148                    if let Some(struct_code) = adapter_bodies.get(&struct_key) {
149                        builder.add_item(struct_code);
150                    }
151                    if let Some(impl_code) = adapter_bodies.get(&impl_key) {
152                        builder.add_item(impl_code);
153                    }
154                }
155                _ => {}
156            }
157        }
158
159        // NAPI has some unique patterns: Js-prefixed names, Option-wrapped fields,
160        // and custom constructor. Use shared generators for enums and functions,
161        // but keep struct/method generation custom.
162        for typ in api
163            .types
164            .iter()
165            .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
166        {
167            if typ.is_opaque {
168                builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(
169                    typ, &cfg, &prefix,
170                ));
171                builder.add_item(&gen_opaque_struct_methods(
172                    typ,
173                    &mapper,
174                    &cfg,
175                    &opaque_types,
176                    &prefix,
177                    &adapter_bodies,
178                ));
179            } else {
180                // Non-opaque structs use #[napi(object)] — plain JS objects without methods.
181                // napi(object) structs cannot have #[napi] impl blocks.
182                // gen_struct adds Default to derives when typ.has_default is true.
183                builder.add_item(&gen_struct(typ, &mapper, &prefix, has_serde));
184            }
185        }
186
187        // Collect struct names so tagged enum codegen knows which Named types have binding structs
188        let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
189
190        for enum_def in &api.enums {
191            builder.add_item(&gen_enum(enum_def, &prefix, has_serde));
192        }
193
194        let exclude_functions: ahash::AHashSet<String> = config
195            .node
196            .as_ref()
197            .map(|c| c.exclude_functions.iter().cloned().collect())
198            .unwrap_or_default();
199
200        for func in &api.functions {
201            if exclude_functions.contains(&func.name) {
202                continue;
203            }
204            let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
205            // Skip sanitized functions when there's no trait bridge that can replace the
206            // sanitized parameter — such functions cannot be auto-delegated. Functions
207            // whose only "sanitized" param is a configured trait_bridge param (e.g.
208            // Option<VisitorHandle> in html-to-markdown) are emitted via gen_bridge_function.
209            if func.sanitized && bridge_param.is_none() {
210                continue;
211            }
212            if let Some((param_idx, bridge_cfg)) = bridge_param {
213                builder.add_item(&crate::trait_bridge::gen_bridge_function(
214                    func,
215                    param_idx,
216                    bridge_cfg,
217                    &mapper,
218                    &cfg,
219                    &Default::default(),
220                    &opaque_types,
221                    &core_import,
222                ));
223            } else {
224                builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types, &prefix));
225            }
226        }
227
228        // Trait bridge wrappers — generate NAPI bridge structs that delegate to JS objects
229        for bridge_cfg in &config.trait_bridges {
230            if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
231                let bridge = crate::trait_bridge::gen_trait_bridge(
232                    trait_type,
233                    bridge_cfg,
234                    &core_import,
235                    &config.error_type(),
236                    &config.error_constructor(),
237                    api,
238                );
239                for imp in &bridge.imports {
240                    builder.add_import(imp);
241                }
242                builder.add_item(&bridge.code);
243            }
244        }
245
246        let binding_to_core = alef_codegen::conversions::convertible_types(api);
247        let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
248        let input_types = alef_codegen::conversions::input_type_names(api);
249        let napi_conv_config = alef_codegen::conversions::ConversionConfig {
250            type_name_prefix: &prefix,
251            cast_large_ints_to_i64: true,
252            cast_f32_to_f64: true,
253            // optionalize_defaults: For types with has_default, conversion generators
254            // make all fields Option<T> and apply defaults via FromNapiValue,
255            // enabling JS users to pass partial objects and omit fields they want defaults for.
256            optionalize_defaults: true,
257            option_duration_on_defaults: true,
258            include_cfg_metadata: true,
259            ..Default::default()
260        };
261        // From/Into conversions using shared parameterized generators
262        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
263            if input_types.contains(&typ.name)
264                && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
265            {
266                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
267                    typ,
268                    &core_import,
269                    &napi_conv_config,
270                ));
271            }
272            if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
273                builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
274                    typ,
275                    &core_import,
276                    &opaque_types,
277                    &napi_conv_config,
278                ));
279            }
280        }
281        for e in &api.enums {
282            let is_tagged_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
283            if is_tagged_data_enum {
284                // Tagged data enums use flattened struct — generate custom conversions
285                builder.add_item(&gen_tagged_enum_binding_to_core(
286                    e,
287                    &core_import,
288                    &prefix,
289                    &struct_names,
290                ));
291                builder.add_item(&gen_tagged_enum_core_to_binding(
292                    e,
293                    &core_import,
294                    &prefix,
295                    &struct_names,
296                ));
297            } else {
298                if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
299                    builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
300                        e,
301                        &core_import,
302                        &napi_conv_config,
303                    ));
304                }
305                if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
306                    builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
307                        e,
308                        &core_import,
309                        &napi_conv_config,
310                    ));
311                }
312            }
313        }
314
315        // Error types (variant name constants + converter functions)
316        for error in &api.errors {
317            builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
318            builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
319        }
320
321        let content = builder.build();
322
323        let output_dir = resolve_output_dir(
324            config.output.node.as_ref(),
325            &config.crate_config.name,
326            "crates/{name}-node/src/",
327        );
328
329        Ok(vec![GeneratedFile {
330            path: PathBuf::from(&output_dir).join("lib.rs"),
331            content,
332            generated_header: false,
333        }])
334    }
335
336    fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
337        let prefix = config.node_type_prefix();
338
339        // Separate exports into functions (plain export) and types (export type)
340        let mut type_exports = vec![];
341        let mut function_exports = vec![];
342
343        // Collect all types (exported with prefix from native module) - export type
344        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
345            type_exports.push(format!("{prefix}{}", typ.name));
346        }
347
348        // Collect all enums as type exports.
349        // With verbatimModuleSyntax enabled, re-exporting const enums as values causes
350        // TS2748/TS1205; using `export type` avoids both errors.
351        for enum_def in &api.enums {
352            type_exports.push(format!("{prefix}{}", enum_def.name));
353        }
354
355        // NAPI errors are thrown as native JS Error objects, not exported as TS types.
356        // Skip error types in the public API re-exports.
357
358        // Collect all functions (exported from native module) - plain export
359        for func in &api.functions {
360            // Convert snake_case to camelCase for JavaScript naming
361            let js_name = to_node_name(&func.name);
362            function_exports.push(js_name);
363        }
364
365        // Sort for consistent output
366        type_exports.sort();
367        function_exports.sort();
368
369        // Generate the index.ts re-export file using a single export block
370        // with inline `type` annotations for verbatimModuleSyntax compatibility.
371        let mut lines = vec![
372            "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
373            "".to_string(),
374        ];
375
376        // Separate value and type exports for verbatimModuleSyntax compatibility.
377        // Value exports (functions) in one block, type exports (structs + enums) in another.
378        if !function_exports.is_empty() {
379            lines.push("export {".to_string());
380            for name in &function_exports {
381                lines.push(format!("  {name},"));
382            }
383            lines.push(format!("}} from '{}';", config.node_package_name()));
384            lines.push("".to_string());
385        }
386        if !type_exports.is_empty() {
387            lines.push("export type {".to_string());
388            for name in &type_exports {
389                lines.push(format!("  {name},"));
390            }
391            lines.push(format!("}} from '{}';", config.node_package_name()));
392        }
393
394        // Append re-exports for custom modules (from [custom_modules] node = [...])
395        let custom_mods = config.custom_modules.for_language(Language::Node);
396        for module_name in custom_mods {
397            lines.push(format!("export * from './{module_name}';"));
398        }
399
400        let content = lines.join("\n");
401
402        // Output path: packages/typescript/src/index.ts
403        let output_path = PathBuf::from("packages/typescript/src/index.ts");
404
405        Ok(vec![GeneratedFile {
406            path: output_path,
407            content,
408            generated_header: false,
409        }])
410    }
411
412    fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
413        let prefix = config.node_type_prefix();
414        let content = gen_dts(api, &prefix);
415
416        // `config.output.node` points to the `src/` directory (e.g., `crates/{name}-node/src/`).
417        // `index.d.ts` belongs at the crate root, one level up from `src/`.
418        // When the configured path ends in `src/` or `src`, strip that suffix to get the crate root.
419        // Falls back to `crates/{name}-node/` if no node output is configured.
420        let src_dir = resolve_output_dir(
421            config.output.node.as_ref(),
422            &config.crate_config.name,
423            "crates/{name}-node/src/",
424        );
425        let crate_root = {
426            let p = PathBuf::from(&src_dir);
427            match p.file_name().and_then(|n| n.to_str()) {
428                Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
429                _ => p,
430            }
431        };
432
433        Ok(vec![GeneratedFile {
434            path: crate_root.join("index.d.ts"),
435            content,
436            generated_header: false,
437        }])
438    }
439
440    fn build_config(&self) -> Option<BuildConfig> {
441        Some(BuildConfig {
442            tool: "napi",
443            crate_suffix: "-node",
444            build_dep: BuildDependency::None,
445            post_build: vec![PostBuildStep::PatchFile {
446                path: "index.d.ts",
447                find: "export declare const enum",
448                replace: "export declare enum",
449            }],
450        })
451    }
452}
453
454/// Generate a NAPI struct with Js-prefixed name and fields wrapped in Option only if optional.
455fn gen_struct(typ: &TypeDef, mapper: &NapiMapper, prefix: &str, has_serde: bool) -> String {
456    let mut struct_builder = StructBuilder::new(&format!("{prefix}{}", typ.name));
457    // Use napi(object) so the struct can be used as function/method parameters (FromNapiValue)
458    struct_builder.add_attr("napi(object)");
459    struct_builder.add_derive("Clone");
460    // Binding types always derive Default, Serialize, and Deserialize.
461    // Default: enables using unwrap_or_default() in constructors for types with has_default.
462    // Serialize/Deserialize: required for FFI/type conversion across binding boundaries.
463    struct_builder.add_derive("Default");
464    // Only derive serde traits when the binding crate has serde as a dependency.
465    // Generating these derives unconditionally causes compile errors in crates
466    // that don't list serde in their Cargo.toml.
467    if has_serde {
468        struct_builder.add_derive("serde::Serialize");
469        struct_builder.add_derive("serde::Deserialize");
470    }
471
472    for field in &typ.fields {
473        let mapped_type = mapper.map_type(&field.ty);
474        // For types with Default, make all fields optional so JS callers
475        // can pass partial objects (missing fields get defaults).
476        // When field.ty is already Optional(T), mapped_type is already Option<T> — don't double-wrap.
477        let field_type = if (field.optional || typ.has_default) && !matches!(field.ty, TypeRef::Optional(_)) {
478            format!("Option<{}>", mapped_type)
479        } else {
480            mapped_type
481        };
482        let js_name = to_node_name(&field.name);
483        let attrs = if js_name != field.name {
484            vec![format!("napi(js_name = \"{}\")", js_name)]
485        } else {
486            vec![]
487        };
488        struct_builder.add_field(&field.name, &field_type, attrs);
489    }
490
491    struct_builder.build()
492}
493
494/// Generate NAPI methods for an opaque struct (delegates to self.inner).
495fn gen_opaque_struct_methods(
496    typ: &TypeDef,
497    mapper: &NapiMapper,
498    cfg: &RustBindingConfig,
499    opaque_types: &AHashSet<String>,
500    prefix: &str,
501    adapter_bodies: &alef_adapters::AdapterBodies,
502) -> String {
503    let mut impl_builder = ImplBuilder::new(&format!("{prefix}{}", typ.name));
504    impl_builder.add_attr("napi");
505
506    let (instance, statics) = partition_methods(&typ.methods);
507
508    for method in &instance {
509        // Skip sanitized methods that have no adapter override — they cannot be delegated
510        // and emitting an unimplemented stub pollutes the public API with dead placeholders.
511        let adapter_key = format!("{}.{}", typ.name, method.name);
512        if method.sanitized && !adapter_bodies.contains_key(&adapter_key) {
513            continue;
514        }
515        impl_builder.add_method(&gen_opaque_instance_method(
516            method,
517            mapper,
518            typ,
519            cfg,
520            opaque_types,
521            prefix,
522            adapter_bodies,
523        ));
524    }
525    for method in &statics {
526        // Skip sanitized static methods that have no adapter override.
527        let adapter_key = format!("{}.{}", typ.name, method.name);
528        if method.sanitized && !adapter_bodies.contains_key(&adapter_key) {
529            continue;
530        }
531        impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types, prefix));
532    }
533
534    impl_builder.build()
535}
536
537/// Generate an opaque instance method that delegates to self.inner.
538fn gen_opaque_instance_method(
539    method: &MethodDef,
540    mapper: &NapiMapper,
541    typ: &TypeDef,
542    cfg: &RustBindingConfig,
543    opaque_types: &AHashSet<String>,
544    prefix: &str,
545    adapter_bodies: &alef_adapters::AdapterBodies,
546) -> String {
547    let params = function_params(&method.params, &|ty| mapper.map_type(ty));
548    let return_type = mapper.map_type(&method.return_type);
549    let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
550
551    let js_name = to_node_name(&method.name);
552    let js_name_attr = if js_name != method.name {
553        format!("(js_name = \"{}\")", js_name)
554    } else {
555        String::new()
556    };
557
558    let async_kw = if method.is_async { "async " } else { "" };
559
560    let type_name = &typ.name;
561    let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
562    let is_ref_mut_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
563    let call_args = napi_gen_call_args(&method.params, opaque_types);
564
565    // Use the shared can_auto_delegate check for opaque instance methods.
566    // Skip delegation if the receiver is RefMut, since Arc<T> doesn't support &mut T.
567    let opaque_can_delegate = !method.sanitized
568        && !is_ref_mut_receiver
569        && (!is_owned_receiver || typ.is_clone)
570        && method
571            .params
572            .iter()
573            .all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
574        && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
575
576    let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
577
578    let async_result_wrap = napi_wrap_return(
579        "result",
580        &method.return_type,
581        type_name,
582        opaque_types,
583        true,
584        method.returns_ref,
585        prefix,
586    );
587
588    let adapter_key = format!("{type_name}.{}", method.name);
589    let body = if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
590        adapter_body.clone()
591    } else if !opaque_can_delegate {
592        // Try serde-based param conversion for methods with non-opaque Named params
593        if cfg.has_serde
594            && !method.sanitized
595            && generators::has_named_params(&method.params, opaque_types)
596            && method.error_type.is_some()
597            && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
598        {
599            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
600            let serde_bindings =
601                generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, "        ");
602            let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
603            let core_call = format!("self.inner.{}({serde_call_args})", method.name);
604            if matches!(method.return_type, TypeRef::Unit) {
605                format!("{serde_bindings}{core_call}{err_conv}?;\n    Ok(())")
606            } else {
607                let wrap = napi_wrap_return(
608                    "result",
609                    &method.return_type,
610                    type_name,
611                    opaque_types,
612                    true,
613                    method.returns_ref,
614                    prefix,
615                );
616                format!("{serde_bindings}let result = {core_call}{err_conv}?;\n    Ok({wrap})")
617            }
618        } else {
619            generators::gen_unimplemented_body(
620                &method.return_type,
621                &format!("{type_name}.{}", method.name),
622                method.error_type.is_some(),
623                cfg,
624                &method.params,
625                opaque_types,
626            )
627        }
628    } else if method.is_async {
629        let inner_clone_line = "let inner = self.inner.clone();\n    ";
630        let core_call_str = make_async_core_call(&method.name);
631        generators::gen_async_body(
632            &core_call_str,
633            cfg,
634            method.error_type.is_some(),
635            &async_result_wrap,
636            true,
637            inner_clone_line,
638            matches!(method.return_type, TypeRef::Unit),
639            Some(&return_type),
640        )
641    } else {
642        // When any non-opaque Named param has is_ref=true, generate let-bindings before the call
643        // to avoid E0716 ("temporary value dropped while borrowed"). The inline `.into()` pattern
644        // creates a temporary that Rust can't borrow for the duration of the call expression.
645        let use_let_bindings = generators::has_named_params(&method.params, opaque_types);
646        let (let_bindings, call_args_for_call) = if use_let_bindings {
647            let bindings = generators::gen_named_let_bindings_pub(&method.params, opaque_types, cfg.core_import);
648            let args = napi_apply_primitive_casts_to_call_args(
649                &generators::gen_call_args_with_let_bindings(&method.params, opaque_types),
650                &method.params,
651            );
652            (bindings, args)
653        } else {
654            (String::new(), napi_gen_call_args(&method.params, opaque_types))
655        };
656        let core_call = if is_owned_receiver {
657            format!("(*self.inner).clone().{}({})", method.name, call_args_for_call)
658        } else {
659            format!("self.inner.{}({})", method.name, call_args_for_call)
660        };
661        if method.error_type.is_some() {
662            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
663            if matches!(method.return_type, TypeRef::Unit) {
664                format!("{let_bindings}{core_call}{err_conv}?;\n    Ok(())")
665            } else {
666                let wrap = napi_wrap_return(
667                    "result",
668                    &method.return_type,
669                    type_name,
670                    opaque_types,
671                    true,
672                    method.returns_ref,
673                    prefix,
674                );
675                format!("{let_bindings}let result = {core_call}{err_conv}?;\n    Ok({wrap})")
676            }
677        } else {
678            format!(
679                "{let_bindings}{}",
680                napi_wrap_return(
681                    &core_call,
682                    &method.return_type,
683                    type_name,
684                    opaque_types,
685                    true,
686                    method.returns_ref,
687                    prefix,
688                )
689            )
690        }
691    };
692
693    let mut attrs = String::new();
694    // Per-item clippy suppression: too_many_arguments when >7 params (including &self)
695    if method.params.len() + 1 > 7 {
696        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
697    }
698    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
699    if method.error_type.is_some() {
700        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
701    }
702    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
703    if generators::is_trait_method_name(&method.name) {
704        attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
705    }
706    format!(
707        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n    \
708         {body}\n}}",
709        method.name
710    )
711}
712
713/// Generate a static method binding.
714fn gen_static_method(
715    method: &MethodDef,
716    mapper: &NapiMapper,
717    typ: &TypeDef,
718    cfg: &RustBindingConfig,
719    opaque_types: &AHashSet<String>,
720    prefix: &str,
721) -> String {
722    let params = function_params(&method.params, &|ty| mapper.map_type(ty));
723    let return_type = mapper.map_type(&method.return_type);
724    let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
725
726    let js_name = to_node_name(&method.name);
727    let js_name_attr = if js_name != method.name {
728        format!("(js_name = \"{}\")", js_name)
729    } else {
730        String::new()
731    };
732
733    let type_name = &typ.name;
734    let core_type_path = typ.rust_path.replace('-', "_");
735    let call_args = napi_gen_call_args(&method.params, opaque_types);
736    let can_delegate_static = can_auto_delegate(method, opaque_types);
737
738    let async_kw = if method.is_async { "async " } else { "" };
739
740    let body = if !can_delegate_static {
741        generators::gen_unimplemented_body(
742            &method.return_type,
743            &format!("{type_name}::{}", method.name),
744            method.error_type.is_some(),
745            cfg,
746            &method.params,
747            opaque_types,
748        )
749    } else if method.is_async {
750        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
751        let return_wrap = napi_wrap_return(
752            "result",
753            &method.return_type,
754            type_name,
755            opaque_types,
756            typ.is_opaque,
757            method.returns_ref,
758            prefix,
759        );
760        generators::gen_async_body(
761            &core_call,
762            cfg,
763            method.error_type.is_some(),
764            &return_wrap,
765            false,
766            "",
767            matches!(method.return_type, TypeRef::Unit),
768            Some(&return_type),
769        )
770    } else {
771        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
772        if method.error_type.is_some() {
773            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
774            let wrapped = napi_wrap_return(
775                "val",
776                &method.return_type,
777                type_name,
778                opaque_types,
779                typ.is_opaque,
780                method.returns_ref,
781                prefix,
782            );
783            if wrapped == "val" {
784                format!("{core_call}{err_conv}")
785            } else {
786                format!("{core_call}.map(|val| {wrapped}){err_conv}")
787            }
788        } else {
789            napi_wrap_return(
790                &core_call,
791                &method.return_type,
792                type_name,
793                opaque_types,
794                typ.is_opaque,
795                method.returns_ref,
796                prefix,
797            )
798        }
799    };
800
801    let mut attrs = String::new();
802    // Per-item clippy suppression: too_many_arguments when >7 params
803    if method.params.len() > 7 {
804        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
805    }
806    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
807    if method.error_type.is_some() {
808        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
809    }
810    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
811    if generators::is_trait_method_name(&method.name) {
812        attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
813    }
814    format!(
815        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n    \
816         {body}\n}}",
817        method.name
818    )
819}
820
821/// Generate a NAPI enum definition using string_enum with Js prefix.
822/// Generate a NAPI enum definition.
823/// For simple enums (no variant fields): generates `#[napi(string_enum)]`.
824/// For tagged enums with data fields: generates a flattened `#[napi(object)]` struct
825/// with a discriminant field and all variant fields as optional.
826fn gen_enum(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
827    let is_tagged_data_enum = enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty());
828
829    if is_tagged_data_enum {
830        return gen_tagged_enum_as_object(enum_def, prefix, has_serde);
831    }
832
833    // Simple string enum
834    let napi_case = enum_def.serde_rename_all.as_deref().and_then(|s| match s {
835        "snake_case" => Some("snake_case"),
836        "camelCase" => Some("camelCase"),
837        "kebab-case" => Some("kebab-case"),
838        "SCREAMING_SNAKE_CASE" => Some("UPPER_SNAKE"),
839        "lowercase" => Some("lowercase"),
840        "UPPERCASE" => Some("UPPERCASE"),
841        "PascalCase" => Some("PascalCase"),
842        _ => None,
843    });
844
845    let string_enum_attr = match napi_case {
846        Some(case) => format!("#[napi(string_enum = \"{case}\")]"),
847        None => "#[napi(string_enum)]".to_string(),
848    };
849
850    let derives = if has_serde {
851        "#[derive(Clone, serde::Serialize, serde::Deserialize)]".to_string()
852    } else {
853        "#[derive(Clone)]".to_string()
854    };
855    let mut lines = vec![
856        string_enum_attr,
857        derives,
858        format!("pub enum {prefix}{} {{", enum_def.name),
859    ];
860
861    for variant in &enum_def.variants {
862        lines.push(format!("    {},", variant.name));
863    }
864
865    lines.push("}".to_string());
866
867    // Default impl for config constructor unwrap_or_default()
868    if let Some(first) = enum_def.variants.first() {
869        lines.push(String::new());
870        lines.push("#[allow(clippy::derivable_impls)]".to_string());
871        lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
872        lines.push(format!("    fn default() -> Self {{ Self::{} }}", first.name));
873        lines.push("}".to_string());
874    }
875
876    lines.join("\n")
877}
878
879/// Generate a tagged enum as a flattened `#[napi(object)]` struct.
880/// E.g. `AuthConfig { Basic { username, password }, Bearer { token } }` becomes:
881/// ```rust,ignore
882/// #[napi(object)]
883/// struct JsAuthConfig {
884///     #[napi(js_name = "type")]
885///     pub auth_type: String,
886///     pub username: Option<String>,
887///     pub password: Option<String>,
888///     pub token: Option<String>,
889/// }
890/// ```
891fn gen_tagged_enum_as_object(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
892    use alef_codegen::type_mapper::TypeMapper;
893    let mapper = NapiMapper::new(prefix.to_string());
894
895    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
896
897    let derive = if has_serde {
898        "#[derive(Clone, serde::Serialize, serde::Deserialize)]"
899    } else {
900        "#[derive(Clone)]"
901    };
902    let mut lines = vec![
903        derive.to_string(),
904        "#[napi(object)]".to_string(),
905        format!("pub struct {prefix}{} {{", enum_def.name),
906        format!("    #[napi(js_name = \"{tag_field}\")]"),
907        format!("    pub {tag_field}_tag: String,"),
908    ];
909
910    // Fields that appear in multiple variants with different Named types cannot be represented
911    // as a single concrete JsXxx type. Store them as String (JSON) instead, and convert
912    // per-variant via serde_json in the From impls.
913    let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
914
915    // Collect all unique fields across all variants (all made optional)
916    let mut seen_fields: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
917    for variant in &enum_def.variants {
918        for field in &variant.fields {
919            if seen_fields.insert(field.name.clone()) {
920                // Sanitized fields and mixed-type Named fields are represented as String
921                // and converted via serde_json in From/Into impls
922                let field_type = if (field.sanitized || mixed_named_fields.contains(&field.name))
923                    && matches!(&field.ty, TypeRef::Named(_))
924                {
925                    "String".to_string()
926                } else {
927                    mapper.map_type(&field.ty).to_string()
928                };
929                let js_name = alef_codegen::naming::to_node_name(&field.name);
930                if js_name != field.name {
931                    lines.push(format!("    #[napi(js_name = \"{js_name}\")]"));
932                }
933                lines.push(format!("    pub {}: Option<{field_type}>,", field.name));
934            }
935        }
936    }
937
938    lines.push("}".to_string());
939
940    // Default impl
941    lines.push(String::new());
942    lines.push("#[allow(clippy::derivable_impls)]".to_string());
943    lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
944    lines.push(format!(
945        "    fn default() -> Self {{ Self {{ {tag_field}_tag: String::new(), {} }} }}",
946        seen_fields
947            .iter()
948            .map(|f| format!("{f}: None"))
949            .collect::<Vec<_>>()
950            .join(", ")
951    ));
952    lines.push("}".to_string());
953
954    lines.join("\n")
955}
956
957/// Generate a free function binding.
958fn gen_function(
959    func: &FunctionDef,
960    mapper: &NapiMapper,
961    cfg: &RustBindingConfig,
962    opaque_types: &AHashSet<String>,
963    prefix: &str,
964) -> String {
965    let params = function_params(&func.params, &|ty| {
966        // Opaque Named params must be received by reference since NAPI opaque
967        // structs don't implement FromNapiValue (they use Arc<T> internally).
968        if let TypeRef::Named(n) = ty {
969            if opaque_types.contains(n.as_str()) {
970                return format!("&{prefix}{n}");
971            }
972        }
973        mapper.map_type(ty)
974    });
975    let return_type = mapper.map_type(&func.return_type);
976    let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
977
978    let js_name = to_node_name(&func.name);
979    let js_name_attr = if js_name != func.name {
980        format!("(js_name = \"{}\")", js_name)
981    } else {
982        String::new()
983    };
984
985    let core_import = cfg.core_import;
986    let core_fn_path = {
987        let path = func.rust_path.replace('-', "_");
988        if path.starts_with(core_import) {
989            path
990        } else {
991            format!("{core_import}::{}", func.name)
992        }
993    };
994
995    // Use let-binding pattern for non-opaque Named params, or for Vec<f32> params that need conversion
996    let use_let_bindings = generators::has_named_params(&func.params, opaque_types)
997        || func.params.iter().any(|p| needs_vec_f32_conversion(&p.ty));
998    let call_args = if use_let_bindings {
999        let base_args = generators::gen_call_args_with_let_bindings(&func.params, opaque_types);
1000        napi_apply_primitive_casts_to_call_args(&base_args, &func.params)
1001    } else {
1002        napi_gen_call_args(&func.params, opaque_types)
1003    };
1004
1005    let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
1006
1007    let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
1008
1009    let async_kw = if func.is_async { "async " } else { "" };
1010
1011    let body = if !can_delegate_fn {
1012        // Try serde-based conversion for non-delegatable functions with Named params
1013        // Only use serde conversion if cfg.has_serde is true (binding crate has serde deps)
1014        if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
1015            let serde_bindings =
1016                generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, "    ");
1017            // Also generate Vec<String>+is_ref bindings (names_refs) since serde doesn't handle them
1018            let vec_str_bindings: String = func.params.iter().filter(|p| {
1019                p.is_ref && matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char))
1020            }).map(|p| {
1021                format!("let {}_refs: Vec<&str> = {}.iter().map(|s| s.as_str()).collect();\n    ", p.name, p.name)
1022            }).collect();
1023            let core_call = format!("{core_fn_path}({call_args})");
1024            let await_kw = if func.is_async { ".await" } else { "" };
1025
1026            if matches!(func.return_type, TypeRef::Unit) {
1027                format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}?;\n    Ok(())")
1028            } else {
1029                let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
1030                if wrapped == "val" {
1031                    format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}")
1032                } else {
1033                    format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}.map(|val| {wrapped}){err_conv}")
1034                }
1035            }
1036        } else {
1037            generators::gen_unimplemented_body(
1038                &func.return_type,
1039                &func.name,
1040                func.error_type.is_some(),
1041                cfg,
1042                &func.params,
1043                opaque_types,
1044            )
1045        }
1046    } else if func.is_async {
1047        // For async delegatable functions, generate let bindings if needed before the async call
1048        let mut let_bindings = if use_let_bindings {
1049            generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
1050        } else {
1051            String::new()
1052        };
1053        // Add Vec<f32> conversion bindings for parameters not already handled
1054        let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
1055        let core_call = format!("{core_fn_path}({call_args})");
1056        let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref, prefix);
1057        let return_type = mapper.map_type(&func.return_type);
1058        generators::gen_async_body(
1059            &core_call,
1060            cfg,
1061            func.error_type.is_some(),
1062            &return_wrap,
1063            false,
1064            &let_bindings,
1065            matches!(func.return_type, TypeRef::Unit),
1066            Some(&return_type),
1067        )
1068    } else {
1069        let core_call = format!("{core_fn_path}({call_args})");
1070        // Generate let bindings for Named params if needed
1071        let mut let_bindings = if use_let_bindings {
1072            generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
1073        } else {
1074            String::new()
1075        };
1076        // Add Vec<f32> conversion bindings for parameters not already handled
1077        let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
1078
1079        if func.error_type.is_some() {
1080            let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
1081            if wrapped == "val" {
1082                format!("{let_bindings}{core_call}{err_conv}")
1083            } else {
1084                format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
1085            }
1086        } else {
1087            format!(
1088                "{let_bindings}{}",
1089                napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref, prefix)
1090            )
1091        }
1092    };
1093
1094    let mut attrs = String::new();
1095    // Per-item clippy suppression: too_many_arguments when >7 params
1096    if func.params.len() > 7 {
1097        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
1098    }
1099    // Per-item clippy suppression: missing_errors_doc for Result-returning functions
1100    if func.error_type.is_some() {
1101        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
1102    }
1103    format!(
1104        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n    \
1105         {body}\n}}",
1106        func.name
1107    )
1108}
1109
1110/// Apply NAPI-specific primitive casts to the call args generated by the generic let-binding handler.
1111/// Adds i64→usize, i64→isize, f64→f32 casts where needed.
1112fn napi_apply_primitive_casts_to_call_args(generic_args: &str, params: &[ParamDef]) -> String {
1113    // Split args by comma and match with params to apply casting
1114    let args_list: Vec<&str> = generic_args.split(',').map(|s| s.trim()).collect();
1115    args_list
1116        .iter()
1117        .zip(params.iter())
1118        .map(|(arg, p)| {
1119            // Special case: Vec<f32> param with is_ref uses the converted variable
1120            if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1121                return format!("&{}_f32", p.name);
1122            }
1123            match &p.ty {
1124                TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
1125                    let core_ty = core_prim_str(prim);
1126                    if p.optional {
1127                        // Optional: arg might be like "param.map(...)" so re-apply map
1128                        if arg.contains(".map(") || arg.contains(".as_") {
1129                            // Already handled, keep as is
1130                            arg.to_string()
1131                        } else {
1132                            format!("{}.map(|v| v as {})", arg, core_ty)
1133                        }
1134                    } else {
1135                        // Non-optional: simple cast
1136                        format!("{} as {}", arg, core_ty)
1137                    }
1138                }
1139                _ => arg.to_string(),
1140            }
1141        })
1142        .collect::<Vec<_>>()
1143        .join(", ")
1144}
1145
1146/// Generate let bindings for Vec<f32> parameters that need f64→f32 conversion.
1147/// This handles the case where NAPI maps f32→f64, but a function param is Vec<f32> taking a reference.
1148fn gen_vec_f32_conversion_bindings(params: &[ParamDef]) -> String {
1149    let mut bindings = String::new();
1150    for p in params {
1151        if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1152            let conv_name = format!("{}_f32", p.name);
1153            bindings.push_str(&format!(
1154                "    let {conv_name}: Vec<f32> = {}.iter().map(|&x| x as f32).collect();\n",
1155                p.name
1156            ));
1157        }
1158    }
1159    bindings
1160}
1161
1162/// NAPI-specific call args that casts i64 params to u64/usize where the core expects it.
1163/// Properly handles is_ref for reference parameters and complex type conversions.
1164fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
1165    params
1166        .iter()
1167        .map(|p| {
1168            // Special case: Vec<f32> param with is_ref uses the converted variable
1169            if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1170                return format!("&{}_f32", p.name);
1171            }
1172            match &p.ty {
1173                TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
1174                    let core_ty = core_prim_str(prim);
1175                    if p.optional {
1176                        format!("{}.map(|v| v as {})", p.name, core_ty)
1177                    } else {
1178                        format!("{} as {}", p.name, core_ty)
1179                    }
1180                }
1181                TypeRef::Duration => {
1182                    if p.optional {
1183                        format!("{}.map(|v| std::time::Duration::from_millis(v.max(0) as u64))", p.name)
1184                    } else {
1185                        format!("std::time::Duration::from_millis({}.max(0) as u64)", p.name)
1186                    }
1187                }
1188                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1189                    if p.optional {
1190                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
1191                    } else {
1192                        format!("&{}.inner", p.name)
1193                    }
1194                }
1195                TypeRef::Named(_) => {
1196                    if p.optional {
1197                        if p.is_ref {
1198                            format!("{}.as_ref()", p.name)
1199                        } else {
1200                            format!("{}.map(Into::into)", p.name)
1201                        }
1202                    } else {
1203                        format!("{}.into()", p.name)
1204                    }
1205                }
1206                TypeRef::String | TypeRef::Char => {
1207                    if p.optional {
1208                        if p.is_ref {
1209                            format!("{}.as_deref()", p.name)
1210                        } else {
1211                            p.name.clone()
1212                        }
1213                    } else if p.is_ref {
1214                        format!("&{}", p.name)
1215                    } else {
1216                        p.name.clone()
1217                    }
1218                }
1219                TypeRef::Path => {
1220                    if p.optional {
1221                        if p.is_ref {
1222                            format!("{}.as_deref().map(std::path::Path::new)", p.name)
1223                        } else {
1224                            format!("{}.map(std::path::PathBuf::from)", p.name)
1225                        }
1226                    } else if p.is_ref {
1227                        format!("std::path::Path::new(&{})", p.name)
1228                    } else {
1229                        format!("std::path::PathBuf::from({})", p.name)
1230                    }
1231                }
1232                TypeRef::Bytes => {
1233                    if p.optional {
1234                        if p.is_ref {
1235                            format!("{}.as_deref()", p.name)
1236                        } else {
1237                            p.name.clone()
1238                        }
1239                    } else if p.is_ref {
1240                        format!("&{}", p.name)
1241                    } else {
1242                        p.name.clone()
1243                    }
1244                }
1245                TypeRef::Vec(inner) => {
1246                    if p.optional {
1247                        if p.is_ref {
1248                            format!("{}.as_deref()", p.name)
1249                        } else {
1250                            p.name.clone()
1251                        }
1252                    } else if p.is_ref && matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) {
1253                        format!("&{}_refs", p.name)
1254                    } else if p.is_ref {
1255                        format!("&{}", p.name)
1256                    } else {
1257                        p.name.clone()
1258                    }
1259                }
1260                TypeRef::Map(_, _) => {
1261                    if p.optional {
1262                        if p.is_ref {
1263                            format!("{}.as_ref()", p.name)
1264                        } else {
1265                            p.name.clone()
1266                        }
1267                    } else if p.is_ref {
1268                        format!("&{}", p.name)
1269                    } else {
1270                        p.name.clone()
1271                    }
1272                }
1273                _ => p.name.clone(),
1274            }
1275        })
1276        .collect::<Vec<_>>()
1277        .join(", ")
1278}
1279
1280/// NAPI-specific return wrapping for opaque instance methods.
1281/// Extends the shared `wrap_return` with i64 casts for u64/usize/isize primitives.
1282fn napi_wrap_return(
1283    expr: &str,
1284    return_type: &TypeRef,
1285    type_name: &str,
1286    opaque_types: &AHashSet<String>,
1287    self_is_opaque: bool,
1288    returns_ref: bool,
1289    prefix: &str,
1290) -> String {
1291    match return_type {
1292        TypeRef::Primitive(p) if needs_napi_cast(p) => {
1293            format!("{expr} as i64")
1294        }
1295        TypeRef::Duration => format!("{expr}.as_millis() as i64"),
1296        // Opaque Named returns need prefix
1297        TypeRef::Named(n) if n == type_name && self_is_opaque => {
1298            if returns_ref {
1299                format!("Self {{ inner: Arc::new({expr}.clone()) }}")
1300            } else {
1301                format!("Self {{ inner: Arc::new({expr}) }}")
1302            }
1303        }
1304        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1305            if returns_ref {
1306                format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
1307            } else {
1308                format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
1309            }
1310        }
1311        TypeRef::Named(_) => {
1312            if returns_ref {
1313                format!("{expr}.clone().into()")
1314            } else {
1315                format!("{expr}.into()")
1316            }
1317        }
1318        _ => generators::wrap_return(
1319            expr,
1320            return_type,
1321            type_name,
1322            opaque_types,
1323            self_is_opaque,
1324            returns_ref,
1325            false,
1326        ),
1327    }
1328}
1329
1330/// NAPI-specific return wrapping for free functions (no type_name context).
1331fn napi_wrap_return_fn(
1332    expr: &str,
1333    return_type: &TypeRef,
1334    opaque_types: &AHashSet<String>,
1335    returns_ref: bool,
1336    prefix: &str,
1337) -> String {
1338    match return_type {
1339        TypeRef::Primitive(p) if needs_napi_cast(p) => {
1340            format!("{expr} as i64")
1341        }
1342        TypeRef::Duration => format!("{expr}.as_millis() as i64"),
1343        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1344            if returns_ref {
1345                format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
1346            } else {
1347                format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
1348            }
1349        }
1350        TypeRef::Named(_) => {
1351            if returns_ref {
1352                format!("{expr}.clone().into()")
1353            } else {
1354                format!("{expr}.into()")
1355            }
1356        }
1357        TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1358            if returns_ref {
1359                format!("{expr}.into()")
1360            } else {
1361                expr.to_string()
1362            }
1363        }
1364        TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
1365        TypeRef::Json => format!("{expr}.to_string()"),
1366        TypeRef::Optional(inner) => match inner.as_ref() {
1367            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1368                if returns_ref {
1369                    format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }})")
1370                } else {
1371                    format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v) }})")
1372                }
1373            }
1374            TypeRef::Named(_) => {
1375                if returns_ref {
1376                    format!("{expr}.map(|v| v.clone().into())")
1377                } else {
1378                    format!("{expr}.map(Into::into)")
1379                }
1380            }
1381            TypeRef::Vec(inner) => match inner.as_ref() {
1382                TypeRef::Named(_) => {
1383                    if returns_ref {
1384                        format!("{expr}.map(|v| v.into_iter().map(|x| x.clone().into()).collect())")
1385                    } else {
1386                        format!("{expr}.map(|v| v.into_iter().map(Into::into).collect())")
1387                    }
1388                }
1389                _ => expr.to_string(),
1390            },
1391            TypeRef::Path => {
1392                format!("{expr}.map(Into::into)")
1393            }
1394            TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1395                if returns_ref {
1396                    format!("{expr}.map(Into::into)")
1397                } else {
1398                    expr.to_string()
1399                }
1400            }
1401            _ => expr.to_string(),
1402        },
1403        TypeRef::Vec(inner) => match inner.as_ref() {
1404            TypeRef::Primitive(p) if needs_napi_cast(p) => {
1405                // Vec<usize>, Vec<f32>, etc. need element-wise casting to i64 or f64
1406                let target_ty = match p {
1407                    alef_core::ir::PrimitiveType::F32 => "f64",
1408                    _ => "i64", // u64, usize, isize, u32
1409                };
1410                format!("{expr}.into_iter().map(|v| v as {target_ty}).collect()")
1411            }
1412            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1413                if returns_ref {
1414                    format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }}).collect()")
1415                } else {
1416                    format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v) }}).collect()")
1417                }
1418            }
1419            TypeRef::Named(_) => {
1420                if returns_ref {
1421                    format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
1422                } else {
1423                    format!("{expr}.into_iter().map(Into::into).collect()")
1424                }
1425            }
1426            TypeRef::Path => {
1427                format!("{expr}.into_iter().map(Into::into).collect()")
1428            }
1429            TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1430                if returns_ref {
1431                    format!("{expr}.into_iter().map(Into::into).collect()")
1432                } else {
1433                    expr.to_string()
1434                }
1435            }
1436            _ => expr.to_string(),
1437        },
1438        _ => expr.to_string(),
1439    }
1440}
1441
1442/// Check if a type is Vec<f32> which needs element-wise conversion from f64 in NAPI.
1443fn needs_vec_f32_conversion(ty: &TypeRef) -> bool {
1444    matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(alef_core::ir::PrimitiveType::F32)))
1445}
1446
1447fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
1448    // U32 maps to u32 in both NAPI and core, so no cast needed.
1449    // U64/Usize/Isize map to i64 in NAPI but u64/usize/isize in core.
1450    // F32 maps to f64 in NAPI but f32 in core.
1451    matches!(
1452        p,
1453        alef_core::ir::PrimitiveType::U64
1454            | alef_core::ir::PrimitiveType::Usize
1455            | alef_core::ir::PrimitiveType::Isize
1456            | alef_core::ir::PrimitiveType::F32
1457    )
1458}
1459
1460fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
1461    match p {
1462        alef_core::ir::PrimitiveType::U64 => "u64",
1463        alef_core::ir::PrimitiveType::Usize => "usize",
1464        alef_core::ir::PrimitiveType::Isize => "isize",
1465        alef_core::ir::PrimitiveType::F32 => "f32",
1466        _ => unreachable!(),
1467    }
1468}
1469
1470/// Generate a global Tokio runtime for NAPI async support.
1471fn gen_tokio_runtime() -> String {
1472    "static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
1473    tokio::runtime::Builder::new_multi_thread()
1474        .enable_all()
1475        .build()
1476        .expect(\"Failed to create Tokio runtime\")
1477});"
1478    .to_string()
1479}
1480
1481/// Generate an `index.d.ts` file for the NAPI binding crate.
1482///
1483/// NAPI-RS generates `const enum` in its auto-generated `.d.ts`, which is incompatible
1484/// with `verbatimModuleSyntax` (const enums cannot be re-exported as values). This
1485/// function produces an equivalent `.d.ts` with `export declare enum` (regular enum)
1486/// so the file can be committed and used directly without a post-build patch step.
1487///
1488/// The output format matches what NAPI-RS would generate after patching, using the same
1489/// alphabetical ordering and type declarations seen in the committed `index.d.ts` files.
1490fn gen_dts(api: &ApiSurface, prefix: &str) -> String {
1491    let header = hash::header(CommentStyle::DoubleSlash);
1492    let mut lines: Vec<String> = header.lines().map(|l| l.to_string()).collect();
1493    lines.push("/* eslint-disable */".to_string());
1494
1495    // Collect all declarations: opaque types (classes), plain structs (interfaces), enums, functions.
1496    // Sort each group alphabetically to produce stable, deterministic output.
1497
1498    // Opaque types → `export declare class`
1499    let mut opaque_types: Vec<&TypeDef> = api.types.iter().filter(|t| t.is_opaque).collect();
1500    opaque_types.sort_by(|a, b| a.name.cmp(&b.name));
1501
1502    // Plain structs → `export interface`
1503    let mut plain_types: Vec<&TypeDef> = api.types.iter().filter(|t| !t.is_opaque).collect();
1504    plain_types.sort_by(|a, b| a.name.cmp(&b.name));
1505
1506    // Enums → `export declare enum`
1507    let mut sorted_enums: Vec<&EnumDef> = api.enums.iter().collect();
1508    sorted_enums.sort_by(|a, b| a.name.cmp(&b.name));
1509
1510    // Functions → `export declare function`
1511    let mut sorted_fns: Vec<&FunctionDef> = api.functions.iter().collect();
1512    sorted_fns.sort_by(|a, b| a.name.cmp(&b.name));
1513
1514    // Build a merged list of all declarations sorted by their Js-prefixed name so the
1515    // output is fully alphabetical (matching the committed index.d.ts format).
1516    enum Decl<'a> {
1517        Class(&'a TypeDef),
1518        Interface(&'a TypeDef),
1519        Enum(&'a EnumDef),
1520        Function(&'a FunctionDef),
1521    }
1522
1523    let mut all_decls: Vec<(String, Decl<'_>)> = Vec::new();
1524    for t in &opaque_types {
1525        all_decls.push((format!("{prefix}{}", t.name), Decl::Class(t)));
1526    }
1527    for t in &plain_types {
1528        all_decls.push((format!("{prefix}{}", t.name), Decl::Interface(t)));
1529    }
1530    for e in &sorted_enums {
1531        all_decls.push((format!("{prefix}{}", e.name), Decl::Enum(e)));
1532    }
1533    for f in &sorted_fns {
1534        all_decls.push((to_node_name(&f.name), Decl::Function(f)));
1535    }
1536    all_decls.sort_by_key(|a| a.0.to_lowercase());
1537
1538    for (_, decl) in &all_decls {
1539        lines.push(String::new());
1540        match decl {
1541            Decl::Class(typ) => {
1542                lines.extend(format_jsdoc(&typ.doc, ""));
1543                lines.push(format!("export declare class {prefix}{} {{", typ.name));
1544                for method in &typ.methods {
1545                    let js_name = to_node_name(&method.name);
1546                    let params = dts_params(&method.params, prefix);
1547                    let ret = dts_return_type(
1548                        &method.return_type,
1549                        method.error_type.is_some(),
1550                        method.is_async,
1551                        prefix,
1552                    );
1553                    lines.extend(format_jsdoc(&method.doc, "  "));
1554                    if method.is_static {
1555                        lines.push(format!("  static {js_name}({params}): {ret}"));
1556                    } else {
1557                        lines.push(format!("  {js_name}({params}): {ret}"));
1558                    }
1559                }
1560                lines.push("}".to_string());
1561            }
1562            Decl::Interface(typ) => {
1563                lines.extend(format_jsdoc(&typ.doc, ""));
1564                lines.push(format!("export interface {prefix}{} {{", typ.name));
1565                for field in &typ.fields {
1566                    let js_name = to_node_name(&field.name);
1567                    let ts_ty = dts_type(&field.ty, prefix);
1568                    lines.extend(format_jsdoc(&field.doc, "  "));
1569                    // Only mark a field optional when the underlying Rust type is Option<T>.
1570                    // Required fields must not carry `?` — callers are expected to provide them.
1571                    if matches!(field.ty, TypeRef::Optional(_)) {
1572                        lines.push(format!("  {js_name}?: {ts_ty}"));
1573                    } else {
1574                        lines.push(format!("  {js_name}: {ts_ty}"));
1575                    }
1576                }
1577                lines.push("}".to_string());
1578            }
1579            Decl::Enum(e) => {
1580                let is_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
1581                lines.extend(format_jsdoc(&e.doc, ""));
1582                if is_data_enum {
1583                    // Discriminated union: emit a type alias instead of an enum declaration.
1584                    // Each variant becomes an object literal type with the tag field and its own fields.
1585                    let tag_field = e.serde_tag.as_deref().unwrap_or("type");
1586                    let mut member_lines: Vec<String> = Vec::new();
1587                    for variant in &e.variants {
1588                        let tag_value = variant
1589                            .serde_rename
1590                            .as_deref()
1591                            .map(|s| s.to_string())
1592                            .unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
1593                        let mut obj_fields: Vec<String> = vec![format!("{tag_field}: '{tag_value}'")];
1594                        for field in &variant.fields {
1595                            let js_name = to_node_name(&field.name);
1596                            let ts_ty = dts_type(&field.ty, prefix);
1597                            if matches!(field.ty, TypeRef::Optional(_)) {
1598                                obj_fields.push(format!("{js_name}?: {ts_ty}"));
1599                            } else {
1600                                obj_fields.push(format!("{js_name}: {ts_ty}"));
1601                            }
1602                        }
1603                        member_lines.push(format!("  | {{ {} }}", obj_fields.join("; ")));
1604                    }
1605                    lines.push(format!("export type {prefix}{} =", e.name));
1606                    lines.extend(member_lines);
1607                } else {
1608                    lines.push(format!("export declare enum {prefix}{} {{", e.name));
1609                    for variant in &e.variants {
1610                        // NAPI string_enum: variant values follow serde_rename_all casing.
1611                        // Prefer explicit serde_rename, then apply rename_all, then fall back to variant name.
1612                        let value = variant
1613                            .serde_rename
1614                            .as_deref()
1615                            .map(|s| s.to_string())
1616                            .unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
1617                        lines.extend(format_jsdoc(&variant.doc, "  "));
1618                        lines.push(format!("  {} = \"{}\",", variant.name, value));
1619                    }
1620                    lines.push("}".to_string());
1621                }
1622            }
1623            Decl::Function(func) => {
1624                let js_name = to_node_name(&func.name);
1625                let params = dts_params(&func.params, prefix);
1626                let ret = dts_return_type(&func.return_type, func.error_type.is_some(), func.is_async, prefix);
1627                lines.extend(format_jsdoc(&func.doc, ""));
1628                lines.push(format!("export declare function {js_name}({params}): {ret};"));
1629            }
1630        }
1631    }
1632
1633    lines.push(String::new());
1634    lines.join("\n")
1635}
1636
1637/// Format a rustdoc string as JSDoc comment lines with the given `indent` prefix.
1638///
1639/// Returns an empty `Vec` when `doc` is empty. For a single-line doc, emits
1640/// `["/** Description */"]`. For multi-line docs, emits the block form:
1641/// `["/**", " * line1", " * line2", " */"]`, each prefixed by `indent`.
1642fn format_jsdoc(doc: &str, indent: &str) -> Vec<String> {
1643    let doc = doc.trim();
1644    if doc.is_empty() {
1645        return vec![];
1646    }
1647    let lines: Vec<&str> = doc.lines().collect();
1648    if lines.len() == 1 {
1649        vec![format!("{indent}/** {} */", lines[0].trim())]
1650    } else {
1651        let mut out = Vec::with_capacity(lines.len() + 2);
1652        out.push(format!("{indent}/**"));
1653        for line in &lines {
1654            let trimmed = line.trim();
1655            if trimmed.is_empty() {
1656                out.push(format!("{indent} *"));
1657            } else {
1658                out.push(format!("{indent} * {trimmed}"));
1659            }
1660        }
1661        out.push(format!("{indent} */"));
1662        out
1663    }
1664}
1665
1666/// Map an IR `TypeRef` to its TypeScript equivalent for `.d.ts` generation.
1667fn dts_type(ty: &TypeRef, prefix: &str) -> String {
1668    match ty {
1669        TypeRef::Primitive(p) => match p {
1670            alef_core::ir::PrimitiveType::Bool => "boolean".to_string(),
1671            alef_core::ir::PrimitiveType::U8
1672            | alef_core::ir::PrimitiveType::U16
1673            | alef_core::ir::PrimitiveType::U32
1674            | alef_core::ir::PrimitiveType::I8
1675            | alef_core::ir::PrimitiveType::I16
1676            | alef_core::ir::PrimitiveType::I32
1677            | alef_core::ir::PrimitiveType::F32
1678            | alef_core::ir::PrimitiveType::F64 => "number".to_string(),
1679            // NAPI maps u64/usize/isize to i64 on the Rust side; JS sees it as number.
1680            alef_core::ir::PrimitiveType::U64
1681            | alef_core::ir::PrimitiveType::I64
1682            | alef_core::ir::PrimitiveType::Usize
1683            | alef_core::ir::PrimitiveType::Isize => "number".to_string(),
1684        },
1685        TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
1686        TypeRef::Bytes => "Uint8Array".to_string(),
1687        TypeRef::Json => "unknown".to_string(),
1688        TypeRef::Duration => "number".to_string(),
1689        TypeRef::Unit => "void".to_string(),
1690        TypeRef::Optional(inner) => format!("{} | undefined | null", dts_type(inner, prefix)),
1691        TypeRef::Vec(inner) => format!("Array<{}>", dts_type(inner, prefix)),
1692        TypeRef::Map(k, v) => format!("Record<{}, {}>", dts_type(k, prefix), dts_type(v, prefix)),
1693        TypeRef::Named(name) => format!("{prefix}{name}"),
1694    }
1695}
1696
1697/// Render a list of parameters as a TypeScript parameter string for `.d.ts`.
1698fn dts_params(params: &[ParamDef], prefix: &str) -> String {
1699    // TypeScript requires optional parameters to come after all required parameters (TS1016).
1700    // If the Rust source has optional params followed by required params (e.g., `lang: Option<&str>`,
1701    // `code: &str`), we must reorder: required first, then optional, preserving relative order within
1702    // each group.
1703    let mut required: Vec<&ParamDef> = Vec::new();
1704    let mut optional: Vec<&ParamDef> = Vec::new();
1705    for p in params {
1706        if p.optional {
1707            optional.push(p);
1708        } else {
1709            required.push(p);
1710        }
1711    }
1712    // If no reordering is needed (already ordered), use original order to avoid churn.
1713    let ordered: Vec<&ParamDef> = if params
1714        .iter()
1715        .zip(required.iter().chain(optional.iter()))
1716        .all(|(a, b)| std::ptr::eq(a as *const ParamDef, *b as *const ParamDef))
1717    {
1718        params.iter().collect()
1719    } else {
1720        required.into_iter().chain(optional).collect()
1721    };
1722    ordered
1723        .iter()
1724        .map(|p| {
1725            let js_name = to_node_name(&p.name);
1726            let ts_ty = dts_type(&p.ty, prefix);
1727            if p.optional {
1728                format!("{js_name}?: {ts_ty} | undefined | null")
1729            } else {
1730                format!("{js_name}: {ts_ty}")
1731            }
1732        })
1733        .collect::<Vec<_>>()
1734        .join(", ")
1735}
1736
1737/// Render the TypeScript return type for a function/method in `.d.ts`.
1738///
1739/// Async functions return `Promise<T>`. Functions that can error still return `T`
1740/// (NAPI throws JS exceptions on error, so the `.d.ts` signature just shows the success type).
1741fn dts_return_type(ret: &TypeRef, _has_error: bool, is_async: bool, prefix: &str) -> String {
1742    let base = match ret {
1743        TypeRef::Unit => "void".to_string(),
1744        other => dts_type(other, prefix),
1745    };
1746    if is_async { format!("Promise<{base}>") } else { base }
1747}
1748
1749/// Apply a serde `rename_all` rule to a PascalCase variant name, returning the serialized string.
1750///
1751/// NAPI `string_enum` serializes variant names using the same rule as serde's `rename_all`.
1752/// When a variant has no explicit `serde_rename`, the enum-level `rename_all` applies.
1753fn apply_rename_all(variant_name: &str, rename_all: Option<&str>) -> String {
1754    match rename_all {
1755        Some("snake_case") => {
1756            // PascalCase → snake_case: insert underscore before each uppercase letter (after the first)
1757            let mut out = String::with_capacity(variant_name.len() + 4);
1758            for (i, c) in variant_name.chars().enumerate() {
1759                if c.is_uppercase() && i > 0 {
1760                    out.push('_');
1761                }
1762                out.extend(c.to_lowercase());
1763            }
1764            out
1765        }
1766        Some("camelCase") => {
1767            // PascalCase → camelCase: lowercase the first character only
1768            let mut chars = variant_name.chars();
1769            match chars.next() {
1770                None => String::new(),
1771                Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
1772            }
1773        }
1774        Some("kebab-case") => {
1775            let mut out = String::with_capacity(variant_name.len() + 4);
1776            for (i, c) in variant_name.chars().enumerate() {
1777                if c.is_uppercase() && i > 0 {
1778                    out.push('-');
1779                }
1780                out.extend(c.to_lowercase());
1781            }
1782            out
1783        }
1784        Some("SCREAMING_SNAKE_CASE") => {
1785            let mut out = String::with_capacity(variant_name.len() + 4);
1786            for (i, c) in variant_name.chars().enumerate() {
1787                if c.is_uppercase() && i > 0 {
1788                    out.push('_');
1789                }
1790                out.extend(c.to_uppercase());
1791            }
1792            out
1793        }
1794        Some("lowercase") => variant_name.to_lowercase(),
1795        Some("UPPERCASE") => variant_name.to_uppercase(),
1796        // PascalCase and unknown rules: use the variant name as-is
1797        _ => variant_name.to_string(),
1798    }
1799}
1800
1801/// Generate `From<JsTaggedEnum> for core::TaggedEnum` for a flattened struct representation.
1802fn gen_tagged_enum_binding_to_core(
1803    enum_def: &EnumDef,
1804    core_import: &str,
1805    prefix: &str,
1806    struct_names: &ahash::AHashSet<String>,
1807) -> String {
1808    use alef_core::ir::TypeRef;
1809    use std::fmt::Write;
1810    let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1811    let binding_name = format!("{prefix}{}", enum_def.name);
1812    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1813
1814    // Determine which Named fields use binding structs vs serde JSON String.
1815    // A field uses a binding struct only if: (1) it has a binding struct in struct_names,
1816    // (2) it's not sanitized, and (3) the field name maps to a single Named type across
1817    // all variants (not shared with different types).
1818    let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
1819    // Fields with different Named types across variants are stored as String (JSON) in the
1820    // binding struct and must be deserialized per-variant via serde_json.
1821    let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
1822
1823    let mut out = String::with_capacity(512);
1824    writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
1825    writeln!(out, "    fn from(val: {binding_name}) -> Self {{").ok();
1826    writeln!(out, "        match val.{tag_field}_tag.as_str() {{").ok();
1827
1828    for variant in &enum_def.variants {
1829        let default_tag = variant.name.to_lowercase();
1830        let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1831        if variant.fields.is_empty() {
1832            writeln!(out, "            \"{tag_value}\" => Self::{},", variant.name).ok();
1833        } else {
1834            let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1835            let field_exprs: Vec<String> = variant
1836                .fields
1837                .iter()
1838                .map(|f| {
1839                    let has_binding = fields_with_binding_struct.contains(f.name.as_str());
1840                    let is_mixed = mixed_named_fields.contains(&f.name);
1841                    if f.optional {
1842                        match &f.ty {
1843                            TypeRef::Path => {
1844                                format!("val.{}.map(std::path::PathBuf::from)", f.name)
1845                            }
1846                            TypeRef::Named(n) if is_mixed => {
1847                                // Mixed-type field: stored as String (JSON), deserialize per variant
1848                                let core_type = format!("{core_import}::{n}");
1849                                format!(
1850                                    "val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok())",
1851                                    f.name
1852                                )
1853                            }
1854                            TypeRef::Named(_) if has_binding => {
1855                                format!("val.{}.map(|v| v.into())", f.name)
1856                            }
1857                            // Non-sanitized Named fields with a single consistent type are stored
1858                            // as Option<JsXxx> in the binding struct, so use .into() conversion.
1859                            TypeRef::Named(_) => {
1860                                format!("val.{}.map(|v| v.into())", f.name)
1861                            }
1862                            TypeRef::Primitive(p) if needs_napi_cast(p) => {
1863                                let core_ty = core_prim_str(p);
1864                                format!("val.{}.map(|v| v as {core_ty})", f.name)
1865                            }
1866                            _ => {
1867                                format!("val.{}", f.name)
1868                            }
1869                        }
1870                    } else if f.sanitized {
1871                        let expr = "Default::default()".to_string();
1872                        if f.is_boxed { format!("Box::new({expr})") } else { expr }
1873                    } else {
1874                        let expr = match &f.ty {
1875                            TypeRef::Named(n) if is_mixed => {
1876                                // Mixed-type field: stored as String (JSON), deserialize per variant
1877                                let core_type = format!("{core_import}::{n}");
1878                                format!(
1879                                    "val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok()).unwrap_or_default()",
1880                                    f.name
1881                                )
1882                            }
1883                            TypeRef::Named(_) if has_binding => {
1884                                format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
1885                            }
1886                            // Non-sanitized Named fields with a single consistent type are stored
1887                            // as Option<JsXxx> in the binding struct, so use .into() conversion.
1888                            TypeRef::Named(_) => {
1889                                format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
1890                            }
1891                            TypeRef::Path => {
1892                                format!("val.{}.map(std::path::PathBuf::from).unwrap_or_default()", f.name)
1893                            }
1894                            TypeRef::Primitive(p) if needs_napi_cast(p) => {
1895                                let core_ty = core_prim_str(p);
1896                                format!("val.{}.map(|v| v as {core_ty}).unwrap_or_default()", f.name)
1897                            }
1898                            _ => {
1899                                format!("val.{}.unwrap_or_default()", f.name)
1900                            }
1901                        };
1902                        if f.is_boxed { format!("Box::new({expr})") } else { expr }
1903                    }
1904                })
1905                .collect();
1906            if is_tuple {
1907                writeln!(
1908                    out,
1909                    "            \"{tag_value}\" => Self::{}({}),",
1910                    variant.name,
1911                    field_exprs.join(", ")
1912                )
1913                .ok();
1914            } else {
1915                let field_inits: Vec<String> = variant
1916                    .fields
1917                    .iter()
1918                    .zip(field_exprs.iter())
1919                    .map(|(f, expr)| format!("{}: {expr}", f.name))
1920                    .collect();
1921                writeln!(
1922                    out,
1923                    "            \"{tag_value}\" => Self::{} {{ {} }},",
1924                    variant.name,
1925                    field_inits.join(", ")
1926                )
1927                .ok();
1928            }
1929        }
1930    }
1931
1932    // Default fallback to first variant
1933    if let Some(first) = enum_def.variants.first() {
1934        if first.fields.is_empty() {
1935            writeln!(out, "            _ => Self::{},", first.name).ok();
1936        } else {
1937            let is_tuple = alef_codegen::conversions::is_tuple_variant(&first.fields);
1938            if is_tuple {
1939                let defaults: Vec<&str> = first.fields.iter().map(|_| "Default::default()").collect();
1940                writeln!(out, "            _ => Self::{}({}),", first.name, defaults.join(", ")).ok();
1941            } else {
1942                let defaults: Vec<String> = first
1943                    .fields
1944                    .iter()
1945                    .map(|f| format!("{}: Default::default()", f.name))
1946                    .collect();
1947                writeln!(
1948                    out,
1949                    "            _ => Self::{} {{ {} }},",
1950                    first.name,
1951                    defaults.join(", ")
1952                )
1953                .ok();
1954            }
1955        }
1956    }
1957
1958    writeln!(out, "        }}").ok();
1959    writeln!(out, "    }}").ok();
1960    write!(out, "}}").ok();
1961    out
1962}
1963
1964/// Generate `From<core::TaggedEnum> for JsTaggedEnum` for a flattened struct representation.
1965fn gen_tagged_enum_core_to_binding(
1966    enum_def: &EnumDef,
1967    core_import: &str,
1968    prefix: &str,
1969    struct_names: &ahash::AHashSet<String>,
1970) -> String {
1971    use std::fmt::Write;
1972    let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1973    let binding_name = format!("{prefix}{}", enum_def.name);
1974    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1975    let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
1976    // Fields with different Named types across variants are stored as String (JSON) in the
1977    // binding struct and must be serialized per-variant via serde_json.
1978    let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
1979
1980    // Collect all field names across all variants
1981    let all_fields: Vec<String> = {
1982        let mut fields = std::collections::BTreeSet::new();
1983        for v in &enum_def.variants {
1984            for f in &v.fields {
1985                fields.insert(f.name.clone());
1986            }
1987        }
1988        fields.into_iter().collect()
1989    };
1990
1991    let mut out = String::with_capacity(512);
1992    writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
1993    writeln!(out, "    fn from(val: {core_path}) -> Self {{").ok();
1994    writeln!(out, "        match val {{").ok();
1995
1996    for variant in &enum_def.variants {
1997        let default_tag = variant.name.to_lowercase();
1998        let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1999        let _variant_field_names: std::collections::BTreeSet<String> =
2000            variant.fields.iter().map(|f| f.name.clone()).collect();
2001
2002        if variant.fields.is_empty() {
2003            writeln!(
2004                out,
2005                "            {core_path}::{} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2006                variant.name,
2007                all_fields
2008                    .iter()
2009                    .map(|f| format!("{f}: None"))
2010                    .collect::<Vec<_>>()
2011                    .join(", ")
2012            )
2013            .ok();
2014        } else {
2015            use alef_core::ir::TypeRef;
2016            let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
2017            let variant_field_map: std::collections::BTreeMap<&str, &alef_core::ir::FieldDef> =
2018                variant.fields.iter().map(|f| (f.name.as_str(), f)).collect();
2019            let destructured: Vec<String> = variant
2020                .fields
2021                .iter()
2022                .map(|f| {
2023                    if f.sanitized {
2024                        if is_tuple {
2025                            format!("_{}", f.name)
2026                        } else {
2027                            format!("{}: _{}", f.name, f.name)
2028                        }
2029                    } else {
2030                        f.name.clone()
2031                    }
2032                })
2033                .collect();
2034            let field_inits: Vec<String> = all_fields
2035                .iter()
2036                .map(|f| {
2037                    if let Some(field) = variant_field_map.get(f.as_str()) {
2038                        let has_binding = fields_with_binding_struct.contains(f.as_str());
2039                        let is_mixed = mixed_named_fields.contains(f.as_str());
2040                        if field.optional {
2041                            match &field.ty {
2042                                TypeRef::Path => format!("{f}: {f}.map(|p| p.to_string_lossy().to_string())"),
2043                                TypeRef::Named(_) if is_mixed => {
2044                                    // Mixed-type field: serialize to JSON String for the binding struct
2045                                    format!("{f}: {f}.and_then(|v| serde_json::to_string(&v).ok())")
2046                                }
2047                                TypeRef::Named(_) if has_binding => {
2048                                    format!("{f}: {f}.map(|v| v.into())")
2049                                }
2050                                // Non-sanitized Named fields with a single consistent type are stored
2051                                // as Option<JsXxx> in the binding struct, so use .into() conversion.
2052                                TypeRef::Named(_) => {
2053                                    format!("{f}: {f}.map(|v| v.into())")
2054                                }
2055                                _ => format!("{f}: {f}"),
2056                            }
2057                        } else if field.sanitized {
2058                            format!("{f}: None")
2059                        } else {
2060                            match &field.ty {
2061                                TypeRef::Named(_) if is_mixed => {
2062                                    // Mixed-type field: serialize to JSON String for the binding struct
2063                                    format!("{f}: serde_json::to_string(&{f}).ok()")
2064                                }
2065                                TypeRef::Named(_) if has_binding => format!("{f}: Some({f}.into())"),
2066                                // Non-sanitized Named fields with a single consistent type are stored
2067                                // as Option<JsXxx> in the binding struct, so use .into() conversion.
2068                                TypeRef::Named(_) => format!("{f}: Some({f}.into())"),
2069                                TypeRef::Path => format!("{f}: Some({f}.to_string_lossy().to_string())"),
2070                                TypeRef::Primitive(p) if needs_napi_cast(p) => {
2071                                    match p {
2072                                        alef_core::ir::PrimitiveType::F32 => format!("{f}: Some({f} as f64)"),
2073                                        alef_core::ir::PrimitiveType::U64
2074                                        | alef_core::ir::PrimitiveType::Usize
2075                                        | alef_core::ir::PrimitiveType::Isize => format!("{f}: Some({f} as i64)"),
2076                                        // U32 stays as-is in NAPI
2077                                        _ => format!("{f}: Some({f})"),
2078                                    }
2079                                }
2080                                _ => format!("{f}: Some({f})"),
2081                            }
2082                        }
2083                    } else {
2084                        format!("{f}: None")
2085                    }
2086                })
2087                .collect();
2088            if is_tuple {
2089                writeln!(
2090                    out,
2091                    "            {core_path}::{}({}) => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2092                    variant.name,
2093                    destructured.join(", "),
2094                    field_inits.join(", ")
2095                )
2096                .ok();
2097            } else {
2098                writeln!(
2099                    out,
2100                    "            {core_path}::{} {{ {} }} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2101                    variant.name,
2102                    destructured.join(", "),
2103                    field_inits.join(", ")
2104                )
2105                .ok();
2106            }
2107        }
2108    }
2109
2110    writeln!(out, "        }}").ok();
2111    writeln!(out, "    }}").ok();
2112    write!(out, "}}").ok();
2113    out
2114}
2115
2116/// Determine which Named fields in a tagged enum have **different** Named types across variants.
2117/// These fields cannot use a single `JsXxx` binding type, so they are stored as `String` (JSON)
2118/// and converted via `serde_json` per variant in the From impls.
2119fn tagged_enum_mixed_named_fields(enum_def: &EnumDef) -> ahash::AHashSet<String> {
2120    use alef_core::ir::TypeRef;
2121    let mut field_types: std::collections::HashMap<&str, ahash::AHashSet<&str>> = std::collections::HashMap::new();
2122
2123    for variant in &enum_def.variants {
2124        for field in &variant.fields {
2125            if field.sanitized {
2126                continue;
2127            }
2128            if let TypeRef::Named(n) = &field.ty {
2129                field_types.entry(&field.name).or_default().insert(n.as_str());
2130            }
2131        }
2132    }
2133
2134    field_types
2135        .into_iter()
2136        .filter(|(_, types)| types.len() > 1)
2137        .map(|(name, _)| name.to_string())
2138        .collect()
2139}
2140
2141/// Determine which Named fields in a tagged enum use binding structs (Into conversion)
2142/// vs serde JSON String flattening. A field uses a binding struct only if:
2143/// 1. The field name maps to a single Named type across all variants
2144/// 2. That Named type has a binding struct (in struct_names)
2145/// 3. The field is not sanitized
2146fn tagged_enum_binding_struct_fields<'a>(
2147    enum_def: &'a EnumDef,
2148    struct_names: &ahash::AHashSet<String>,
2149) -> ahash::AHashSet<&'a str> {
2150    use alef_core::ir::TypeRef;
2151    let mut field_types: std::collections::HashMap<&str, Vec<&str>> = std::collections::HashMap::new();
2152    let mut sanitized_fields: ahash::AHashSet<&str> = ahash::AHashSet::new();
2153
2154    for variant in &enum_def.variants {
2155        for field in &variant.fields {
2156            if field.sanitized {
2157                sanitized_fields.insert(&field.name);
2158            }
2159            if let TypeRef::Named(n) = &field.ty {
2160                field_types.entry(&field.name).or_default().push(n);
2161            }
2162        }
2163    }
2164
2165    let mut result = ahash::AHashSet::new();
2166    for (field_name, types) in &field_types {
2167        if sanitized_fields.contains(field_name) {
2168            continue;
2169        }
2170        // All variants sharing this field name must have the same Named type
2171        if types.iter().all(|t| *t == types[0]) && struct_names.contains(types[0]) {
2172            result.insert(*field_name);
2173        }
2174    }
2175    result
2176}
2177
2178#[cfg(test)]
2179mod tests {
2180    use super::*;
2181    use alef_core::ir::{ParamDef, TypeRef};
2182
2183    fn make_param(name: &str, optional: bool) -> ParamDef {
2184        ParamDef {
2185            name: name.to_string(),
2186            ty: TypeRef::String,
2187            optional,
2188            default: None,
2189            sanitized: false,
2190            typed_default: None,
2191            is_ref: false,
2192            is_mut: false,
2193            newtype_wrapper: None,
2194            original_type: None,
2195        }
2196    }
2197
2198    /// TypeScript TS1016: required parameter must not follow optional parameter.
2199    /// A visitor method like `visit_code_block(ctx, lang?: Option<str>, code: str)`
2200    /// must be reordered to `visit_code_block(ctx, code, lang?)` in the `.d.ts`.
2201    #[test]
2202    fn dts_params_reorders_required_after_optional() {
2203        let params = vec![
2204            make_param("ctx", false),
2205            make_param("lang", true),
2206            make_param("code", false),
2207        ];
2208        let result = dts_params(&params, "Js");
2209        // Required params (ctx, code) must precede optional param (lang)
2210        let ctx_pos = result.find("ctx:").expect("ctx not found");
2211        let code_pos = result.find("code:").expect("code not found");
2212        let lang_pos = result.find("lang?:").expect("lang? not found");
2213        assert!(ctx_pos < lang_pos, "ctx should come before lang?: {result}");
2214        assert!(code_pos < lang_pos, "code should come before lang?: {result}");
2215    }
2216
2217    /// When params are already in valid order (all required before all optional),
2218    /// the output must be unchanged — no unnecessary reordering.
2219    #[test]
2220    fn dts_params_preserves_already_valid_order() {
2221        let params = vec![
2222            make_param("ctx", false),
2223            make_param("code", false),
2224            make_param("lang", true),
2225        ];
2226        let result = dts_params(&params, "Js");
2227        assert_eq!(result, "ctx: string, code: string, lang?: string | undefined | null");
2228    }
2229
2230    /// All-required params: order must be preserved exactly.
2231    #[test]
2232    fn dts_params_all_required_preserves_order() {
2233        let params = vec![make_param("a", false), make_param("b", false), make_param("c", false)];
2234        let result = dts_params(&params, "Js");
2235        assert_eq!(result, "a: string, b: string, c: string");
2236    }
2237}