use convert_case::{Case, Casing};
use crate::builtin::derive_common::{
CompareFieldOptions, 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_expr, parse_ts_macro_input, ts_ident,
};
pub struct HashField {
pub name: String,
pub ts_type: String,
}
pub fn generate_field_hash_for_interface(
field: &HashField,
var: &str,
resolved: Option<&ResolvedTypeRef>,
registry: Option<&TypeRegistry>,
) -> String {
let field_name = &field.name;
let ts_type = &field.ts_type;
if let (Some(resolved), Some(registry)) = (resolved, registry) {
if !resolved.is_collection
&& resolved.registry_key.is_some()
&& type_has_derive(registry, &resolved.base_type_name, "Hash")
{
let fn_name = standalone_fn_name(&resolved.base_type_name, "HashCode");
return format!("{fn_name}({var}.{field_name})");
}
if resolved.is_collection
&& let Some(elem) = collection_element_type(resolved)
&& elem.registry_key.is_some()
&& type_has_derive(registry, &elem.base_type_name, "Hash")
{
let elem_fn = standalone_fn_name(&elem.base_type_name, "HashCode");
let base = resolved.base_type_name.as_str();
match base {
"Map" => {
return format!(
"({var}.{field_name} instanceof Map \
? Array.from({var}.{field_name}.entries()).reduce((h, [k, v]) => \
(h * 31 + String(k).split('').reduce((hh, c) => (hh * 31 + c.charCodeAt(0)) | 0, 0) + \
{elem_fn}(v)) | 0, 0) \
: 0)"
);
}
"Set" => {
return format!(
"({var}.{field_name} instanceof Set \
? Array.from({var}.{field_name}).reduce((h, v) => \
(h * 31 + {elem_fn}(v)) | 0, 0) \
: 0)"
);
}
_ => {
return format!(
"(Array.isArray({var}.{field_name}) \
? {var}.{field_name}.reduce((h, v) => \
(h * 31 + {elem_fn}(v)) | 0, 0) \
: 0)"
);
}
}
}
}
if is_primitive_type(ts_type) {
match ts_type.as_str() {
"number" => {
format!(
"(Number.isInteger({var}.{field_name}) \
? {var}.{field_name} | 0 \
: {var}.{field_name}.toString().split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))"
)
}
"bigint" => {
format!(
"{var}.{field_name}.toString().split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)"
)
}
"string" => {
format!(
"({var}.{field_name} ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)"
)
}
"boolean" => {
format!("({var}.{field_name} ? 1231 : 1237)")
}
_ => {
format!("({var}.{field_name} != null ? 1 : 0)")
}
}
} else if ts_type.ends_with("[]") || ts_type.starts_with("Array<") {
format!(
"(Array.isArray({var}.{field_name}) \
? {var}.{field_name}.reduce((h, v) => \
(h * 31 + (typeof (v as any)?.hashCode === 'function' \
? (v as any).hashCode() \
: (v != null ? String(v).split('').reduce((hh, c) => (hh * 31 + c.charCodeAt(0)) | 0, 0) : 0))) | 0, 0) \
: 0)"
)
} else if ts_type == "Date" {
format!("({var}.{field_name} instanceof Date ? {var}.{field_name}.getTime() | 0 : 0)")
} else if ts_type.starts_with("Map<") {
format!(
"({var}.{field_name} instanceof Map \
? Array.from({var}.{field_name}.entries()).reduce((h, [k, v]) => \
(h * 31 + String(k).split('').reduce((hh, c) => (hh * 31 + c.charCodeAt(0)) | 0, 0) + \
(typeof (v as any)?.hashCode === 'function' ? (v as any).hashCode() : 0)) | 0, 0) \
: 0)"
)
} else if ts_type.starts_with("Set<") {
format!(
"({var}.{field_name} instanceof Set \
? Array.from({var}.{field_name}).reduce((h, v) => \
(h * 31 + (typeof (v as any)?.hashCode === 'function' \
? (v as any).hashCode() \
: (v != null ? String(v).split('').reduce((hh, c) => (hh * 31 + c.charCodeAt(0)) | 0, 0) : 0))) | 0, 0) \
: 0)"
)
} else {
format!(
"(typeof ({var}.{field_name} as any)?.hashCode === 'function' \
? ({var}.{field_name} as any).hashCode() \
: ({var}.{field_name} != null \
? JSON.stringify({var}.{field_name}).split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0) \
: 0))"
)
}
}
#[ts_macro_derive(
Hash,
description = "Generates a hashCode() method for hashing",
attributes(hash)
)]
pub fn derive_hash_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 = input.name();
let class_ident = ts_ident!(class_name);
let hash_fields: Vec<HashField> = class
.fields()
.iter()
.filter_map(|field| {
let opts = CompareFieldOptions::from_decorators(&field.decorators, "hash");
if opts.skip {
return None;
}
Some(HashField {
name: field.name.clone(),
ts_type: field.ts_type.clone(),
})
})
.collect();
let _has_fields = !hash_fields.is_empty();
let fn_name_ident = ts_ident!("{}HashCode", class_name.to_case(Case::Camel));
let fn_name_expr: Expr = fn_name_ident.clone().into();
let _hash_exprs: Vec<Expr> = hash_fields
.iter()
.map(|f| {
let resolved = resolved_fields.and_then(|rf| rf.get(&f.name));
let expr_src =
generate_field_hash_for_interface(f, "value", resolved, type_registry);
let expr = parse_ts_expr(&expr_src).map_err(|err| {
MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Hash): invalid hash expression for '{}': {err:?}",
f.name
),
)
})?;
Ok(*expr)
})
.collect::<Result<_, MacroforgeError>>()?;
let standalone = ts_template! {
export function @{fn_name_ident}(value: @{class_ident.clone()}): number {
let hash = 17;
{#if _has_fields}
{#for hash_expr in _hash_exprs}
hash = (hash * 31 + @{hash_expr}) | 0;
{/for}
{/if}
return hash;
}
};
let class_body = ts_template!(Within {
static hashCode(value: @{class_ident.clone()}): number {
return @{fn_name_expr}(value);
}
});
Ok(ts_template! {
{$typescript standalone}
{$typescript class_body}
})
}
Data::Enum(enum_data) => {
let enum_name = input.name();
let fn_name_ident = ts_ident!("{}HashCode", enum_name.to_case(Case::Camel));
let is_string_enum = enum_data.variants().iter().all(|v| v.value.is_string());
if is_string_enum {
Ok(ts_template! {
export function @{fn_name_ident}(value: @{ts_ident!(enum_name)}): number {
let hash = 0;
for (let i = 0; i < value.length; i++) {
hash = (hash * 31 + value.charCodeAt(i)) | 0;
}
return hash;
}
})
} else {
Ok(ts_template! {
export function @{fn_name_ident}(value: @{ts_ident!(enum_name)}): number {
return value as number;
}
})
}
}
Data::Interface(interface) => {
let interface_name = input.name();
let hash_fields: Vec<HashField> = interface
.fields()
.iter()
.filter_map(|field| {
let opts = CompareFieldOptions::from_decorators(&field.decorators, "hash");
if opts.skip {
return None;
}
Some(HashField {
name: field.name.clone(),
ts_type: field.ts_type.clone(),
})
})
.collect();
let _has_fields = !hash_fields.is_empty();
let _hash_exprs: Vec<Expr> = hash_fields
.iter()
.map(|f| {
let resolved = resolved_fields.and_then(|rf| rf.get(&f.name));
let expr_src =
generate_field_hash_for_interface(f, "value", resolved, type_registry);
let expr = parse_ts_expr(&expr_src).map_err(|err| {
MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Hash): invalid hash expression for '{}': {err:?}",
f.name
),
)
})?;
Ok(*expr)
})
.collect::<Result<_, MacroforgeError>>()?;
let fn_name_ident = ts_ident!("{}HashCode", interface_name.to_case(Case::Camel));
Ok(ts_template! {
export function @{fn_name_ident}(value: @{ts_ident!(interface_name)}): number {
let hash = 17;
{#if _has_fields}
{#for hash_expr in _hash_exprs}
hash = (hash * 31 + @{hash_expr}) | 0;
{/for}
{/if}
return hash;
}
})
}
Data::TypeAlias(type_alias) => {
let type_name = input.name();
if type_alias.is_object() {
let hash_fields: Vec<HashField> = type_alias
.as_object()
.unwrap()
.iter()
.filter_map(|field| {
let opts = CompareFieldOptions::from_decorators(&field.decorators, "hash");
if opts.skip {
return None;
}
Some(HashField {
name: field.name.clone(),
ts_type: field.ts_type.clone(),
})
})
.collect();
let _has_fields = !hash_fields.is_empty();
let _hash_exprs: Vec<Expr> = hash_fields
.iter()
.map(|f| {
let resolved = resolved_fields.and_then(|rf| rf.get(&f.name));
let expr_src =
generate_field_hash_for_interface(f, "value", resolved, type_registry);
let expr = parse_ts_expr(&expr_src).map_err(|err| {
MacroforgeError::new(
input.decorator_span(),
format!(
"@derive(Hash): invalid hash expression for '{}': {err:?}",
f.name
),
)
})?;
Ok(*expr)
})
.collect::<Result<_, MacroforgeError>>()?;
let fn_name_ident = ts_ident!("{}HashCode", type_name.to_case(Case::Camel));
Ok(ts_template! {
export function @{fn_name_ident}(value: @{ts_ident!(type_name)}): number {
let hash = 17;
{#if _has_fields}
{#for hash_expr in _hash_exprs}
hash = (hash * 31 + @{hash_expr}) | 0;
{/for}
{/if}
return hash;
}
})
} else {
let fn_name_ident = ts_ident!("{}HashCode", type_name.to_case(Case::Camel));
Ok(ts_template! {
export function @{fn_name_ident}(value: @{ts_ident!(type_name)}): number {
const str = JSON.stringify(value);
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash * 31 + str.charCodeAt(i)) | 0;
}
return hash;
}
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_macro_output() {
let hash_fields: Vec<HashField> = vec![HashField {
name: "id".to_string(),
ts_type: "number".to_string(),
}];
let _has_fields = !hash_fields.is_empty();
let _hash_exprs: Vec<Expr> = hash_fields
.iter()
.map(|f| {
let expr_src = generate_field_hash_for_interface(f, "value", None, None);
*parse_ts_expr(&expr_src).expect("hash expr should parse")
})
.collect();
let output = ts_template!(Within {
hashCode(): number {
let hash = 17;
{#if _has_fields}
{#for hash_expr in _hash_exprs}
hash = (hash * 31 + @{hash_expr}) | 0;
{/for}
{/if}
return hash;
}
});
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 Hash macro output should parse as class members"
);
assert!(
source.contains("hashCode"),
"Should contain hashCode method"
);
}
#[test]
fn test_field_hash_number() {
let field = HashField {
name: "id".to_string(),
ts_type: "number".to_string(),
};
let result = generate_field_hash_for_interface(&field, "value", None, None);
assert!(result.contains("Number.isInteger"));
}
#[test]
fn test_field_hash_string() {
let field = HashField {
name: "name".to_string(),
ts_type: "string".to_string(),
};
let result = generate_field_hash_for_interface(&field, "value", None, None);
assert!(result.contains("split"));
assert!(result.contains("charCodeAt"));
}
#[test]
fn test_field_hash_boolean() {
let field = HashField {
name: "active".to_string(),
ts_type: "boolean".to_string(),
};
let result = generate_field_hash_for_interface(&field, "value", None, None);
assert!(result.contains("1231")); assert!(result.contains("1237"));
}
#[test]
fn test_field_hash_date() {
let field = HashField {
name: "createdAt".to_string(),
ts_type: "Date".to_string(),
};
let result = generate_field_hash_for_interface(&field, "value", None, None);
assert!(result.contains("getTime"));
}
#[test]
fn test_field_hash_object() {
let field = HashField {
name: "user".to_string(),
ts_type: "User".to_string(),
};
let result = generate_field_hash_for_interface(&field, "value", None, None);
assert!(result.contains("hashCode"));
}
}