use super::*;
use crate::SubtypeFailureReason;
use crate::TypeInterner;
use crate::db::QueryDatabase;
use crate::def::DefId;
use crate::{
CallSignature, CallableShape, ConditionalType, FunctionShape, IndexSignature, MappedType,
ObjectFlags, ObjectShape, ParamInfo, PropertyInfo, SymbolRef, TemplateSpan, TupleElement,
TypeEnvironment, TypeParamInfo, TypeSubstitution, Visibility, instantiate_type,
};
fn make_animal_dog(interner: &TypeInterner) -> (TypeId, TypeId) {
let name = interner.intern_string("name");
let breed = interner.intern_string("breed");
let animal = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let dog = interner.object(vec![
PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
},
PropertyInfo::new(breed, TypeId::STRING),
]);
(animal, dog)
}
fn make_object_interface(interner: &TypeInterner) -> TypeId {
let method = |return_type| FunctionShape {
params: Vec::new(),
this_type: None,
return_type,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
};
let method_with_any = |return_type| FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::ANY)],
this_type: None,
return_type,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
};
let constructor = PropertyInfo::new(interner.intern_string("constructor"), TypeId::ANY);
let to_string = PropertyInfo::method(
interner.intern_string("toString"),
interner.function(method(TypeId::STRING)),
);
let to_locale = PropertyInfo::method(
interner.intern_string("toLocaleString"),
interner.function(method(TypeId::STRING)),
);
let value_of = PropertyInfo::method(
interner.intern_string("valueOf"),
interner.function(method(TypeId::ANY)),
);
let has_own = PropertyInfo::method(
interner.intern_string("hasOwnProperty"),
interner.function(method_with_any(TypeId::BOOLEAN)),
);
let is_proto = PropertyInfo::method(
interner.intern_string("isPrototypeOf"),
interner.function(method_with_any(TypeId::BOOLEAN)),
);
let prop_enum = PropertyInfo::method(
interner.intern_string("propertyIsEnumerable"),
interner.function(method_with_any(TypeId::BOOLEAN)),
);
interner.object(vec![
constructor,
to_string,
to_locale,
value_of,
has_own,
is_proto,
prop_enum,
])
}
#[test]
fn test_any_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
assert!(checker.is_assignable(TypeId::ANY, TypeId::STRING));
assert!(checker.is_assignable(TypeId::STRING, TypeId::ANY));
}
#[test]
fn test_unknown_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
assert!(checker.is_assignable(TypeId::STRING, TypeId::UNKNOWN));
assert!(checker.is_assignable(TypeId::UNKNOWN, TypeId::ANY));
assert!(checker.is_assignable(TypeId::UNKNOWN, TypeId::UNKNOWN));
assert!(!checker.is_assignable(TypeId::UNKNOWN, TypeId::STRING));
}
#[test]
fn test_error_type_permissive() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
assert!(checker.is_assignable(TypeId::ERROR, TypeId::STRING));
assert!(checker.is_assignable(TypeId::STRING, TypeId::ERROR));
assert!(checker.is_assignable(TypeId::ERROR, TypeId::ERROR));
}
#[test]
fn test_error_poisoning_union_normalization() {
let interner = TypeInterner::new();
let union = interner.union(vec![TypeId::STRING, TypeId::ERROR]);
assert_eq!(union, TypeId::ERROR);
}
#[test]
fn test_recursion_depth_limit_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
fn nest_array(interner: &TypeInterner, base: TypeId, depth: usize) -> TypeId {
let mut ty = base;
for _ in 0..depth {
ty = interner.array(ty);
}
ty
}
let deep_string = nest_array(&interner, TypeId::STRING, 120);
let deep_number = nest_array(&interner, TypeId::NUMBER, 120);
let result = checker.is_assignable(deep_string, deep_number);
assert!(!result);
let deep_string2 = nest_array(&interner, TypeId::STRING, 120);
assert!(checker.is_assignable(deep_string, deep_string2));
}
#[test]
fn test_base_constraint_assignability_compat() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let t_param = interner.intern(TypeData::TypeParameter(TypeParamInfo {
name: interner.intern_string("T"),
constraint: Some(TypeId::STRING),
default: None,
is_const: false,
}));
let u_param = interner.intern(TypeData::TypeParameter(TypeParamInfo {
name: interner.intern_string("U"),
constraint: Some(TypeId::STRING),
default: None,
is_const: false,
}));
let v_param = interner.intern(TypeData::TypeParameter(TypeParamInfo {
name: interner.intern_string("V"),
constraint: Some(TypeId::NUMBER),
default: None,
is_const: false,
}));
assert!(checker.is_assignable(t_param, TypeId::STRING));
assert!(!checker.is_assignable(t_param, TypeId::NUMBER));
assert!(!checker.is_assignable(t_param, u_param));
assert!(!checker.is_assignable(t_param, v_param));
}
#[test]
fn test_function_bivariance_default() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let (animal, dog) = make_animal_dog(&interner);
let fn_dog = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(dog)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let fn_animal = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(animal)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(fn_dog, fn_animal));
}
#[test]
fn test_function_variance_strict() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let (animal, dog) = make_animal_dog(&interner);
let fn_dog = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(dog)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let fn_animal = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(animal)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(!checker.is_assignable(fn_dog, fn_animal));
}
#[test]
fn test_array_covariance_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let (animal, dog) = make_animal_dog(&interner);
let dog_array = interner.array(dog);
let animal_array = interner.array(animal);
assert!(checker.is_assignable(dog_array, animal_array));
assert!(!checker.is_assignable(animal_array, dog_array));
}
#[test]
fn test_optional_parameter_assignability_allows_extra_optional() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let source = interner.function(FunctionShape {
params: vec![
ParamInfo {
name: Some(interner.intern_string("x")),
type_id: TypeId::STRING,
optional: false,
rest: false,
},
ParamInfo {
name: Some(interner.intern_string("y")),
type_id: TypeId::NUMBER,
optional: true,
rest: false,
},
],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.function(FunctionShape {
params: vec![ParamInfo {
name: Some(interner.intern_string("x")),
type_id: TypeId::STRING,
optional: false,
rest: false,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_optional_parameter_assignability_rejects_required_extra() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let source = interner.function(FunctionShape {
params: vec![
ParamInfo {
name: Some(interner.intern_string("x")),
type_id: TypeId::STRING,
optional: false,
rest: false,
},
ParamInfo {
name: Some(interner.intern_string("y")),
type_id: TypeId::NUMBER,
optional: false,
rest: false,
},
],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.function(FunctionShape {
params: vec![
ParamInfo {
name: Some(interner.intern_string("x")),
type_id: TypeId::STRING,
optional: false,
rest: false,
},
ParamInfo {
name: Some(interner.intern_string("y")),
type_id: TypeId::NUMBER,
optional: true,
rest: false,
},
],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_this_parameter_assignability_respects_strictness() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let source = interner.function(FunctionShape {
params: Vec::new(),
this_type: Some(TypeId::STRING),
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let string_or_number = interner.union(vec![TypeId::STRING, TypeId::NUMBER]);
let target = interner.function(FunctionShape {
params: Vec::new(),
this_type: Some(string_or_number),
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(source, target));
checker.set_strict_function_types(true);
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_rest_parameter_assignability_rejects_incompatible_fixed() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let source = interner.function(FunctionShape {
params: vec![ParamInfo {
name: Some(interner.intern_string("args")),
type_id: interner.array(TypeId::STRING),
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.function(FunctionShape {
params: vec![ParamInfo {
name: Some(interner.intern_string("x")),
type_id: TypeId::NUMBER,
optional: false,
rest: false,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(!checker.is_assignable(source, target));
}
#[test]
#[ignore = "Method bivariance/strict function types not fully implemented"]
fn test_method_bivariance_even_strict() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let name = interner.intern_string("fn");
let string_or_number = interner.union(vec![TypeId::STRING, TypeId::NUMBER]);
let source_fn = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target_fn = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(string_or_number)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let source = interner.object(vec![PropertyInfo {
name,
type_id: source_fn,
write_type: source_fn,
optional: false,
readonly: false,
is_method: true,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object(vec![PropertyInfo {
name,
type_id: target_fn,
write_type: target_fn,
optional: false,
readonly: false,
is_method: true,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_function_property_stays_strict() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let name = interner.intern_string("fn");
let string_or_number = interner.union(vec![TypeId::STRING, TypeId::NUMBER]);
let source_fn = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target_fn = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(string_or_number)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let source = interner.object(vec![PropertyInfo {
name,
type_id: source_fn,
write_type: source_fn,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object(vec![PropertyInfo {
name,
type_id: target_fn,
write_type: target_fn,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_function_return_covariance() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let (animal, dog) = make_animal_dog(&interner);
let returns_dog = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: dog,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let returns_animal = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: animal,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(returns_dog, returns_animal));
assert!(!checker.is_assignable(returns_animal, returns_dog));
}
#[test]
fn test_void_return_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let returns_number = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::NUMBER,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let returns_void = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(returns_number, returns_void));
assert!(!checker.is_assignable(returns_void, returns_number));
}
#[test]
fn test_void_undefined_return_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let returns_void = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let returns_undefined = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::UNDEFINED,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(returns_undefined, returns_void));
assert!(!checker.is_assignable(returns_void, returns_undefined));
}
#[test]
fn test_constructor_void_return_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let instance = interner.object(vec![PropertyInfo::new(
interner.intern_string("value"),
TypeId::NUMBER,
)]);
let returns_instance = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: instance,
type_params: Vec::new(),
type_predicate: None,
is_constructor: true,
is_method: false,
});
let returns_void = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: true,
is_method: false,
});
assert!(checker.is_assignable(returns_instance, returns_void));
assert!(!checker.is_assignable(returns_void, returns_instance));
}
#[test]
fn test_construct_signature_void_return_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let instance = interner.object(vec![PropertyInfo::new(
interner.intern_string("value"),
TypeId::NUMBER,
)]);
let returns_instance = interner.callable(CallableShape {
symbol: None,
call_signatures: Vec::new(),
construct_signatures: vec![CallSignature {
params: Vec::new(),
this_type: None,
return_type: instance,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
properties: Vec::new(),
..Default::default()
});
let returns_void = interner.callable(CallableShape {
symbol: None,
call_signatures: Vec::new(),
construct_signatures: vec![CallSignature {
params: Vec::new(),
this_type: None,
return_type: TypeId::VOID,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
properties: Vec::new(),
..Default::default()
});
assert!(checker.is_assignable(returns_instance, returns_void));
assert!(!checker.is_assignable(returns_void, returns_instance));
}
#[test]
fn test_call_signature_void_return_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let returns_number = interner.callable(CallableShape {
symbol: None,
call_signatures: vec![CallSignature {
params: Vec::new(),
this_type: None,
return_type: TypeId::NUMBER,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
let returns_void = interner.callable(CallableShape {
symbol: None,
call_signatures: vec![CallSignature {
params: Vec::new(),
this_type: None,
return_type: TypeId::VOID,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
assert!(checker.is_assignable(returns_number, returns_void));
assert!(!checker.is_assignable(returns_void, returns_number));
}
#[test]
fn test_call_signature_void_undefined_return_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let returns_void = interner.callable(CallableShape {
symbol: None,
call_signatures: vec![CallSignature {
params: Vec::new(),
this_type: None,
return_type: TypeId::VOID,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
let returns_undefined = interner.callable(CallableShape {
symbol: None,
call_signatures: vec![CallSignature {
params: Vec::new(),
this_type: None,
return_type: TypeId::UNDEFINED,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
assert!(checker.is_assignable(returns_undefined, returns_void));
assert!(!checker.is_assignable(returns_void, returns_undefined));
}
#[test]
fn test_explain_failure_missing_property() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let (animal, dog) = make_animal_dog(&interner);
let breed_name = interner.intern_string("breed");
let reason = checker.explain_failure(animal, dog);
assert!(
matches!(reason, Some(SubtypeFailureReason::MissingProperty { property_name, .. })
if property_name == breed_name)
);
}
#[test]
fn test_explain_failure_parameter_mismatch_strict() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let (animal, dog) = make_animal_dog(&interner);
let fn_dog = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(dog)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let fn_animal = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(animal)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let reason = checker.explain_failure(fn_dog, fn_animal);
assert!(matches!(
reason,
Some(SubtypeFailureReason::ParameterTypeMismatch { param_index: 0, .. })
));
}
#[test]
fn test_weak_type_rejects_no_common_properties() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let weak_target = interner.object(vec![PropertyInfo::opt(a, TypeId::NUMBER)]);
let source = interner.object(vec![PropertyInfo::new(b, TypeId::NUMBER)]);
assert!(!checker.is_assignable(source, weak_target));
assert!(matches!(
checker.explain_failure(source, weak_target),
Some(SubtypeFailureReason::NoCommonProperties { .. })
));
}
#[test]
fn test_weak_type_allows_overlap() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let weak_target = interner.object(vec![PropertyInfo::opt(a, TypeId::NUMBER)]);
let source = interner.object(vec![PropertyInfo::new(a, TypeId::NUMBER)]);
assert!(checker.is_assignable(source, weak_target));
}
#[test]
fn test_weak_type_skips_empty_target() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let empty_target = interner.object(Vec::new());
let source = interner.object(vec![PropertyInfo::new(a, TypeId::NUMBER)]);
assert!(checker.is_assignable(source, empty_target));
}
#[test]
fn test_weak_union_rejects_no_common_properties() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let c = interner.intern_string("c");
let weak_a = interner.object(vec![PropertyInfo::opt(a, TypeId::NUMBER)]);
let weak_b = interner.object(vec![PropertyInfo::opt(b, TypeId::NUMBER)]);
let target = interner.union(vec![weak_a, weak_b]);
let source = interner.object(vec![PropertyInfo::new(c, TypeId::NUMBER)]);
assert!(!checker.is_assignable(source, target));
assert!(matches!(
checker.explain_failure(source, target),
Some(SubtypeFailureReason::TypeMismatch { .. })
));
}
#[test]
fn test_weak_union_rejects_no_common_properties_with_refs() {
let interner = TypeInterner::new();
let mut env = TypeEnvironment::new();
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let c = interner.intern_string("c");
let weak_a = interner.object(vec![PropertyInfo::opt(a, TypeId::NUMBER)]);
let weak_b = interner.object(vec![PropertyInfo::opt(b, TypeId::NUMBER)]);
let def_id_a = DefId(1);
let def_id_b = DefId(2);
env.insert_def(def_id_a, weak_a);
env.insert_def(def_id_b, weak_b);
let target = interner.union(vec![interner.lazy(def_id_a), interner.lazy(def_id_b)]);
let source = interner.object(vec![PropertyInfo::new(c, TypeId::NUMBER)]);
let mut checker = CompatChecker::with_resolver(&interner, &env);
assert!(!checker.is_assignable(source, target));
assert!(matches!(
checker.explain_failure(source, target),
Some(SubtypeFailureReason::TypeMismatch { .. })
));
}
#[test]
fn test_weak_union_allows_overlap() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let weak_a = interner.object(vec![PropertyInfo::opt(a, TypeId::NUMBER)]);
let weak_b = interner.object(vec![PropertyInfo::opt(b, TypeId::NUMBER)]);
let target = interner.union(vec![weak_a, weak_b]);
let source = interner.object(vec![PropertyInfo::new(a, TypeId::NUMBER)]);
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_weak_union_source_with_one_common_member_allows() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let c = interner.intern_string("c");
let weak_a = interner.object(vec![PropertyInfo::opt(a, TypeId::NUMBER)]);
let weak_b = interner.object(vec![PropertyInfo::opt(b, TypeId::NUMBER)]);
let target = interner.union(vec![weak_a, weak_b]);
let source_with_a = interner.object(vec![PropertyInfo::new(a, TypeId::NUMBER)]);
let source_with_c = interner.object(vec![PropertyInfo::new(c, TypeId::NUMBER)]);
let source = interner.union(vec![source_with_a, source_with_c]);
assert!(
checker.is_assignable(source, target),
"Union source with one overlapping member should be assignable to weak union target"
);
}
#[test]
fn test_weak_union_source_all_members_lack_common_rejects() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let c = interner.intern_string("c");
let d = interner.intern_string("d");
let weak_a = interner.object(vec![PropertyInfo::opt(a, TypeId::NUMBER)]);
let weak_b = interner.object(vec![PropertyInfo::opt(b, TypeId::NUMBER)]);
let target = interner.union(vec![weak_a, weak_b]);
let source_with_c = interner.object(vec![PropertyInfo::new(c, TypeId::NUMBER)]);
let source_with_d = interner.object(vec![PropertyInfo::new(d, TypeId::NUMBER)]);
let source = interner.union(vec![source_with_c, source_with_d]);
assert!(
!checker.is_assignable(source, target),
"Union source where all members lack common property should be rejected"
);
}
#[test]
fn test_weak_union_nested_union_source() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let c = interner.intern_string("c");
let d = interner.intern_string("d");
let weak_a = interner.object(vec![PropertyInfo::opt(a, TypeId::NUMBER)]);
let weak_b = interner.object(vec![PropertyInfo::opt(b, TypeId::NUMBER)]);
let target = interner.union(vec![weak_a, weak_b]);
let source_with_a = interner.object(vec![PropertyInfo::new(a, TypeId::NUMBER)]);
let source_with_c = interner.object(vec![PropertyInfo::new(c, TypeId::NUMBER)]);
let source_with_d = interner.object(vec![PropertyInfo::new(d, TypeId::NUMBER)]);
let inner_union = interner.union(vec![source_with_a, source_with_c]);
let source = interner.union(vec![inner_union, source_with_d]);
assert!(
checker.is_assignable(source, target),
"Nested union source with one overlapping member should be assignable"
);
}
#[test]
fn test_weak_union_with_intersection_source() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let c = interner.intern_string("c");
let weak_a = interner.object(vec![PropertyInfo::opt(a, TypeId::NUMBER)]);
let weak_b = interner.object(vec![PropertyInfo::opt(b, TypeId::NUMBER)]);
let target = interner.union(vec![weak_a, weak_b]);
let source = interner.object(vec![
PropertyInfo::new(a, TypeId::NUMBER),
PropertyInfo::new(c, TypeId::NUMBER),
]);
assert!(
checker.is_assignable(source, target),
"Intersection source with common property should be assignable to weak union"
);
}
#[test]
fn test_rest_any_bivariant_even_strict() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let rest_any = interner.array(TypeId::ANY);
let target = interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: rest_any,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let source = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::NUMBER)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_rest_unknown_bivariant_even_strict() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let rest_unknown = interner.array(TypeId::UNKNOWN);
let target = interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: rest_unknown,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let source = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_rest_unknown_bivariant_strict_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let rest_unknown = interner.array(TypeId::UNKNOWN);
let target = interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: rest_unknown,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let source = interner.function(FunctionShape {
params: vec![
ParamInfo::unnamed(TypeId::NUMBER),
ParamInfo::unnamed(TypeId::STRING),
],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable_strict(source, target));
}
#[test]
fn test_rest_number_not_bivariant_even_strict() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let rest_number = interner.array(TypeId::NUMBER);
let target = interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: rest_number,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let source = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_rest_unknown_vs_number_assignability_strict() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let rest_unknown = interner.array(TypeId::UNKNOWN);
let target_unknown = interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: rest_unknown,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let rest_number = interner.array(TypeId::NUMBER);
let target_number = interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: rest_number,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let source = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(source, target_unknown));
assert!(!checker.is_assignable(source, target_number));
}
#[test]
fn test_rest_any_still_checks_return_type() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let rest_any = interner.array(TypeId::ANY);
let target = interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: rest_any,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::NUMBER,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let source = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_explain_failure_skips_rest_unknown() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let rest_unknown = interner.array(TypeId::UNKNOWN);
let target = interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: rest_unknown,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let source = interner.function(FunctionShape {
params: vec![
ParamInfo::unnamed(TypeId::NUMBER),
ParamInfo::unnamed(TypeId::STRING),
],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.explain_failure(source, target).is_none());
}
#[test]
fn test_explain_failure_reports_rest_mismatch() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let rest_number = interner.array(TypeId::NUMBER);
let target = interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: rest_number,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let source = interner.function(FunctionShape {
params: vec![
ParamInfo::unnamed(TypeId::NUMBER),
ParamInfo::unnamed(TypeId::STRING),
],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let reason = checker.explain_failure(source, target);
assert!(matches!(
reason,
Some(SubtypeFailureReason::ParameterTypeMismatch { .. })
));
if let Some(SubtypeFailureReason::ParameterTypeMismatch {
param_index,
source_param,
target_param,
}) = reason
{
assert_eq!(param_index, 1);
assert_eq!(source_param, TypeId::STRING);
assert_eq!(target_param, TypeId::NUMBER);
}
}
#[test]
fn test_explain_failure_reports_rest_mismatch_source_rest() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let source = interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: interner.array(TypeId::STRING),
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::NUMBER)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let reason = checker.explain_failure(source, target);
assert!(matches!(
reason,
Some(SubtypeFailureReason::ParameterTypeMismatch { .. })
));
if let Some(SubtypeFailureReason::ParameterTypeMismatch {
param_index,
source_param,
target_param,
}) = reason
{
assert_eq!(param_index, 0);
assert_eq!(source_param, TypeId::STRING);
assert_eq!(target_param, TypeId::NUMBER);
}
}
#[test]
fn test_empty_object_accepts_non_nullish() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let empty_object = interner.object(Vec::new());
assert!(checker.is_assignable(TypeId::STRING, empty_object));
assert!(checker.is_assignable(TypeId::NUMBER, empty_object));
let array = interner.array(TypeId::NUMBER);
assert!(checker.is_assignable(array, empty_object));
let func = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(func, empty_object));
}
#[test]
fn test_empty_object_rejects_nullish_and_unknown() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let empty_object = interner.object(Vec::new());
assert!(!checker.is_assignable(TypeId::NULL, empty_object));
assert!(!checker.is_assignable(TypeId::UNDEFINED, empty_object));
assert!(!checker.is_assignable(TypeId::VOID, empty_object));
assert!(!checker.is_assignable(TypeId::UNKNOWN, empty_object));
}
#[test]
fn test_strict_null_checks_toggle() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let empty_object = interner.object(Vec::new());
let nullable_string = interner.union(vec![TypeId::STRING, TypeId::NULL]);
assert!(!checker.is_assignable(TypeId::NULL, TypeId::STRING));
assert!(!checker.is_assignable(nullable_string, TypeId::STRING));
assert!(!checker.is_assignable(nullable_string, empty_object));
checker.set_strict_null_checks(false);
assert!(checker.is_assignable(TypeId::NULL, TypeId::STRING));
assert!(checker.is_assignable(TypeId::UNDEFINED, TypeId::NUMBER));
assert!(checker.is_assignable(nullable_string, TypeId::STRING));
assert!(checker.is_assignable(TypeId::UNDEFINED, empty_object));
assert!(checker.is_assignable(nullable_string, empty_object));
}
#[test]
fn test_no_unchecked_indexed_access_toggle() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let indexed = interner.object_with_index(ObjectShape {
symbol: None,
flags: ObjectFlags::empty(),
properties: Vec::new(),
string_index: Some(IndexSignature {
key_type: TypeId::STRING,
value_type: TypeId::NUMBER,
readonly: false,
}),
number_index: None,
});
let index_access = interner.intern(TypeData::IndexAccess(indexed, TypeId::STRING));
let number_or_undefined = interner.union(vec![TypeId::NUMBER, TypeId::UNDEFINED]);
assert!(checker.is_assignable(index_access, TypeId::NUMBER));
checker.set_no_unchecked_indexed_access(true);
assert!(!checker.is_assignable(index_access, TypeId::NUMBER));
assert!(checker.is_assignable(index_access, number_or_undefined));
}
#[test]
fn test_no_unchecked_indexed_access_primitive_index() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let index_access = interner.intern(TypeData::IndexAccess(TypeId::STRING, TypeId::NUMBER));
let string_or_undefined = interner.union(vec![TypeId::STRING, TypeId::UNDEFINED]);
assert!(checker.is_assignable(index_access, TypeId::STRING));
checker.set_no_unchecked_indexed_access(true);
assert!(!checker.is_assignable(index_access, TypeId::STRING));
assert!(checker.is_assignable(index_access, string_or_undefined));
}
#[test]
fn test_no_unchecked_indexed_access_array_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let string_array = interner.array(TypeId::STRING);
let index_access = interner.intern(TypeData::IndexAccess(string_array, TypeId::NUMBER));
let string_or_undefined = interner.union(vec![TypeId::STRING, TypeId::UNDEFINED]);
assert!(checker.is_assignable(index_access, TypeId::STRING));
checker.set_no_unchecked_indexed_access(true);
assert!(!checker.is_assignable(index_access, TypeId::STRING));
assert!(checker.is_assignable(index_access, string_or_undefined));
}
#[test]
fn test_no_unchecked_object_index_signature_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let indexed = interner.object_with_index(ObjectShape {
symbol: None,
flags: ObjectFlags::empty(),
properties: Vec::new(),
string_index: Some(IndexSignature {
key_type: TypeId::STRING,
value_type: TypeId::NUMBER,
readonly: false,
}),
number_index: None,
});
let index_access = interner.intern(TypeData::IndexAccess(indexed, TypeId::NUMBER));
let number_or_undefined = interner.union(vec![TypeId::NUMBER, TypeId::UNDEFINED]);
assert!(checker.is_assignable(index_access, TypeId::NUMBER));
checker.set_no_unchecked_indexed_access(true);
assert!(!checker.is_assignable(index_access, TypeId::NUMBER));
assert!(checker.is_assignable(index_access, number_or_undefined));
}
#[test]
fn test_correlated_union_index_access_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let kind = interner.intern_string("kind");
let key_a = interner.intern_string("a");
let key_b = interner.intern_string("b");
let obj_a = interner.object(vec![
PropertyInfo::new(kind, interner.literal_string("a")),
PropertyInfo::new(key_a, TypeId::NUMBER),
]);
let obj_b = interner.object(vec![
PropertyInfo::new(kind, interner.literal_string("b")),
PropertyInfo::new(key_b, TypeId::STRING),
]);
let union_obj = interner.union(vec![obj_a, obj_b]);
let key_union = interner.union(vec![
interner.literal_string("a"),
interner.literal_string("b"),
]);
let index_access = interner.intern(TypeData::IndexAccess(union_obj, key_union));
let expected = interner.union(vec![TypeId::NUMBER, TypeId::STRING]);
assert!(checker.is_assignable(index_access, expected));
assert!(!checker.is_assignable(index_access, TypeId::NUMBER));
}
#[test]
fn test_object_keyword_accepts_non_primitives() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("name");
let obj = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(checker.is_assignable(obj, TypeId::OBJECT));
let array = interner.array(TypeId::NUMBER);
assert!(checker.is_assignable(array, TypeId::OBJECT));
let tuple = interner.tuple(vec![TupleElement {
type_id: TypeId::NUMBER,
name: None,
optional: false,
rest: false,
}]);
assert!(checker.is_assignable(tuple, TypeId::OBJECT));
let func = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(func, TypeId::OBJECT));
}
#[test]
fn test_object_keyword_rejects_primitives() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
assert!(!checker.is_assignable(TypeId::STRING, TypeId::OBJECT));
assert!(!checker.is_assignable(TypeId::NUMBER, TypeId::OBJECT));
}
#[test]
fn test_object_interface_accepts_primitives() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let object_interface = make_object_interface(&interner);
assert!(checker.is_assignable(TypeId::STRING, object_interface));
assert!(checker.is_assignable(TypeId::NUMBER, object_interface));
assert!(checker.is_assignable(TypeId::BOOLEAN, object_interface));
assert!(checker.is_assignable(TypeId::SYMBOL, object_interface));
let template = interner.template_literal(vec![
TemplateSpan::Text(interner.intern_string("prefix")),
TemplateSpan::Type(TypeId::STRING),
TemplateSpan::Text(interner.intern_string("suffix")),
]);
assert!(checker.is_assignable(template, object_interface));
}
#[test]
fn test_object_trifecta_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let empty_object = interner.object(Vec::new());
let object_interface = make_object_interface(&interner);
assert!(checker.is_assignable(TypeId::STRING, empty_object));
assert!(checker.is_assignable(TypeId::STRING, object_interface));
assert!(!checker.is_assignable(TypeId::STRING, TypeId::OBJECT));
}
#[test]
fn test_split_accessor_allows_wider_setter_in_source() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("x");
let source = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: interner.union2(TypeId::STRING, TypeId::NUMBER),
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_split_accessor_rejects_wider_setter_in_target() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("x");
let source = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: interner.union2(TypeId::STRING, TypeId::NUMBER),
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_function_type_accepts_callables() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let function_top = interner.callable(CallableShape {
symbol: None,
call_signatures: Vec::new(),
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
let function = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::NUMBER)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(function, function_top));
let callable = interner.callable(CallableShape {
symbol: None,
call_signatures: vec![CallSignature {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::BOOLEAN,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
assert!(checker.is_assignable(callable, function_top));
}
#[test]
fn test_function_type_rejects_non_callables() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let function_top = interner.callable(CallableShape {
symbol: None,
call_signatures: Vec::new(),
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
let name = interner.intern_string("name");
let obj = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(!checker.is_assignable(obj, function_top));
}
#[test]
fn test_function_type_not_assignable_to_specific_callable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let function_top = interner.callable(CallableShape {
symbol: None,
call_signatures: Vec::new(),
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
let specific_callable = interner.callable(CallableShape {
symbol: None,
call_signatures: vec![CallSignature {
params: vec![ParamInfo::unnamed(TypeId::NUMBER)],
this_type: None,
return_type: TypeId::STRING,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
assert!(!checker.is_assignable(function_top, specific_callable));
}
#[test]
fn test_tuple_array_assignability_tuple_to_array() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let tuple = interner.tuple(vec![
TupleElement {
type_id: TypeId::STRING,
name: None,
optional: false,
rest: false,
},
TupleElement {
type_id: TypeId::NUMBER,
name: None,
optional: false,
rest: false,
},
]);
let elem_union = interner.union(vec![TypeId::STRING, TypeId::NUMBER]);
let array = interner.array(elem_union);
assert!(checker.is_assignable(tuple, array));
}
#[test]
fn test_tuple_array_assignability_tuple_to_array_rejects() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let tuple = interner.tuple(vec![
TupleElement {
type_id: TypeId::STRING,
name: None,
optional: false,
rest: false,
},
TupleElement {
type_id: TypeId::NUMBER,
name: None,
optional: false,
rest: false,
},
]);
let array = interner.array(TypeId::STRING);
assert!(!checker.is_assignable(tuple, array));
}
#[test]
fn test_tuple_array_assignability_array_to_tuple_rejects() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let array = interner.array(TypeId::STRING);
let tuple = interner.tuple(vec![
TupleElement {
type_id: TypeId::STRING,
name: None,
optional: false,
rest: false,
},
TupleElement {
type_id: TypeId::STRING,
name: None,
optional: false,
rest: false,
},
]);
assert!(!checker.is_assignable(array, tuple));
}
#[test]
fn test_tuple_array_assignability_empty_array_to_optional_tuple() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let never_array = interner.array(TypeId::NEVER);
let optional_tuple = interner.tuple(vec![TupleElement {
type_id: TypeId::STRING,
name: None,
optional: true,
rest: false,
}]);
let empty_tuple = interner.tuple(Vec::new());
let rest_tuple = interner.tuple(vec![TupleElement {
type_id: interner.array(TypeId::STRING),
name: None,
optional: false,
rest: true,
}]);
assert!(checker.is_assignable(never_array, empty_tuple));
assert!(checker.is_assignable(never_array, optional_tuple));
assert!(checker.is_assignable(never_array, rest_tuple));
}
#[test]
fn test_apparent_string_members_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let length = interner.intern_string("length");
let to_upper = interner.intern_string("toUpperCase");
let to_upper_type = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.object(vec![
PropertyInfo::new(length, TypeId::NUMBER),
PropertyInfo::method(to_upper, to_upper_type),
]);
assert!(checker.is_assignable(TypeId::STRING, target));
let literal = interner.literal_string("hello");
assert!(checker.is_assignable(literal, target));
}
#[test]
fn test_apparent_string_members_include_substr_and_locale_compare() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let locale_compare = interner.intern_string("localeCompare");
let substr = interner.intern_string("substr");
let locale_compare_type = interner.function(FunctionShape {
params: vec![ParamInfo {
name: Some(interner.intern_string("that")),
type_id: TypeId::ANY,
optional: false,
rest: false,
}],
this_type: None,
return_type: TypeId::NUMBER,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let substr_type = interner.function(FunctionShape {
params: vec![
ParamInfo {
name: Some(interner.intern_string("start")),
type_id: TypeId::ANY,
optional: false,
rest: false,
},
ParamInfo {
name: Some(interner.intern_string("length")),
type_id: TypeId::ANY,
optional: true,
rest: false,
},
],
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.object(vec![
PropertyInfo::method(locale_compare, locale_compare_type),
PropertyInfo::method(substr, substr_type),
]);
assert!(checker.is_assignable(TypeId::STRING, target));
}
#[test]
fn test_apparent_string_members_include_legacy_and_unicode() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let normalize = interner.intern_string("normalize");
let is_well_formed = interner.intern_string("isWellFormed");
let fontcolor = interner.intern_string("fontcolor");
let normalize_type = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let is_well_formed_type = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::BOOLEAN,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let fontcolor_type = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.object(vec![
PropertyInfo::method(normalize, normalize_type),
PropertyInfo::method(is_well_formed, is_well_formed_type),
PropertyInfo::method(fontcolor, fontcolor_type),
]);
assert!(checker.is_assignable(TypeId::STRING, target));
}
#[test]
fn test_apparent_string_members_reject_mismatch() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let length = interner.intern_string("length");
let target = interner.object(vec![PropertyInfo::new(length, TypeId::STRING)]);
assert!(!checker.is_assignable(TypeId::STRING, target));
}
#[test]
fn test_apparent_number_method_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let to_fixed = interner.intern_string("toFixed");
let to_fixed_type = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.object(vec![PropertyInfo::method(to_fixed, to_fixed_type)]);
assert!(checker.is_assignable(TypeId::NUMBER, target));
}
#[test]
fn test_apparent_number_method_not_assignable_to_number() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let to_fixed = interner.intern_string("toFixed");
let to_fixed_type = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.object(vec![PropertyInfo::method(to_fixed, to_fixed_type)]);
assert!(!checker.is_assignable(target, TypeId::NUMBER));
}
#[test]
fn test_apparent_number_member_rejects_mismatch() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let rest_any = interner.array(TypeId::ANY);
let method = |return_type| {
interner.function(FunctionShape {
params: vec![ParamInfo {
name: None,
type_id: rest_any,
optional: false,
rest: true,
}],
this_type: None,
return_type,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
})
};
let to_fixed = interner.intern_string("toFixed");
let mismatch = interner.object(vec![PropertyInfo::method(to_fixed, method(TypeId::NUMBER))]);
assert!(!checker.is_assignable(TypeId::NUMBER, mismatch));
}
#[test]
fn test_number_interface_boxing_assignability() {
let interner = TypeInterner::new();
let def_id = DefId(1);
let number_interface = interner.lazy(def_id);
let to_fixed = interner.intern_string("toFixed");
let to_fixed_type = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let number_object = interner.object(vec![PropertyInfo::method(to_fixed, to_fixed_type)]);
let mut env = TypeEnvironment::new();
env.insert_def(def_id, number_object);
let mut checker = CompatChecker::with_resolver(&interner, &env);
assert!(checker.is_assignable(TypeId::NUMBER, number_interface));
assert!(!checker.is_assignable(number_interface, TypeId::NUMBER));
}
#[test]
fn test_apparent_boolean_members_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let to_string = interner.intern_string("toString");
let to_string_type = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.object(vec![PropertyInfo::method(to_string, to_string_type)]);
assert!(checker.is_assignable(TypeId::BOOLEAN, target));
}
#[test]
fn test_apparent_bigint_members_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let value_of = interner.intern_string("valueOf");
let value_of_type = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::BIGINT,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.object(vec![PropertyInfo::method(value_of, value_of_type)]);
assert!(checker.is_assignable(TypeId::BIGINT, target));
}
#[test]
fn test_apparent_symbol_members_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let description = interner.intern_string("description");
let to_string = interner.intern_string("toString");
let description_type = interner.union(vec![TypeId::STRING, TypeId::UNDEFINED]);
let to_string_type = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let target = interner.object(vec![
PropertyInfo::new(description, description_type),
PropertyInfo::method(to_string, to_string_type),
]);
assert!(checker.is_assignable(TypeId::SYMBOL, target));
}
#[test]
fn test_apparent_string_number_index_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let target = interner.object_with_index(ObjectShape {
symbol: None,
flags: ObjectFlags::empty(),
properties: Vec::new(),
string_index: None,
number_index: Some(IndexSignature {
key_type: TypeId::NUMBER,
value_type: TypeId::STRING,
readonly: false,
}),
});
assert!(checker.is_assignable(TypeId::STRING, target));
}
#[test]
fn test_apparent_string_rejects_string_index_signature() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let target = interner.object_with_index(ObjectShape {
symbol: None,
flags: ObjectFlags::empty(),
properties: Vec::new(),
string_index: Some(IndexSignature {
key_type: TypeId::STRING,
value_type: TypeId::STRING,
readonly: false,
}),
number_index: None,
});
assert!(!checker.is_assignable(TypeId::STRING, target));
}
#[test]
fn test_optional_property_allows_undefined() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("x");
let source = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::UNDEFINED,
write_type: TypeId::UNDEFINED,
optional: true,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: true,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_optional_property_rejects_required_target() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("x");
let source = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: true,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_optional_property_rejects_string_index_signature() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("x");
let source = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: true,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object_with_index(ObjectShape {
symbol: None,
flags: ObjectFlags::empty(),
properties: Vec::new(),
string_index: Some(IndexSignature {
key_type: TypeId::STRING,
value_type: TypeId::NUMBER,
readonly: false,
}),
number_index: None,
});
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_exact_optional_property_rejects_undefined() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_exact_optional_property_types(true);
let name = interner.intern_string("x");
let source = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::UNDEFINED,
write_type: TypeId::UNDEFINED,
optional: true,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: true,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_exact_optional_property_allows_string_index_signature() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_exact_optional_property_types(true);
let name = interner.intern_string("x");
let source = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: true,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object_with_index(ObjectShape {
symbol: None,
flags: ObjectFlags::empty(),
properties: Vec::new(),
string_index: Some(IndexSignature {
key_type: TypeId::STRING,
value_type: TypeId::NUMBER,
readonly: false,
}),
number_index: None,
});
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_rest_any_callable_target_from_function() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let rest_any = interner.array(TypeId::ANY);
let target = interner.callable(CallableShape {
symbol: None,
call_signatures: vec![CallSignature {
params: vec![ParamInfo {
name: None,
type_id: rest_any,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
let source = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::NUMBER)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_rest_unknown_callable_target_from_callable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let rest_unknown = interner.array(TypeId::UNKNOWN);
let target = interner.callable(CallableShape {
symbol: None,
call_signatures: vec![CallSignature {
params: vec![ParamInfo {
name: None,
type_id: rest_unknown,
optional: false,
rest: true,
}],
this_type: None,
return_type: TypeId::VOID,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
let source = interner.callable(CallableShape {
symbol: None,
call_signatures: vec![CallSignature {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::VOID,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
construct_signatures: Vec::new(),
properties: Vec::new(),
..Default::default()
});
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_mapped_type_over_number_keys_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let constraint = interner.intern(TypeData::KeyOf(TypeId::NUMBER));
let mapped = interner.mapped(MappedType {
type_param: TypeParamInfo {
name: interner.intern_string("K"),
constraint: None,
default: None,
is_const: false,
},
constraint,
name_type: None,
template: TypeId::BOOLEAN,
readonly_modifier: None,
optional_modifier: None,
});
let to_fixed = interner.intern_string("toFixed");
let expected = interner.object(vec![PropertyInfo::new(to_fixed, TypeId::BOOLEAN)]);
let mismatch = interner.object(vec![PropertyInfo::new(to_fixed, TypeId::NUMBER)]);
assert!(checker.is_assignable(mapped, expected));
assert!(!checker.is_assignable(mapped, mismatch));
assert!(!checker.is_assignable(expected, mapped));
}
#[test]
fn test_mapped_type_over_string_keys_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let constraint = interner.intern(TypeData::KeyOf(TypeId::STRING));
let mapped = interner.mapped(MappedType {
type_param: TypeParamInfo {
name: interner.intern_string("K"),
constraint: None,
default: None,
is_const: false,
},
constraint,
name_type: None,
template: TypeId::BOOLEAN,
readonly_modifier: None,
optional_modifier: None,
});
let to_upper = interner.intern_string("toUpperCase");
let expected = interner.object(vec![PropertyInfo::new(to_upper, TypeId::BOOLEAN)]);
let mismatch = interner.object(vec![PropertyInfo::new(to_upper, TypeId::NUMBER)]);
assert!(checker.is_assignable(mapped, expected));
assert!(!checker.is_assignable(mapped, mismatch));
assert!(!checker.is_assignable(expected, mapped));
}
#[test]
fn test_mapped_type_over_boolean_keys_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let constraint = interner.intern(TypeData::KeyOf(TypeId::BOOLEAN));
let mapped = interner.mapped(MappedType {
type_param: TypeParamInfo {
name: interner.intern_string("K"),
constraint: None,
default: None,
is_const: false,
},
constraint,
name_type: None,
template: TypeId::NUMBER,
readonly_modifier: None,
optional_modifier: None,
});
let to_string = interner.intern_string("toString");
let expected = interner.object(vec![PropertyInfo::new(to_string, TypeId::NUMBER)]);
let mismatch = interner.object(vec![PropertyInfo::new(to_string, TypeId::STRING)]);
assert!(checker.is_assignable(mapped, expected));
assert!(!checker.is_assignable(mapped, mismatch));
assert!(!checker.is_assignable(expected, mapped));
}
#[test]
fn test_mapped_type_key_remap_filters_keys() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let prop_a = PropertyInfo::new(interner.intern_string("a"), TypeId::STRING);
let prop_b = PropertyInfo::new(interner.intern_string("b"), TypeId::NUMBER);
let obj = interner.object(vec![prop_a.clone(), prop_b.clone()]);
let key_a = interner.literal_string("a");
let key_b = interner.literal_string("b");
let keys = interner.union(vec![key_a, key_b]);
let key_param = TypeParamInfo {
name: interner.intern_string("K"),
constraint: Some(keys),
default: None,
is_const: false,
};
let key_param_id = interner.intern(TypeData::TypeParameter(key_param.clone()));
let name_type = interner.conditional(ConditionalType {
check_type: key_param_id,
extends_type: key_a,
true_type: TypeId::NEVER,
false_type: key_param_id,
is_distributive: true,
});
let template = interner.intern(TypeData::IndexAccess(obj, key_param_id));
let mapped = interner.mapped(MappedType {
type_param: key_param,
constraint: keys,
name_type: Some(name_type),
template,
readonly_modifier: None,
optional_modifier: None,
});
let expected = interner.object(vec![prop_b]);
let requires_a = interner.object(vec![prop_a]);
assert!(checker.is_assignable(mapped, expected));
assert!(checker.is_assignable(expected, mapped));
assert!(!checker.is_assignable(mapped, requires_a));
}
#[test]
fn test_conditional_tuple_wrapper_no_distribution_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let t_name = interner.intern_string("T");
let t_param = interner.intern(TypeData::TypeParameter(TypeParamInfo {
name: t_name,
constraint: None,
default: None,
is_const: false,
}));
let tuple_check = interner.tuple(vec![TupleElement {
type_id: t_param,
name: None,
optional: false,
rest: false,
}]);
let tuple_extends = interner.tuple(vec![TupleElement {
type_id: TypeId::STRING,
name: None,
optional: false,
rest: false,
}]);
let conditional = interner.conditional(ConditionalType {
check_type: tuple_check,
extends_type: tuple_extends,
true_type: TypeId::NUMBER,
false_type: TypeId::BOOLEAN,
is_distributive: false,
});
let string_or_number = interner.union(vec![TypeId::STRING, TypeId::NUMBER]);
let mut subst = TypeSubstitution::new();
subst.insert(t_name, string_or_number);
let instantiated = instantiate_type(&interner, conditional, &subst);
assert!(checker.is_assignable(instantiated, TypeId::BOOLEAN));
assert!(!checker.is_assignable(instantiated, TypeId::NUMBER));
}
#[test]
fn test_keyof_intersection_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let obj_a = interner.object(vec![PropertyInfo::new(
interner.intern_string("a"),
TypeId::NUMBER,
)]);
let obj_b = interner.object(vec![PropertyInfo::new(
interner.intern_string("b"),
TypeId::STRING,
)]);
let intersection = interner.intersection(vec![obj_a, obj_b]);
let keyof_a = interner.intern(TypeData::KeyOf(obj_a));
let keyof_intersection = interner.intern(TypeData::KeyOf(intersection));
assert!(checker.is_assignable(keyof_a, keyof_intersection));
assert!(!checker.is_assignable(keyof_intersection, keyof_a));
}
#[test]
fn test_keyof_union_index_signature_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let string_index = interner.object_with_index(ObjectShape {
symbol: None,
flags: ObjectFlags::empty(),
properties: Vec::new(),
string_index: Some(IndexSignature {
key_type: TypeId::STRING,
value_type: TypeId::NUMBER,
readonly: false,
}),
number_index: None,
});
let number_index = interner.object_with_index(ObjectShape {
symbol: None,
flags: ObjectFlags::empty(),
properties: Vec::new(),
string_index: None,
number_index: Some(IndexSignature {
key_type: TypeId::NUMBER,
value_type: TypeId::NUMBER,
readonly: false,
}),
});
let union = interner.union(vec![string_index, number_index]);
let keyof_union = interner.intern(TypeData::KeyOf(union));
assert!(checker.is_assignable(keyof_union, TypeId::NUMBER));
assert!(!checker.is_assignable(keyof_union, TypeId::STRING));
}
#[test]
fn test_keyof_union_intersection_only_shared_keys() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let prop_a = PropertyInfo::new(interner.intern_string("a"), TypeId::NUMBER);
let prop_b = PropertyInfo::new(interner.intern_string("b"), TypeId::STRING);
let prop_c = PropertyInfo::new(interner.intern_string("c"), TypeId::BOOLEAN);
let obj_ab = interner.object(vec![prop_a.clone(), prop_b]);
let obj_ac = interner.object(vec![prop_a, prop_c]);
let union = interner.union(vec![obj_ab, obj_ac]);
let keyof_union = interner.intern(TypeData::KeyOf(union));
let key_a = interner.literal_string("a");
let key_b = interner.literal_string("b");
let key_c = interner.literal_string("c");
assert!(checker.is_assignable(key_a, keyof_union));
assert!(!checker.is_assignable(key_b, keyof_union));
assert!(!checker.is_assignable(key_c, keyof_union));
}
#[test]
fn test_intersection_reduction_disjoint_discriminant_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let kind = interner.intern_string("kind");
let obj_a = interner.object(vec![PropertyInfo::new(kind, interner.literal_string("a"))]);
let obj_b = interner.object(vec![PropertyInfo::new(kind, interner.literal_string("b"))]);
let intersection = interner.intersection(vec![obj_a, obj_b]);
assert!(checker.is_assignable(intersection, TypeId::NEVER));
assert!(checker.is_assignable(intersection, TypeId::STRING));
}
#[test]
fn test_intersection_reduction_disjoint_primitives() {
let interner = TypeInterner::new();
let intersection = interner.intersection(vec![TypeId::STRING, TypeId::NUMBER]);
assert_eq!(intersection, TypeId::NEVER);
}
#[test]
fn test_unique_symbol_nominal_assignability() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let sym_a = interner.intern(TypeData::UniqueSymbol(SymbolRef(1)));
let sym_b = interner.intern(TypeData::UniqueSymbol(SymbolRef(2)));
assert!(checker.is_assignable(sym_a, TypeId::SYMBOL));
assert!(!checker.is_assignable(TypeId::SYMBOL, sym_a));
assert!(checker.is_assignable(sym_a, sym_a));
assert!(!checker.is_assignable(sym_a, sym_b));
}
#[test]
fn test_template_literal_expansion_limit_widens_to_string() {
let interner = TypeInterner::new();
let count = crate::intern::TEMPLATE_LITERAL_EXPANSION_LIMIT + 1;
let mut members = Vec::with_capacity(count);
for idx in 0..count {
let literal = interner.literal_string(&format!("k{idx}"));
members.push(literal);
}
let union = interner.union(members);
let template = interner.template_literal(vec![TemplateSpan::Type(union)]);
assert_eq!(template, TypeId::STRING);
}
#[test]
fn test_weak_type_all_optional_properties_detection() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let weak_target = interner.object(vec![
PropertyInfo::opt(a, TypeId::STRING),
PropertyInfo::opt(b, TypeId::NUMBER),
]);
let c = interner.intern_string("c");
let source = interner.object(vec![PropertyInfo::new(c, TypeId::BOOLEAN)]);
assert!(!checker.is_assignable(source, weak_target));
assert!(matches!(
checker.explain_failure(source, weak_target),
Some(SubtypeFailureReason::NoCommonProperties { .. })
));
}
#[test]
fn test_weak_type_with_index_signature_not_weak() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let target = interner.object_with_index(ObjectShape {
symbol: None,
flags: ObjectFlags::empty(),
properties: vec![PropertyInfo::opt(a, TypeId::STRING)],
string_index: Some(IndexSignature {
key_type: TypeId::STRING,
value_type: TypeId::ANY,
readonly: false,
}),
number_index: None,
});
let b = interner.intern_string("b");
let source = interner.object(vec![PropertyInfo::new(b, TypeId::NUMBER)]);
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_weak_type_empty_source_accepted() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let weak_target = interner.object(vec![PropertyInfo::opt(a, TypeId::STRING)]);
let empty_source = interner.object(Vec::new());
assert!(checker.is_assignable(empty_source, weak_target));
}
#[test]
fn test_weak_union_with_all_weak_members() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let weak_a = interner.object(vec![PropertyInfo::opt(a, TypeId::STRING)]);
let weak_b = interner.object(vec![PropertyInfo::opt(b, TypeId::NUMBER)]);
let weak_union = interner.union(vec![weak_a, weak_b]);
let c = interner.intern_string("c");
let source = interner.object(vec![PropertyInfo::new(c, TypeId::BOOLEAN)]);
assert!(!checker.is_assignable(source, weak_union));
}
#[test]
fn test_weak_union_with_non_weak_member_not_weak() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let a = interner.intern_string("a");
let b = interner.intern_string("b");
let weak_type = interner.object(vec![PropertyInfo::opt(a, TypeId::STRING)]);
let non_weak_type = interner.object(vec![PropertyInfo {
name: b,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: false, readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let union = interner.union(vec![weak_type, non_weak_type]);
let source_matching_non_weak = interner.object(vec![PropertyInfo {
name: b,
type_id: TypeId::NUMBER, write_type: TypeId::NUMBER,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(
checker.is_assignable(source_matching_non_weak, union),
"Source matching non-weak member should be assignable to union"
);
let c = interner.intern_string("c");
let source_no_match = interner.object(vec![PropertyInfo::new(c, TypeId::BOOLEAN)]);
assert!(
!checker.is_assignable(source_no_match, union),
"Source not matching any union member should not be assignable"
);
}
#[test]
fn test_exact_optional_property_types_distinguishes_undefined_from_missing() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_exact_optional_property_types(true);
let x = interner.intern_string("x");
let optional_number = interner.object(vec![PropertyInfo::opt(x, TypeId::NUMBER)]);
let number_or_undefined = interner.union(vec![TypeId::NUMBER, TypeId::UNDEFINED]);
let _explicit_undefined = interner.object(vec![PropertyInfo::new(x, number_or_undefined)]);
assert!(
!checker.is_assignable(optional_number, _explicit_undefined),
"Optional property should not be assignable to explicit undefined union in exact mode"
);
assert!(
!checker.is_assignable(_explicit_undefined, optional_number),
"Explicit undefined union should not be assignable to optional property in exact mode"
);
}
#[test]
fn test_exact_optional_property_types_false_allows_undefined() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_exact_optional_property_types(false);
let x = interner.intern_string("x");
let optional_number = interner.object(vec![PropertyInfo::opt(x, TypeId::NUMBER)]);
let number_or_undefined = interner.union(vec![TypeId::NUMBER, TypeId::UNDEFINED]);
let _explicit_undefined = interner.object(vec![PropertyInfo::new(x, number_or_undefined)]);
let just_undefined = interner.object(vec![PropertyInfo::new(x, TypeId::UNDEFINED)]);
assert!(
checker.is_assignable(just_undefined, optional_number),
"Explicit undefined should be assignable to optional property in non-exact mode"
);
}
#[test]
fn test_exact_optional_property_types_toggle_behavior() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let x = interner.intern_string("x");
let optional_number = interner.object(vec![PropertyInfo::opt(x, TypeId::NUMBER)]);
let just_undefined = interner.object(vec![PropertyInfo::new(x, TypeId::UNDEFINED)]);
assert!(checker.is_assignable(just_undefined, optional_number));
checker.set_exact_optional_property_types(true);
assert!(!checker.is_assignable(just_undefined, optional_number));
checker.set_exact_optional_property_types(false);
assert!(checker.is_assignable(just_undefined, optional_number));
}
#[test]
fn test_strict_null_checks_off_null_assignable_to_anything() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_null_checks(false);
assert!(checker.is_assignable(TypeId::NULL, TypeId::STRING));
assert!(checker.is_assignable(TypeId::NULL, TypeId::NUMBER));
assert!(checker.is_assignable(TypeId::NULL, TypeId::BOOLEAN));
assert!(checker.is_assignable(TypeId::NULL, TypeId::VOID));
assert!(checker.is_assignable(TypeId::UNDEFINED, TypeId::STRING));
assert!(checker.is_assignable(TypeId::UNDEFINED, TypeId::NUMBER));
assert!(checker.is_assignable(TypeId::UNDEFINED, TypeId::BOOLEAN));
}
#[test]
fn test_strict_null_checks_on_null_not_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_null_checks(true);
assert!(!checker.is_assignable(TypeId::NULL, TypeId::STRING));
assert!(!checker.is_assignable(TypeId::NULL, TypeId::NUMBER));
assert!(!checker.is_assignable(TypeId::NULL, TypeId::BOOLEAN));
assert!(!checker.is_assignable(TypeId::NULL, TypeId::VOID));
assert!(!checker.is_assignable(TypeId::UNDEFINED, TypeId::STRING));
assert!(!checker.is_assignable(TypeId::UNDEFINED, TypeId::NUMBER));
assert!(!checker.is_assignable(TypeId::UNDEFINED, TypeId::BOOLEAN));
}
#[test]
fn test_strict_null_checks_union_with_null() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let nullable_string = interner.union(vec![TypeId::STRING, TypeId::NULL]);
let undefinable_number = interner.union(vec![TypeId::NUMBER, TypeId::UNDEFINED]);
assert!(!checker.is_assignable(nullable_string, TypeId::STRING)); assert!(checker.is_assignable(TypeId::STRING, nullable_string)); assert!(!checker.is_assignable(undefinable_number, TypeId::NUMBER)); assert!(checker.is_assignable(TypeId::NUMBER, undefinable_number));
checker.set_strict_null_checks(false);
assert!(checker.is_assignable(nullable_string, TypeId::STRING));
assert!(checker.is_assignable(TypeId::NULL, TypeId::STRING));
assert!(checker.is_assignable(undefinable_number, TypeId::NUMBER));
assert!(checker.is_assignable(TypeId::UNDEFINED, TypeId::NUMBER));
}
#[test]
fn test_strict_null_checks_empty_object() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let empty_object = interner.object(Vec::new());
assert!(!checker.is_assignable(TypeId::NULL, empty_object));
assert!(!checker.is_assignable(TypeId::UNDEFINED, empty_object));
checker.set_strict_null_checks(false);
assert!(checker.is_assignable(TypeId::NULL, empty_object));
assert!(checker.is_assignable(TypeId::UNDEFINED, empty_object));
}
#[test]
fn test_void_return_exception_functions() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let void_fn = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let string_fn = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let number_fn = interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::NUMBER,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(
checker.is_assignable(string_fn, void_fn),
"Function returning string should be assignable to void function"
);
assert!(
checker.is_assignable(number_fn, void_fn),
"Function returning number should be assignable to void function"
);
assert!(
!checker.is_assignable(void_fn, string_fn),
"Void function should NOT be assignable to string function"
);
}
#[test]
fn test_void_return_exception_with_parameters() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let x = interner.intern_string("x");
let void_fn = interner.function(FunctionShape {
params: vec![ParamInfo::required(x, TypeId::NUMBER)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let string_number_fn = interner.function(FunctionShape {
params: vec![ParamInfo::required(x, TypeId::STRING)],
this_type: None,
return_type: TypeId::NUMBER,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(
!checker.is_assignable(string_number_fn, void_fn),
"Parameter mismatch should still cause rejection"
);
}
#[test]
fn test_void_return_exception_constructors() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let instance_type = interner.object(vec![PropertyInfo::new(
interner.intern_string("x"),
TypeId::NUMBER,
)]);
let void_ctor = interner.object(vec![PropertyInfo {
name: interner.intern_string("constructor"),
type_id: interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: true,
is_method: false,
}),
write_type: TypeId::ANY,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let instance_ctor = interner.object(vec![PropertyInfo {
name: interner.intern_string("constructor"),
type_id: interner.function(FunctionShape {
params: Vec::new(),
this_type: None,
return_type: instance_type,
type_params: Vec::new(),
type_predicate: None,
is_constructor: true,
is_method: false,
}),
write_type: TypeId::ANY,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(
checker.is_assignable(instance_ctor, void_ctor),
"Constructor returning instance should be assignable to void constructor"
);
}
#[test]
fn test_method_bivariance_allows_derived_methods() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let method_name = interner.intern_string("compare");
let base = interner.object(vec![PropertyInfo::new(
interner.intern_string("x"),
TypeId::STRING,
)]);
let base_method = interner.object(vec![PropertyInfo {
visibility: Visibility::Public,
parent_id: None,
name: method_name,
type_id: interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(base)],
this_type: Some(base),
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: true, }),
write_type: TypeId::ANY,
optional: false,
readonly: false,
is_method: true,
}]);
let derived = interner.object(vec![
PropertyInfo::new(interner.intern_string("x"), TypeId::STRING),
PropertyInfo::new(interner.intern_string("y"), TypeId::NUMBER),
]);
let derived_method = interner.object(vec![PropertyInfo {
name: method_name,
type_id: interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(derived)],
this_type: Some(derived),
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: true, }),
write_type: TypeId::ANY,
optional: false,
readonly: false,
is_method: true,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(
checker.is_assignable(derived_method, base_method),
"Derived method with narrower 'this' parameter should be assignable to Base method due to bivariance"
);
}
#[test]
#[ignore = "Method bivariance/strict function types not fully implemented"]
fn test_method_bivariance_persists_with_strict_function_types() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let method_name = interner.intern_string("method");
let base = interner.object(vec![PropertyInfo::new(
interner.intern_string("a"),
TypeId::STRING,
)]);
let base_with_method = interner.object(vec![PropertyInfo {
visibility: Visibility::Public,
parent_id: None,
name: method_name,
type_id: interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(base)],
this_type: Some(base),
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: true,
}),
write_type: TypeId::ANY,
optional: false,
readonly: false,
is_method: true,
}]);
let derived = interner.object(vec![
PropertyInfo::new(interner.intern_string("a"), TypeId::STRING),
PropertyInfo::new(interner.intern_string("b"), TypeId::NUMBER),
]);
let derived_with_method = interner.object(vec![PropertyInfo {
name: method_name,
type_id: interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(derived)],
this_type: Some(derived),
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: true,
}),
write_type: TypeId::ANY,
optional: false,
readonly: false,
is_method: true,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(
checker.is_assignable(derived_with_method, base_with_method),
"Methods should remain bivariant even with strictFunctionTypes"
);
}
#[test]
#[ignore = "Method bivariance/strict function types not fully implemented"]
fn test_function_variance_strict_function_types_affects_functions_not_methods() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
let (animal, dog) = make_animal_dog(&interner);
let fn_dog = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(dog)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false, });
let fn_animal = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(animal)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false, });
assert!(
!checker.is_assignable(fn_dog, fn_animal),
"Standalone functions should be contravariant with strictFunctionTypes"
);
let method_name = interner.intern_string("method");
let obj_with_dog_method = interner.object(vec![PropertyInfo {
name: method_name,
type_id: fn_dog,
write_type: TypeId::ANY,
optional: false,
readonly: false,
is_method: true, visibility: Visibility::Public,
parent_id: None,
}]);
let obj_with_animal_method = interner.object(vec![PropertyInfo {
name: method_name,
type_id: fn_animal,
write_type: TypeId::ANY,
optional: false,
readonly: false,
is_method: true, visibility: Visibility::Public,
parent_id: None,
}]);
assert!(
checker.is_assignable(obj_with_dog_method, obj_with_animal_method),
"Methods should be bivariant even with strictFunctionTypes"
);
}
#[test]
fn test_strict_mode_enables_all_strict_flags() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
assert!(!checker.is_assignable(TypeId::NULL, TypeId::STRING));
let (animal, dog) = make_animal_dog(&interner);
let fn_dog = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(dog)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let fn_animal = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(animal)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(
checker.is_assignable(fn_dog, fn_animal),
"Functions should be bivariant by default"
);
checker.set_strict_function_types(true);
assert!(
!checker.is_assignable(fn_dog, fn_animal),
"Functions should be contravariant with strictFunctionTypes"
);
}
#[test]
fn test_compiler_options_independent_toggles() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
assert!(!checker.is_assignable(TypeId::NULL, TypeId::STRING));
checker.set_strict_null_checks(false);
assert!(checker.is_assignable(TypeId::NULL, TypeId::STRING));
checker.set_strict_null_checks(true);
let x = interner.intern_string("x");
let optional_number = interner.object(vec![PropertyInfo::opt(x, TypeId::NUMBER)]);
let number_or_undefined = interner.union(vec![TypeId::NUMBER, TypeId::UNDEFINED]);
let explicit_union = interner.object(vec![PropertyInfo::new(x, number_or_undefined)]);
assert!(
checker.is_assignable(explicit_union, optional_number),
"Explicit number|undefined should be assignable to optional number in default mode"
);
checker.set_exact_optional_property_types(true);
assert!(
!checker.is_assignable(explicit_union, optional_number),
"Explicit number|undefined should NOT be assignable to optional number in exact mode"
);
let indexed = interner.object_with_index(ObjectShape {
symbol: None,
flags: ObjectFlags::empty(),
properties: Vec::new(),
string_index: Some(IndexSignature {
key_type: TypeId::STRING,
value_type: TypeId::NUMBER,
readonly: false,
}),
number_index: None,
});
let index_access = interner.intern(TypeData::IndexAccess(indexed, TypeId::STRING));
checker.set_exact_optional_property_types(false);
assert!(checker.is_assignable(index_access, TypeId::NUMBER));
checker.set_no_unchecked_indexed_access(true);
assert!(!checker.is_assignable(index_access, TypeId::NUMBER));
}
#[test]
fn test_function_intrinsic_accepts_any_function() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let simple_fn = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::NUMBER,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
assert!(
checker.is_assignable(simple_fn, TypeId::FUNCTION),
"Any function should be assignable to Function intrinsic"
);
}
#[test]
fn test_function_intrinsic_accepts_callable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let callable = interner.callable(CallableShape {
symbol: None,
call_signatures: vec![CallSignature {
type_params: Vec::new(),
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::NUMBER,
type_predicate: None,
is_method: false,
}],
construct_signatures: Vec::new(),
properties: Vec::new(),
string_index: None,
number_index: None,
});
assert!(
checker.is_assignable(callable, TypeId::FUNCTION),
"Callable types should be assignable to Function intrinsic"
);
}
#[test]
fn test_function_intrinsic_rejects_non_callable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
assert!(
!checker.is_assignable(TypeId::STRING, TypeId::FUNCTION),
"String should NOT be assignable to Function intrinsic"
);
assert!(
!checker.is_assignable(TypeId::NUMBER, TypeId::FUNCTION),
"Number should NOT be assignable to Function intrinsic"
);
let obj = interner.object(vec![PropertyInfo::new(
interner.intern_string("x"),
TypeId::NUMBER,
)]);
assert!(
!checker.is_assignable(obj, TypeId::FUNCTION),
"Plain object should NOT be assignable to Function intrinsic"
);
}
#[test]
fn test_function_intrinsic_with_union_of_callables() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let fn1 = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::NUMBER,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let fn2 = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::NUMBER)],
this_type: None,
return_type: TypeId::STRING,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let union_fn = interner.union(vec![fn1, fn2]);
assert!(
checker.is_assignable(union_fn, TypeId::FUNCTION),
"Union of callables should be assignable to Function intrinsic"
);
}
#[test]
fn test_function_intrinsic_with_union_non_callable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let fn1 = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(TypeId::STRING)],
this_type: None,
return_type: TypeId::NUMBER,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false,
});
let mixed_union = interner.union(vec![fn1, TypeId::STRING]);
assert!(
!checker.is_assignable(mixed_union, TypeId::FUNCTION),
"Mixed union (callable | non-callable) should NOT be assignable to Function"
);
}
#[test]
fn test_union_intersection_distributivity_basic() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("name");
let age = interner.intern_string("age");
let type_a = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let type_b = interner.object(vec![PropertyInfo::new(age, TypeId::NUMBER)]);
let type_c = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let union_ab = interner.union(vec![type_a, type_b]);
let intersection = interner.intersection(vec![union_ab, type_c]);
let a_and_c = interner.intersection(vec![type_a, type_c]);
assert!(
checker.is_assignable(intersection, a_and_c),
"(A | B) & C should distribute correctly"
);
}
#[test]
fn test_intersection_union_distributivity() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("name");
let age = interner.intern_string("age");
let type_a = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let type_b = interner.object(vec![PropertyInfo::new(age, TypeId::NUMBER)]);
let type_c = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let union_bc = interner.union(vec![type_b, type_c]);
let intersection = interner.intersection(vec![type_a, union_bc]);
assert!(
checker.is_assignable(type_a, intersection),
"A & (B | C) should distribute to (A & B) | (A & C)"
);
}
#[test]
fn test_distributivity_with_primitives() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let str_num = interner.union(vec![TypeId::STRING, TypeId::NUMBER]);
let result = interner.intersection(vec![str_num, TypeId::STRING]);
assert!(
checker.is_assignable(TypeId::STRING, result),
"(string | number) & string should be string"
);
}
#[test]
fn test_weak_type_detection_with_all_strict_options() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(true);
checker.set_strict_null_checks(true);
checker.set_exact_optional_property_types(true);
checker.set_no_unchecked_indexed_access(true);
let x = interner.intern_string("x");
let y = interner.intern_string("y");
let weak_type = interner.object(vec![
PropertyInfo::opt(x, TypeId::STRING),
PropertyInfo::opt(y, TypeId::NUMBER),
]);
let source = interner.object(vec![PropertyInfo::new(
interner.intern_string("z"),
TypeId::BOOLEAN,
)]);
assert!(
!checker.is_assignable(source, weak_type),
"Weak type detection should work with all strict options enabled"
);
}
#[test]
fn test_weak_union_detection_improved() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let x = interner.intern_string("x");
let y = interner.intern_string("y");
let weak1 = interner.object(vec![PropertyInfo::opt(x, TypeId::STRING)]);
let weak2 = interner.object(vec![PropertyInfo::opt(y, TypeId::NUMBER)]);
let weak_union = interner.union(vec![weak1, weak2]);
let source = interner.object(vec![PropertyInfo::new(
interner.intern_string("z"),
TypeId::BOOLEAN,
)]);
assert!(
!checker.is_assignable(source, weak_union),
"Weak union detection should reject source with no common properties"
);
}
#[test]
fn test_all_compiler_options_combinations() {
let interner = TypeInterner::new();
let x = interner.intern_string("x");
let optional_number = interner.object(vec![PropertyInfo::opt(x, TypeId::NUMBER)]);
let number_or_undefined = interner.union(vec![TypeId::NUMBER, TypeId::UNDEFINED]);
let explicit_union = interner.object(vec![PropertyInfo::new(x, number_or_undefined)]);
let test_cases = vec![
(false, false, false, "all defaults"),
(true, false, false, "strictFunctionTypes only"),
(false, true, false, "exactOptionalProperties only"),
(false, false, true, "noUncheckedIndexedAccess only"),
(true, true, false, "strict + exact"),
(true, false, true, "strict + noUnchecked"),
(false, true, true, "exact + noUnchecked"),
(true, true, true, "all strict"),
];
for (strict_fn, exact, no_unchecked, desc) in test_cases {
let mut checker = CompatChecker::new(&interner);
checker.set_strict_function_types(strict_fn);
checker.set_exact_optional_property_types(exact);
checker.set_no_unchecked_indexed_access(no_unchecked);
let expected = !exact; let result = checker.is_assignable(explicit_union, optional_number);
assert_eq!(result, expected, "Failed for: {desc} (exact={exact})");
}
}
#[test]
#[ignore = "Method bivariance/strict function types not fully implemented"]
fn test_strict_function_types_affects_methods_independently() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("name");
let animal = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let breed = interner.intern_string("breed");
let dog = interner.object(vec![
PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
},
PropertyInfo::new(breed, TypeId::STRING),
]);
let fn_animal = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(animal)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false, });
let fn_dog = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(dog)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: false, });
assert!(
checker.is_assignable(fn_dog, fn_animal),
"Functions should be bivariant by default"
);
checker.set_strict_function_types(true);
assert!(
!checker.is_assignable(fn_dog, fn_animal),
"Functions should be contravariant with strictFunctionTypes"
);
let method_animal = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(animal)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: true, });
let method_dog = interner.function(FunctionShape {
params: vec![ParamInfo::unnamed(dog)],
this_type: None,
return_type: TypeId::VOID,
type_params: Vec::new(),
type_predicate: None,
is_constructor: false,
is_method: true, });
assert!(
checker.is_assignable(method_dog, method_animal),
"Methods should remain bivariant even with strictFunctionTypes"
);
}
#[test]
fn test_no_unchecked_indexed_access_with_nested_types() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let nested_array = interner.array(interner.array(TypeId::STRING));
checker.set_no_unchecked_indexed_access(true);
assert!(
!checker.is_assignable(TypeId::STRING, nested_array),
"With noUncheckedIndexedAccess, array indexing includes undefined"
);
}
#[test]
fn test_keyof_union_contravariance() {
let interner = TypeInterner::new();
let checker = CompatChecker::new(&interner);
let name = interner.intern_string("name");
let age = interner.intern_string("age");
let type_a = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let type_b = interner.object(vec![PropertyInfo::new(age, TypeId::NUMBER)]);
let union_ab = interner.union(vec![type_a, type_b]);
let keyof_union = crate::evaluate_keyof(&interner, union_ab);
assert_eq!(
keyof_union,
TypeId::NEVER,
"keyof (A | B) with disjoint keys should be never"
);
let name_prop = PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
};
let type_c = interner.object(vec![
name_prop.clone(),
PropertyInfo::new(interner.intern_string("x"), TypeId::NUMBER),
]);
let type_d = interner.object(vec![
name_prop,
PropertyInfo::new(interner.intern_string("y"), TypeId::BOOLEAN),
]);
let union_cd = interner.union(vec![type_c, type_d]);
let keyof_union_cd = crate::evaluate_keyof(&interner, union_cd);
let name_literal = interner.literal_string("name");
assert_eq!(
keyof_union_cd, name_literal,
"keyof (C | D) with common 'name' key should be 'name'"
);
let _ = checker;
}
#[test]
fn test_keyof_intersection_distributivity() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("name");
let age = interner.intern_string("age");
let type_a = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let type_b = interner.object(vec![
PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
},
PropertyInfo::new(age, TypeId::NUMBER),
]);
let intersection_ab = interner.intersection(vec![type_a, type_b]);
let keyof_intersection = interner.intern(TypeData::KeyOf(intersection_ab));
let name_literal = interner.intern(TypeData::Literal(crate::LiteralValue::String(name)));
let age_literal = interner.intern(TypeData::Literal(crate::LiteralValue::String(age)));
assert!(
checker.is_assignable(name_literal, keyof_intersection),
"keyof (A & B) should include 'name'"
);
assert!(
checker.is_assignable(age_literal, keyof_intersection),
"keyof (A & B) should include 'age'"
);
}
#[test]
fn test_keyof_with_union_of_objects_with_common_properties() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("name");
let age = interner.intern_string("age");
let type_a = interner.object(vec![
PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
},
PropertyInfo::new(age, TypeId::NUMBER),
]);
let email = interner.intern_string("email");
let type_b = interner.object(vec![
PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
},
PropertyInfo::new(email, TypeId::STRING),
]);
let union_ab = interner.union(vec![type_a, type_b]);
let keyof_union = interner.intern(TypeData::KeyOf(union_ab));
let name_literal = interner.intern(TypeData::Literal(crate::LiteralValue::String(name)));
let age_literal = interner.intern(TypeData::Literal(crate::LiteralValue::String(age)));
let email_literal = interner.intern(TypeData::Literal(crate::LiteralValue::String(email)));
assert!(
checker.is_assignable(name_literal, keyof_union),
"keyof (A | B) should include common property 'name'"
);
assert!(
!checker.is_assignable(age_literal, keyof_union),
"keyof (A | B) should NOT include 'age' (only in A)"
);
assert!(
!checker.is_assignable(email_literal, keyof_union),
"keyof (A | B) should NOT include 'email' (only in B)"
);
}
#[test]
fn test_best_common_type_array_literal_inference() {
let interner = TypeInterner::new();
let ctx = crate::infer::InferenceContext::new(&interner);
let types = vec![TypeId::NUMBER, TypeId::STRING, TypeId::BOOLEAN];
let bct = ctx.best_common_type(&types);
let expected = interner.union(vec![TypeId::NUMBER, TypeId::STRING, TypeId::BOOLEAN]);
assert_eq!(bct, expected, "BCT of mixed types should be their union");
}
#[test]
fn test_best_common_type_with_supertype() {
let interner = TypeInterner::new();
let ctx = crate::infer::InferenceContext::new(&interner);
let name = interner.intern_string("name");
let animal = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let breed = interner.intern_string("breed");
let dog = interner.object(vec![
PropertyInfo {
name,
type_id: TypeId::STRING,
write_type: TypeId::STRING,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
},
PropertyInfo::new(breed, TypeId::STRING),
]);
let types = vec![animal, dog];
let bct = ctx.best_common_type(&types);
assert!(
interner.is_subtype_of(animal, bct),
"Animal should be subtype of BCT"
);
}
#[test]
fn test_best_common_type_empty_array() {
let interner = TypeInterner::new();
let ctx = crate::infer::InferenceContext::new(&interner);
let types: Vec<TypeId> = vec![];
let bct = ctx.best_common_type(&types);
assert_eq!(bct, TypeId::UNKNOWN, "BCT of empty array should be unknown");
}
#[test]
fn test_best_common_type_single_element() {
let interner = TypeInterner::new();
let ctx = crate::infer::InferenceContext::new(&interner);
let types = vec![TypeId::STRING];
let bct = ctx.best_common_type(&types);
assert_eq!(
bct,
TypeId::STRING,
"BCT of single element should be that element"
);
}
#[test]
fn test_best_common_type_with_literal_widening() {
let interner = TypeInterner::new();
let ctx = crate::infer::InferenceContext::new(&interner);
let types = vec![TypeId::NUMBER, TypeId::STRING];
let bct = ctx.best_common_type(&types);
let expected = interner.union(vec![TypeId::NUMBER, TypeId::STRING]);
assert_eq!(
bct, expected,
"BCT of number and string should be their union"
);
}
#[test]
fn test_private_brand_lazy_self_resolution_does_not_recurse() {
struct SelfReferentialLazyResolver {
def_id: DefId,
lazy_type: TypeId,
}
impl TypeResolver for SelfReferentialLazyResolver {
fn resolve_ref(&self, _symbol: SymbolRef, _interner: &dyn TypeDatabase) -> Option<TypeId> {
None
}
fn resolve_lazy(&self, def_id: DefId, _interner: &dyn TypeDatabase) -> Option<TypeId> {
if def_id == self.def_id {
Some(self.lazy_type)
} else {
None
}
}
}
let interner = TypeInterner::new();
let def_id = DefId(42);
let lazy_type = interner.intern(TypeData::Lazy(def_id));
let resolver = SelfReferentialLazyResolver { def_id, lazy_type };
let checker = CompatChecker::with_resolver(&interner, &resolver);
assert_eq!(
checker.private_brand_assignability_override(lazy_type, lazy_type),
None
);
}
#[test]
fn test_private_brand_same_brand_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let brand = interner.intern_string("__private_brand_Foo");
let source = interner.object(vec![PropertyInfo::new(brand, TypeId::NEVER)]);
let target = interner.object(vec![PropertyInfo::new(brand, TypeId::NEVER)]);
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_private_brand_different_brand_not_assignable() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let brand1 = interner.intern_string("__private_brand_Foo");
let brand2 = interner.intern_string("__private_brand_Bar");
let source = interner.object(vec![PropertyInfo::new(brand1, TypeId::NEVER)]);
let target = interner.object(vec![PropertyInfo::new(brand2, TypeId::NEVER)]);
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_private_brand_source_without_brand_not_assignable_to_target_with_brand() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let brand = interner.intern_string("__private_brand_Foo");
let name = interner.intern_string("value");
let source = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object(vec![
PropertyInfo::new(brand, TypeId::NEVER),
PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
},
]);
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_private_brand_source_with_brand_assignable_to_target_without_brand() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let brand = interner.intern_string("__private_brand_Foo");
let name = interner.intern_string("value");
let source = interner.object(vec![
PropertyInfo::new(brand, TypeId::NEVER),
PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
},
]);
let target = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_private_brand_neither_has_brand_falls_through() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let name = interner.intern_string("value");
let source = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
let target = interner.object(vec![PropertyInfo {
name,
type_id: TypeId::NUMBER,
write_type: TypeId::NUMBER,
optional: false,
readonly: false,
is_method: false,
visibility: Visibility::Public,
parent_id: None,
}]);
assert!(checker.is_assignable(source, target));
}
#[test]
fn test_private_brand_callable_with_brand() {
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let brand1 = interner.intern_string("__private_brand_Foo");
let brand2 = interner.intern_string("__private_brand_Bar");
let source = interner.callable(CallableShape {
symbol: None,
call_signatures: Vec::new(),
construct_signatures: vec![CallSignature {
params: Vec::new(),
this_type: None,
return_type: TypeId::ANY,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
properties: vec![PropertyInfo::new(brand1, TypeId::NEVER)],
..Default::default()
});
let target = interner.callable(CallableShape {
symbol: None,
call_signatures: Vec::new(),
construct_signatures: vec![CallSignature {
params: Vec::new(),
this_type: None,
return_type: TypeId::ANY,
type_predicate: None,
type_params: Vec::new(),
is_method: false,
}],
properties: vec![PropertyInfo::new(brand2, TypeId::NEVER)],
..Default::default()
});
assert!(!checker.is_assignable(source, target));
}
#[test]
fn test_mapped_to_mapped_readonly_assignable_to_partial() {
use crate::MappedModifier;
let interner = TypeInterner::new();
let mut checker = CompatChecker::new(&interner);
let t_name = interner.intern_string("T");
let t_param = interner.intern(TypeData::TypeParameter(TypeParamInfo {
name: t_name,
constraint: None,
default: None,
is_const: false,
}));
let keyof_t = interner.intern(TypeData::KeyOf(t_param));
let k_name = interner.intern_string("K");
let t_k = interner.intern(TypeData::IndexAccess(
t_param,
interner.intern(TypeData::TypeParameter(TypeParamInfo {
name: k_name,
constraint: None,
default: None,
is_const: false,
})),
));
let readonly_t = interner.mapped(MappedType {
type_param: TypeParamInfo {
name: k_name,
constraint: None,
default: None,
is_const: false,
},
constraint: keyof_t,
name_type: None,
template: t_k,
readonly_modifier: Some(MappedModifier::Add),
optional_modifier: None,
});
let partial_t = interner.mapped(MappedType {
type_param: TypeParamInfo {
name: k_name,
constraint: None,
default: None,
is_const: false,
},
constraint: keyof_t,
name_type: None,
template: t_k,
readonly_modifier: None,
optional_modifier: Some(MappedModifier::Add),
});
assert!(
checker.is_assignable(readonly_t, partial_t),
"Readonly<T> should be assignable to Partial<T>"
);
}