use convert_case::{Case, Casing};
use crate::builtin::derive_common::{
DefaultFieldOptions, get_type_default, has_known_default, is_primitive_type, type_has_derive,
};
use crate::macros::{ts_macro_derive, ts_template};
use crate::swc_ecma_ast::{Expr, Ident};
use crate::ts_syn::abi::ir::type_registry::TypeRegistry;
use crate::ts_syn::ts_ident;
use crate::ts_syn::{
Data, DeriveInput, MacroforgeError, TsStream, emit_expr, parse_ts_expr, parse_ts_macro_input,
};
struct DefaultField {
name: String,
value: String,
}
fn validate_default_fields(
fields: &[(String, String)], parent_name: &str,
registry: Option<&TypeRegistry>,
) {
let registry = match registry {
Some(r) => r,
None => return,
};
for (field_name, ts_type) in fields {
let t = ts_type.trim();
if is_primitive_type(t)
|| t.ends_with("[]")
|| t.starts_with("Array<")
|| t.starts_with("Map<")
|| t.starts_with("Set<")
|| t == "Date"
|| t.contains('|')
{
continue;
}
let type_known = registry.get(t).is_some() || registry.get_all(t).next().is_some();
if type_known && !type_has_derive(registry, t, "Default") {
eprintln!(
"[macroforge] warning: field `{field_name}` in `{parent_name}` has type `{t}` \
which does not derive Default — generated code may fail at runtime"
);
}
}
}
#[ts_macro_derive(
Default,
description = "Generates a static defaultValue() factory method",
attributes(default)
)]
pub fn derive_default_macro(mut input: TsStream) -> Result<TsStream, MacroforgeError> {
let input = parse_ts_macro_input!(input as DeriveInput);
let type_registry = input.context.type_registry.as_ref();
match &input.data {
Data::Class(class) => {
let class_name = input.name();
let class_ident = ts_ident!(class_name);
let class_expr: Expr = class_ident.clone().into();
let fields_to_validate: Vec<(String, String)> = class
.fields()
.iter()
.filter(|f| {
!f.optional && !DefaultFieldOptions::from_decorators(&f.decorators).has_default
})
.map(|f| (f.name.clone(), f.ts_type.clone()))
.collect();
validate_default_fields(&fields_to_validate, class_name, type_registry);
let missing_defaults: Vec<&str> = class
.fields()
.iter()
.filter(|field| {
if field.optional {
return false;
}
if DefaultFieldOptions::from_decorators(&field.decorators).has_default {
return false;
}
if has_known_default(&field.ts_type) {
return false;
}
true
})
.map(|f| f.name.as_str())
.collect();
if !missing_defaults.is_empty() {
return Err(MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default) cannot determine default for non-primitive fields. Add @default(value) to: {}",
missing_defaults.join(", ")
),
));
}
let field_data: Vec<(Ident, Expr)> = class
.fields()
.iter()
.filter(|field| !field.optional)
.map(|field| {
let opts = DefaultFieldOptions::from_decorators(&field.decorators);
let default_value = opts
.value
.unwrap_or_else(|| get_type_default(&field.ts_type));
let value_expr = parse_ts_expr(&default_value).map_err(|err| {
MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default): invalid default expression for '{}': {err:?}",
field.name
),
)
})?;
Ok((ts_ident!(field.name.as_str()), *value_expr))
})
.collect::<Result<_, MacroforgeError>>()?;
let _ = &field_data; let class_body = ts_template!(Within {
static defaultValue(): @{class_ident.clone()} {
const instance = new @{class_expr.clone()}();
{#for (name_ident, value_expr) in field_data}
instance.@{name_ident} = @{value_expr};
{/for}
return instance;
}
});
let fn_name_ident = ts_ident!("{}DefaultValue", class_name.to_case(Case::Camel));
Ok(ts_template! {
{$typescript class_body}
export function @{fn_name_ident}(): @{class_ident.clone()} {
return @{class_expr.clone()}.defaultValue();
}
})
}
Data::Enum(enum_data) => {
let enum_name = input.name();
let enum_ident = ts_ident!(enum_name);
let default_variant = enum_data.variants().iter().find(|v| {
v.decorators
.iter()
.any(|d| d.name.eq_ignore_ascii_case("default"))
});
match default_variant {
Some(variant) => {
let variant_name = &variant.name;
let fn_name_ident = ts_ident!("{}DefaultValue", enum_name.to_case(Case::Camel));
let enum_expr: Expr = ts_ident!(enum_name).into();
let variant_ident = ts_ident!(variant_name.as_str());
Ok(ts_template! {
export function @{fn_name_ident}(): @{enum_ident} {
return @{enum_expr}.@{variant_ident};
}
})
}
None => Err(MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default) on enum requires exactly one variant with @default attribute. \
Add @default to one variant of {}",
enum_name
),
)),
}
}
Data::Interface(interface) => {
let interface_name = input.name();
let interface_ident = ts_ident!(interface_name);
let fields_to_validate: Vec<(String, String)> = interface
.fields()
.iter()
.filter(|f| {
!f.optional && !DefaultFieldOptions::from_decorators(&f.decorators).has_default
})
.map(|f| (f.name.clone(), f.ts_type.clone()))
.collect();
validate_default_fields(&fields_to_validate, interface_name, type_registry);
let missing_defaults: Vec<&str> = interface
.fields()
.iter()
.filter(|field| {
if field.optional {
return false;
}
if DefaultFieldOptions::from_decorators(&field.decorators).has_default {
return false;
}
if has_known_default(&field.ts_type) {
return false;
}
true
})
.map(|f| f.name.as_str())
.collect();
if !missing_defaults.is_empty() {
return Err(MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default) cannot determine default for non-primitive fields. Add @default(value) to: {}",
missing_defaults.join(", ")
),
));
}
let default_fields: Vec<DefaultField> = interface
.fields()
.iter()
.filter(|field| !field.optional)
.map(|field| {
let opts = DefaultFieldOptions::from_decorators(&field.decorators);
DefaultField {
name: field.name.clone(),
value: opts
.value
.unwrap_or_else(|| get_type_default(&field.ts_type)),
}
})
.collect();
let has_defaults = !default_fields.is_empty();
let fn_name_ident = ts_ident!("{}DefaultValue", interface_name.to_case(Case::Camel));
if has_defaults {
let object_fields: Vec<(Ident, Expr)> = default_fields
.iter()
.map(|f| {
let value_expr = parse_ts_expr(&f.value).map_err(|err| {
MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default): invalid default expression for '{}': {err:?}",
f.name
),
)
})?;
Ok((ts_ident!(f.name.as_str()), *value_expr))
})
.collect::<Result<_, MacroforgeError>>()?;
let mut props = String::new();
for (name_ident, value_expr) in &object_fields {
let name = name_ident.sym.as_ref();
let value = emit_expr(value_expr);
props.push_str(&format!("{name}: {value},\n"));
}
let return_stmt = format!("return {{\n{props}}} as {interface_name};");
let return_stmt_stream = TsStream::from_string(return_stmt);
Ok(ts_template! {
export function @{fn_name_ident}(): @{interface_ident.clone()} {
{$typescript return_stmt_stream}
}
})
} else {
let return_stmt = format!("return {{}} as {interface_name};");
let return_stmt_stream = TsStream::from_string(return_stmt);
Ok(ts_template! {
export function @{fn_name_ident}(): @{interface_ident.clone()} {
{$typescript return_stmt_stream}
}
})
}
}
Data::TypeAlias(type_alias) => {
let type_name = input.name();
let type_params = type_alias.type_params();
let (generic_decl, generic_args) = if type_params.is_empty() {
(String::new(), String::new())
} else {
let params = type_params.join(", ");
(format!("<{}>", params), format!("<{}>", params))
};
let full_type_name = format!("{}{}", type_name, generic_args);
let full_type_ident = ts_ident!(full_type_name.as_str());
let generic_decl_ident = ts_ident!(generic_decl.as_str());
if type_alias.is_object() {
let fields = type_alias.as_object().unwrap();
let fields_to_validate: Vec<(String, String)> = fields
.iter()
.filter(|f| {
!f.optional
&& !DefaultFieldOptions::from_decorators(&f.decorators).has_default
})
.map(|f| (f.name.clone(), f.ts_type.clone()))
.collect();
validate_default_fields(&fields_to_validate, type_name, type_registry);
let missing_defaults: Vec<&str> = fields
.iter()
.filter(|field| {
if field.optional {
return false;
}
if DefaultFieldOptions::from_decorators(&field.decorators).has_default {
return false;
}
if has_known_default(&field.ts_type) {
return false;
}
true
})
.map(|f| f.name.as_str())
.collect();
if !missing_defaults.is_empty() {
return Err(MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default) cannot determine default for non-primitive fields. Add @default(value) to: {}",
missing_defaults.join(", ")
),
));
}
let default_fields: Vec<DefaultField> = fields
.iter()
.filter(|field| !field.optional)
.map(|field| {
let opts = DefaultFieldOptions::from_decorators(&field.decorators);
DefaultField {
name: field.name.clone(),
value: opts
.value
.unwrap_or_else(|| get_type_default(&field.ts_type)),
}
})
.collect();
let has_defaults = !default_fields.is_empty();
let fn_name_ident = ts_ident!("{}DefaultValue", type_name.to_case(Case::Camel));
if has_defaults {
let object_fields: Vec<(Ident, Expr)> = default_fields
.iter()
.map(|f| {
let value_expr = parse_ts_expr(&f.value).map_err(|err| {
MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default): invalid default expression for '{}': {err:?}",
f.name
),
)
})?;
Ok((ts_ident!(f.name.as_str()), *value_expr))
})
.collect::<Result<_, MacroforgeError>>()?;
let mut props = String::new();
for (name_ident, value_expr) in &object_fields {
let name = name_ident.sym.as_ref();
let value = emit_expr(value_expr);
props.push_str(&format!("{name}: {value},\n"));
}
let return_stmt = format!("return {{\n{props}}} as {full_type_name};");
let return_stmt_stream = TsStream::from_string(return_stmt);
Ok(ts_template! {
export function @{fn_name_ident}@{generic_decl_ident}(): @{full_type_ident.clone()} {
{$typescript return_stmt_stream}
}
})
} else {
let return_stmt = format!("return {{}} as {full_type_name};");
let return_stmt_stream = TsStream::from_string(return_stmt);
Ok(ts_template! {
export function @{fn_name_ident}@{generic_decl_ident}(): @{full_type_ident.clone()} {
{$typescript return_stmt_stream}
}
})
}
} else if type_alias.is_union() {
let members = type_alias.as_union().unwrap();
fn build_object_default(fields: &[crate::ts_syn::InterfaceFieldIR]) -> String {
let props: Vec<String> = fields
.iter()
.map(|f| {
let opts = DefaultFieldOptions::from_decorators(&f.decorators);
let value = opts.value.unwrap_or_else(|| get_type_default(&f.ts_type));
format!("{}: {}", f.name, value)
})
.collect();
format!("({{ {} }})", props.join(", "))
}
let parenthesized: Vec<&str> = members
.iter()
.filter_map(|m| m.as_type_ref())
.filter(|t| t.trim().starts_with('('))
.collect();
if !parenthesized.is_empty() {
return Err(MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default): Parenthesized union expressions ({}) are not supported. \
Formatters cannot preserve doc comments inside parentheses. \
Create a named type alias for each variant instead \
(e.g., use `RecordLink<Product>` instead of `(string | Product)`).",
parenthesized.join(", ")
),
));
}
let default_variant_from_member = members.iter().find_map(|member| {
if member.has_decorator("default") {
if let Some(name) = member.type_name() {
return Some(name.to_string());
}
if let Some(fields) = member.as_object() {
return Some(build_object_default(fields));
}
None
} else {
None
}
});
let default_variant_from_member = default_variant_from_member.or_else(|| {
let all_objects = members.iter().all(|m| m.is_object());
if all_objects {
members
.first()
.and_then(|m| m.as_object())
.map(build_object_default)
} else {
None
}
});
let default_variant = default_variant_from_member.or_else(|| {
let default_opts = DefaultFieldOptions::from_decorators(
&input
.attrs
.iter()
.map(|a| a.inner.clone())
.collect::<Vec<_>>(),
);
default_opts.value
});
if let Some(variant) = default_variant {
if variant.is_empty() {
return Err(MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default): resolved an empty default expression for union type '{}'. \
Add @default on a variant or @default(expression) on the type.",
type_name
),
));
}
let is_expression = variant.contains('.') || variant.contains('(');
let is_string_literal = variant.starts_with('"')
|| variant.starts_with('\'')
|| variant.starts_with('`');
let is_primitive_value = variant.parse::<f64>().is_ok()
|| variant == "true"
|| variant == "false"
|| variant == "null";
let default_expr = if is_expression || is_string_literal || is_primitive_value {
variant } else {
get_type_default(&variant)
};
let type_params = type_alias.type_params();
let has_generics = !type_params.is_empty();
let generic_params = if has_generics {
format!("<{}>", type_params.join(", "))
} else {
String::new()
};
let return_type = if has_generics {
format!("{}<{}>", type_name, type_params.join(", "))
} else {
type_name.to_string()
};
let return_type_ident = ts_ident!(return_type.as_str());
let generic_params_ident = ts_ident!(generic_params.as_str());
let fn_name_ident = ts_ident!("{}DefaultValue", type_name.to_case(Case::Camel));
let return_expr = parse_ts_expr(&default_expr).map_err(|err| {
MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default): invalid default expression for '{}': {err:?}",
type_name
),
)
})?;
Ok(ts_template! {
export function @{fn_name_ident}@{generic_params_ident}(): @{return_type_ident} {
return @{return_expr};
}
})
} else {
Err(MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default) on union type '{}' requires @default on one variant \
or @default(VariantName.defaultValue()) on the type.",
type_name
),
))
}
} else {
let default_opts = DefaultFieldOptions::from_decorators(
&input
.attrs
.iter()
.map(|a| a.inner.clone())
.collect::<Vec<_>>(),
);
if let Some(default_variant) = default_opts.value {
let fn_name_ident = ts_ident!("{}DefaultValue", type_name.to_case(Case::Camel));
let return_expr = parse_ts_expr(&default_variant).map_err(|err| {
MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default): invalid default expression for '{}': {err:?}",
type_name
),
)
})?;
Ok(ts_template! {
export function @{fn_name_ident}@{generic_decl_ident}(): @{full_type_ident.clone()} {
return @{return_expr};
}
})
} else {
Err(MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Default) on type '{}' requires @default(value) to specify the default.",
type_name
),
))
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_macro_output() {
let class_name = "User";
let class_ident = ts_ident!(class_name);
let default_fields: Vec<DefaultField> = vec![
DefaultField {
name: "id".to_string(),
value: "0".to_string(),
},
DefaultField {
name: "name".to_string(),
value: r#""""#.to_string(),
},
];
let output = ts_template!(Within {
static defaultValue(): @{class_ident.clone()} {
const instance = new @{class_ident.clone()}();
{#if !default_fields.is_empty()}
{#for f in default_fields.iter()}
instance.@{ts_ident!(f.name.as_str())} = @{*parse_ts_expr(&f.value).expect("should parse")};
{/for}
{/if}
return instance;
}
});
let source = output.source();
let body_content = source
.strip_prefix("/* @macroforge:body */")
.unwrap_or(source);
let wrapped = format!("class __Temp {{ {} }}", body_content);
assert!(
macroforge_ts_syn::parse_ts_stmt(&wrapped).is_ok(),
"Generated Default macro output should parse as class members"
);
assert!(
source.contains("defaultValue"),
"Should contain defaultValue method"
);
assert!(source.contains("static"), "Should be a static method");
}
#[test]
fn test_default_field_assignment() {
let fields: Vec<DefaultField> = vec![
DefaultField {
name: "count".to_string(),
value: "42".to_string(),
},
DefaultField {
name: "items".to_string(),
value: "[]".to_string(),
},
];
let assignments = fields
.iter()
.map(|f| format!("instance.{} = {};", f.name, f.value))
.collect::<Vec<_>>()
.join("\n");
assert!(assignments.contains("instance.count = 42;"));
assert!(assignments.contains("instance.items = [];"));
}
}