Skip to main content

alef_codegen/generators/
methods.rs

1use crate::generators::binding_helpers::{
2    apply_return_newtype_unwrap, gen_async_body, gen_call_args, gen_call_args_with_let_bindings,
3    gen_lossy_binding_to_core_fields, gen_lossy_binding_to_core_fields_mut, gen_named_let_bindings_pub,
4    gen_serde_let_bindings, gen_unimplemented_body, has_named_params, is_simple_non_opaque_param, wrap_return,
5};
6use crate::generators::{AdapterBodies, AsyncPattern, RustBindingConfig};
7use crate::shared::{constructor_parts, function_params, function_sig_defaults, partition_methods};
8use crate::type_mapper::TypeMapper;
9use ahash::AHashSet;
10use alef_core::ir::{MethodDef, TypeDef, TypeRef};
11use std::fmt::Write;
12
13/// Returns true when `name` matches a known trait method that would trigger
14/// `clippy::should_implement_trait`.
15pub fn is_trait_method_name(name: &str) -> bool {
16    crate::generators::TRAIT_METHOD_NAMES.contains(&name)
17}
18
19/// Generate a constructor method.
20pub fn gen_constructor(typ: &TypeDef, mapper: &dyn TypeMapper, cfg: &RustBindingConfig) -> String {
21    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
22
23    // For types with has_default, generate optional kwargs-style constructor
24    let (param_list, sig_defaults, assignments) = if typ.has_default {
25        crate::shared::config_constructor_parts_with_options(&typ.fields, &map_fn, cfg.option_duration_on_defaults)
26    } else {
27        constructor_parts(&typ.fields, &map_fn)
28    };
29
30    let mut out = String::with_capacity(512);
31    // Per-item clippy suppression: too_many_arguments when >7 params
32    if typ.fields.len() > 7 {
33        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
34    }
35    writeln!(out, "    #[must_use]").ok();
36    if cfg.needs_signature {
37        writeln!(
38            out,
39            "    {}{}{}",
40            cfg.signature_prefix, sig_defaults, cfg.signature_suffix
41        )
42        .ok();
43    }
44    write!(
45        out,
46        "    {}\n    pub fn new({param_list}) -> Self {{\n        Self {{ {assignments} }}\n    }}",
47        cfg.constructor_attr
48    )
49    .ok();
50    out
51}
52
53/// Generate an instance method.
54///
55/// When `is_opaque` is true, generates delegation to `self.inner` via Arc clone
56/// instead of converting self to core type.
57///
58/// `opaque_types` is the set of opaque type names, used for correct return wrapping.
59pub fn gen_method(
60    method: &MethodDef,
61    mapper: &dyn TypeMapper,
62    cfg: &RustBindingConfig,
63    typ: &TypeDef,
64    is_opaque: bool,
65    opaque_types: &AHashSet<String>,
66    adapter_bodies: &AdapterBodies,
67) -> String {
68    let type_name = &typ.name;
69    // Use the full rust_path (with hyphens replaced by underscores) for core type references
70    let core_type_path = typ.rust_path.replace('-', "_");
71
72    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
73    let params = function_params(&method.params, &map_fn);
74    let return_type = mapper.map_type(&method.return_type);
75    let ret = mapper.wrap_return(&return_type, method.error_type.is_some());
76
77    let core_import = cfg.core_import;
78
79    // When non-opaque Named params have is_ref=true, we need let bindings so the
80    // converted value outlives the borrow. Use gen_call_args_with_let_bindings in that case.
81    let has_ref_named_params = method
82        .params
83        .iter()
84        .any(|p| p.is_ref && matches!(&p.ty, TypeRef::Named(n) if !opaque_types.contains(n.as_str())));
85    let (call_args, ref_let_bindings) = if has_ref_named_params {
86        (
87            gen_call_args_with_let_bindings(&method.params, opaque_types),
88            gen_named_let_bindings_pub(&method.params, opaque_types, core_import),
89        )
90    } else {
91        (gen_call_args(&method.params, opaque_types), String::new())
92    };
93
94    let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
95    let is_ref_mut_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
96
97    // Detect non-opaque RefMut methods that can use the functional clone-mutate-return pattern.
98    // These cannot use &mut self in frozen PyO3 classes or immutable WASM structs, so instead
99    // we generate: clone self to core, apply mutation, convert back to Self.
100    // Conditions: non-opaque, RefMut receiver, no trait source (trait methods need special handling),
101    // all params delegatable (Named types are allowed — gen_call_args handles them via .into()),
102    // and not sanitized.
103    let is_functional_ref_mut = !is_opaque
104        && is_ref_mut_receiver
105        && !method.sanitized
106        && method.trait_source.is_none()
107        && method
108            .params
109            .iter()
110            .all(|p| !p.sanitized && crate::shared::is_delegatable_param(&p.ty, opaque_types));
111
112    // Methods from trait impls can't be called on Arc<dyn Trait> through deref.
113    // Skip these unless there's an adapter body that can handle them.
114    let is_trait_method = method.trait_source.is_some();
115
116    // Auto-delegate opaque methods: unwrap Arc for params, wrap Arc for returns.
117    // Owned receivers require the type to implement Clone (builder pattern).
118    // RefMut receivers can't be delegated on Arc<T> (Arc only gives &self, not &mut self).
119    // Trait methods can't be delegated on opaque types (Arc deref doesn't expose trait methods).
120    // Async methods are allowed — gen_async_body handles them below.
121    let opaque_can_delegate = is_opaque
122        && !method.sanitized
123        && !is_ref_mut_receiver
124        && !is_trait_method
125        && (!is_owned_receiver || typ.is_clone)
126        && method
127            .params
128            .iter()
129            .all(|p| !p.sanitized && crate::shared::is_opaque_delegatable_type(&p.ty))
130        && crate::shared::is_opaque_delegatable_type(&method.return_type);
131
132    // Build the core call expression: opaque types delegate to self.inner directly,
133    // non-opaque types convert self to core type first.
134    let make_core_call = |method_name: &str| -> String {
135        if is_opaque {
136            if is_owned_receiver {
137                // Owned receiver: clone out of Arc to get an owned value
138                format!("(*self.inner).clone().{method_name}({call_args})")
139            } else {
140                format!("self.inner.{method_name}({call_args})")
141            }
142        } else {
143            format!("{core_type_path}::from(self.clone()).{method_name}({call_args})")
144        }
145    };
146
147    // For async opaque methods, we clone the Arc before moving into the future.
148    let make_async_core_call = |method_name: &str| -> String {
149        if is_opaque {
150            format!("inner.{method_name}({call_args})")
151        } else {
152            format!("{core_type_path}::from(self.clone()).{method_name}({call_args})")
153        }
154    };
155
156    // Generate the body: convert self to core type, call method, convert result back
157    //
158    // For opaque types, wrap the return value appropriately:
159    //   - Named(self) → Self { inner: Arc::new(result) }
160    //   - Named(other) → OtherType::from(result)
161    //   - primitives/String/Vec/Unit → pass through
162    let result_expr = apply_return_newtype_unwrap("result", &method.return_newtype_wrapper);
163    let async_result_wrap = if is_opaque {
164        wrap_return(
165            &result_expr,
166            &method.return_type,
167            type_name,
168            opaque_types,
169            is_opaque,
170            method.returns_ref,
171            method.returns_cow,
172        )
173    } else {
174        // For non-opaque types, only use From conversion if the return type is simple
175        // enough. Named return types may not have a From impl.
176        match &method.return_type {
177            TypeRef::Named(_) | TypeRef::Json => format!("{result_expr}.into()"),
178            _ => result_expr.clone(),
179        }
180    };
181
182    let body = if !opaque_can_delegate {
183        // Check if an adapter provides the body
184        let adapter_key = format!("{}.{}", type_name, method.name);
185        if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
186            adapter_body.clone()
187        } else if cfg.has_serde
188            && is_opaque
189            && !method.sanitized
190            && !is_trait_method
191            && has_named_params(&method.params, opaque_types)
192            && method.error_type.is_some()
193            && crate::shared::is_opaque_delegatable_type(&method.return_type)
194        {
195            // Serde-based param conversion for opaque methods with non-opaque Named params.
196            // NOTE: Only executed when has_serde=true, ensuring serde_json calls are gated.
197            let err_conv = match cfg.async_pattern {
198                AsyncPattern::Pyo3FutureIntoPy => {
199                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
200                }
201                AsyncPattern::NapiNativeAsync => {
202                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
203                }
204                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
205                _ => ".map_err(|e| e.to_string())",
206            };
207            let serde_bindings =
208                gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, "        ");
209            let serde_call_args = gen_call_args_with_let_bindings(&method.params, opaque_types);
210            let core_call = format!("self.inner.{}({serde_call_args})", method.name);
211            if matches!(method.return_type, TypeRef::Unit) {
212                format!("{serde_bindings}{core_call}{err_conv}?;\n        Ok(())")
213            } else {
214                let wrap = wrap_return(
215                    "result",
216                    &method.return_type,
217                    type_name,
218                    opaque_types,
219                    is_opaque,
220                    method.returns_ref,
221                    method.returns_cow,
222                );
223                format!("{serde_bindings}let result = {core_call}{err_conv}?;\n        Ok({wrap})")
224            }
225        } else if is_functional_ref_mut {
226            // Functional clone-mutate-return pattern for non-opaque RefMut methods.
227            // PyO3 frozen classes and WASM structs don't support &mut self, so instead:
228            //   1. Convert binding self to a mutable core type.
229            //   2. Call the mutating core method (which changes core_self in place).
230            //   3. Convert the mutated core type back to the binding type and return Self.
231            //
232            // The generated signature uses &self -> Self (or -> Result<Self, E> if fallible),
233            // making the method work correctly with immutable binding wrappers.
234            let field_conversions =
235                gen_lossy_binding_to_core_fields_mut(typ, cfg.core_import, cfg.option_duration_on_defaults);
236            let core_call = format!("core_self.{}({call_args})", method.name);
237            if method.error_type.is_some() {
238                let err_conv = match cfg.async_pattern {
239                    AsyncPattern::Pyo3FutureIntoPy => {
240                        ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
241                    }
242                    AsyncPattern::NapiNativeAsync => {
243                        ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
244                    }
245                    AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
246                    _ => ".map_err(|e| e.to_string())",
247                };
248                format!("{field_conversions}{core_call}{err_conv}?;\n        Ok(core_self.into())")
249            } else {
250                format!("{field_conversions}{core_call};\n        core_self.into()")
251            }
252        } else if !is_opaque
253            && !method.sanitized
254            && method
255                .params
256                .iter()
257                .all(|p| !p.sanitized && is_simple_non_opaque_param(&p.ty))
258            && crate::shared::is_delegatable_return(&method.return_type)
259        {
260            // Non-opaque delegation: construct core type field-by-field, call method, convert back.
261            // Sanitized fields use Default::default() (lossy but functional for builder pattern).
262            let is_ref_mut = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
263            let field_conversions = if is_ref_mut {
264                gen_lossy_binding_to_core_fields_mut(typ, cfg.core_import, cfg.option_duration_on_defaults)
265            } else {
266                gen_lossy_binding_to_core_fields(typ, cfg.core_import, cfg.option_duration_on_defaults)
267            };
268            let core_call = format!("core_self.{}({call_args})", method.name);
269            let newtype_suffix = if method.return_newtype_wrapper.is_some() {
270                ".0"
271            } else {
272                ""
273            };
274            let result_wrap = match &method.return_type {
275                // When returns_cow=true the core returns Cow<'_, T>: call .into_owned() to
276                // obtain an owned T before the binding→core From conversion.
277                // When returns_ref=true (or &T / Cow<'_, T> via the old flag), same treatment.
278                TypeRef::Named(n) if n == type_name && (method.returns_cow || method.returns_ref) => {
279                    ".into_owned().into()".to_string()
280                }
281                TypeRef::Named(_) if method.returns_cow || method.returns_ref => ".into_owned().into()".to_string(),
282                TypeRef::Named(n) if n == type_name => ".into()".to_string(),
283                TypeRef::Named(_) => ".into()".to_string(),
284                TypeRef::String | TypeRef::Bytes | TypeRef::Path => {
285                    if method.returns_ref {
286                        ".to_owned()".to_string()
287                    } else {
288                        ".into()".to_string()
289                    }
290                }
291                // Optional<Named>: when core returns Option<&T>, need .map(|v| v.clone().into())
292                TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
293                    if method.returns_ref {
294                        ".map(|v| v.clone().into())".to_string()
295                    } else {
296                        ".map(Into::into)".to_string()
297                    }
298                }
299                // Optional<String>: when core returns Option<&str>, need .map(|v| v.to_owned())
300                TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Bytes) => {
301                    if method.returns_ref {
302                        ".map(|v| v.to_owned())".to_string()
303                    } else {
304                        String::new()
305                    }
306                }
307                _ => String::new(),
308            };
309            if method.error_type.is_some() {
310                let err_conv = match cfg.async_pattern {
311                    AsyncPattern::Pyo3FutureIntoPy => {
312                        ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
313                    }
314                    AsyncPattern::NapiNativeAsync => {
315                        ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
316                    }
317                    AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
318                    _ => ".map_err(|e| e.to_string())",
319                };
320                format!(
321                    "{field_conversions}let result = {core_call}{err_conv}?;\n        Ok(result{newtype_suffix}{result_wrap})"
322                )
323            } else {
324                format!("{field_conversions}{core_call}{newtype_suffix}{result_wrap}")
325            }
326        } else if is_opaque
327            && !method.sanitized
328            && !is_ref_mut_receiver
329            && (!is_owned_receiver || typ.is_clone)
330            && method.error_type.is_none()
331            && method
332                .params
333                .iter()
334                .all(|p| !p.sanitized && crate::shared::is_opaque_delegatable_type(&p.ty))
335            && matches!(&method.return_type, TypeRef::Named(n) if n == type_name)
336        {
337            // Builder pattern for opaque types: method returns Self without error type.
338            // Delegate to core method and wrap result back in Self { inner: Arc::new(...) }.
339            let core_call = if is_owned_receiver {
340                format!("(*self.inner).clone().{}({call_args})", method.name)
341            } else {
342                format!("self.inner.{}({call_args})", method.name)
343            };
344            let unwrapped = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
345            format!("Self {{ inner: Arc::new({unwrapped}) }}")
346        } else if !is_opaque
347            && !method.sanitized
348            && !is_ref_mut_receiver
349            && (!is_owned_receiver || typ.is_clone)
350            && method.error_type.is_none()
351            && method
352                .params
353                .iter()
354                .all(|p| !p.sanitized && is_simple_non_opaque_param(&p.ty))
355            && matches!(&method.return_type, TypeRef::Named(n) if n == type_name)
356        {
357            // Builder pattern for non-opaque types: method returns Self without error type.
358            // Construct core type field-by-field, call method, convert result back via .into().
359            let is_ref_mut = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
360            let field_conversions = if is_ref_mut {
361                gen_lossy_binding_to_core_fields_mut(typ, cfg.core_import, cfg.option_duration_on_defaults)
362            } else {
363                gen_lossy_binding_to_core_fields(typ, cfg.core_import, cfg.option_duration_on_defaults)
364            };
365            let core_call = format!("core_self.{}({call_args})", method.name);
366            let newtype_suffix = if method.return_newtype_wrapper.is_some() {
367                ".0"
368            } else {
369                ""
370            };
371            let result_wrap = if method.returns_cow || method.returns_ref {
372                ".into_owned().into()"
373            } else {
374                ".into()"
375            };
376            format!("{field_conversions}{core_call}{newtype_suffix}{result_wrap}")
377        } else {
378            gen_unimplemented_body(
379                &method.return_type,
380                &format!("{type_name}.{}", method.name),
381                method.error_type.is_some(),
382                cfg,
383                &method.params,
384            )
385        }
386    } else if method.is_async {
387        let inner_clone_line = if is_opaque {
388            "let inner = self.inner.clone();\n        "
389        } else {
390            ""
391        };
392        let core_call_str = make_async_core_call(&method.name);
393        gen_async_body(
394            &core_call_str,
395            cfg,
396            method.error_type.is_some(),
397            &async_result_wrap,
398            is_opaque,
399            inner_clone_line,
400            matches!(method.return_type, TypeRef::Unit),
401            Some(&return_type),
402        )
403    } else {
404        let core_call = make_core_call(&method.name);
405        if method.error_type.is_some() {
406            // Backend-specific error conversion
407            let err_conv = match cfg.async_pattern {
408                AsyncPattern::Pyo3FutureIntoPy => {
409                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
410                }
411                AsyncPattern::NapiNativeAsync => {
412                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
413                }
414                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
415                _ => ".map_err(|e| e.to_string())",
416            };
417            if is_opaque {
418                if matches!(method.return_type, TypeRef::Unit) {
419                    // Unit return: avoid let_unit_value by not binding the result
420                    format!("{core_call}{err_conv}?;\n        Ok(())")
421                } else {
422                    let wrap = wrap_return(
423                        &result_expr,
424                        &method.return_type,
425                        type_name,
426                        opaque_types,
427                        is_opaque,
428                        method.returns_ref,
429                        method.returns_cow,
430                    );
431                    format!("let result = {core_call}{err_conv}?;\n        Ok({wrap})")
432                }
433            } else {
434                format!("{core_call}{err_conv}")
435            }
436        } else if is_opaque {
437            let unwrapped_call = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
438            wrap_return(
439                &unwrapped_call,
440                &method.return_type,
441                type_name,
442                opaque_types,
443                is_opaque,
444                method.returns_ref,
445                method.returns_cow,
446            )
447        } else {
448            core_call
449        }
450    };
451    // Prepend let bindings for non-opaque Named ref params (needed for borrow lifetime)
452    let body = if ref_let_bindings.is_empty() {
453        body
454    } else {
455        format!("{ref_let_bindings}{body}")
456    };
457
458    let needs_py = method.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
459    let self_param = match (needs_py, params.is_empty()) {
460        (true, true) => "&self, py: Python<'py>",
461        (true, false) => "&self, py: Python<'py>, ",
462        (false, true) => "&self",
463        (false, false) => "&self, ",
464    };
465
466    // For async PyO3 methods, override return type to PyResult<Bound<'py, PyAny>>
467    // and add the 'py lifetime generic on the method name.
468    // For functional RefMut methods, override to Self (or Result<Self, E>) because the
469    // generated body clones self, applies the mutation, and returns the updated value.
470    let ret = if needs_py {
471        "PyResult<Bound<'py, PyAny>>".to_string()
472    } else if is_functional_ref_mut {
473        mapper.wrap_return("Self", method.error_type.is_some())
474    } else {
475        ret
476    };
477    let method_lifetime = if needs_py { "<'py>" } else { "" };
478
479    // Wrap long signature if necessary
480    let (sig_start, sig_params, sig_end) = if self_param.len() + params.len() > 100 {
481        let wrapped_params = method
482            .params
483            .iter()
484            .map(|p| {
485                let ty = if p.optional {
486                    format!("Option<{}>", mapper.map_type(&p.ty))
487                } else {
488                    mapper.map_type(&p.ty)
489                };
490                format!("{}: {}", p.name, ty)
491            })
492            .collect::<Vec<_>>()
493            .join(",\n        ");
494        let py_param = if needs_py { "\n        py: Python<'py>," } else { "" };
495        (
496            format!(
497                "pub fn {}{method_lifetime}(\n        &self,{}\n        ",
498                method.name, py_param
499            ),
500            wrapped_params,
501            "\n    ) -> ".to_string(),
502        )
503    } else {
504        (
505            format!("pub fn {}{method_lifetime}({}", method.name, self_param),
506            params,
507            ") -> ".to_string(),
508        )
509    };
510
511    let mut out = String::with_capacity(1024);
512    // Per-item clippy suppression: too_many_arguments when >7 params (including &self and py)
513    let total_params = method.params.len() + 1 + if needs_py { 1 } else { 0 };
514    if total_params > 7 {
515        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
516    }
517    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
518    if method.error_type.is_some() {
519        writeln!(out, "    #[allow(clippy::missing_errors_doc)]").ok();
520    }
521    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
522    if is_trait_method_name(&method.name) {
523        writeln!(out, "    #[allow(clippy::should_implement_trait)]").ok();
524    }
525    if cfg.needs_signature {
526        let sig = function_sig_defaults(&method.params);
527        writeln!(out, "    {}{}{}", cfg.signature_prefix, sig, cfg.signature_suffix).ok();
528    }
529    write!(
530        out,
531        "    {}{}{}{} {{\n        \
532         {body}\n    }}",
533        sig_start, sig_params, sig_end, ret,
534    )
535    .ok();
536    out
537}
538
539/// Generate a static method.
540pub fn gen_static_method(
541    method: &MethodDef,
542    mapper: &dyn TypeMapper,
543    cfg: &RustBindingConfig,
544    typ: &TypeDef,
545    adapter_bodies: &AdapterBodies,
546    opaque_types: &AHashSet<String>,
547) -> String {
548    let type_name = &typ.name;
549    // Use the full rust_path (with hyphens replaced by underscores) for core type references
550    let core_type_path = typ.rust_path.replace('-', "_");
551    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
552    let params = function_params(&method.params, &map_fn);
553    let return_type = mapper.map_type(&method.return_type);
554    let ret = mapper.wrap_return(&return_type, method.error_type.is_some());
555
556    let core_import = cfg.core_import;
557
558    // Use let bindings when any non-opaque Named or Vec<Named> params exist.
559    // This includes Vec<Named> without is_ref=true, which need element conversion.
560    let use_let_bindings = has_named_params(&method.params, opaque_types);
561    let (call_args, ref_let_bindings) = if use_let_bindings {
562        (
563            gen_call_args_with_let_bindings(&method.params, opaque_types),
564            gen_named_let_bindings_pub(&method.params, opaque_types, core_import),
565        )
566    } else {
567        (gen_call_args(&method.params, opaque_types), String::new())
568    };
569
570    let can_delegate = crate::shared::can_auto_delegate(method, opaque_types);
571
572    let body = if !can_delegate {
573        // Check if an adapter provides the body
574        let adapter_key = format!("{}.{}", type_name, method.name);
575        if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
576            adapter_body.clone()
577        } else {
578            gen_unimplemented_body(
579                &method.return_type,
580                &format!("{type_name}::{}", method.name),
581                method.error_type.is_some(),
582                cfg,
583                &method.params,
584            )
585        }
586    } else if method.is_async {
587        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
588        let return_wrap = format!("{return_type}::from(result)");
589        gen_async_body(
590            &core_call,
591            cfg,
592            method.error_type.is_some(),
593            &return_wrap,
594            false,
595            "",
596            matches!(method.return_type, TypeRef::Unit),
597            Some(&return_type),
598        )
599    } else {
600        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
601        if method.error_type.is_some() {
602            // Backend-specific error conversion
603            let err_conv = match cfg.async_pattern {
604                AsyncPattern::Pyo3FutureIntoPy => {
605                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
606                }
607                AsyncPattern::NapiNativeAsync => {
608                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
609                }
610                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
611                _ => ".map_err(|e| e.to_string())",
612            };
613            // Wrap the Ok value if the return type needs conversion (e.g. PathBuf→String)
614            let val_expr = apply_return_newtype_unwrap("val", &method.return_newtype_wrapper);
615            let wrapped = wrap_return(
616                &val_expr,
617                &method.return_type,
618                type_name,
619                opaque_types,
620                typ.is_opaque,
621                method.returns_ref,
622                method.returns_cow,
623            );
624            if wrapped == val_expr {
625                format!("{core_call}{err_conv}")
626            } else {
627                format!("{core_call}.map(|val| {wrapped}){err_conv}")
628            }
629        } else {
630            // Wrap return value for non-error case too (e.g. PathBuf→String)
631            let unwrapped_call = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
632            wrap_return(
633                &unwrapped_call,
634                &method.return_type,
635                type_name,
636                opaque_types,
637                typ.is_opaque,
638                method.returns_ref,
639                method.returns_cow,
640            )
641        }
642    };
643    // Prepend let bindings for non-opaque Named ref params
644    let body = if ref_let_bindings.is_empty() {
645        body
646    } else {
647        format!("{ref_let_bindings}{body}")
648    };
649
650    let static_needs_py = method.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
651
652    // For async PyO3 static methods, override return type and add lifetime generic.
653    let ret = if static_needs_py {
654        "PyResult<Bound<'py, PyAny>>".to_string()
655    } else {
656        ret
657    };
658    let method_lifetime = if static_needs_py { "<'py>" } else { "" };
659
660    // Wrap long signature if necessary
661    let (sig_start, sig_params, sig_end) = if params.len() > 100 {
662        let wrapped_params = method
663            .params
664            .iter()
665            .map(|p| {
666                let ty = if p.optional {
667                    format!("Option<{}>", mapper.map_type(&p.ty))
668                } else {
669                    mapper.map_type(&p.ty)
670                };
671                format!("{}: {}", p.name, ty)
672            })
673            .collect::<Vec<_>>()
674            .join(",\n        ");
675        // For async PyO3, add py parameter
676        if static_needs_py {
677            (
678                format!("pub fn {}{method_lifetime}(py: Python<'py>,\n        ", method.name),
679                wrapped_params,
680                "\n    ) -> ".to_string(),
681            )
682        } else {
683            (
684                format!("pub fn {}(\n        ", method.name),
685                wrapped_params,
686                "\n    ) -> ".to_string(),
687            )
688        }
689    } else if static_needs_py {
690        (
691            format!("pub fn {}{method_lifetime}(py: Python<'py>, ", method.name),
692            params,
693            ") -> ".to_string(),
694        )
695    } else {
696        (format!("pub fn {}(", method.name), params, ") -> ".to_string())
697    };
698
699    let mut out = String::with_capacity(1024);
700    // Per-item clippy suppression: too_many_arguments when >7 params (including py)
701    let total_params = method.params.len() + if static_needs_py { 1 } else { 0 };
702    if total_params > 7 {
703        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
704    }
705    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
706    if method.error_type.is_some() {
707        writeln!(out, "    #[allow(clippy::missing_errors_doc)]").ok();
708    }
709    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
710    if is_trait_method_name(&method.name) {
711        writeln!(out, "    #[allow(clippy::should_implement_trait)]").ok();
712    }
713    if let Some(attr) = cfg.static_attr {
714        writeln!(out, "    #[{attr}]").ok();
715    }
716    if cfg.needs_signature {
717        let sig = function_sig_defaults(&method.params);
718        writeln!(out, "    {}{}{}", cfg.signature_prefix, sig, cfg.signature_suffix).ok();
719    }
720    write!(
721        out,
722        "    {}{}{}{} {{\n        \
723         {body}\n    }}",
724        sig_start, sig_params, sig_end, ret,
725    )
726    .ok();
727    out
728}
729
730/// Generate a full methods impl block (non-opaque types).
731pub fn gen_impl_block(
732    typ: &TypeDef,
733    mapper: &dyn TypeMapper,
734    cfg: &RustBindingConfig,
735    adapter_bodies: &AdapterBodies,
736    opaque_types: &AHashSet<String>,
737) -> String {
738    let (instance, statics) = partition_methods(&typ.methods);
739    if instance.is_empty() && statics.is_empty() && typ.fields.is_empty() {
740        return String::new();
741    }
742
743    let prefixed_name = format!("{}{}", cfg.type_name_prefix, typ.name);
744    let mut out = String::with_capacity(2048);
745    if let Some(block_attr) = cfg.method_block_attr {
746        writeln!(out, "#[{block_attr}]").ok();
747    }
748    writeln!(out, "impl {prefixed_name} {{").ok();
749
750    // Constructor
751    if !typ.fields.is_empty() {
752        out.push_str(&gen_constructor(typ, mapper, cfg));
753        out.push_str("\n\n");
754    }
755
756    // Instance methods
757    for m in &instance {
758        out.push_str(&gen_method(m, mapper, cfg, typ, false, opaque_types, adapter_bodies));
759        out.push_str("\n\n");
760    }
761
762    // Static methods
763    for m in &statics {
764        out.push_str(&gen_static_method(m, mapper, cfg, typ, adapter_bodies, opaque_types));
765        out.push_str("\n\n");
766    }
767
768    // Trim trailing newlines inside impl block
769    let trimmed = out.trim_end();
770    let mut result = trimmed.to_string();
771    result.push_str("\n}");
772    result
773}
774
775/// Generate a full impl block for an opaque type, delegating methods to `self.inner`.
776///
777/// `opaque_types` is the set of type names that are opaque wrappers (use `Arc<inner>`).
778/// This is needed so that return-type wrapping uses the correct pattern for cross-type returns.
779pub fn gen_opaque_impl_block(
780    typ: &TypeDef,
781    mapper: &dyn TypeMapper,
782    cfg: &RustBindingConfig,
783    opaque_types: &AHashSet<String>,
784    adapter_bodies: &AdapterBodies,
785) -> String {
786    let (instance, statics) = partition_methods(&typ.methods);
787    if instance.is_empty() && statics.is_empty() {
788        return String::new();
789    }
790
791    let mut out = String::with_capacity(2048);
792    let prefixed_name = format!("{}{}", cfg.type_name_prefix, typ.name);
793    if let Some(block_attr) = cfg.method_block_attr {
794        writeln!(out, "#[{block_attr}]").ok();
795    }
796    writeln!(out, "impl {prefixed_name} {{").ok();
797
798    // Instance methods — delegate to self.inner
799    for m in &instance {
800        out.push_str(&gen_method(m, mapper, cfg, typ, true, opaque_types, adapter_bodies));
801        out.push_str("\n\n");
802    }
803
804    // Static methods
805    for m in &statics {
806        out.push_str(&gen_static_method(m, mapper, cfg, typ, adapter_bodies, opaque_types));
807        out.push_str("\n\n");
808    }
809
810    let trimmed = out.trim_end();
811    let mut result = trimmed.to_string();
812    result.push_str("\n}");
813    result
814}