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    if cfg.has_serde {
43        sb.add_derive("serde::Serialize");
44    }
45    for field in &typ.fields {
46        // Skip cfg-gated fields — they depend on features that may not be enabled
47        // for this binding crate. Including them would require the binding struct to
48        // handle conditional compilation which struct literal initializers can't express.
49        if field.cfg.is_some() {
50            continue;
51        }
52        let ty = if field.optional {
53            mapper.optional(&mapper.map_type(&field.ty))
54        } else {
55            mapper.map_type(&field.ty)
56        };
57        let attrs: Vec<String> = cfg.field_attrs.iter().map(|a| a.to_string()).collect();
58        sb.add_field_with_doc(&field.name, &ty, attrs, &field.doc);
59    }
60    sb.build()
61}
62
63/// Generate a `Default` impl for a non-opaque binding struct with `has_default`.
64/// All fields use their type's Default::default().
65/// Optional fields use None instead of Default::default().
66/// This enables the struct to be used with `unwrap_or_default()` in config constructors.
67pub fn gen_struct_default_impl(typ: &TypeDef, name_prefix: &str) -> String {
68    let full_name = format!("{}{}", name_prefix, typ.name);
69    let mut out = String::with_capacity(256);
70    writeln!(out, "impl Default for {} {{", full_name).ok();
71    writeln!(out, "    fn default() -> Self {{").ok();
72    writeln!(out, "        Self {{").ok();
73    for field in &typ.fields {
74        if field.cfg.is_some() {
75            continue;
76        }
77        let default_val = match &field.ty {
78            TypeRef::Optional(_) => "None".to_string(),
79            _ => "Default::default()".to_string(),
80        };
81        writeln!(out, "            {}: {},", field.name, default_val).ok();
82    }
83    // Add synthetic field defaults for cfg-gated fields exposed in NAPI binding.
84    // When name_prefix is "Js" (NAPI backend), we add synthetic fields for known cfg-gated fields.
85    if name_prefix == "Js" && typ.name == "ConversionResult" {
86        // ConversionResult has a metadata: HtmlMetadata field behind #[cfg(feature = "metadata")]
87        // Default to None for the Option<JsHtmlMetadata> field.
88        writeln!(out, "            metadata: None,").ok();
89    }
90    writeln!(out, "        }}").ok();
91    writeln!(out, "    }}").ok();
92    write!(out, "}}").ok();
93    out
94}
95
96/// Generate an opaque wrapper struct with `inner: Arc<core::Type>`.
97/// For trait types, uses `Arc<dyn Type + Send + Sync>`.
98pub fn gen_opaque_struct(typ: &TypeDef, cfg: &RustBindingConfig) -> String {
99    let mut out = String::with_capacity(512);
100    if !cfg.struct_derives.is_empty() {
101        writeln!(out, "#[derive(Clone)]").ok();
102    }
103    for attr in cfg.struct_attrs {
104        writeln!(out, "#[{attr}]").ok();
105    }
106    writeln!(out, "pub struct {} {{", typ.name).ok();
107    let core_path = typ.rust_path.replace('-', "_");
108    if typ.is_trait {
109        writeln!(out, "    inner: Arc<dyn {core_path} + Send + Sync>,").ok();
110    } else {
111        writeln!(out, "    inner: Arc<{core_path}>,").ok();
112    }
113    write!(out, "}}").ok();
114    out
115}
116
117/// Generate an opaque wrapper struct with `inner: Arc<core::Type>` and a `Js` prefix.
118pub fn gen_opaque_struct_prefixed(typ: &TypeDef, cfg: &RustBindingConfig, prefix: &str) -> String {
119    let mut out = String::with_capacity(512);
120    if !cfg.struct_derives.is_empty() {
121        writeln!(out, "#[derive(Clone)]").ok();
122    }
123    for attr in cfg.struct_attrs {
124        writeln!(out, "#[{attr}]").ok();
125    }
126    let core_path = typ.rust_path.replace('-', "_");
127    writeln!(out, "pub struct {}{} {{", prefix, typ.name).ok();
128    if typ.is_trait {
129        writeln!(out, "    inner: Arc<dyn {core_path} + Send + Sync>,").ok();
130    } else {
131        writeln!(out, "    inner: Arc<{core_path}>,").ok();
132    }
133    write!(out, "}}").ok();
134    out
135}