use super::*;
use crate::ts_syn::ResolvedTypeRef;
use crate::ts_syn::abi::SpanIR;
fn span() -> SpanIR {
SpanIR::new(0, 0)
}
fn make_decorator(name: &str, args: &str) -> crate::ts_syn::abi::DecoratorIR {
crate::ts_syn::abi::DecoratorIR {
name: name.into(),
args_src: args.into(),
span: span(),
#[cfg(feature = "swc")]
node: None,
}
}
#[test]
fn test_compare_field_skip() {
let decorator = make_decorator("partialEq", "skip");
let opts = CompareFieldOptions::from_decorators(&[decorator], "partialEq");
assert!(opts.skip);
}
#[test]
fn test_compare_field_no_skip() {
let decorator = make_decorator("partialEq", "");
let opts = CompareFieldOptions::from_decorators(&[decorator], "partialEq");
assert!(!opts.skip);
}
#[test]
fn test_compare_field_skip_false() {
let decorator = make_decorator("hash", "skip: false");
let opts = CompareFieldOptions::from_decorators(&[decorator], "hash");
assert!(!opts.skip);
}
#[test]
fn test_default_field_with_string_value() {
let decorator = make_decorator("default", r#""hello""#);
let opts = DefaultFieldOptions::from_decorators(&[decorator]);
assert!(opts.has_default);
assert_eq!(opts.value.as_deref(), Some(r#""hello""#));
}
#[test]
fn test_default_field_with_number_value() {
let decorator = make_decorator("default", "42");
let opts = DefaultFieldOptions::from_decorators(&[decorator]);
assert!(opts.has_default);
assert_eq!(opts.value.as_deref(), Some("42"));
}
#[test]
fn test_default_field_with_array_value() {
let decorator = make_decorator("default", "[]");
let opts = DefaultFieldOptions::from_decorators(&[decorator]);
assert!(opts.has_default);
assert_eq!(opts.value.as_deref(), Some("[]"));
}
#[test]
fn test_default_field_with_named_value() {
let decorator = make_decorator("default", r#"{ value: "test" }"#);
let opts = DefaultFieldOptions::from_decorators(&[decorator]);
assert!(opts.has_default);
assert_eq!(opts.value.as_deref(), Some("test"));
}
#[test]
fn test_is_primitive_type() {
assert!(is_primitive_type("string"));
assert!(is_primitive_type("number"));
assert!(is_primitive_type("boolean"));
assert!(is_primitive_type("bigint"));
assert!(!is_primitive_type("Date"));
assert!(!is_primitive_type("User"));
assert!(!is_primitive_type("string[]"));
}
#[test]
fn test_is_numeric_type() {
assert!(is_numeric_type("number"));
assert!(is_numeric_type("bigint"));
assert!(!is_numeric_type("string"));
assert!(!is_numeric_type("boolean"));
}
#[test]
fn test_get_type_default() {
assert_eq!(get_type_default("string"), r#""""#);
assert_eq!(get_type_default("number"), "0");
assert_eq!(get_type_default("boolean"), "false");
assert_eq!(get_type_default("bigint"), "0n");
assert_eq!(get_type_default("string[]"), "[]");
assert_eq!(get_type_default("Array<number>"), "[]");
assert_eq!(get_type_default("Map<string, number>"), "new Map()");
assert_eq!(get_type_default("Set<string>"), "new Set()");
assert_eq!(get_type_default("Date"), "new Date()");
assert_eq!(get_type_default("User"), "userDefaultValue()");
assert_eq!(
get_type_default("RecordLink<Service>"),
"recordLinkDefaultValue<Service>()"
);
assert_eq!(
get_type_default("Result<User, Error>"),
"resultDefaultValue<User, Error>()"
);
assert_eq!(get_type_default("{ [key: string]: number }"), "{}");
assert_eq!(get_type_default("{ foo: string; bar: number }"), "{}");
assert_eq!(get_type_default("{ [K in keyof T]: V }"), "{}");
}
#[test]
fn test_get_type_default_object_literal_before_union_split() {
assert_eq!(get_type_default("{ a: string | number }"), "{}");
assert_eq!(
get_type_default("{ status: \"active\" | \"inactive\" }"),
"{}"
);
}
#[test]
fn test_get_type_default_union_with_primitive() {
assert_eq!(get_type_default("string | Account"), r#""""#);
assert_eq!(get_type_default("string | Employee"), r#""""#);
assert_eq!(get_type_default("string | Appointment"), r#""""#);
assert_eq!(get_type_default("string | Site"), r#""""#);
assert_eq!(get_type_default("number | Custom"), "0");
assert_eq!(get_type_default("boolean | Foo"), "false");
assert_eq!(get_type_default("bigint | Bar"), "0n");
assert_eq!(get_type_default("Account | string"), r#""""#);
}
#[test]
fn test_get_type_default_union_with_literal() {
assert_eq!(
get_type_default(r#""Estimate" | "Invoice""#),
r#""Estimate""#
);
assert_eq!(
get_type_default(r#""active" | "pending" | "completed""#),
r#""active""#
);
}
#[test]
fn test_get_type_default_union_custom_types() {
assert_eq!(
get_type_default("Account | Employee"),
"accountDefaultValue()"
);
}
#[test]
fn test_get_type_default_nullable_union() {
assert_eq!(get_type_default("string | null"), "null");
assert_eq!(get_type_default("Account | undefined"), "null");
assert_eq!(get_type_default("string | Account | null"), "null");
}
#[test]
fn test_is_generic_type() {
assert!(is_generic_type("RecordLink<Service>"));
assert!(is_generic_type("Map<string, number>"));
assert!(is_generic_type("Array<User>"));
assert!(is_generic_type("Result<T, E>"));
assert!(!is_generic_type("User"));
assert!(!is_generic_type("string"));
assert!(!is_generic_type("number[]")); }
#[test]
fn test_parse_generic_type() {
assert_eq!(
parse_generic_type("RecordLink<Service>"),
Some(("RecordLink", "Service"))
);
assert_eq!(
parse_generic_type("Map<string, number>"),
Some(("Map", "string, number"))
);
assert_eq!(
parse_generic_type("Result<Array<User>, Error>"),
Some(("Result", "Array<User>, Error"))
);
assert_eq!(parse_generic_type("User"), None);
assert_eq!(parse_generic_type("string"), None);
assert_eq!(parse_generic_type("Array<User"), None);
}
use crate::ts_syn::abi::ir::type_registry::{TypeDefinitionIR, TypeRegistry, TypeRegistryEntry};
use crate::ts_syn::abi::{ClassIR, InterfaceIR};
fn zero_span() -> SpanIR {
SpanIR::new(0, 0)
}
fn make_registry_with_derives() -> TypeRegistry {
let mut registry = TypeRegistry::new();
let user_entry = TypeRegistryEntry {
name: "User".to_string(),
file_path: "/project/src/user.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Class(ClassIR {
name: "User".to_string(),
span: zero_span(),
body_span: zero_span(),
is_abstract: false,
type_params: vec![],
heritage: vec![],
decorators: vec![make_decorator(
"derive",
"Clone, Hash, PartialEq, Debug, Default",
)],
#[cfg(feature = "swc")]
decorators_ast: vec![],
fields: vec![],
methods: vec![],
#[cfg(feature = "swc")]
members: vec![],
}),
file_imports: vec![],
};
registry.insert(user_entry, "/project");
let order_entry = TypeRegistryEntry {
name: "Order".to_string(),
file_path: "/project/src/order.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Interface(InterfaceIR {
name: "Order".to_string(),
span: zero_span(),
body_span: zero_span(),
type_params: vec![],
heritage: vec![],
decorators: vec![make_decorator("derive", "Clone")],
fields: vec![],
methods: vec![],
}),
file_imports: vec![],
};
registry.insert(order_entry, "/project");
let product_entry = TypeRegistryEntry {
name: "Product".to_string(),
file_path: "/project/src/product.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Class(ClassIR {
name: "Product".to_string(),
span: zero_span(),
body_span: zero_span(),
is_abstract: false,
type_params: vec![],
heritage: vec![],
decorators: vec![],
#[cfg(feature = "swc")]
decorators_ast: vec![],
fields: vec![],
methods: vec![],
#[cfg(feature = "swc")]
members: vec![],
}),
file_imports: vec![],
};
registry.insert(product_entry, "/project");
registry
}
#[test]
fn test_type_has_derive() {
let registry = make_registry_with_derives();
assert!(type_has_derive(®istry, "User", "Clone"));
assert!(type_has_derive(®istry, "User", "Hash"));
assert!(type_has_derive(®istry, "User", "PartialEq"));
assert!(type_has_derive(®istry, "User", "Debug"));
assert!(type_has_derive(®istry, "User", "Default"));
assert!(!type_has_derive(®istry, "User", "Ord"));
assert!(type_has_derive(®istry, "Order", "Clone"));
assert!(!type_has_derive(®istry, "Order", "Hash"));
assert!(!type_has_derive(®istry, "Product", "Clone"));
assert!(!type_has_derive(®istry, "Unknown", "Clone"));
}
#[test]
fn test_type_has_derive_ambiguous_name() {
let mut registry = TypeRegistry::new();
let phone_entry = TypeRegistryEntry {
name: "PhoneNumber".to_string(),
file_path: "/project/src/types/phone-number.svelte.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Interface(InterfaceIR {
name: "PhoneNumber".to_string(),
span: zero_span(),
body_span: zero_span(),
type_params: vec![],
heritage: vec![],
decorators: vec![make_decorator(
"derive",
"Default, Serialize, Deserialize, Gigaform",
)],
fields: vec![],
methods: vec![],
}),
file_imports: vec![],
};
registry.insert(phone_entry, "/project");
let barrel_entry = TypeRegistryEntry {
name: "PhoneNumber".to_string(),
file_path: "/project/src/types/all-types.svelte.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Interface(InterfaceIR {
name: "PhoneNumber".to_string(),
span: zero_span(),
body_span: zero_span(),
type_params: vec![],
heritage: vec![],
decorators: vec![make_decorator(
"derive",
"Default, Serialize, Deserialize, Gigaform",
)],
fields: vec![],
methods: vec![],
}),
file_imports: vec![],
};
registry.insert(barrel_entry, "/project");
assert!(
registry
.ambiguous_names
.contains(&"PhoneNumber".to_string())
);
assert!(type_has_derive(®istry, "PhoneNumber", "Gigaform"));
assert!(type_has_derive(®istry, "PhoneNumber", "Default"));
assert!(type_has_derive(®istry, "PhoneNumber", "Serialize"));
assert!(type_has_derive(®istry, "PhoneNumber", "Deserialize"));
}
#[test]
fn test_type_has_derive_case_insensitive() {
let registry = make_registry_with_derives();
assert!(type_has_derive(®istry, "User", "clone"));
assert!(type_has_derive(®istry, "User", "CLONE"));
}
#[test]
fn test_resolved_type_has_derive() {
let registry = make_registry_with_derives();
let resolved = ResolvedTypeRef {
raw_type: "User".to_string(),
base_type_name: "User".to_string(),
registry_key: Some("src/user.ts::User".to_string()),
is_collection: false,
is_optional: false,
type_args: vec![],
};
assert!(resolved_type_has_derive(®istry, &resolved, "Clone"));
assert!(!resolved_type_has_derive(®istry, &resolved, "Ord"));
}
#[test]
fn test_collection_element_type() {
let user_ref = ResolvedTypeRef {
raw_type: "User".to_string(),
base_type_name: "User".to_string(),
registry_key: Some("src/user.ts::User".to_string()),
is_collection: false,
is_optional: false,
type_args: vec![],
};
let array_ref = ResolvedTypeRef {
raw_type: "User[]".to_string(),
base_type_name: "User".to_string(),
registry_key: Some("src/user.ts::User".to_string()),
is_collection: true,
is_optional: false,
type_args: vec![user_ref.clone()],
};
let elem = collection_element_type(&array_ref);
assert!(elem.is_some());
assert_eq!(elem.unwrap().base_type_name, "User");
let string_ref = ResolvedTypeRef {
raw_type: "string".to_string(),
base_type_name: "string".to_string(),
registry_key: None,
is_collection: false,
is_optional: false,
type_args: vec![],
};
let map_ref = ResolvedTypeRef {
raw_type: "Map<string, User>".to_string(),
base_type_name: "Map".to_string(),
registry_key: None,
is_collection: true,
is_optional: false,
type_args: vec![string_ref.clone(), user_ref.clone()],
};
let elem = collection_element_type(&map_ref);
assert!(elem.is_some());
assert_eq!(elem.unwrap().base_type_name, "User");
assert!(collection_element_type(&user_ref).is_none());
}
#[test]
fn test_map_key_type() {
let string_ref = ResolvedTypeRef {
raw_type: "string".to_string(),
base_type_name: "string".to_string(),
registry_key: None,
is_collection: false,
is_optional: false,
type_args: vec![],
};
let user_ref = ResolvedTypeRef {
raw_type: "User".to_string(),
base_type_name: "User".to_string(),
registry_key: Some("src/user.ts::User".to_string()),
is_collection: false,
is_optional: false,
type_args: vec![],
};
let map_ref = ResolvedTypeRef {
raw_type: "Map<string, User>".to_string(),
base_type_name: "Map".to_string(),
registry_key: None,
is_collection: true,
is_optional: false,
type_args: vec![string_ref.clone(), user_ref.clone()],
};
let key = map_key_type(&map_ref);
assert!(key.is_some());
assert_eq!(key.unwrap().base_type_name, "string");
assert!(map_key_type(&user_ref).is_none());
}
#[test]
fn test_standalone_fn_name() {
assert_eq!(standalone_fn_name("User", "Clone"), "userClone");
assert_eq!(standalone_fn_name("User", "HashCode"), "userHashCode");
assert_eq!(standalone_fn_name("User", "Equals"), "userEquals");
assert_eq!(standalone_fn_name("Order", "ToString"), "orderToString");
assert_eq!(
standalone_fn_name("MyLongType", "PartialCompare"),
"myLongTypePartialCompare"
);
}
use crate::ts_syn::DataTypeAlias;
use crate::ts_syn::abi::InterfaceFieldIR;
use crate::ts_syn::abi::ir::type_alias::{TypeAliasIR, TypeBody, TypeMember, TypeMemberKind};
fn make_field(name: &str, ts_type: &str) -> InterfaceFieldIR {
InterfaceFieldIR {
name: name.to_string(),
span: zero_span(),
ts_type: ts_type.to_string(),
optional: false,
readonly: false,
decorators: vec![],
}
}
#[test]
fn test_flatten_intersection_all_inline_objects() {
let members = vec![
TypeMember::new(TypeMemberKind::Object {
fields: vec![make_field("variant", "'Account'")],
}),
TypeMember::new(TypeMemberKind::Object {
fields: vec![make_field("id", "number"), make_field("name", "string")],
}),
];
let result = flatten_intersection_fields(&members, None);
assert!(result.is_some());
let fields = result.unwrap();
assert_eq!(fields.len(), 3);
assert_eq!(fields[0].name, "variant");
assert_eq!(fields[1].name, "id");
assert_eq!(fields[2].name, "name");
}
#[test]
fn test_flatten_intersection_deduplicates_fields() {
let members = vec![
TypeMember::new(TypeMemberKind::Object {
fields: vec![make_field("id", "number"), make_field("name", "string")],
}),
TypeMember::new(TypeMemberKind::Object {
fields: vec![make_field("id", "number"), make_field("email", "string")],
}),
];
let result = flatten_intersection_fields(&members, None);
assert!(result.is_some());
let fields = result.unwrap();
assert_eq!(fields.len(), 3); assert_eq!(fields[0].name, "id");
assert_eq!(fields[1].name, "name");
assert_eq!(fields[2].name, "email");
}
#[test]
fn test_flatten_intersection_with_type_ref_resolved() {
let mut registry = TypeRegistry::new();
let account_entry = TypeRegistryEntry {
name: "Account".to_string(),
file_path: "/project/src/account.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Interface(InterfaceIR {
name: "Account".to_string(),
span: zero_span(),
body_span: zero_span(),
type_params: vec![],
heritage: vec![],
decorators: vec![],
fields: vec![make_field("id", "number"), make_field("balance", "number")],
methods: vec![],
}),
file_imports: vec![],
};
registry.insert(account_entry, "/project");
let members = vec![
TypeMember::new(TypeMemberKind::Object {
fields: vec![make_field("variant", "'Account'")],
}),
TypeMember::new(TypeMemberKind::TypeRef("Account".to_string())),
];
let result = flatten_intersection_fields(&members, Some(®istry));
assert!(result.is_some());
let fields = result.unwrap();
assert_eq!(fields.len(), 3);
assert_eq!(fields[0].name, "variant");
assert_eq!(fields[1].name, "id");
assert_eq!(fields[2].name, "balance");
}
#[test]
fn test_flatten_intersection_unresolvable_type_ref_returns_none() {
let members = vec![
TypeMember::new(TypeMemberKind::Object {
fields: vec![make_field("variant", "'Account'")],
}),
TypeMember::new(TypeMemberKind::TypeRef("Account".to_string())),
];
let result = flatten_intersection_fields(&members, None);
assert!(result.is_none());
}
#[test]
fn test_flatten_intersection_type_ref_not_in_registry() {
let registry = TypeRegistry::new();
let members = vec![TypeMember::new(TypeMemberKind::TypeRef(
"Unknown".to_string(),
))];
let result = flatten_intersection_fields(&members, Some(®istry));
assert!(result.is_none());
}
#[test]
fn test_flatten_intersection_skips_literals() {
let members = vec![
TypeMember::new(TypeMemberKind::Literal("'active'".to_string())),
TypeMember::new(TypeMemberKind::Object {
fields: vec![make_field("id", "number")],
}),
];
let result = flatten_intersection_fields(&members, None);
assert!(result.is_some());
let fields = result.unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].name, "id");
}
#[test]
fn test_get_effective_fields_object_type() {
let ta = DataTypeAlias {
inner: TypeAliasIR {
name: "Point".to_string(),
span: zero_span(),
decorators: vec![],
type_params: vec![],
body: TypeBody::Object {
fields: vec![make_field("x", "number"), make_field("y", "number")],
},
},
};
let result = get_effective_fields(&ta, None);
assert!(result.is_some());
assert_eq!(result.unwrap().len(), 2);
}
#[test]
fn test_get_effective_fields_intersection_type() {
let ta = DataTypeAlias {
inner: TypeAliasIR {
name: "AdminUser".to_string(),
span: zero_span(),
decorators: vec![],
type_params: vec![],
body: TypeBody::Intersection(vec![
TypeMember::new(TypeMemberKind::Object {
fields: vec![make_field("role", "'admin'")],
}),
TypeMember::new(TypeMemberKind::Object {
fields: vec![make_field("name", "string")],
}),
]),
},
};
let result = get_effective_fields(&ta, None);
assert!(result.is_some());
let fields = result.unwrap();
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name, "role");
assert_eq!(fields[1].name, "name");
}
#[test]
fn test_get_effective_fields_union_returns_none() {
let ta = DataTypeAlias {
inner: TypeAliasIR {
name: "Status".to_string(),
span: zero_span(),
decorators: vec![],
type_params: vec![],
body: TypeBody::Union(vec![
TypeMember::new(TypeMemberKind::Literal("'active'".to_string())),
TypeMember::new(TypeMemberKind::Literal("'inactive'".to_string())),
]),
},
};
let result = get_effective_fields(&ta, None);
assert!(result.is_none());
}
#[test]
fn test_fields_from_definition_interface() {
let def = TypeDefinitionIR::Interface(InterfaceIR {
name: "User".to_string(),
span: zero_span(),
body_span: zero_span(),
type_params: vec![],
heritage: vec![],
decorators: vec![],
fields: vec![make_field("id", "number"), make_field("name", "string")],
methods: vec![],
});
let result = fields_from_definition(&def, None);
assert!(result.is_some());
let fields = result.unwrap();
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name, "id");
}