Skip to main content

alef_codegen/generators/
structs.rs

1use crate::builder::StructBuilder;
2use crate::generators::RustBindingConfig;
3use crate::type_mapper::TypeMapper;
4use alef_core::ir::{TypeDef, TypeRef};
5use std::fmt::Write;
6
7/// Check if any two field names are similar enough to trigger clippy::similar_names.
8/// This detects patterns like "sub_symbol" and "sup_symbol" (differ by 1-2 chars).
9fn has_similar_names(names: &[&String]) -> bool {
10    for (i, &name1) in names.iter().enumerate() {
11        for &name2 in &names[i + 1..] {
12            // Simple heuristic: if names differ by <= 2 characters and have same length, flag it
13            if name1.len() == name2.len() && diff_count(name1, name2) <= 2 {
14                return true;
15            }
16        }
17    }
18    false
19}
20
21/// Count how many characters differ between two strings of equal length.
22fn diff_count(s1: &str, s2: &str) -> usize {
23    s1.chars().zip(s2.chars()).filter(|(c1, c2)| c1 != c2).count()
24}
25
26/// Generate a struct definition using the builder.
27pub fn gen_struct(typ: &TypeDef, mapper: &dyn TypeMapper, cfg: &RustBindingConfig) -> String {
28    let mut sb = StructBuilder::new(&typ.name);
29    for attr in cfg.struct_attrs {
30        sb.add_attr(attr);
31    }
32
33    // Check if struct has similar field names (e.g., sub_symbol and sup_symbol)
34    let field_names: Vec<_> = typ.fields.iter().filter(|f| f.cfg.is_none()).map(|f| &f.name).collect();
35    if has_similar_names(&field_names) {
36        sb.add_attr("allow(clippy::similar_names)");
37    }
38
39    for d in cfg.struct_derives {
40        sb.add_derive(d);
41    }
42    // Types with has_default get #[derive(Default)] — all fields use Default::default()
43    // so the manual impl is unnecessary and triggers clippy::derivable_impls.
44    if typ.has_default {
45        sb.add_derive("Default");
46    }
47    if cfg.has_serde {
48        sb.add_derive("serde::Serialize");
49    }
50    for field in &typ.fields {
51        // Skip cfg-gated fields — they depend on features that may not be enabled
52        // for this binding crate. Including them would require the binding struct to
53        // handle conditional compilation which struct literal initializers can't express.
54        if field.cfg.is_some() {
55            continue;
56        }
57        let ty = if field.optional {
58            mapper.optional(&mapper.map_type(&field.ty))
59        } else {
60            mapper.map_type(&field.ty)
61        };
62        let attrs: Vec<String> = cfg.field_attrs.iter().map(|a| a.to_string()).collect();
63        sb.add_field_with_doc(&field.name, &ty, attrs, &field.doc);
64    }
65    sb.build()
66}
67
68/// Generate a `Default` impl for a non-opaque binding struct with `has_default`.
69/// All fields use their type's Default::default().
70/// Optional fields use None instead of Default::default().
71/// This enables the struct to be used with `unwrap_or_default()` in config constructors.
72pub fn gen_struct_default_impl(typ: &TypeDef, name_prefix: &str) -> String {
73    let full_name = format!("{}{}", name_prefix, typ.name);
74    let mut out = String::with_capacity(256);
75    writeln!(out, "impl Default for {} {{", full_name).ok();
76    writeln!(out, "    fn default() -> Self {{").ok();
77    writeln!(out, "        Self {{").ok();
78    for field in &typ.fields {
79        if field.cfg.is_some() {
80            continue;
81        }
82        let default_val = match &field.ty {
83            TypeRef::Optional(_) => "None".to_string(),
84            _ => "Default::default()".to_string(),
85        };
86        writeln!(out, "            {}: {},", field.name, default_val).ok();
87    }
88    // Add synthetic field defaults for cfg-gated fields exposed in NAPI binding.
89    // When name_prefix is "Js" (NAPI backend), we add synthetic fields for known cfg-gated fields.
90    if name_prefix == "Js" && typ.name == "ConversionResult" {
91        // ConversionResult has a metadata: HtmlMetadata field behind #[cfg(feature = "metadata")]
92        // Default to None for the Option<JsHtmlMetadata> field.
93        writeln!(out, "            metadata: None,").ok();
94    }
95    writeln!(out, "        }}").ok();
96    writeln!(out, "    }}").ok();
97    write!(out, "}}").ok();
98    out
99}
100
101/// Generate an opaque wrapper struct with `inner: Arc<core::Type>`.
102/// For trait types, uses `Arc<dyn Type + Send + Sync>`.
103pub fn gen_opaque_struct(typ: &TypeDef, cfg: &RustBindingConfig) -> String {
104    let mut out = String::with_capacity(512);
105    if !cfg.struct_derives.is_empty() {
106        writeln!(out, "#[derive(Clone)]").ok();
107    }
108    for attr in cfg.struct_attrs {
109        writeln!(out, "#[{attr}]").ok();
110    }
111    writeln!(out, "pub struct {} {{", typ.name).ok();
112    let core_path = typ.rust_path.replace('-', "_");
113    if typ.is_trait {
114        writeln!(out, "    inner: Arc<dyn {core_path} + Send + Sync>,").ok();
115    } else {
116        writeln!(out, "    inner: Arc<{core_path}>,").ok();
117    }
118    write!(out, "}}").ok();
119    out
120}
121
122/// Generate an opaque wrapper struct with `inner: Arc<core::Type>` and a `Js` prefix.
123pub fn gen_opaque_struct_prefixed(typ: &TypeDef, cfg: &RustBindingConfig, prefix: &str) -> String {
124    let mut out = String::with_capacity(512);
125    if !cfg.struct_derives.is_empty() {
126        writeln!(out, "#[derive(Clone)]").ok();
127    }
128    for attr in cfg.struct_attrs {
129        writeln!(out, "#[{attr}]").ok();
130    }
131    let core_path = typ.rust_path.replace('-', "_");
132    writeln!(out, "pub struct {}{} {{", prefix, typ.name).ok();
133    if typ.is_trait {
134        writeln!(out, "    inner: Arc<dyn {core_path} + Send + Sync>,").ok();
135    } else {
136        writeln!(out, "    inner: Arc<{core_path}>,").ok();
137    }
138    write!(out, "}}").ok();
139    out
140}