use convert_case::{Case, Casing};
use crate::builtin::derive_common::{
collection_element_type, is_primitive_type, standalone_fn_name, type_has_derive,
};
use crate::macros::{ts_macro_derive, ts_template};
use crate::swc_ecma_ast::Expr;
use crate::ts_syn::abi::ir::type_registry::{ResolvedTypeRef, TypeRegistry};
use crate::ts_syn::{Data, DeriveInput, MacroforgeError, TsStream, parse_ts_macro_input, ts_ident};
fn generate_clone_expr(
field_name: &str,
ts_type: &str,
var: &str,
resolved: Option<&ResolvedTypeRef>,
registry: Option<&TypeRegistry>,
) -> String {
let access = format!("{var}.{field_name}");
if let (Some(resolved), Some(registry)) = (resolved, registry) {
if resolved.is_optional {
let inner_expr =
generate_clone_for_resolved(field_name, ts_type, var, resolved, registry);
if inner_expr != access {
return format!("{access} != null ? {inner_expr} : {access}");
}
return access;
}
return generate_clone_for_resolved(field_name, ts_type, var, resolved, registry);
}
generate_clone_expr_fallback(field_name, ts_type, var)
}
fn generate_clone_for_resolved(
field_name: &str,
ts_type: &str,
var: &str,
resolved: &ResolvedTypeRef,
registry: &TypeRegistry,
) -> String {
let access = format!("{var}.{field_name}");
if !resolved.is_collection
&& resolved.registry_key.is_some()
&& type_has_derive(registry, &resolved.base_type_name, "Clone")
{
let fn_name = standalone_fn_name(&resolved.base_type_name, "Clone");
return format!("{fn_name}({access})");
}
if resolved.base_type_name == "Date" && !resolved.is_collection {
return format!("new Date({access}.getTime())");
}
if resolved.is_collection
&& let Some(elem) = collection_element_type(resolved)
{
let base = resolved.base_type_name.as_str();
match base {
_ if base != "Map" && base != "Set" => {
let elem_clone = element_clone_expr(elem, registry, "v");
if elem_clone == "v" {
return format!("[...{access}]");
}
return format!("{access}.map(v => {elem_clone})");
}
"Set" => {
let elem_clone = element_clone_expr(elem, registry, "v");
if elem_clone == "v" {
return format!("new Set({access})");
}
return format!("new Set(Array.from({access}).map(v => {elem_clone}))");
}
"Map" => {
let value_clone = element_clone_expr(elem, registry, "v");
if value_clone == "v" {
return format!("new Map({access})");
}
return format!(
"new Map(Array.from({access}.entries()).map(([k, v]) => [k, {value_clone}]))"
);
}
_ => {}
}
}
generate_clone_expr_fallback(field_name, ts_type, var)
}
fn element_clone_expr(elem: &ResolvedTypeRef, registry: &TypeRegistry, var: &str) -> String {
if elem.registry_key.is_some() && type_has_derive(registry, &elem.base_type_name, "Clone") {
return format!(
"{}({var})",
standalone_fn_name(&elem.base_type_name, "Clone")
);
}
if elem.base_type_name == "Date" {
return format!("new Date({var}.getTime())");
}
if is_primitive_type(&elem.base_type_name) {
return var.to_string();
}
var.to_string()
}
fn generate_clone_expr_fallback(field_name: &str, ts_type: &str, var: &str) -> String {
let access = format!("{var}.{field_name}");
let t = ts_type.trim();
if is_primitive_type(t) {
return access;
}
if t == "Date" {
return format!("new Date({access}.getTime())");
}
if t.ends_with("[]") || t.starts_with("Array<") {
return format!("[...{access}]");
}
if t.starts_with("Set<") {
return format!("new Set({access})");
}
if t.starts_with("Map<") {
return format!("new Map({access})");
}
access
}
#[ts_macro_derive(Clone, description = "Generates a clone() method for deep cloning")]
pub fn derive_clone_macro(mut input: TsStream) -> Result<TsStream, MacroforgeError> {
let input = parse_ts_macro_input!(input as DeriveInput);
let resolved_fields = input.context.resolved_fields.as_ref();
let type_registry = input.context.type_registry.as_ref();
match &input.data {
Data::Class(class) => {
let class_name = class.inner.name.clone();
let class_ident = ts_ident!(class_name.clone());
let fn_name_ident = ts_ident!("{}Clone", class_name.to_case(Case::Camel));
let fn_name_expr: Expr = fn_name_ident.clone().into();
let mut clone_body = String::new();
for field in class.fields() {
let resolved = resolved_fields.and_then(|rf| rf.get(&field.name));
let expr = generate_clone_expr(
&field.name,
&field.ts_type,
"value",
resolved,
type_registry,
);
clone_body.push_str(&format!("cloned.{} = {};\n", field.name, expr));
}
let standalone = ts_template! {
export function @{fn_name_ident}(value: @{class_ident}): @{class_ident} {
const cloned = Object.create(Object.getPrototypeOf(value));
{$typescript TsStream::from_string(clone_body)}
return cloned;
}
};
let class_body = ts_template!(Within {
static clone(value: @{class_ident}): @{class_ident} {
return @{fn_name_expr}(value);
}
});
if std::env::var("MF_DEBUG_CLONE").is_ok() {
eprintln!("[MF_DEBUG_CLONE] standalone:\n{}", standalone.source());
eprintln!("[MF_DEBUG_CLONE] class_body:\n{}", class_body.source());
}
Ok(standalone.merge(class_body))
}
Data::Enum(_) => {
let enum_name = input.name();
let fn_name_ident = ts_ident!("{}Clone", enum_name.to_case(Case::Camel));
Ok(ts_template! {
export function @{fn_name_ident}(value: @{ts_ident!(enum_name)}): @{ts_ident!(enum_name)} {
return value;
}
})
}
Data::Interface(interface) => {
let interface_ident = ts_ident!(interface.inner.name.clone());
let fn_name_ident =
ts_ident!("{}Clone", interface.inner.name.clone().to_case(Case::Camel));
let mut clone_body = String::new();
for field in interface.fields() {
let resolved = resolved_fields.and_then(|rf| rf.get(&field.name));
let expr = generate_clone_expr(
&field.name,
&field.ts_type,
"value",
resolved,
type_registry,
);
clone_body.push_str(&format!("result.{} = {};\n", field.name, expr));
}
Ok(ts_template! {
export function @{fn_name_ident}(value: @{interface_ident}): @{interface_ident} {
const result = {} as any;
{$typescript TsStream::from_string(clone_body)}
return result as @{interface_ident};
}
})
}
Data::TypeAlias(type_alias) => {
let type_name = input.name();
let fn_name_ident = ts_ident!("{}Clone", type_name.to_case(Case::Camel));
if type_alias.is_object() {
let mut clone_body = String::new();
for field in type_alias.as_object().unwrap() {
let resolved = resolved_fields.and_then(|rf| rf.get(&field.name));
let expr = generate_clone_expr(
&field.name,
&field.ts_type,
"value",
resolved,
type_registry,
);
clone_body.push_str(&format!("result.{} = {};\n", field.name, expr));
}
Ok(ts_template! {
export function @{fn_name_ident}(value: @{ts_ident!(type_name)}): @{ts_ident!(type_name)} {
const result = {} as any;
{$typescript TsStream::from_string(clone_body)}
return result as @{ts_ident!(type_name)};
}
})
} else {
Ok(ts_template! {
export function @{fn_name_ident}(value: @{ts_ident!(type_name)}): @{ts_ident!(type_name)} {
if (typeof value === "object" && value !== null) {
return { ...value } as @{ts_ident!(type_name)};
}
return value;
}
})
}
}
}
}