#[cfg(test)]
mod test {
use emmylua_parser::{LuaAstNode, LuaTableField};
use smol_str::SmolStr;
use crate::{
LuaMemberId, LuaMemberKey, LuaSemanticDeclId, LuaType, LuaUnionType, VirtualWorkspace,
};
#[test]
fn test_issue_318() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
local map = {
a = 'a',
b = 'b',
c = 'c',
}
local key --- @type string
c = map[key] -- type should be ('a'|'b'|'c'|nil)
"#,
);
let c_ty = ws.expr_ty("c");
let union_type = LuaType::Union(
LuaUnionType::from_vec(vec![
LuaType::StringConst(SmolStr::new("a").into()),
LuaType::StringConst(SmolStr::new("b").into()),
LuaType::StringConst(SmolStr::new("c").into()),
LuaType::Nil,
])
.into(),
);
assert_eq!(c_ty, union_type);
}
#[test]
fn test_issue_1075_large_table_dynamic_string_key() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
let names = (0..256)
.map(|i| format!(" ITEM_{i} = \"Item {i}\","))
.collect::<Vec<_>>()
.join("\n");
let strings = format!(
r#"
STRINGS = {{
NAMES = {{
{names}
}}
}}
"#
);
ws.def_files(vec![
("strings.lua", &strings),
(
"skinsutils.lua",
r#"
function get_skin_name(name)
return STRINGS.NAMES[string.upper(name)]
end
Result = get_skin_name("item_1")
"#,
),
]);
let result_ty = ws.expr_ty("Result");
let expected_ty = ws.ty("string?");
assert!(ws.check_type(&result_ty, &expected_ty));
}
#[test]
fn test_exact_missing_table_key_does_not_scan_broad_members() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
local t = {
a = 1,
b = "b",
}
value = t["missing"]
"#,
);
assert_eq!(ws.expr_ty("value"), LuaType::Nil);
}
#[test]
fn test_issue_314_generic_inheritance() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
---@class foo<T>: T
local foo_mt = {}
---@type foo<{a: string}>
local bar = { a = 'test' }
c = bar.a -- should be string
---@class buz<T>: foo<T>
local buz_mt = {}
---@type buz<{a: integer}>
local qux = { a = 5 }
d = qux.a -- should be integer
"#,
);
let c_ty = ws.expr_ty("c");
let d_ty = ws.expr_ty("d");
assert_eq!(c_ty, LuaType::String);
assert_eq!(d_ty, LuaType::Integer);
}
#[test]
fn test_issue_397() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @class A
--- @field field? integer
--- @class B : A
--- @field field integer
--- @type B
local b = { field = 1 }
local key1 --- @type 'field'
local key2 = 'field'
a = b.field -- type is integer - correct
d = b['field'] -- type is integer - correct
e = b[key1] -- type is integer? - wrong
f = b[key2] -- type is integer? - wrong
"#,
);
let a_ty = ws.expr_ty("a");
let d_ty = ws.expr_ty("d");
let e_ty = ws.expr_ty("e");
let f_ty = ws.expr_ty("f");
assert_eq!(a_ty, LuaType::Integer);
assert_eq!(d_ty, LuaType::Integer);
assert_eq!(e_ty, LuaType::Integer);
assert_eq!(f_ty, LuaType::Integer);
}
#[test]
fn test_keyof() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class SuiteHooks
---@field beforeAll string
---@field afterAll number
---@type SuiteHooks
local hooks = {}
---@type keyof SuiteHooks
local name = "beforeAll"
A = hooks[name]
"#,
);
let ty = ws.expr_ty("A");
let expected =
LuaType::Union(LuaUnionType::from_vec(vec![LuaType::String, LuaType::Number]).into());
assert_eq!(ty, expected);
}
#[test]
fn test_local_shadow_global_member_owner() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
local table = {}
table.unpack = 1
A = table.unpack
"#,
);
assert_eq!(ws.expr_ty("A"), LuaType::IntegerConst(1));
}
#[test]
fn test_assign_table_literal_preserves_class_fields() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field a string
---@field b? number
---@type A
local a
a = { a = "hello" }
c = a.a
"#,
);
assert_eq!(
ws.expr_ty("c"),
LuaType::StringConst(SmolStr::new("hello").into())
);
}
#[test]
fn test_assign_object_return_preserves_class_fields() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field a string|number
---@field b number
---@return {a: string}
local function make()
return { a = "hello" }
end
---@type A
local a
a = make()
c = a.a
d = a.b
"#,
);
assert_eq!(ws.expr_ty("c"), LuaType::String);
assert_eq!(ws.expr_ty("d"), LuaType::Number);
}
#[test]
fn test_assign_table_literal_preserves_class_fields_from_antecedent() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field a string
---@field b? number
---@type A
local global_a
---@return A
local function make()
return global_a
end
local a = make()
a = { a = "hello" }
c = a.a
"#,
);
assert_eq!(
ws.expr_ty("c"),
LuaType::StringConst(SmolStr::new("hello").into())
);
}
#[test]
fn test_assign_from_nil_uses_expr_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local a
a = "hello"
b = a
"#,
);
assert_eq!(
ws.expr_ty("b"),
LuaType::StringConst(SmolStr::new("hello").into())
);
}
#[test]
fn test_global_member_owner_prefers_declared_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Foo
---@field existing string
---@type Foo
Foo = {}
Foo.extra = 1
---@type Foo
local other
A = other.extra
"#,
);
assert_eq!(ws.expr_ty("A"), LuaType::Nil);
}
#[test]
fn test_non_name_prefix_uses_inferred_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local t = {}
(t).bar = "hi"
A = t.bar
"#,
);
assert_eq!(
ws.expr_ty("A"),
LuaType::StringConst(SmolStr::new("hi").into())
);
}
#[test]
fn test_nested_unresolved_prefix_keeps_member_owner_retry() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
M.child.leaf = 1
M = {
child = {},
}
A = M.child.leaf
"#,
);
assert_eq!(ws.expr_ty("A"), LuaType::IntegerConst(1));
}
#[test]
fn test_table_expr_key_string() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
local key = tostring(1)
local t = { [key] = 1 }
value = t[key]
"#,
);
let value_ty = ws.expr_ty("value");
assert!(
matches!(value_ty, LuaType::Integer | LuaType::IntegerConst(_)),
"expected integer type, got {:?}",
value_ty
);
}
#[test]
fn test_table_expr_key_doc_const() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
---@type 'field'
local key = "field"
local t = { [key] = 1 }
value = t[key]
"#,
);
let value_ty = ws.expr_ty("value");
assert!(
matches!(value_ty, LuaType::Integer | LuaType::IntegerConst(_)),
"expected integer type, got {:?}",
value_ty
);
}
#[test]
fn test_union_member_access_preserves_never() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field y never
---@class B
---@field y never
---@return A|B
local function make() end
local value = make()
result = value.y
"#,
);
assert_eq!(ws.expr_ty("result"), ws.ty("never"));
}
#[test]
fn test_table_expr_index_preserves_never() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return { y: number } & { y: string }
local function impossible() end
local t = {
a = impossible().y,
}
result = t["a"]
"#,
);
assert_eq!(ws.expr_ty("result"), ws.ty("never"));
}
#[test]
fn test_rawget_guard_narrows_matching_index_expr() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
---@class T
---@field x? integer
---@type T
local t = {}
if rawget(t, "x") then
result = t.x
end
"#,
);
assert_eq!(ws.expr_ty("result"), LuaType::Integer);
}
#[test]
fn test_rawget_doc_function_guard_narrows_matching_index_expr() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
---@class T
---@field x? integer
---@type T
local t = {}
---@class Utils
---@field get fun(tbl: T, key: "x"): std.RawGet<T, "x">
---@type Utils
local utils = { get = rawget }
if utils.get(t, "x") then
result = t.x
end
"#,
);
assert_eq!(ws.expr_ty("result"), LuaType::Integer);
}
#[test]
fn test_rawget_alias_guard_narrows_matching_index_expr() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
---@class T
---@field x? integer
---@type T
local t = {}
local get = rawget
if get(t, "x") then
result = t.x
end
"#,
);
assert_eq!(ws.expr_ty("result"), LuaType::Integer);
}
#[test]
fn test_type_guard_call_narrows_matching_index_expr() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@generic T
---@param inst any
---@param type `T`
---@return TypeGuard<T>
local function instance_of(inst, type)
return true
end
---@class T
---@field x? string|integer
---@type T
local t = {}
if instance_of(t.x, "string") then
result = t.x
end
"#,
);
assert_eq!(ws.expr_ty("result"), LuaType::String);
}
#[test]
fn test_alias_predicate_guard_narrows_matching_index_expr() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class T
---@field x? integer
---@type T
local t = {}
local ok = t.x ~= nil
if ok then
result = t.x
end
"#,
);
assert_eq!(ws.expr_ty("result"), LuaType::Integer);
}
#[test]
fn test_alias_chain_predicate_guard_keeps_matching_index_expr_wide() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class T
---@field x? integer
---@type T
local t = {}
local has_x = t.x ~= nil
local ok = has_x
if ok then
result = t.x
end
"#,
);
assert_eq!(ws.expr_ty("result"), ws.ty("integer?"));
}
#[test]
fn test_member_origin_owner_switches_cache_for_cross_file_table_field() {
let mut ws = VirtualWorkspace::new();
let defs_file = ws.def_file(
"defs.lua",
r#"
---@class CrossFileOwner
---@field fn fun(): string
local function fallback()
end
---@type CrossFileOwner
local value = {
fn = fallback,
}
"#,
);
let main_file = ws.def_file("main.lua", "local main = 1");
let root = ws
.analysis
.compilation
.get_db()
.get_vfs()
.get_syntax_tree(&defs_file)
.expect("defs tree must exist")
.get_chunk_node();
let table_field = root
.descendants::<LuaTableField>()
.next()
.expect("table field must exist");
let member_id = LuaMemberId::new(table_field.get_syntax_id(), defs_file);
let semantic_model = ws
.analysis
.compilation
.get_semantic_model(main_file)
.expect("main model must exist");
let origin = semantic_model
.get_member_origin_owner(member_id)
.expect("origin owner must resolve");
let LuaSemanticDeclId::Member(origin_member_id) = origin else {
panic!("expected member origin, got {origin:?}");
};
let origin_member = ws
.analysis
.compilation
.get_db()
.get_member_index()
.get_member(&origin_member_id)
.expect("origin member must exist");
assert_eq!(
origin_member.get_key(),
&LuaMemberKey::Name(SmolStr::new("fn"))
);
assert!(origin_member.is_field());
}
}