#![cfg(feature = "typecheck")]
use luaur_rt::{check, check_with_definitions, Error, Lua, TypeDiagnostic};
#[test]
fn free_check_accepts_well_typed() {
check("local x: number = 1\nreturn x").expect("well-typed source should check clean");
}
#[test]
fn free_check_rejects_mismatch_with_populated_line() {
let diagnostics: Vec<TypeDiagnostic> =
check("--!strict\nlocal x: number = \"oops\"").expect_err("type mismatch should fail");
assert!(!diagnostics.is_empty(), "expected at least one diagnostic");
assert_eq!(
diagnostics[0].line, 2,
"diagnostic should point at line 2: {diagnostics:?}"
);
assert!(
diagnostics[0].column >= 1,
"column should be 1-based and populated: {diagnostics:?}"
);
assert!(
!diagnostics[0].in_definitions,
"a script error is not an in-definitions error"
);
let msg = &diagnostics[0].message;
assert!(
msg.contains("number") && msg.contains("string"),
"message should mention the number/string mismatch: {msg}"
);
}
#[test]
fn with_definitions_introduces_and_checks_host_fn() {
let bare = check("--!strict\nlocal n: number = add(1, 2)\nreturn n");
assert!(bare.is_err(), "undeclared host fn should not type-check");
check_with_definitions(
"--!strict\nlocal n: number = add(1, 2)\nreturn n",
"declare function add(a: number, b: number): number",
)
.expect("declared host fn should type-check");
let misuse = check_with_definitions(
"--!strict\nlocal s: string = add(1, 2)\nreturn s",
"declare function add(a: number, b: number): number",
)
.expect_err("number result assigned to string should fail");
let joined: String = misuse
.iter()
.map(|d| d.message.clone())
.collect::<Vec<_>>()
.join("\n");
assert!(
joined.contains("number") && joined.contains("string"),
"diagnostic should mention number/string: {joined}"
);
}
#[test]
fn malformed_definitions_flagged_in_definitions() {
let diagnostics = check_with_definitions(
"return 1",
"declare function add(a: number, b: number: number", )
.expect_err("malformed host definitions should fail");
assert!(
diagnostics.iter().any(|d| d.in_definitions),
"malformed-definition diagnostic should carry in_definitions == true: {diagnostics:?}"
);
}
#[test]
fn lua_check_clean_and_mismatch() {
let lua = Lua::new();
lua.check("local x: number = 1\nreturn x")
.expect("clean source should check");
let err = lua
.check("--!strict\nlocal x: number = \"oops\"")
.expect_err("mismatch should fail");
match err {
Error::TypeError(v) => {
assert!(!v.is_empty(), "expected a diagnostic");
assert_eq!(v[0].line, 2, "diagnostic should point at line 2: {v:?}");
}
other => panic!("expected Error::TypeError, got {other:?}"),
}
}
#[test]
fn add_definitions_persists_for_lua_check() {
let lua = Lua::new();
lua.add_definitions("declare function add(a: number, b: number): number")
.expect("valid definitions should register");
lua.check("--!strict\nlocal n: number = add(1, 2)\nreturn n")
.expect("declared host fn should type-check after add_definitions");
}
#[test]
fn add_definitions_persists_for_chunk_check() {
let lua = Lua::new();
lua.add_definitions("declare function greet(name: string): string")
.expect("valid definitions should register");
let c = lua.load("--!strict\nlocal s: string = greet(\"world\")\nreturn s");
c.check()
.expect("chunk should type-check against host defs");
}
#[test]
fn add_definitions_rejects_malformed() {
let lua = Lua::new();
let err = lua
.add_definitions("declare function add(a: number, b: number: number") .expect_err("malformed definitions should be rejected");
match err {
Error::TypeError(v) => {
assert!(
v.iter().any(|d| d.in_definitions),
"malformed-definition error should be in_definitions: {v:?}"
);
}
other => panic!("expected Error::TypeError, got {other:?}"),
}
}
#[test]
fn lua_check_with_definitions_one_off_does_not_persist() {
let lua = Lua::new();
lua.check_with_definitions(
"--!strict\nlocal n: number = add(1, 2)\nreturn n",
"declare function add(a: number, b: number): number",
)
.expect("one-off defs should be in scope for this check");
let err = lua.check("--!strict\nlocal n: number = add(1, 2)\nreturn n");
assert!(
err.is_err(),
"one-off definitions must not persist on the Lua"
);
}
#[test]
fn check_then_run_static_check_is_advisory() {
let lua = Lua::new();
let ill_typed = "--!strict\nlocal s: string = 42\nreturn s";
let c = lua.load(ill_typed);
assert!(
matches!(c.check(), Err(Error::TypeError(_))),
"ill-typed chunk should fail Chunk::check"
);
let value: i64 = lua
.load(ill_typed)
.eval()
.expect("dynamically-typed Luau should still run the ill-typed chunk");
assert_eq!(value, 42, "the chunk should evaluate to 42 at runtime");
}
#[test]
fn check_is_deterministic_for_refined_unions() {
let src = "local function f(v: string | number | boolean)\n return (v or 0) + 1\nend\n";
let baseline = format!("{:?}", check(src));
assert!(
check(src).is_err(),
"the union-vs-number mismatch should produce a diagnostic"
);
for i in 0..64 {
let again = format!("{:?}", check(src));
assert_eq!(
baseline, again,
"check() must be deterministic, but result diverged on iteration {i}"
);
}
}
#[test]
fn check_does_not_abort_on_self_referential_alias() {
let src = "type T = Pt\ntype Pt = string | { f: T } & boolean\ntype Pair = T\n";
let _ = check(src);
let _ = check("type A = A\n");
let _ = check("type X = Y\ntype Y = { next: X }?\ntype Z = X\n");
}
#[test]
fn check_does_not_abort_on_bound_free_type_pack_promotion() {
let src = "type T = V | T\n\
type U = {{{boolean}}}\n\
type V = V | number\n\
local h: boolean\n\
c = -129\n\
local h = 550\n\
local h: T\n\
type Pair = V\n\
local x: {string & unknown & unknown & number & unknown & V & number & unknown} & T & T & T & {T} & T & T & T & T = ({x = h:h(h:h(h:e(true), 366.49), -function(c) do\n\
e = e\n\
e = e\n\
end\n\
do\n\
e = e\n\
end\n\
end), e = 0 + 0})\n\
type T<T> = number\n";
let _ = check(src);
}
#[test]
fn check_does_not_abort_on_bound_table_or_function_promotion() {
let src = "type T = any\n\
type U = never\n\
type V = boolean?\n\
local function b(z: BoxT, z: T & T): V\n\
if z:z(z:g(z:c(), ((rawequal(true)))), z) then\n\
local c = {f = {}, d = h, [true] = z:z(z:z(#{d = false, false} .. true, z:z(y(z:z(z:z((((((((c[#z:z(nil, nil)]))))))), select()), ((z:h(true, true)))), nil), ((((((((((((((x.x)))))))))))))))), ((((0)))))}\n\
return 0\n\
else\n\
return 0\n\
end\n\
local a = 0\n\
return 0\n\
end\n\
type T<T> = number\n";
let _ = check(src);
}