#[cfg(test)]
mod test {
use crate::{DiagnosticCode, EmmyrcLuaVersion, FileId, LuaType, VirtualWorkspace};
use emmylua_parser::{LuaAstNode, LuaAstToken, LuaLocalName, LuaNameExpr};
use ntest::timeout;
const STACKED_TYPE_GUARDS: usize = 180;
const MAXWELLHOME_ARRAY_VALUES: usize = 2048;
const ISSUE_1100_HIGHLIGHT_GROUPS: usize = 2048;
const REPEATED_SELF_ASSIGNMENT_STEPS: usize = 512;
const REPEATED_SELF_ASSIGNMENT_VARIANT_STEPS: usize = 128;
fn last_name_expr_type(ws: &VirtualWorkspace, file_id: FileId, name: &str) -> LuaType {
let tree = ws
.analysis
.compilation
.get_db()
.get_vfs()
.get_syntax_tree(&file_id)
.expect("syntax tree must exist");
let semantic_model = ws
.analysis
.compilation
.get_semantic_model(file_id)
.expect("semantic model must exist");
let name_expr = tree
.get_chunk_node()
.descendants::<LuaNameExpr>()
.filter(|name_expr| {
name_expr
.get_name_token()
.is_some_and(|token| token.get_name_text() == name)
})
.last()
.expect("name expr must exist");
semantic_model
.get_semantic_info(name_expr.syntax().clone().into())
.expect("name expr semantic info must exist")
.typ
}
#[test]
fn test_closure_return() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
--- @generic T, U
--- @param arr T[]
--- @param op fun(item: T, index: integer): U
--- @return U[]
function map(arr, op)
end
"#,
);
let ty = ws.expr_ty(
r#"
map({ 1, 2, 3 }, function(item, i)
return tostring(item)
end)
"#,
);
let expected = ws.ty("string[]");
assert_eq!(ty, expected);
}
#[test]
fn test_issue_140_1() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
---@class Object
---@class T
local inject2class ---@type (Object| T)?
if jsonClass then
if inject2class then
A = inject2class
end
end
"#,
);
let ty = ws.expr_ty("A");
let type_desc = ws.humanize_type(ty);
assert_eq!(type_desc, "T");
}
#[test]
fn test_issue_140_2() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
local msgBody ---@type { _hgQuiteMsg : 1 }?
if not msgBody or not msgBody._hgQuiteMsg then
end
"#
));
}
#[test]
fn test_issue_140_3() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
local SELF ---@type unknown
if SELF ~= nil then
SELF:OnDestroy()
end
"#
));
}
#[test]
fn test_issue_107() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
---@type {bar?: fun():string}
local props
if props.bar then
local foo = props.bar()
end
if type(props.bar) == 'function' then
local foo = props.bar()
end
local foo = props.bar and props.bar() or nil
"#
));
}
#[test]
fn test_stacked_same_var_type_guards_build_semantic_model() {
let mut ws = VirtualWorkspace::new();
let repeated_guards =
"if type(value) ~= 'string' then return end\n".repeat(STACKED_TYPE_GUARDS);
let block = format!(
r#"
local value ---@type string|integer|boolean
{repeated_guards}
local narrowed ---@type string
narrowed = value
"#,
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for stacked same-variable type guard repro"
);
assert!(ws.has_no_diagnostic(DiagnosticCode::AssignTypeMismatch, &block));
}
#[test]
fn test_stacked_same_var_truthiness_guards_build_semantic_model() {
let mut ws = VirtualWorkspace::new();
let repeated_guards = "if not value then return end\n".repeat(STACKED_TYPE_GUARDS);
let block = format!(
r#"
local value ---@type string?
{repeated_guards}
after_guard = value
"#,
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for stacked same-variable truthiness repro"
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_stacked_same_var_call_type_guards_build_semantic_model() {
let mut ws = VirtualWorkspace::new();
let repeated_guards =
"if not instance_of(value, 'string') then return end\n".repeat(STACKED_TYPE_GUARDS);
let block = format!(
r#"
---@generic T
---@param inst any
---@param type `T`
---@return TypeGuard<T>
local function instance_of(inst, type)
return true
end
local value ---@type string|integer|boolean
{repeated_guards}
after_guard = value
"#,
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for stacked same-variable call type guard repro"
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_stacked_local_call_alias_type_guards_build_semantic_model() {
let mut ws = VirtualWorkspace::new();
let repeated_guards = "if not pred(value) then return end\n".repeat(STACKED_TYPE_GUARDS);
let block = format!(
r#"
---@param v any
---@return TypeGuard<string>
local function is_string(v)
return true
end
local pred = is_string
local value ---@type string|integer|boolean
{repeated_guards}
after_guard = value
"#,
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for stacked local call alias type guard repro"
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_stacked_same_var_call_type_guard_eq_false_build_semantic_model() {
let mut ws = VirtualWorkspace::new();
let repeated_guards = "if instance_of(value, 'string') == false then return end\n"
.repeat(STACKED_TYPE_GUARDS);
let block = format!(
r#"
---@generic T
---@param inst any
---@param type `T`
---@return TypeGuard<T>
local function instance_of(inst, type)
return true
end
local value ---@type string|integer|boolean
{repeated_guards}
after_guard = value
"#,
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for stacked binary call type guard repro"
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_flow_assigned_call_type_guard_prefix_keeps_narrowing() {
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
local guard
guard = instance_of
local value ---@type string|integer|boolean
if guard(value, "string") then
after_guard = value
end
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_condition_narrowed_call_type_guard_prefix_keeps_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@param guard (fun(v: any): TypeGuard<string>)?
---@param value string|integer|boolean
local function f(guard, value)
if guard and guard(value) then
after_guard = value
end
end
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_plain_call_condition_keeps_inner_call_prefix_type() {
let mut ws = VirtualWorkspace::new();
let code = r#"
local function a() end
local function b() end
b()
if a() then
b()
inner = b
end
"#;
ws.def(code);
let ty = ws.expr_ty("inner");
assert!(ty.is_function());
let mut diag_ws = VirtualWorkspace::new();
assert!(diag_ws.has_no_diagnostic(DiagnosticCode::CallNonCallable, code));
}
#[test]
fn test_false_call_condition_keeps_inner_unrelated_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return false
local function always_false()
return false
end
---@type string
local value = "ok"
if always_false() then
inner = value
end
"#,
);
assert_eq!(ws.expr_ty("inner"), ws.ty("string"));
}
#[test]
fn test_true_call_condition_keeps_else_call_prefix_type() {
let mut ws = VirtualWorkspace::new();
let code = r#"
---@return true
local function always_true()
return true
end
local function b() end
if always_true() then
else
b()
end
"#;
assert!(ws.has_no_diagnostic(DiagnosticCode::CallNonCallable, code));
}
#[test]
fn test_false_call_condition_assignment_does_not_contribute_to_merge() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return false
local function always_false()
return false
end
local value = "before"
if always_false() then
value = 1
end
after = value
"#,
);
let after = ws.expr_ty("after");
assert_eq!(ws.humanize_type(after), "string");
}
#[test]
fn test_false_call_condition_missing_field_assignment_does_not_contribute_to_merge() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return false
local function is_windows()
return false
end
local command = "ls"
local config = {}
if is_windows() then
command = config.windows_command
end
after = command
"#,
);
let after = ws.expr_ty("after");
assert_eq!(ws.humanize_type(after), "string");
}
#[test]
fn test_reachable_assignment_over_never_value_contributes_to_merge() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class NeverBox
---@field value never
---@return NeverBox
local function make_box() end
local value = make_box().value
local cond ---@type boolean
if cond then
value = 1
end
after = value
"#,
);
assert_eq!(ws.expr_ty("after"), LuaType::IntegerConst(1));
}
#[test]
fn test_false_call_condition_tag_cast_does_not_contribute_to_merge() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return false
local function always_false()
return false
end
local value = "before"
if always_false() then
---@cast value integer
end
after = value
"#,
);
let after = ws.expr_ty("after");
assert_eq!(ws.humanize_type(after), r#""before""#);
}
#[test]
fn test_false_call_condition_doc_assignment_does_not_contribute_to_merge() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return false
local function always_false()
return false
end
local value = "before"
if always_false() then
---@type integer
value = 1
end
after = value
"#,
);
assert_eq!(ws.expr_ty("after"), ws.ty("string"));
}
#[test]
fn test_branch_join_keeps_union_when_only_one_side_narrows() {
let mut ws = VirtualWorkspace::new();
let block = r#"
local cond ---@type boolean
local value ---@type string|integer
if cond then
if type(value) ~= 'string' then
return
end
end
after_join = value
"#;
let file_id = ws.def(block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for branch join merge-safety repro"
);
assert_eq!(ws.expr_ty("after_join"), ws.ty("string|integer"));
}
#[test]
fn test_stacked_same_field_truthiness_guards_build_semantic_model() {
let mut ws = VirtualWorkspace::new();
let repeated_guards = "if not value.foo then return end\n".repeat(STACKED_TYPE_GUARDS);
let block = format!(
r#"
---@class HasFoo
---@field foo string
---@class NoFoo
---@field bar integer
local value ---@type HasFoo|NoFoo
{repeated_guards}
after_guard = value
"#,
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for stacked same-field truthiness repro"
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("HasFoo"));
}
#[test]
fn test_stacked_return_cast_guards_build_semantic_model() {
let mut ws = VirtualWorkspace::new();
let repeated_guards =
"if not is_player(creature) then return end\n".repeat(STACKED_TYPE_GUARDS);
let block = format!(
r#"
---@class Creature
---@class Player: Creature
---@class Monster: Creature
---@return boolean
---@return_cast creature Player else Monster
local function is_player(creature)
return true
end
local creature ---@type Creature
{repeated_guards}
after_guard = creature
"#,
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for stacked return-cast repro"
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("Player"));
}
#[test]
fn test_stacked_return_cast_self_guards_build_semantic_model() {
let mut ws = VirtualWorkspace::new();
let repeated_guards =
"if not creature:is_player() then return end\n".repeat(STACKED_TYPE_GUARDS);
let block = format!(
r#"
---@class Creature
---@class Player: Creature
---@class Monster: Creature
local creature = {{}}
---@return boolean
---@return_cast self Player else Monster
function creature:is_player()
return true
end
{repeated_guards}
after_guard = creature
"#,
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for stacked self return-cast repro"
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("Player"));
}
#[cfg(feature = "slow-tests")]
#[test]
fn test_large_linear_assignment_file_builds_semantic_model() {
let mut ws = VirtualWorkspace::new();
let mut block = String::from(
r#"
local value ---@type integer
value = 1
"#,
);
for i in 0..2048 {
block.push_str(&format!("local alias_{i} = value\n"));
block.push_str(&format!("value = alias_{i}\n"));
}
block.push_str("after_assign = value\n");
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for large linear assignment stress case"
);
let after_assign = ws.expr_ty("after_assign");
assert_eq!(ws.humanize_type(after_assign), "integer");
}
#[test]
#[timeout(5000)]
fn test_issue_1094_self_call_fallback_stress() {
let mut ws = VirtualWorkspace::new();
let repeated_calls = (2..=30)
.map(|i| format!("if count == 0 then count = self:api{i}():api(code) end\n"))
.collect::<String>();
let block = format!(
r#"
function class(className, super)
end
local Test = class("Test")
function Test:api1(code, isBind)
local count = self:api():api(code)
{repeated_calls}
return count
end
"#
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for repeated self-call fallback stress repro"
);
}
#[test]
#[timeout(5000)]
fn test_issue_1100_repeated_table_field_index_reads_after_unrelated_conditions() {
let mut ws = VirtualWorkspace::new();
let repeated_groups = (0..ISSUE_1100_HIGHLIGHT_GROUPS)
.map(|i| {
format!(
"if enabled('group_{i}') then\n hi('Group{i}', {{ fg = p.base0E, bg = p.base01, attr = nil, sp = nil }})\nend\n"
)
})
.collect::<String>();
let block = format!(
r#"
---@type {{ base01: string, base0E: string }}
local palette = {{ base01 = "a", base0E = "b" }}
local function enabled(name)
return name ~= ""
end
local function hi(group, args)
end
local p = palette
{repeated_groups}
result = p.base0E
"#
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for repeated palette field reads"
);
assert_eq!(ws.expr_ty("result"), ws.ty("string"));
}
#[test]
fn test_issue_1028_maxwellhome_like_large_array_builds_semantic_model() {
let mut ws = VirtualWorkspace::new();
let mut block = String::from(
r#"
---@type integer
local tile = ({
layers = {
{
data = {
"#,
);
for i in 0..MAXWELLHOME_ARRAY_VALUES {
block.push_str(&format!(" {},\n", i % 3));
}
block.push_str(
r#"
},
},
},
}).layers[1].data[1024]
"#,
);
let file_id = ws.def_file("maxwellhome.lua", &block);
let semantic_model = ws
.analysis
.compilation
.get_semantic_model(file_id)
.expect("expected semantic model for maxwellhome-like large array stress case");
let local_name = ws.get_node::<LuaLocalName>(file_id);
let token = local_name.get_name_token().expect("name token must exist");
let info = semantic_model
.get_semantic_info(token.syntax().clone().into())
.expect("semantic info must exist");
assert_eq!(ws.humanize_type(info.typ), "integer");
}
#[test]
fn test_pending_replay_order_uses_type_guard_before_self_return_cast_lookup() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Player
---@class Monster
local checker = {}
---@return boolean
---@return_cast self Player else Monster
function checker:is_player()
return true
end
local branch ---@type boolean
local creature = branch and checker or false
if type(creature) ~= "table" then
return
end
if not creature:is_player() then
return
end
after_guard = creature
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("Player"));
}
#[test]
fn test_decl_initializer_assert_after_forward_method_guard_keeps_value_type() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
let file_id = ws.def(
r#"
---@class Foo
local Foo = {}
---@type string?
local nullable_string
function Foo:main()
assert(self:defined_later())
local _v3 = assert(nullable_string)
end
---@return boolean
function Foo:defined_later()
return false
end
"#,
);
let tree = ws
.analysis
.compilation
.get_db()
.get_vfs()
.get_syntax_tree(&file_id)
.expect("syntax tree must exist");
let semantic_model = ws
.analysis
.compilation
.get_semantic_model(file_id)
.expect("semantic model must exist");
let local_name = tree
.get_chunk_node()
.descendants::<LuaLocalName>()
.find(|name| {
name.get_name_token()
.is_some_and(|token| token.get_name_text() == "_v3")
})
.expect("_v3 local name must exist");
let token = local_name.get_name_token().expect("_v3 token must exist");
let info = semantic_model
.get_semantic_info(token.syntax().clone().into())
.expect("_v3 semantic info must exist");
assert_eq!(ws.humanize_type(info.typ), "string");
}
#[test]
fn test_pending_replay_order_with_three_guards_before_self_lookup() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class PlayerA
---@class MonsterA
---@class PlayerB
---@class MonsterB
local checker_a = {
kind = "checker_a",
}
---@return boolean
---@return_cast self PlayerA else MonsterA
function checker_a:is_player()
return true
end
local checker_b = {
kind = "checker_b",
}
---@return boolean
---@return_cast self PlayerB else MonsterB
function checker_b:is_player()
return true
end
local allow_false ---@type boolean
local choose_a ---@type boolean
local creature = allow_false and false or (choose_a and checker_a or checker_b)
if type(creature) ~= "table" then
return
end
if creature.kind ~= "checker_a" then
return
end
if creature:is_player() == false then
return
end
after_guard = creature
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("PlayerA"));
}
#[test]
fn test_return_cast_self_guard_uses_prior_narrowing_for_method_lookup() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Player
---@class Monster
local checker = {
kind = "checker",
}
---@return boolean
---@return_cast self Player else Monster
function checker:is_player()
return true
end
local monster = {
kind = "monster",
}
local branch ---@type boolean
local creature = branch and checker or monster
if creature.kind ~= "checker" then
return
end
if not creature:is_player() then
return
end
after_guard = creature
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("Player"));
}
#[test]
fn test_return_cast_self_guard_without_prior_method_lookup_narrowing_does_not_apply() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Player
---@class Monster
local checker = {
kind = "checker",
}
---@return boolean
---@return_cast self Player else Monster
function checker:is_player()
return true
end
local monster = {
kind = "monster",
}
local branch ---@type boolean
local creature = branch and checker or monster
before_guard = creature
if not creature:is_player() then
return
end
after_guard = creature
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.expr_ty("before_guard"));
}
#[test]
fn test_return_cast_self_guard_with_multiple_method_candidates_uses_prior_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class PlayerA
---@class MonsterA
---@class PlayerB
---@class MonsterB
local checker_a = {
kind = "checker_a",
}
---@return boolean
---@return_cast self PlayerA else MonsterA
function checker_a:is_player()
return true
end
local checker_b = {
kind = "checker_b",
}
---@return boolean
---@return_cast self PlayerB else MonsterB
function checker_b:is_player()
return true
end
local branch ---@type boolean
local creature = branch and checker_a or checker_b
if creature.kind ~= "checker_a" then
return
end
if not creature:is_player() then
return
end
after_guard = creature
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("PlayerA"));
}
#[test]
fn test_return_cast_self_guard_with_non_callable_member_uses_prior_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Player
---@class Monster
local checker = {
kind = "checker",
}
---@return boolean
---@return_cast self Player else Monster
function checker:is_player()
return true
end
local monster = {
kind = "monster",
is_player = false,
}
local branch ---@type boolean
local creature = branch and checker or monster
if creature.kind ~= "checker" then
return
end
if not creature:is_player() then
return
end
after_guard = creature
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("Player"));
}
#[test]
fn test_return_cast_self_guard_eq_false_uses_prior_narrowing_for_method_lookup() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Player
---@class Monster
local checker = {
kind = "checker",
}
---@return boolean
---@return_cast self Player else Monster
function checker:is_player()
return true
end
local monster = {
kind = "monster",
is_player = false,
}
local branch ---@type boolean
local creature = branch and checker or monster
if creature.kind ~= "checker" then
return
end
if creature:is_player() == false then
return
end
after_guard = creature
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("Player"));
}
#[test]
fn test_issue_100() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
local f = io.open('', 'wb')
if not f then
error("Could not open a file")
end
f:write('')
"#
));
}
#[test]
fn test_issue_93() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
local text --- @type string[]?
if staged then
local text1 --- @type string[]?
text = text1
else
local text2 --- @type string[]?
text = text2
end
if not text then
return
end
--- @param _a string[]
local function foo(_a) end
foo(text)
"#
));
}
#[test]
fn test_null_function_field() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
---@class A
---@field aaa? fun(a: string)
local c ---@type A
if c.aaa then
c.aaa("aaa")
end
"#
))
}
#[test]
fn test_issue_162() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
--- @class Foo
--- @field a? fun()
--- @param _o Foo
function bar(_o) end
bar({})
"#
));
}
#[test]
fn test_redefine() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::UndefinedField,
r#"
---@class AA
---@field b string
local a = 1
a = 1
---@type AA
local a
print(a.b)
"#
));
}
#[test]
fn test_issue_165() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
local a --- @type table?
if not a or #a == 0 then
return
end
print(a.h)
"#
));
}
#[test]
fn test_issue_160() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
local a --- @type table?
if not a then
assert(a)
end
print(a.field)
"#
));
}
#[test]
fn test_issue_210() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
--- @class A
--- @field b integer
local a = {}
--- @type A
a = { b = 1 }
--- @param _a A
local function foo(_a) end
foo(a)
"#
));
}
#[test]
fn test_doc_function_assignment_narrowing0() {
let mut ws = VirtualWorkspace::new();
let code = r#"
local i --- @type integer|fun():string
i = "str"
A = i
"#;
ws.def(code);
let a = ws.expr_ty("A");
let a_desc = ws.humanize_type_detailed(a);
assert_eq!(a_desc, "\"str\"");
}
#[test]
fn test_doc_member_assignment_prefers_annotation_source() {
let mut ws = VirtualWorkspace::new();
let code = r#"
local t = {}
t.a = "hello"
---@type string|number
t.a = 1
b = t.a
"#;
ws.def(code);
assert_eq!(ws.expr_ty("b"), ws.ty("integer"));
}
#[test]
fn test_assignment_narrow_drops_nil_on_mismatch() {
let mut ws = VirtualWorkspace::new();
let code = r#"
local a ---@type string?
a = 1
b = a
"#;
ws.def(code);
assert_eq!(ws.expr_ty("b"), LuaType::IntegerConst(1));
}
#[test]
fn test_doc_member_assignment_falls_back_to_annotation() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local t = {}
---@type string|number
t.a = true
b = t.a
"#,
);
let b = ws.expr_ty("b");
let expected_ty = ws.ty("string|number");
let expected = ws.humanize_type(expected_ty);
assert_eq!(ws.humanize_type(b), expected);
}
#[test]
fn test_doc_function_assignment_narrowing() {
let mut ws = VirtualWorkspace::new();
let code = r#"
local i --- @type integer|fun():string
i = function() end
_ = i()
A = i
"#;
ws.def(code);
assert!(ws.has_no_diagnostic(DiagnosticCode::CallNonCallable, code));
assert!(ws.has_no_diagnostic(DiagnosticCode::NeedCheckNil, code));
let a = ws.expr_ty("A");
let a_desc = ws.humanize_type_detailed(a);
assert_eq!(a_desc, "fun()");
}
#[test]
fn test_numeric_for_len_expr_narrows_loop_body_value_to_non_nil() {
let mut ws = VirtualWorkspace::new();
let code = r#"
---@type false|fun(...)[]?
local calls
for i = 1, #calls do
calls[i](...)
end
"#;
assert!(ws.has_no_diagnostic(DiagnosticCode::NeedCheckNil, code));
}
#[test]
fn test_issue_224() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::ReturnTypeMismatch,
r#"
--- @class A
--- @param opts? A
--- @return A
function foo(opts)
opts = opts or {}
return opts
end
"#
));
}
#[test]
fn test_elseif() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
---@class D11
---@field public a string
---@type D11|nil
local a
if not a then
elseif a.a then
print(a.a)
end
"#
));
}
#[test]
fn test_elseif_chain_keeps_previous_false_conditions() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
local stat ---@type { size: integer }?
if math.random() > 0.5 then
elseif not stat then
elseif stat.size > 0 then
end
"#
));
}
#[test]
fn test_not_logical_and_return_narrows_rhs_to_truthy() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
local n ---@type number?
if not (n and n > 0) then
return
end
n = n + 1
"#
));
}
#[test]
fn test_issue_266() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
r#"
--- @return string
function baz() end
local a
a = baz() -- a has type nil but should be string
d = a
"#
));
let d = ws.expr_ty("d");
let d_desc = ws.humanize_type(d);
assert_eq!(d_desc, "string");
}
#[test]
fn test_issue_277() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@param t? table
function myfun3(t)
if type(t) ~= 'table' then
return
end
a = t
end
"#,
);
let a = ws.expr_ty("a");
let a_desc = ws.humanize_type(a);
assert_eq!(a_desc, "table");
}
#[test]
fn test_docint() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local stack = 0
if stack ~= 0 then
a = stack
end
"#,
);
let a = ws.expr_ty("a");
let a_desc = ws.humanize_type(a);
assert_eq!(a_desc, "integer");
}
#[test]
fn test_issue_921_or_with_empty_table() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @class Opts
--- @field a? string
local opts --- @type Opts?
-- Test expression type: opts or {} should narrow to Opts
E = opts or {}
"#,
);
let e_ty = ws.expr_ty("E");
assert_eq!(ws.humanize_type(e_ty), "Opts");
}
#[test]
fn test_issue_921_or_with_table_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local opts --- @type table?
-- Test with plain table? type
E = opts or {}
"#,
);
let e_ty = ws.expr_ty("E");
assert_eq!(ws.humanize_type(e_ty), "table");
}
#[test]
fn test_issue_921_self_assignment_with_table() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local opts --- @type table?
opts = opts or {}
E = opts
"#,
);
let e_ty = ws.expr_ty("E");
assert_eq!(ws.humanize_type(e_ty), "table");
}
#[test]
fn test_issue_921_self_assignment_with_class_empty_table() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @class Opts
--- @field a? string
local opts0 --- @type Opts?
local opts1 --- @type Opts?
opts0 = opts0 or {}
opts1 = opts0 or { a = 'a' }
E0 = opts0
E1 = opts1
"#,
);
let e0_ty = ws.expr_ty("E0");
assert_eq!(ws.humanize_type(e0_ty), "Opts");
let e1_ty = ws.expr_ty("E1");
assert_eq!(ws.humanize_type(e1_ty), "Opts");
}
#[test]
fn test_issue_921_and_with_string_nullable() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @class Opts
--- @field a? string
local opts --- @type Opts
-- When opts.a is string?, result should be table|nil
-- The table {'a'} is inferred as a tuple containing 'a'
E = opts.a and { 'a' }
"#,
);
let e_ty = ws.expr_ty("E");
assert_eq!(ws.humanize_type(e_ty), r#"("a")?"#);
}
#[test]
fn test_issue_921_and_with_boolean_nullable_table() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @class Opts
--- @field b? boolean
local opts --- @type Opts
-- When opts.b is boolean?, result should be false|nil|table
E = opts.b and { 'b' }
"#,
);
let e_ty = ws.expr_ty("E");
assert_eq!(ws.humanize_type(e_ty), r#"(false|("b"))?"#);
}
#[test]
fn test_issue_921_and_with_boolean_nullable_string() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local bool --- @type boolean?
-- When bool is boolean?, result should be false|nil|'a'
E = bool and 'a'
"#,
);
let e_ty = ws.expr_ty("E");
assert_eq!(ws.humanize_type(e_ty), r#"(false|"a")?"#);
}
#[test]
fn test_issue_147() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local d ---@type string?
if d then
local d2 = function(...)
e = d
end
end
"#,
);
let e = ws.expr_ty("e");
assert_eq!(e, LuaType::String);
}
#[test]
fn test_issue_325() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
while condition do
local a ---@type string?
if not a then
break
end
b = a
end
"#,
);
let b = ws.expr_ty("b");
assert_eq!(b, LuaType::String);
}
#[test]
fn test_while_loop_post_flow_keeps_incoming_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local condition ---@type boolean
local value ---@type string?
while condition do
value = "loop"
end
after_loop = value
"#,
);
assert_eq!(ws.expr_ty("after_loop"), ws.ty("string?"));
}
#[test]
fn test_repeat_loop_post_flow_keeps_body_assignment() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local condition ---@type boolean
local value ---@type string?
repeat
value = "loop"
until condition
after_loop = value
"#,
);
assert_eq!(ws.expr_ty("after_loop"), ws.ty("string"));
}
#[test]
fn test_numeric_for_loop_post_flow_keeps_incoming_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local stop ---@type integer
local value ---@type string?
for i = 1, stop do
value = "loop"
end
after_loop = value
"#,
);
assert_eq!(ws.expr_ty("after_loop"), ws.ty("string?"));
}
#[test]
fn test_numeric_for_post_flow_adds_body_assignment_for_print_arg() {
let mut ws = VirtualWorkspace::new();
let file_id = ws.def(
r#"
---@class MyClass
local thing = nil
for i = 1, 10 do
thing = {} --[[@as MyClass]]
end
print(thing)
"#,
);
let thing_type = last_name_expr_type(&ws, file_id, "thing");
let thing_type_desc = ws.humanize_type(thing_type);
assert!(thing_type_desc.contains("MyClass"), "{thing_type_desc}");
}
#[test]
fn test_dynamic_numeric_for_post_flow_ignores_body_assignment_for_print_arg() {
let mut ws = VirtualWorkspace::new();
let file_id = ws.def(
r#"
---@class MyClass
local stop ---@type integer
local thing = nil
for i = 1, stop do
thing = {} --[[@as MyClass]]
end
print(thing)
"#,
);
let thing_type = last_name_expr_type(&ws, file_id, "thing");
assert_eq!(ws.humanize_type(thing_type), "nil");
}
#[test]
fn test_while_true_break_post_flow_adds_body_assignment_for_print_arg() {
let mut ws = VirtualWorkspace::new();
let file_id = ws.def(
r#"
---@class MyClass
local thing = nil
while true do
thing = {} --[[@as MyClass]]
break
end
print(thing)
"#,
);
let thing_type = last_name_expr_type(&ws, file_id, "thing");
let thing_type_desc = ws.humanize_type(thing_type);
assert!(thing_type_desc.contains("MyClass"), "{thing_type_desc}");
}
#[test]
fn test_dynamic_while_post_flow_ignores_body_assignment_for_print_arg() {
let mut ws = VirtualWorkspace::new();
let file_id = ws.def(
r#"
---@class MyClass
local condition ---@type boolean
local thing = nil
while condition do
thing = {} --[[@as MyClass]]
break
end
print(thing)
"#,
);
let thing_type = last_name_expr_type(&ws, file_id, "thing");
assert_eq!(ws.humanize_type(thing_type), "nil");
}
#[test]
fn test_while_false_post_flow_ignores_body_assignment_for_print_arg() {
let mut ws = VirtualWorkspace::new();
let file_id = ws.def(
r#"
---@class MyClass
local thing = nil
while false do
thing = {} --[[@as MyClass]]
end
print(thing)
"#,
);
let thing_type = last_name_expr_type(&ws, file_id, "thing");
assert_eq!(ws.humanize_type(thing_type), "nil");
}
#[test]
#[timeout(5000)]
fn test_unreachable_continue_loop_after_return_does_not_cycle() {
let mut ws = VirtualWorkspace::new();
let mut emmyrc = ws.get_emmyrc();
emmyrc.runtime.version = EmmyrcLuaVersion::LuaJITExt;
ws.update_emmyrc(emmyrc);
ws.def(
r#"
local side_effect = 0
return boolean
local continue_counter = 0
for i = 1, 5 do
if i % 2 == 0 then
continue
end
continue_counter = continue_counter + 1
end
"#,
);
}
#[test]
fn test_for_in_loop_post_flow_keeps_incoming_type_after_break() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
local value ---@type string?
for _, _value in ipairs({ "loop" }) do
value = "loop"
break
end
after_loop = value
"#,
);
assert_eq!(ws.expr_ty("after_loop"), ws.ty("string?"));
}
#[test]
fn test_nested_while_loop_post_flow_keeps_incoming_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local outer_condition ---@type boolean
local inner_condition ---@type boolean
local value ---@type string?
while outer_condition do
while inner_condition do
value = "loop"
break
end
break
end
after_loop = value
"#,
);
assert_eq!(ws.expr_ty("after_loop"), ws.ty("string?"));
}
#[test]
fn test_issue_347() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::ReturnTypeMismatch,
r#"
--- @param x 'a'|'b'
--- @return 'a'|'b'
function foo(x)
if x ~= 'a' and x ~= 'b' then
error('invalid behavior')
end
return x
end
"#,
));
}
#[test]
fn test_issue_339() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @class A
local a --- @type A|string
if type(a) == 'table' then
b = a -- a should be A
else
c = a -- a should be string
end
"#,
);
let b = ws.expr_ty("b");
let b_expected = ws.ty("A");
assert_eq!(b, b_expected);
let c = ws.expr_ty("c");
let c_expected = ws.ty("string");
assert_eq!(c, c_expected);
}
#[test]
fn test_narrow_after_error_branches() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local r --- @type string?
local a --- @type boolean
if not r then
if a then
error()
else
error()
end
end
b = r -- should be string
"#,
);
let b = ws.expr_ty("b");
assert_eq!(b, LuaType::String);
}
#[test]
fn test_issue_877_never_return_call_narrows_after_guard() {
let mut ws = VirtualWorkspace::new();
let source = r#"
---@param _num integer
local function accept_num(_num)
end
---@return never
local function panic()
error("panic!")
end
---@type integer?
local num
if num == nil then
panic()
end
accept_num(num)
after_guard = num
"#;
assert!(ws.has_no_diagnostic(DiagnosticCode::ParamTypeMismatch, source));
assert_eq!(ws.expr_ty("after_guard"), LuaType::Integer);
}
#[test]
fn test_never_return_call_after_branch_statement_narrows_after_guard() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return never
local function panic()
error("panic!")
end
---@type integer?
local num
if num == nil then
local reason = "bad"
panic()
end
after_guard = num
"#,
);
assert_eq!(ws.expr_ty("after_guard"), LuaType::Integer);
}
#[test]
fn test_unknown_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local a
b = a
"#,
);
let b = ws.expr_ty("b");
let b_expected = ws.ty("nil");
assert_eq!(b, b_expected);
}
#[test]
fn test_issue_367() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local files
local function init()
if files then
return
end
files = {}
a = files -- a 与 files 现在均为 nil
end
"#,
);
let a = ws.expr_ty("a");
assert!(a != LuaType::Nil);
ws.def(
r#"
---@alias D10.data
---| number
---| string
---| boolean
---| table
---| nil
---@param data D10.data
local function init(data)
---@cast data table
b = data -- data 现在仍为 `10.data` 而不是 `table`
end
"#,
);
let b = ws.expr_ty("b");
let b_desc = ws.humanize_type(b);
assert_eq!(b_desc, "table");
}
#[test]
fn test_issue_1045() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local f = {
[8] = function(aaa)
---@cast aaa number
b = aaa
end
}
"#,
);
let b = ws.expr_ty("b");
let b_desc = ws.humanize_type(b);
assert_eq!(b_desc, "number");
}
#[test]
fn test_issue_364() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
---@param k integer
---@param t table<integer,integer>
function foo(k, t)
if t and t[k] then
return t[k]
end
if t then
-- t is nil -- incorrect
t[k] = 1 -- t may be nil -- incorrect
end
end
"#,
));
}
#[test]
fn test_issue_382() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
---@class Trigger
---@class Event
---@field private wait_pushing? Trigger[]
local M
---@param trigger Trigger
function M:add_trigger(trigger)
if not self.wait_pushing then
self.wait_pushing = {}
end
self.wait_pushing[1] = trigger
end
---@private
function M:check_waiting()
if self.wait_pushing then
end
end
"#,
));
}
#[test]
fn test_issue_369() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @enum myenum
local myenum = { A = 1 }
--- @param x myenum|{}
function foo(x)
if type(x) ~= 'table' then
a = x
else
b = x
end
end
"#,
);
let a = ws.expr_ty("a");
let a_expected = ws.ty("myenum");
assert_eq!(a, a_expected);
let b = ws.expr_ty("b");
let b_expected = ws.ty("{}");
assert_eq!(b, b_expected);
}
#[test]
fn test_issue_373() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @alias myalias string|string[]
--- @param x myalias
function foo(x)
if type(x) == 'string' then
a = x
elseif type(x) == 'table' then
b = x
end
end
"#,
);
let a = ws.expr_ty("a");
let a_expected = ws.ty("string");
assert_eq!(a, a_expected);
let b = ws.expr_ty("b");
let b_expected = ws.ty("string[]");
assert_eq!(b, b_expected);
}
#[test]
fn test_call_cast() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return boolean
---@return_cast n integer
local function isInteger(n)
return true
end
local a ---@type integer | string
if isInteger(a) then
d = a
else
e = a
end
"#,
);
let d = ws.expr_ty("d");
let d_expected = ws.ty("integer");
assert_eq!(d, d_expected);
let e = ws.expr_ty("e");
let e_expected = ws.ty("string");
assert_eq!(e, e_expected);
}
#[test]
fn test_call_cast2() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class My2
---@class My1
---@class My3:My2,My1
local m = {}
---@return boolean
---@return_cast self My1
function m:isMy1()
end
---@return boolean
---@return_cast self My2
function m:isMy2()
end
if m:isMy1() then
a = m
elseif m:isMy2() then
b = m
end
"#,
);
let a = ws.expr_ty("a");
let a_expected = ws.ty("My1");
assert_eq!(a, a_expected);
let b = ws.expr_ty("b");
let b_expected = ws.ty("My2");
assert_eq!(b, b_expected);
}
#[test]
fn test_issue_423() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
r#"
--- @return string?
local function bar() end
--- @param a? string
function foo(a)
if not a then
a = bar()
assert(a)
end
--- @type string
local _ = a -- incorrect error
end
"#,
));
}
#[test]
fn test_issue_472() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::UnnecessaryIf,
r#"
worldLightLevel = 0
worldLightColor = 0
Gmae = {}
---@param color integer
---@param level integer
function Game.setWorldLight(color, level)
local previousColor = worldLightColor
local previousLevel = worldLightLevel
worldLightColor = color
worldLightLevel = level
if worldLightColor ~= previousColor or worldLightLevel ~= previousLevel then
-- Do something...
end
end
"#
))
}
#[test]
fn test_issue_478() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::ReturnTypeMismatch,
r#"
--- @param line string
--- @param b boolean
--- @return string
function foo(line, b)
return b and line or line
end
"#
));
}
#[test]
fn test_issue_491() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::ReturnTypeMismatch,
r#"
---@param srow integer?
function foo(srow)
srow = srow or 0
return function()
---@return integer
return function()
return srow
end
end
end
"#
));
}
#[test]
fn test_issue_288() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
--- @alias MyFun fun(): string[]
local f --- @type MyFun
if type(f) == 'function' then
_, res = pcall(f)
end
"#,
);
let res = ws.expr_ty("res");
let expected_ty = ws.ty("string|string[]");
assert_eq!(res, expected_ty);
}
#[test]
fn test_issue_480() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.has_no_diagnostic(
DiagnosticCode::UnnecessaryAssert,
r#"
--- @param a integer?
--- @param c boolean
function foo(a, c)
if c then
a = 1
end
assert(a)
end
"#,
);
}
#[test]
fn test_issue_526() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @alias A { kind: 'A'}
--- @alias B { kind: 'B'}
local x --- @type A|B
if x.kind == 'A' then
a = x
return
end
b = x
"#,
);
let a = ws.expr_ty("a");
let a_expected = ws.ty("A");
assert_eq!(a, a_expected);
let b = ws.expr_ty("b");
let b_expected = ws.ty("B");
assert_eq!(b, b_expected);
}
#[test]
fn test_issue_583() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
r#"
--- @param sha string
local function get_hash_color(sha)
local r, g, b = sha:match('(%x)%x(%x)%x(%x)')
assert(r and g and b, 'Invalid hash color')
local _ = r --- @type string
local _ = g --- @type string
local _ = b --- @type string
end
"#,
);
}
#[test]
fn test_issue_584() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
r#"
local function foo()
for _ in ipairs({}) do
break
end
local a
if a == nil then
a = 1
local _ = a --- @type integer
end
end
"#,
);
}
#[test]
fn test_feature_inherit_flow_from_const_local() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
local ret --- @type string | nil
local h = type(ret) == "string"
if h then
a = ret
end
local e = type(ret)
if e == "string" then
b = ret
end
"#,
);
let a = ws.expr_ty("a");
let a_expected = ws.ty("string");
assert_eq!(a, a_expected);
let b = ws.expr_ty("b");
let b_expected = ws.ty("string");
assert_eq!(b, b_expected);
}
#[test]
fn test_feature_initializer_alias_keeps_flow_type() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
local x --- @type string | integer
if type(x) ~= "string" then
return
end
local y = x
after = y
"#,
);
let after = ws.expr_ty("after");
let after_expected = ws.ty("string");
assert_eq!(after, after_expected);
}
#[test]
fn test_feature_const_local_alias_chain_does_not_inherit_flow() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
local ret --- @type string | nil
local is_string = type(ret) == "string"
local ok = is_string
if ok then
a = ret
end
"#,
);
let a = ws.expr_ty("a");
let a_expected = ws.ty("string?");
assert_eq!(a, a_expected);
}
#[test]
fn test_feature_generic_type_guard() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@generic T
---@param type `T`
---@return TypeGuard<T>
local function instanceOf(inst, type)
return true
end
local ret --- @type string | nil
if instanceOf(ret, "string") then
a = ret
end
"#,
);
let a = ws.expr_ty("a");
let a_expected = ws.ty("string");
assert_eq!(a, a_expected);
}
#[test]
fn test_feature_type_guard_narrows_parent_to_child() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@alias TypeGuard<T> boolean
---@class Parent
---@class Child : Parent
---@field test fun(): void
---@param instance Parent
---@return TypeGuard<Child>
local function instance_of_child(instance)
return true
end
local value ---@type Parent
if instance_of_child(value) then
narrowed = value
end
"#,
);
assert_eq!(ws.expr_ty("narrowed"), ws.ty("Child"));
}
#[test]
fn test_issue_598() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
---@class A<T>
A = {}
---@class IDisposable
---@class B<T>: IDisposable
---@class AnonymousObserver<T>: IDisposable
---@generic T
---@return AnonymousObserver<T>
function createAnonymousObserver()
end
"#,
);
assert!(ws.has_no_diagnostic(
DiagnosticCode::ReturnTypeMismatch,
r#"
---@param observer fun(value: T) | B<T>
---@return IDisposable
function A:subscribe(observer)
local typ = type(observer)
if typ == 'function' then
---@cast observer fun(value: T)
observer = createAnonymousObserver()
elseif typ == 'table' then
---@cast observer -function
observer = createAnonymousObserver()
end
return observer
end
"#,
));
assert!(!ws.has_no_diagnostic(
DiagnosticCode::ReturnTypeMismatch,
r#"
---@param observer fun(value: T) | B<T>
---@return IDisposable
function A:test2(observer)
local typ = type(observer)
if typ == 'table' then
---@cast observer -function
observer = createAnonymousObserver()
end
return observer
end
"#,
));
}
#[test]
fn test_assignment_in_all_type_alias_branches_drops_original_union() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
---@class FlowAliasA
local A
---@class FlowAliasDisposable
---@class FlowAliasAnonymousObserver: FlowAliasDisposable
---@return FlowAliasAnonymousObserver
local function createAnonymousObserver()
end
---@param observer fun() | string
function A:subscribe(observer)
local typ = type(observer)
if typ == 'function' then
observer = createAnonymousObserver()
elseif typ == 'string' then
observer = createAnonymousObserver()
else
after_else_observer = observer
end
after_observer = observer
end
"#,
);
let after_else_observer = ws.expr_ty("after_else_observer");
assert_eq!(ws.humanize_type(after_else_observer), "never");
let after_observer = ws.expr_ty("after_observer");
assert_eq!(
ws.humanize_type(after_observer),
"FlowAliasAnonymousObserver"
);
assert!(ws.has_no_diagnostic(
DiagnosticCode::ReturnTypeMismatch,
r#"
---@class A
local A
---@class IDisposable
---@class AnonymousObserver: IDisposable
---@return AnonymousObserver
local function createAnonymousObserver()
end
---@param observer fun() | string
---@return IDisposable
function A:subscribe(observer)
local typ = type(observer)
if typ == 'function' then
---@diagnostic disable-next-line: assign-type-mismatch
observer = createAnonymousObserver()
elseif typ == 'string' then
---@diagnostic disable-next-line: assign-type-mismatch
observer = createAnonymousObserver()
end
return observer
end
"#,
));
}
#[test]
fn test_issue_524() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@type string[]
local d = {}
if #d == 2 then
a = d[1]
b = d[2]
c = d[3]
end
for i = 1, #d do
e = d[i]
end
"#,
);
let a = ws.expr_ty("a");
let a_expected = ws.ty("string");
assert_eq!(a, a_expected);
let b = ws.expr_ty("b");
let b_expected = ws.ty("string");
assert_eq!(b, b_expected);
let c = ws.expr_ty("c");
let c_expected = ws.ty("string?");
assert_eq!(c, c_expected);
let e = ws.expr_ty("e");
let e_expected = ws.ty("string");
assert_eq!(e, e_expected);
}
#[test]
fn test_issue_600() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
---@class Test2
---@field test string[]
---@field test2? string
local a = {}
if a.test[1] and a.test[1].char(123) then
end
"#,
));
}
#[test]
fn test_issue_585() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
assert!(ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
r#"
local a --- @type type?
if type(a) == 'string' then
local _ = a --- @type type
end
"#,
));
}
#[test]
fn test_issue_627() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field type "point"
---@field handle number
---@class B
---@field type "unit"
---@field handle string
---@param a number
function testA(a)
end
---@param a string
function testB(a)
end
"#,
);
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
---@param target A | B
function test(target)
if target.type == 'point' then
testA(target.handle)
end
if target.type == 'unit' then
testB(target.handle)
end
end
"#,
));
}
#[test]
fn test_issue_622() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Test.A
---@field base number
---@field add number
T = {}
---@enum Test.op
Op = {
base = "base",
add = "add",
};
"#,
);
ws.def(
r#"
---@param op Test.op
---@param value number
---@return boolean
function T:SetValue(op, value)
local oldValue = self[op]
if oldValue == value then
return false
end
A = oldValue
return true
end
"#,
);
let a = ws.expr_ty("A");
assert_eq!(ws.humanize_type(a), "number");
}
#[test]
fn test_nil_1() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@type number?
local angle
if angle ~= nil and angle >= 0 then
A = angle
end
"#,
);
let a = ws.expr_ty("A");
assert_eq!(ws.humanize_type(a), "number");
}
#[test]
fn test_type_narrow() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@generic T: table
---@param obj T | function
---@return T?
function bindGC(obj)
if type(obj) == 'table' then
A = obj
end
end
"#,
);
let db = &ws.analysis.compilation.db;
let decl_id = db
.get_global_index()
.get_global_decl_ids("A")
.unwrap()
.first()
.unwrap()
.clone();
let typ = db
.get_type_index()
.get_type_cache(&decl_id.into())
.unwrap()
.as_type();
assert_eq!(ws.humanize_type(typ.clone()), "T");
}
#[test]
fn test_issue_630() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
ws.def(
r#"
---@class A
---@field Abc string?
A = {}
"#,
);
ws.def(
r#"
function A:test()
if not rawget(self, 'Abc') then
self.Abc = "a"
end
B = self.Abc
C = self
end
"#,
);
let a = ws.expr_ty("B");
assert_eq!(ws.humanize_type(a), "string");
let c = ws.expr_ty("C");
assert_eq!(ws.humanize_type(c), "A");
}
#[test]
fn test_error_function() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
---@class Result
---@field value string?
Result = {}
function getValue()
---@type Result?
local result
if result then
error(result.value)
end
end
"#,
));
}
#[test]
fn test_array_flow() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
assert!(ws.has_no_diagnostic(
DiagnosticCode::NeedCheckNil,
r#"
for i = 1, #_G.arg do
print(_G.arg[i].char())
end
"#,
));
}
#[test]
fn test_issue_641() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
assert!(ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
r#"
local b --- @type boolean
local tar = b and 'a' or 'b'
if tar == 'a' then
end
--- @type 'a'|'b'
local _ = tar
"#,
));
}
#[test]
fn test_self_1() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Node
---@field parent? Node
---@class Subject<T>: Node
---@field package root? Node
Subject = {}
"#,
);
ws.def(
r#"
function Subject:add()
if self == self.parent then
A = self
end
end
"#,
);
let a = ws.expr_ty("A");
assert_eq!(ws.humanize_type(a), "Node");
}
#[test]
fn test_return_cast_multi_file() {
let mut ws = VirtualWorkspace::new();
ws.def_file(
"test.lua",
r#"
local M = {}
--- @return boolean
--- @return_cast _obj function
function M.is_callable(_obj) end
return M
"#,
);
ws.def(
r#"
local test = require("test")
local obj
if test.is_callable(obj) then
o = obj
end
"#,
);
let a = ws.expr_ty("o");
let expected = LuaType::Function;
assert_eq!(a, expected);
}
#[test]
fn test_issue_734() {
let mut ws = VirtualWorkspace::new();
assert!(ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
r#"
local a --- @type string[]
assert(#a >= 1)
--- @type string
_ = a[1]
assert(#a == 1)
--- @type string
_ = a[1]
--- @type string
_2 = a[1]
"#
));
}
#[test]
fn test_return_cast_with_fallback() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Creature
---@class Player: Creature
---@class Monster: Creature
---@return boolean
---@return_cast creature Player else Monster
local function isPlayer(creature)
return true
end
local creature ---@type Creature
if isPlayer(creature) then
a = creature
else
b = creature
end
"#,
);
let a = ws.expr_ty("a");
let a_expected = ws.ty("Player");
assert_eq!(a, a_expected);
let b = ws.expr_ty("b");
let b_expected = ws.ty("Monster");
assert_eq!(b, b_expected);
}
#[test]
fn test_return_cast_with_fallback_self() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Creature
---@class Player: Creature
---@class Monster: Creature
local m = {}
---@return boolean
---@return_cast self Player else Monster
function m:isPlayer()
end
if m:isPlayer() then
a = m
else
b = m
end
"#,
);
let a = ws.expr_ty("a");
let a_expected = ws.ty("Player");
assert_eq!(a, a_expected);
let b = ws.expr_ty("b");
let b_expected = ws.ty("Monster");
assert_eq!(b, b_expected);
}
#[test]
fn test_return_cast_backward_compatibility() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return boolean
---@return_cast n integer
local function isInteger(n)
return true
end
local a ---@type integer | string
if isInteger(a) then
d = a
else
e = a
end
"#,
);
let d = ws.expr_ty("d");
let d_expected = ws.ty("integer");
assert_eq!(d, d_expected);
let e = ws.expr_ty("e");
let e_expected = ws.ty("string");
assert_eq!(e, e_expected);
}
#[test]
fn test_issue_868() {
let mut ws = VirtualWorkspace::new();
ws.has_no_diagnostic(
DiagnosticCode::AssignTypeMismatch,
r#"
local a --- @type string|{foo:boolean, bar:string}
if a.foo then
--- @type string
local _ = a.bar
end
"#,
);
}
#[test]
fn test_or_empty_table_non_table_compatible() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local a --- @type string?
-- When left type is NOT table-compatible, should not narrow
E = a or {}
"#,
);
let e_ty = ws.expr_ty("E");
assert_eq!(ws.humanize_type(e_ty), "(string|table)");
}
#[test]
fn test_or_empty_table_with_nonempty_class() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @class MyClass
--- @field x number
local obj --- @type MyClass?
E = obj or {}
"#,
);
let e_ty = ws.expr_ty("E");
assert_eq!(ws.humanize_type(e_ty), "(MyClass|table)");
}
#[test]
fn test_or_table_literal_with_required_fields_narrows_to_class() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @class Foo
--- @field [integer] string
--- @field other number
local foo --- @type Foo?
E = foo or { other = 5 }
"#,
);
let e_ty = ws.expr_ty("E");
assert_eq!(ws.humanize_type(e_ty), "Foo");
}
#[test]
fn test_or_empty_table_union_of_tables() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @class A
--- @field a number
--- @class B
--- @field b string
local obj --- @type (A|B)?
-- Union of class types is table-compatible
E = obj or {}
"#,
);
let e_ty = ws.expr_ty("E");
let type_str = ws.humanize_type_detailed(e_ty);
assert_eq!(type_str, "(A|B|table)");
}
#[test]
fn test_assignment_from_wider_single_return_call_drops_branch_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Foo
---@field kind "foo"
---@field a integer
---@class Bar
---@field kind "bar"
---@field b integer
---@param ok boolean
---@return Foo|Bar
local function pick(ok)
if ok then
return { kind = "foo", a = 1 }
end
return { kind = "bar", b = 2 }
end
local ok ---@type boolean
local x ---@type Foo|Bar
if x.kind == "foo" then
x = pick(ok)
after_assign = x
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("Foo|Bar"));
}
#[test]
fn test_assignment_from_call_index_rhs_keeps_precise_rhs_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Foo
---@field kind "foo"
---@class Bar
---@field kind "bar"
---@class Baz
---@field kind "baz"
---@class Box
---@field value Bar
---@return Box
local function get_box()
end
local x ---@type Foo|Bar|Baz
if x.kind == "foo" then
x = get_box().value
after_assign = x
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("Bar"));
}
#[test]
fn test_assignment_table_rhs_keeps_multiple_narrowed_field_values() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class LeftFoo
---@field kind "foo"
---@class LeftBar
---@field kind "bar"
---@class RightBaz
---@field kind "baz"
---@class RightQux
---@field kind "qux"
local left ---@type LeftFoo|LeftBar
local right ---@type RightBaz|RightQux
if left.kind == "foo" and right.kind == "baz" then
local pair = { left = left, right = right }
after_left = pair.left
after_right = pair.right
end
"#,
);
assert_eq!(ws.expr_ty("after_left"), ws.ty("LeftFoo"));
assert_eq!(ws.expr_ty("after_right"), ws.ty("RightBaz"));
}
#[test]
fn test_assignment_and_rhs_keeps_narrowed_index_on_second_operand() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Left
---@class RightFoo
---@field kind "foo"
---@field value string
---@class RightBar
---@field kind "bar"
---@field value integer
local left ---@type Left?
local right ---@type RightFoo|RightBar
if left and right.kind == "foo" then
local result = left and right.value
after_assign = result
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("string"));
}
#[test]
fn test_assignment_rhs_keeps_multiple_flow_dependencies() {
let mut ws = VirtualWorkspace::new();
let left_guards = "if not left then return end\n".repeat(STACKED_TYPE_GUARDS);
let right_guards = "if not right then return end\n".repeat(STACKED_TYPE_GUARDS);
let block = format!(
r#"
---@class Pattern
---@operator mul(Pattern): Pattern
---@class PatternFactory
---@field new fun(value: string): Pattern
local factory ---@type PatternFactory
local left ---@type Pattern?
local right ---@type Pattern?
{left_guards}
{right_guards}
left = left * factory.new("x") * right
after_assign = left
"#,
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for multi-dependency RHS assignment repro"
);
let after_assign = ws.expr_ty("after_assign");
assert_eq!(ws.humanize_type(after_assign), "Pattern");
}
#[test]
fn test_assignment_binary_rhs_replays_non_self_dependency() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class FooValue
---@field kind "foo"
---@field value integer
---@class BarValue
---@field kind "bar"
---@field value string
local right ---@type FooValue|BarValue
if right.kind == "foo" then
local value
value = right.value + 1
after_assign = value
end
"#,
);
let after_assign = ws.expr_ty("after_assign");
assert_eq!(ws.humanize_type(after_assign), "integer");
}
#[test]
fn test_assignment_rhs_keeps_flow_dependent_concat_operator() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Rope
---@operator concat(Rope): Rope
local left ---@type Rope?
local right ---@type Rope?
if not left then return end
if not right then return end
left = left .. right
after_assign = left
"#,
);
let after_assign = ws.expr_ty("after_assign");
assert_eq!(ws.humanize_type(after_assign), "Rope");
}
#[test]
fn test_assignment_rhs_keeps_flow_dependent_add_operator() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Counter
---@operator add(Counter): Counter
local left ---@type Counter?
local right ---@type Counter?
if not left then return end
if not right then return end
left = left + right
after_assign = left
"#,
);
let after_assign = ws.expr_ty("after_assign");
assert_eq!(ws.humanize_type(after_assign), "Counter");
}
#[test]
#[timeout(5000)]
fn test_issue_1114_repeated_self_dependent_assignments_build_semantic_model() {
let cases = [
(
"concat",
r#"local value = """#,
"value = value .. config.pic[idx][index]",
REPEATED_SELF_ASSIGNMENT_STEPS,
),
(
"add",
"local value = 0",
"value = value + config.pic[idx][index]",
REPEATED_SELF_ASSIGNMENT_VARIANT_STEPS,
),
(
"parenthesized concat",
r#"local value = """#,
"value = (value .. config.pic[idx][index])",
REPEATED_SELF_ASSIGNMENT_VARIANT_STEPS,
),
(
"unary",
"local value = 0",
"value = -(value + config.pic[idx][index])",
REPEATED_SELF_ASSIGNMENT_VARIANT_STEPS,
),
(
"comparison",
"local value = true",
"value = value == config.pic[idx][index]",
REPEATED_SELF_ASSIGNMENT_VARIANT_STEPS,
),
];
for (name, init, assignment, steps) in cases {
let mut ws = VirtualWorkspace::new();
let repeated_assignments = format!("{assignment}\n").repeat(steps);
let block = format!(
r#"
function f(config, idx, index)
{init}
{repeated_assignments}
return value
end
"#
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for repeated self-dependent {name} assignment"
);
}
}
#[test]
#[timeout(5000)]
fn test_issue_1116_generic_call_index_replay_builds_semantic_model() {
let mut ws = VirtualWorkspace::new();
let repeated_assignments =
"value = id(value .. config.pic[idx][index])\n".repeat(REPEATED_SELF_ASSIGNMENT_STEPS);
let block = format!(
r#"
---@generic T
---@param value T
---@return T
local function id(value)
return value
end
function f(config, idx, index)
local value
{repeated_assignments}
return value
end
"#
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for generic call index replay repro"
);
}
#[test]
fn test_binary_assignment_infer_error_keeps_previous_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local value = "prior"
value = config.pic + 1
after_assign = value
"#,
);
let after_assign = ws.expr_ty("after_assign");
assert_eq!(ws.humanize_type(after_assign), "string");
}
#[test]
fn test_eq_uses_branch_narrowed_rhs_ref_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local x ---@type string|integer
local y ---@type string|integer
if type(y) ~= "string" then
return
end
if x == y then
after_guard = x
end
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_eq_uses_branch_narrowed_rhs_index_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Foo
---@field kind "foo"
---@class Bar
---@field kind "bar"
local x ---@type "foo"|"bar"
local y ---@type Foo|Bar
if y.kind == "foo" then
if x == y.kind then
after_guard = x
end
end
"#,
);
let after_guard = ws.expr_ty("after_guard");
assert_eq!(ws.humanize_type(after_guard), r#""foo""#);
}
#[test]
fn test_initializer_uses_branch_narrowed_dynamic_key() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class T
---@field foo string
---@field bar integer
local t ---@type T
local key ---@type "foo"|"bar"
if true then
key = "foo"
local value = t[key]
after_guard = value
end
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_eq_uses_branch_narrowed_dynamic_rhs_key() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class T
---@field foo string
---@field bar integer
local t ---@type T
local key ---@type "foo"|"bar"
local x ---@type string|integer
key = "foo"
if x == t[key] then
after_guard = x
end
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_field_literal_eq_uses_branch_narrowed_dynamic_key() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Foo
---@field kind "foo"
---@field value_key string
---@field value string
---@class Bar
---@field kind "bar"
---@field value_key "foo"
---@field value integer
local obj ---@type Foo|Bar
local key ---@type "kind"|"value_key"
key = "kind"
if obj[key] == "foo" then
after_guard = obj.value
end
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_field_truthy_uses_branch_narrowed_dynamic_key() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Present
---@field present true
---@field other true
---@field value string
---@class Missing
---@field present false?
---@field other true
---@field value integer
local obj ---@type Present|Missing
local key ---@type "present"|"other"
key = "present"
if obj[key] then
after_guard = obj.value
end
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_stacked_dynamic_field_truthy_guards_build_semantic_model() {
let mut ws = VirtualWorkspace::new();
let repeated_guards = "if not obj[key] then return end\n".repeat(STACKED_TYPE_GUARDS);
let block = format!(
r#"
---@class PresentDynamic
---@field present true
---@field other true
---@field value string
---@class MissingDynamic
---@field present false?
---@field other true
---@field value integer
local obj ---@type PresentDynamic|MissingDynamic
local key ---@type "present"|"other"
key = "present"
{repeated_guards}
after_guard = obj.value
"#,
);
let file_id = ws.def(&block);
assert!(
ws.analysis
.compilation
.get_semantic_model(file_id)
.is_some(),
"expected semantic model for stacked dynamic-field truthiness repro"
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_field_literal_eq_uses_branch_narrowed_dynamic_key_index_dependency() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class FooIndexKey
---@field kind "foo"
---@field value_key string
---@field value string
---@class BarIndexKey
---@field kind "bar"
---@field value_key "foo"
---@field value integer
local obj ---@type FooIndexKey|BarIndexKey
local keys = { "kind", "value_key" }
local slot ---@type 1|2
slot = 1
if obj[keys[slot]] == "foo" then
after_guard = obj.value
end
"#,
);
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
}
#[test]
fn test_assignment_after_pending_return_cast_guard_drops_branch_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Creature
---@class Player: Creature
---@class Monster: Creature
---@param creature Creature
---@return boolean
---@return_cast creature Player else Monster
local function is_player(creature)
return true
end
local creature ---@type Creature
local next_creature ---@type Creature
if not is_player(creature) then
return
end
before_assign = creature
creature = next_creature
after_assign = creature
"#,
);
assert_eq!(ws.expr_ty("before_assign"), ws.ty("Player"));
assert_eq!(ws.expr_ty("after_assign"), ws.ty("Creature"));
}
#[test]
fn test_assignment_after_binary_call_guard_eq_false_drops_branch_narrowing() {
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
local value ---@type string|integer|boolean
local next_value ---@type string|integer|boolean
if instance_of(value, 'string') == false then
return
end
before_assign = value
value = next_value
after_assign = value
"#,
);
assert_eq!(ws.expr_ty("before_assign"), ws.ty("string"));
assert_eq!(ws.expr_ty("after_assign"), ws.ty("string|integer|boolean"));
}
#[test]
fn test_assignment_after_mixed_eager_and_pending_guards_drops_branch_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Player
---@field kind "player"
---@class Monster
---@field kind "monster"
---@param creature Player|Monster
---@return boolean
---@return_cast creature Player else Monster
local function is_player(creature)
return true
end
local creature ---@type Player|Monster
local next_creature ---@type Player|Monster
if creature.kind ~= "player" then
return
end
if not is_player(creature) then
return
end
before_assign = creature
creature = next_creature
after_assign = creature
"#,
);
assert_eq!(ws.expr_ty("before_assign"), ws.ty("Player"));
assert_eq!(ws.expr_ty("after_assign"), ws.ty("Player|Monster"));
}
#[test]
fn test_assignment_missing_rhs_slot_drops_branch_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local cond ---@type boolean
local y = cond and "s" or 1
if type(y) == "string" then
local x
x, y = 1
after_assign = y
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("nil"));
}
#[test]
fn test_assignment_exhausted_return_slot_drops_branch_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return string
local function one()
end
local cond ---@type boolean
local y = cond and "s" or 1
if type(y) == "string" then
local x
x, y = one()
after_assign = y
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("nil"));
}
#[test]
fn test_assignment_from_nullable_union_keeps_rhs_members() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local x ---@type string?
local y ---@type number?
if x then
x = y
after_assign = x
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("number?"));
}
#[test]
fn test_enum_flag_bitop_assignment_keeps_declared_field_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@enum SubscriberFlags
local SubscriberFlags = {
Tracking = 1 << 0
}
---@class Subscriber
---@field flags SubscriberFlags
---@type Subscriber
local subscriber
subscriber.flags = subscriber.flags & ~SubscriberFlags.Tracking
after_bitop = subscriber.flags
"#,
);
assert_eq!(ws.expr_ty("after_bitop"), ws.ty("SubscriberFlags"));
}
#[test]
fn test_index_expr_replay_keeps_literal_field_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class T
---@field x "foo"|"bar"
local t ---@type T
local x ---@type "foo"|"bar"
if t.x == "foo" then
if x == t.x then
after_guard = x
end
end
"#,
);
let after_guard = ws.expr_ty("after_guard");
assert_eq!(ws.humanize_type(after_guard), r#""foo""#);
}
#[test]
fn test_assignment_from_partially_overlapping_union_keeps_rhs_members() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local x ---@type string|number
local y ---@type integer|string
if x == 1 then
x = y
after_assign = x
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("integer|string"));
}
#[test]
fn test_empty_table_fallback_assignment_keeps_indexed_table_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@type table<int, table<int, table>>
local archiveCache = {}
local playerCache = archiveCache[0]
if not playerCache then
playerCache = {}
end
A = playerCache
"#,
);
let actual = ws.expr_ty("A");
assert_eq!(ws.humanize_type(actual), "table<integer,table>");
}
#[test]
fn test_empty_table_fallback_assignment_keeps_nullable_table_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local playerCache ---@type table<int, table>?
if not playerCache then
playerCache = {}
end
A = playerCache
"#,
);
let actual = ws.expr_ty("A");
assert_eq!(ws.humanize_type(actual), "table<integer,table>");
}
#[test]
fn test_empty_table_nil_guard_does_not_drop_false_slot_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local playerCache ---@type table<int, table>|false
if playerCache == nil then
playerCache = {}
end
A = playerCache
"#,
);
let actual = ws.expr_ty("A");
assert_eq!(
ws.humanize_type_detailed(actual),
"(table<integer,table>|false)"
);
}
#[test]
fn test_partial_table_reassignment_preserves_branch_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Foo
---@field kind "foo"
---@field a integer
---@class Bar
---@field kind "bar"
---@field b integer
local x ---@type Foo|Bar
if x.kind == "foo" then
x = {}
x.kind = "foo"
x.a = 1
after_assign = x
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("Foo"));
}
#[test]
fn test_partial_table_reassignment_with_discriminant_preserves_branch_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Foo
---@field kind "foo"
---@field a integer
---@class Bar
---@field kind "bar"
---@field b integer
local x ---@type Foo|Bar
if x.kind == "foo" then
x = { kind = "foo" }
x.a = 1
after_assign = x
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("Foo"));
}
#[test]
fn test_exact_string_reassignment_preserves_literal_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local x ---@type string|number
if x == 1 then
x = "a"
after_assign = x
end
"#,
);
let after_assign = ws.expr_ty("after_assign");
assert_eq!(ws.humanize_type(after_assign), r#""a""#);
}
#[test]
fn test_assignment_from_broad_string_drops_literal_branch_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
local x ---@type "a"|boolean
local y ---@type string
if x == "a" then
x = y
after_assign = x
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("string"));
}
#[test]
fn test_partial_table_reassignment_with_conflicting_discriminant_drops_branch_narrowing() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class Foo
---@field kind "foo"
---@field a integer
---@class Bar
---@field kind "bar"
---@field b integer
local x ---@type Foo|Bar
if x.kind == "foo" then
x = { kind = "bar" }
after_assign = x
end
"#,
);
assert_eq!(ws.expr_ty("after_assign"), ws.ty("Foo|Bar"));
}
#[test]
fn test_issue_1048() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
--- @alias RunMode 'run'|'skip'
--- @class Suite
--- @field result string?
--- @field mode RunMode
--- @param a string
function TestSuite(a) end
"#,
);
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
--- @type Suite
local suite
suite.result = 'a'
if suite.mode == "run" then
TestSuite(suite.result)
end
"#,
));
}
#[test]
fn test_discriminant_narrowed_sibling_field_keeps_prior_assignment_flow() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field type "point"
---@field handle string?
---@class B
---@field type "unit"
---@field handle integer?
---@param a string
function testA(a) end
"#,
);
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
---@param target A | B
function test(target)
target.handle = "ready"
if target.type == "point" then
testA(target.handle)
end
end
"#,
));
}
#[test]
fn test_discriminant_narrowed_sibling_field_keeps_prior_truthiness_flow() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field type "point"
---@field handle string?
---@class B
---@field type "unit"
---@field handle integer?
---@param a string
function testA(a) end
"#,
);
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
---@param target A | B
function test(target)
if target.handle then
if target.type == "point" then
testA(target.handle)
end
end
end
"#,
));
}
#[test]
fn test_discriminant_narrowed_sibling_field_keeps_prior_nil_guard_flow() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field type "point"
---@field handle string?
---@class B
---@field type "unit"
---@field handle integer?
---@param a string
function testA(a) end
"#,
);
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
---@param target A | B
function test(target)
if target.handle ~= nil then
if target.type == "point" then
testA(target.handle)
end
end
end
"#,
));
}
#[test]
fn test_discriminant_narrowed_sibling_field_keeps_prior_literal_guard_flow() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field type "point"
---@field handle string?
---@class B
---@field type "unit"
---@field handle integer?
---@param a string
function testA(a) end
"#,
);
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
---@param target A | B
function test(target)
if target.handle == "ready" then
if target.type == "point" then
testA(target.handle)
end
end
end
"#,
));
}
#[test]
fn test_discriminant_false_branch_all_members_match_is_never() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field kind "foo"
---@class B
---@field kind "foo"
"#,
);
ws.def(
r#"
---@param target A | B
function test(target)
if target.kind ~= "foo" then
impossible = target
end
end
"#,
);
assert_eq!(ws.expr_ty("impossible"), ws.ty("never"));
}
#[test]
fn test_discriminant_sibling_projection_preserves_missing_member_nil() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@class A
---@field type "point"
---@field handle string
---@class C
---@field type "point"
---@param a string
function testA(a) end
"#,
);
assert!(!ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
---@param target A | C
function test(target)
if target.type == "point" then
testA(target.handle)
end
end
"#,
));
}
}