From 0bc11af2328fec936282a9da853a898bcee0d713 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 14 Oct 2026 10:01:07 +0100 Subject: [PATCH] fix(semantic): preserve vararg bindings and result types Vararg uses can be missed by reference collection. Type inference also confuses whole tuple values with separate result slots. This causes false diagnostics, missing editor references, and incorrect types. This use must not produce an unused warning: local function forward(...) print(...) end forward('hello') Record each ... use against its function's declaration so reference queries and unused checks can find it. Tuple-valued arguments must remain whole when forwarded or packed: ---@param ... [string, integer] local function pack(...) local packed = {...} return packed[2] -- [string, integer] end pack({'a', 1}, {'b', 2}) A trailing ... in the annotation instead expands the tuple fields into separate arguments and results: ---@param ... [string, integer]... local function forward(...) return ... end local text, count = forward('a', 1) VariadicType::Base previously handled both repetition and tuple expansion. Add a Repeat(LuaType) variant to VariadicType for repeated whole values. This keeps each tuple table as one value when forwarding or packing. Keep Base for declared T... expansion, which can wait until generic types are known, and Multi for known result slots. --- .../src/check/checker/param_count.rs | 1 + .../src/check/checker/param_type_check.rs | 2 +- .../check/checker/unbalanced_assignments.rs | 9 +- .../check/test/assign_type_mismatch_test.rs | 227 ++++++++++ .../src/check/test/call_non_callable_test.rs | 52 +++ .../check/test/unbalanced_assignments_test.rs | 43 ++ .../src/check/test/unnecessary_if_test.rs | 51 +++ .../src/check/test/unused_test.rs | 154 +++++++ .../semantic_db/def/lua_type/types/complex.rs | 55 ++- .../def/lua_type/types/predicates.rs | 9 +- .../def/lua_type/types/traverse.rs | 2 +- .../src/semantic_db/def/name_use.rs | 4 +- .../src/semantic_db/facade.rs | 3 + .../src/semantic_db/facts.rs | 20 + .../src/semantic_db/query.rs | 49 +- .../src/semantic_db/types.rs | 35 ++ .../src/semantic_model/access.rs | 15 +- .../src/semantic_model/flow.rs | 5 +- .../src/semantic_model/infer/mod.rs | 7 +- .../src/semantic_model/infer/overload.rs | 50 +- .../src/semantic_model/infer/test.rs | 427 ++++++++++++++++++ .../src/semantic_model/infer/unify.rs | 19 +- .../src/semantic_model/infer/vm.rs | 296 ++++++------ .../src/semantic_model/member.rs | 21 +- .../src/semantic_model/render/mod.rs | 5 +- .../src/semantic_model/type_check/assign.rs | 6 +- .../semantic_model/type_check/complex_type.rs | 2 +- .../src/semantic_model/type_check/context.rs | 31 +- .../src/semantic_model/type_check/mod.rs | 10 +- .../semantic_model/type_check/simple_type.rs | 54 ++- .../src/semantic_model/type_eval.rs | 25 +- crates/emmylua_doc_cli/src/doc_model.rs | 6 +- .../src/handlers/hover/build_hover.rs | 2 +- .../src/handlers/test/definition_test.rs | 20 + .../src/handlers/test/references_test.rs | 27 ++ 35 files changed, 1488 insertions(+), 256 deletions(-) diff --git a/crates/emmylua_code_analysis/src/check/checker/param_count.rs b/crates/emmylua_code_analysis/src/check/checker/param_count.rs index 98a0d8dc5..d67fa5768 100644 --- a/crates/emmylua_code_analysis/src/check/checker/param_count.rs +++ b/crates/emmylua_code_analysis/src/check/checker/param_count.rs @@ -730,6 +730,7 @@ fn param_count_range( None } } + VariadicType::Repeat(_) => None, VariadicType::Multi(types) => Some(types.len()), }, _ => None, diff --git a/crates/emmylua_code_analysis/src/check/checker/param_type_check.rs b/crates/emmylua_code_analysis/src/check/checker/param_type_check.rs index 18d177004..37e2dfa5c 100644 --- a/crates/emmylua_code_analysis/src/check/checker/param_type_check.rs +++ b/crates/emmylua_code_analysis/src/check/checker/param_type_check.rs @@ -1402,7 +1402,7 @@ fn contains_mapped(ty: &LuaType) -> bool { .any(|(k, v)| contains_mapped(k) || contains_mapped(v)) } Variadic(variadic) => match variadic.as_ref() { - VariadicType::Base(base) => contains_mapped(base), + VariadicType::Base(base) | VariadicType::Repeat(base) => contains_mapped(base), VariadicType::Multi(types) => types.iter().any(contains_mapped), }, Call(call) => call.get_operands().iter().any(contains_mapped), diff --git a/crates/emmylua_code_analysis/src/check/checker/unbalanced_assignments.rs b/crates/emmylua_code_analysis/src/check/checker/unbalanced_assignments.rs index d990dbe7e..14197277a 100644 --- a/crates/emmylua_code_analysis/src/check/checker/unbalanced_assignments.rs +++ b/crates/emmylua_code_analysis/src/check/checker/unbalanced_assignments.rs @@ -1,6 +1,6 @@ //! unbalanced_assignments: not enough values for `local a, b = 1` / `a, b = 1`. -use emmylua_parser::{LuaAstNode, LuaExpr, LuaStat}; +use emmylua_parser::{LuaAstNode, LuaExpr, LuaLiteralToken, LuaStat}; use crate::DiagnosticCode; use crate::semantic_model::SemanticModel; @@ -38,11 +38,14 @@ fn check_unbalanced( vars: &[T], value_exprs: &[LuaExpr], ) { - // No values (`local a, b`) is valid; if the last value is a call (may return multiple values), skip in M0. + // Calls and varargs can supply multiple values only at the end of the list. let Some(last) = value_exprs.last() else { return; }; - if matches!(last, LuaExpr::CallExpr(_)) { + if matches!(last, LuaExpr::CallExpr(_)) + || matches!(last, LuaExpr::LiteralExpr(literal) + if matches!(literal.get_literal(), Some(LuaLiteralToken::Dots(_)))) + { return; } let value_len = value_exprs.len(); diff --git a/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs b/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs index e39c02d2c..c05c49aa3 100644 --- a/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs @@ -3,6 +3,233 @@ mod tests { use crate::check::test::check_source; use crate::{DiagnosticCode, VirtualWorkspace}; + #[test] + fn test_vararg_packing_keeps_tuple_assignment_checks() { + let source = r#" + ---@param ... [string, integer] + local function tuples(...) + local packed = { ... } + ---@type [string, integer] + local first = packed[1] + ---@type [string, integer] + local second = packed[2] + ---@type integer + local wrong = packed[3] + return first, second, wrong + end + return tuples({'a', 1}, {'b', 2}, {'c', 3}) + "#; + let diagnostics = check_source(source); + assert_eq!(diagnostics.len(), 1, "{diagnostics:#?}"); + assert_eq!(diagnostics[0].code, DiagnosticCode::AssignTypeMismatch); + assert_eq!( + usize::from(diagnostics[0].range.start()), + source.find("packed[3]").unwrap() + ); + assert_eq!( + diagnostics[0].message, + "expected `integer` but found `(string,integer)`. expected `integer`, found `(string,integer)`" + ); + } + + #[test] + fn test_vararg_builtin_results_keep_assignment_checks() { + let source = r#" + ---@param ... [string, integer] + local function forward(...) return ... end + local packed = {pcall(forward, {'a', 1}, {'b', 2})} + ---@type [string, integer]? + local valid_tuple = packed[3] + ---@type integer? + local invalid_tuple = packed[3] + ---@param ... integer + local function values(...) return true, ... end + ---@type integer + local valid_integer = select(3, values(1, 2, 3)) + ---@type string? + local invalid_integer = select(3, values(1, 2, 3)) + return valid_tuple, invalid_tuple, valid_integer, invalid_integer + "#; + let diagnostics = check_source(source); + assert_eq!( + diagnostics + .iter() + .map(|diagnostic| (diagnostic.code, usize::from(diagnostic.range.start()))) + .collect::>(), + vec![ + ( + DiagnosticCode::AssignTypeMismatch, + source.find("invalid_tuple = packed[3]").unwrap() + "invalid_tuple = ".len() + ), + ( + DiagnosticCode::AssignTypeMismatch, + source.rfind("select(3, values(1, 2, 3))").unwrap() + ), + ], + "{diagnostics:#?}" + ); + } + + #[test] + fn test_vararg_function_results_keep_callback_parameter_rules() { + let source = r#" + ---@alias Formatter fun(text: string): string + ---@alias Constant fun(): string + ---@alias WrongArgument fun(value: integer): string + local function constant() return 'hello' end + ---@param ... Constant + local function forward(...) return ... end + local function produce() return forward(constant, constant) end + ---@param producer fun(): Formatter... + local function use(producer) + local first, second = producer() + return first('one'), second('two') + end + ---@param producer fun(): Formatter, Formatter... + local function use_tail(producer) + local first, second = producer() + return first('one'), second('two') + end + ---@param ... WrongArgument + local function invalid(...) return ... end + use(produce) + use_tail(produce) + use(invalid) + use_tail(invalid) + constant('extra') + "#; + let diagnostics = check_source(source); + let mut actual = diagnostics + .iter() + .map(|d| (d.code, usize::from(d.range.start()))) + .collect::>(); + actual.sort_by_key(|(_, offset)| *offset); + assert_eq!( + actual, + vec![ + ( + DiagnosticCode::ParamTypeMismatch, + source.find("use(invalid)").unwrap() + "use(".len() + ), + ( + DiagnosticCode::ParamTypeMismatch, + source.find("use_tail(invalid)").unwrap() + "use_tail(".len() + ), + ( + DiagnosticCode::RedundantParameter, + source.find("'extra'").unwrap() + ), + ], + "{diagnostics:#?}" + ); + } + + #[test] + fn test_vararg_callback_numeric_results_follow_assignment_rules() { + let source = r#" + ---@param callback fun(): number... + local function accepts_numbers(callback) return callback() end + ---@param callback fun(): number, number... + local function accepts_number_tail(callback) return callback() end + ---@param ... integer + local function integers(...) return ... end + accepts_numbers(integers) + accepts_number_tail(integers) + ---@param callback fun(): integer... + local function requires_integers(callback) return callback() end + ---@param callback fun(): integer, integer... + local function requires_integer_tail(callback) return callback() end + ---@param ... number + local function numbers(...) return ... end + requires_integers(numbers) + requires_integer_tail(numbers) + "#; + let diagnostics = check_source(source); + let mut actual = diagnostics + .iter() + .map(|d| (d.code, usize::from(d.range.start()))) + .collect::>(); + actual.sort_by_key(|(_, offset)| *offset); + assert_eq!( + actual, + vec![ + ( + DiagnosticCode::ParamTypeMismatch, + source.find("requires_integers(numbers)").unwrap() + "requires_integers(".len() + ), + ( + DiagnosticCode::ParamTypeMismatch, + source.find("requires_integer_tail(numbers)").unwrap() + + "requires_integer_tail(".len() + ), + ], + "{diagnostics:#?}" + ); + } + + #[test] + fn test_vararg_generic_callback_keeps_assignment_and_callback_checks() { + let source = r#" + ---@generic First, Rest + ---@param callback fun(): First, Rest... + ---@return First, Rest... + local function wrap(callback) return callback() end + ---@param ... integer + local function values(...) return ... end + local packed = {wrap(function() return values(1, 2, 3) end)} + ---@type integer + local valid = packed[3] + ---@type string? + local invalid = packed[3] + ---@generic Rest + ---@param callback fun(): string, Rest... + local function requires_text(callback) return callback() end + requires_text(function() return values(1, 2, 3) end) + ---@generic First + ---@param callback fun(): First, string... + local function requires_string_tail(callback) return callback() end + requires_string_tail(values) + ---@param callback fun(): integer... + local function consume_numbers(callback) return callback() end + ---@param ... [integer, integer]... + local function fixed(...) return ... end + consume_numbers(fixed) + ---@generic Results + ---@param callback fun(): Results... + ---@return Results... + local function invoke(callback) return callback() end + ---@param ... [string, integer] + local function tuples(...) return ... end + local function pairs() return tuples({'a', 1}, {'b', 2}) end + invoke(pairs) + return valid, invalid + "#; + let diagnostics = check_source(source); + let mut actual = diagnostics + .iter() + .map(|d| (d.code, usize::from(d.range.start()))) + .collect::>(); + actual.sort_by_key(|(_, offset)| *offset); + assert_eq!( + actual, + vec![ + ( + DiagnosticCode::AssignTypeMismatch, + source.rfind("packed[3]").unwrap() + ), + ( + DiagnosticCode::ParamTypeMismatch, + source.rfind("function() return values").unwrap() + ), + ( + DiagnosticCode::ParamTypeMismatch, + source.rfind("values)").unwrap() + ), + ], + "{diagnostics:#?}" + ); + } + #[test] fn test_table_literal_numeric_key_uses_index_signature() { let diagnostics = check_source( diff --git a/crates/emmylua_code_analysis/src/check/test/call_non_callable_test.rs b/crates/emmylua_code_analysis/src/check/test/call_non_callable_test.rs index dbc681c7b..3d94a721c 100644 --- a/crates/emmylua_code_analysis/src/check/test/call_non_callable_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/call_non_callable_test.rs @@ -4,6 +4,58 @@ mod test { use lsp_types::NumberOrString; use tokio_util::sync::CancellationToken; + #[test] + fn test_vararg_first_function_is_callable() { + let diagnostics = crate::check::test::check_source( + r#" + ---@param ... fun(value: integer): integer + local function apply(...) + return (...)(1) + end + print(apply(function(value) return value end)) + "#, + ); + assert!(diagnostics.is_empty(), "{diagnostics:#?}"); + } + + #[test] + fn test_vararg_first_function_checks_arguments() { + let diagnostics = crate::check::test::check_source( + r#" + ---@param ... fun(value: integer): integer + local function apply(...) + return (...)('bad') + end + print(apply(function(value) return value end)) + "#, + ); + assert_eq!( + crate::check::test::count_by_code(&diagnostics, DiagnosticCode::ParamTypeMismatch), + 1 + ); + assert_eq!( + crate::check::test::count_by_code(&diagnostics, DiagnosticCode::CallNonCallable), + 0 + ); + } + + #[test] + fn test_vararg_first_number_is_not_callable() { + let diagnostics = crate::check::test::check_source( + r#" + ---@param ... integer + local function apply(...) + return (...)(1) + end + print(apply(2)) + "#, + ); + assert_eq!( + crate::check::test::count_by_code(&diagnostics, DiagnosticCode::CallNonCallable), + 1 + ); + } + #[test] fn test_callable_table_accepts_valid_arguments() { let mut ws = VirtualWorkspace::new_with_init_std_lib(); diff --git a/crates/emmylua_code_analysis/src/check/test/unbalanced_assignments_test.rs b/crates/emmylua_code_analysis/src/check/test/unbalanced_assignments_test.rs index 5acddba8e..f5f835604 100644 --- a/crates/emmylua_code_analysis/src/check/test/unbalanced_assignments_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/unbalanced_assignments_test.rs @@ -42,4 +42,47 @@ mod tests { 0 ); } + + #[test] + fn test_vararg_tail_supplies_multiple_values() { + let diags = check_source( + r#" + local function values(...) + local first, second = ... + first, second = 1, ... + print(first, second) + end + values('hello', 'world') + "#, + ); + assert_eq!( + count_by_code(&diags, DiagnosticCode::UnbalancedAssignments), + 0 + ); + } + + #[test] + fn test_vararg_single_value_still_reports_missing_values() { + let source = r#" + local function values(...) + local first, parenthesized = (...) + local other, nonfinal, missing = ..., 'last' + print(first, parenthesized, other, nonfinal, missing) + end + values('hello', 'world') + "#; + let diags = check_source(source); + let offsets: Vec<_> = diags + .iter() + .filter(|d| d.code == DiagnosticCode::UnbalancedAssignments) + .map(|d| usize::from(d.range.start())) + .collect(); + assert_eq!( + offsets, + vec![ + source.find("parenthesized").unwrap(), + source.find("missing").unwrap() + ] + ); + } } diff --git a/crates/emmylua_code_analysis/src/check/test/unnecessary_if_test.rs b/crates/emmylua_code_analysis/src/check/test/unnecessary_if_test.rs index 2cf31c991..fe7c87d33 100644 --- a/crates/emmylua_code_analysis/src/check/test/unnecessary_if_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/unnecessary_if_test.rs @@ -2,6 +2,57 @@ mod test { use crate::{DiagnosticCode, VirtualWorkspace}; + #[test] + fn test_vararg_unknown_and_boolean_conditions_are_not_constant() { + let diagnostics = crate::check::test::check_source( + r#" + local function unknown(...) + if ... then print('present') end + assert(...) + end + ---@param ... boolean + local function boolean(...) + if ... then print('true') end + assert(...) + end + unknown(false) + boolean(false) + "#, + ); + assert!(diagnostics.is_empty(), "{diagnostics:#?}"); + } + + #[test] + fn test_vararg_false_conditions_still_report_impossible_branches() { + let diagnostics = crate::check::test::check_source( + r#" + ---@param ... false + local function always_false(...) + if ... then print('unreachable') end + assert(...) + end + always_false(false) + "#, + ); + let messages: Vec<_> = diagnostics + .iter() + .map(|d| (d.code, d.message.as_str())) + .collect(); + assert_eq!( + messages, + vec![ + ( + DiagnosticCode::UnnecessaryAssert, + "Impossible assert: this expression is always falsy; prefer `error()`" + ), + ( + DiagnosticCode::UnnecessaryIf, + "Impossible `if` statement: this condition is always falsy" + ), + ] + ); + } + #[test] fn test_issue_392() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/check/test/unused_test.rs b/crates/emmylua_code_analysis/src/check/test/unused_test.rs index e5bab26f2..563179bbc 100644 --- a/crates/emmylua_code_analysis/src/check/test/unused_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/unused_test.rs @@ -24,6 +24,160 @@ mod tests { assert_eq!(count_by_code(&diags, DiagnosticCode::Unused), 0); } + #[test] + fn test_used_varargs_forwarded() { + let diags = check_source( + r#" + local function forward(...) + print(...) + end + local function identity(...) + return ... + end + forward(identity('hello')) + "#, + ); + assert_eq!(count_by_code(&diags, DiagnosticCode::Unused), 0); + } + + #[test] + fn test_used_varargs_packed() { + let diags = check_source( + r#" + local function pack(...) + return { ... } + end + local function pack_with_count(...) + return table.pack(...) + end + print(pack('hello'), pack_with_count('hello')) + "#, + ); + assert_eq!(count_by_code(&diags, DiagnosticCode::Unused), 0); + } + + #[test] + fn test_used_varargs_counted() { + let diags = check_source( + r#" + local function count(...) + return select('#', ...) + end + print(count('hello')) + "#, + ); + assert_eq!(count_by_code(&diags, DiagnosticCode::Unused), 0); + } + + #[test] + fn test_unused_varargs_reported() { + let source = r#" + local function ignore(...) + print('hello') + end + ignore('hello') + "#; + let diags = check_source(source); + let unused: Vec<_> = diags + .iter() + .filter(|d| d.code == DiagnosticCode::Unused) + .collect(); + assert_eq!(unused.len(), 1); + assert_eq!( + usize::from(unused[0].range.start()), + source.find("...").unwrap() + ); + assert_eq!(unused[0].range.len(), rowan::TextSize::from(3)); + assert_eq!( + unused[0].message, + "... is never used, if this is intentional, prefix it with an underscore: _..." + ); + } + + #[test] + fn test_used_inner_varargs_do_not_use_outer_varargs() { + let source = r#" + local function outer(...) + local function inner(...) + print(...) + end + inner('hello') + end + outer('ignored') + "#; + let diags = check_source(source); + let unused_offsets: Vec<_> = diags + .iter() + .filter(|d| d.code == DiagnosticCode::Unused) + .map(|d| usize::from(d.range.start())) + .collect(); + assert_eq!(unused_offsets, vec![source.find("...").unwrap()]); + } + + #[test] + fn test_used_outer_varargs_do_not_use_inner_varargs() { + let source = r#" + local function outer(...) + local function inner(...) + print('hello') + end + inner() + print(...) + end + outer('hello') + "#; + let diags = check_source(source); + let unused_offsets: Vec<_> = diags + .iter() + .filter(|d| d.code == DiagnosticCode::Unused) + .map(|d| usize::from(d.range.start())) + .collect(); + assert_eq!(unused_offsets, vec![source.find("inner(...)").unwrap() + 6]); + } + + #[test] + fn test_invalid_inner_varargs_do_not_use_outer_varargs() { + let source = r#" + local function outer(...) + local function inner() + print(...) + end + inner() + end + outer('ignored') + "#; + let diags = check_source(source); + let unused_offsets: Vec<_> = diags + .iter() + .filter(|d| d.code == DiagnosticCode::Unused) + .map(|d| usize::from(d.range.start())) + .collect(); + assert_eq!(unused_offsets, vec![source.find("...").unwrap()]); + let syntax_errors: Vec<_> = diags + .iter() + .filter(|d| d.code == DiagnosticCode::SyntaxError) + .map(|d| (usize::from(d.range.start()), d.message.as_str())) + .collect(); + assert_eq!( + syntax_errors, + vec![( + source.rfind("...").unwrap(), + "Cannot use `...` outside a vararg function." + )] + ); + } + + #[test] + fn test_chunk_varargs_do_not_add_diagnostics() { + let diags = check_source( + r#" + local args = { ... } + return args + "#, + ); + assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}"); + } + #[test] fn test_unused_implicit_self_reports_on_colon() { // The implicit `self` of `function C:foo()` is never used. The diagnostic should diff --git a/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs b/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs index 29500060c..c76206664 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs @@ -729,8 +729,34 @@ impl From for LuaType { #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum VariadicType { + /// Result slots whose types are already known, with an optional variadic tail. + /// A function declared with `---@return string, integer` has two result slots: + /// `Multi([String, Integer])`. A call can return `'a', 1`. Multi(Vec), + + /// A declared `T...`; a tuple `T` supplies separate result slots. + /// + /// ```lua + /// ---@param ... [string, integer]... + /// local function forward(...) return ... end + /// forward('a', 1) + /// ``` + /// The annotation gives `Base(Tuple([String, Integer]))`. This expands to + /// `Multi([String, Integer])`: two separate results, a string and an integer. + /// With a generic `T`, expansion can wait until `T` is known. Base(LuaType), + + /// Any number of whole values of the same type. + /// + /// ```lua + /// ---@param ... [string, integer] + /// local function tuples(...) local packed = {...} end + /// tuples({'a', 1}, {'b', 2}) + /// ``` + /// Here, each argument is a tuple table. The type of `...` is + /// `Repeat(Tuple([String, Integer]))`. In `packed`, element 1 is `{'a', 1}` + /// and element 2 is `{'b', 2}`; both elements have the whole tuple type. + Repeat(LuaType), } impl VariadicType { @@ -761,6 +787,7 @@ impl VariadicType { LuaType::Tuple(tuple) => tuple.get_types().get(idx), _ => Some(t), }, + VariadicType::Repeat(t) => Some(t), } } @@ -776,15 +803,33 @@ impl VariadicType { new_types.extend_from_slice(&types[idx..]); } else { let last = types.len() - 1; - if let LuaType::Variadic(multi) = &types[last] { - let rest_offset = idx - last; - return multi.get_new_variadic_from(rest_offset); + let rest_offset = idx - last; + match &types[last] { + LuaType::Variadic(multi) => { + return multi.get_new_variadic_from(rest_offset); + } + LuaType::Union(union) if types[last].contain_multi_return() => { + let tails = union + .iter() + .map(|ty| { + let tail = VariadicType::Multi(vec![ty.clone()]) + .get_new_variadic_from(rest_offset); + LuaType::Variadic(Rc::new(tail)) + }) + .collect(); + return VariadicType::Multi(vec![LuaType::from_vec(tails)]); + } + _ => {} } } VariadicType::Multi(new_types) } + VariadicType::Base(LuaType::Tuple(tuple)) => { + VariadicType::Multi(tuple.get_types().iter().skip(idx).cloned().collect()) + } VariadicType::Base(t) => VariadicType::Base(t.clone()), + VariadicType::Repeat(t) => VariadicType::Repeat(t.clone()), } } @@ -794,6 +839,7 @@ impl VariadicType { pub fn get_min_len(&self) -> Option { match self { + VariadicType::Repeat(_) => None, VariadicType::Base(base) => match base { LuaType::Tuple(tuple) => Some(tuple.get_types().len()), _ => None, @@ -818,6 +864,7 @@ impl VariadicType { pub fn get_max_len(&self) -> Option { match self { + VariadicType::Repeat(_) => None, VariadicType::Base(base) => match base { LuaType::Tuple(tuple) => Some(tuple.get_types().len()), _ => None, @@ -828,6 +875,8 @@ impl VariadicType { if let LuaType::Variadic(variadic) = t { let len = variadic.get_max_len()?; total_len += len; + } else if t.contain_multi_return() { + return None; } else { total_len += 1; } diff --git a/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/predicates.rs b/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/predicates.rs index 3e4ea611f..48da006f6 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/predicates.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/predicates.rs @@ -114,6 +114,9 @@ impl LuaType { match self { LuaType::Nil | LuaType::Boolean | LuaType::Any | LuaType::Unknown => false, LuaType::BooleanConst(boolean) | LuaType::DocBooleanConst(boolean) => *boolean, + LuaType::Variadic(_) => self + .get_result_slot_type(0) + .is_some_and(|ty| ty.is_always_truthy()), LuaType::Union(u) => u.is_always_truthy(), LuaType::MultiLineUnion(u) => u.iter().all(LuaType::is_always_truthy), LuaType::TypeGuard(_) => false, @@ -124,6 +127,9 @@ impl LuaType { pub fn is_always_falsy(&self) -> bool { match self { LuaType::Nil | LuaType::BooleanConst(false) | LuaType::DocBooleanConst(false) => true, + LuaType::Variadic(_) => self + .get_result_slot_type(0) + .is_none_or(|ty| ty.is_always_falsy()), LuaType::Union(u) => u.is_always_falsy(), LuaType::MultiLineUnion(u) => u.iter().all(LuaType::is_always_falsy), LuaType::TypeGuard(_) => false, @@ -218,7 +224,8 @@ impl LuaType { pub fn get_result_slot_type(&self, idx: usize) -> Option { match self { LuaType::Variadic(variadic) => match variadic.as_ref() { - VariadicType::Base(base) => Some(base.clone()), + VariadicType::Base(_) => variadic.get_type(idx).cloned(), + VariadicType::Repeat(base) => Some(base.clone()), VariadicType::Multi(types) => { let last_idx = types.len().checked_sub(1)?; if idx < last_idx { diff --git a/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/traverse.rs b/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/traverse.rs index fe17fe9a3..0ec2c8092 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/traverse.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/traverse.rs @@ -197,7 +197,7 @@ impl LuaTypeNode for VariadicType { stack.push(ty); } } - VariadicType::Base(ty) => stack.push(ty), + VariadicType::Base(ty) | VariadicType::Repeat(ty) => stack.push(ty), } } } diff --git a/crates/emmylua_code_analysis/src/semantic_db/def/name_use.rs b/crates/emmylua_code_analysis/src/semantic_db/def/name_use.rs index 36e5df7e4..095bd6233 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/def/name_use.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/def/name_use.rs @@ -1,11 +1,11 @@ -//! Name-use site definition (`LuaNameExpr`). +//! Binding-use site definition (name expressions and bound vararg literals). use emmylua_parser::LuaSyntaxId; use smol_str::SmolStr; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct NameUse { - /// Name expression (unique identifier). + /// Name expression or vararg literal (unique identifier). pub syntax: LuaSyntaxId, pub name: SmolStr, } diff --git a/crates/emmylua_code_analysis/src/semantic_db/facade.rs b/crates/emmylua_code_analysis/src/semantic_db/facade.rs index aac68249b..a551dce14 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/facade.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/facade.rs @@ -706,6 +706,9 @@ impl<'db> AnalysisView<'db> { TypeCandidate::Variadic(base) => LuaType::Variadic(Rc::new(VariadicType::Base( self.project_shell_in(file_id, base, generic_names), ))), + TypeCandidate::Repeat(base) => LuaType::Variadic(Rc::new(VariadicType::Repeat( + self.project_shell_in(file_id, base, generic_names), + ))), TypeCandidate::Tuple(types) => LuaType::Tuple(Rc::new(LuaTupleType::new( types .iter() diff --git a/crates/emmylua_code_analysis/src/semantic_db/facts.rs b/crates/emmylua_code_analysis/src/semantic_db/facts.rs index 2687754f6..8df06f4be 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/facts.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/facts.rs @@ -1688,6 +1688,26 @@ impl FactsBuilder { }); } } + LuaAst::LuaLiteralExpr(literal) + if matches!(literal.get_literal(), Some(LuaLiteralToken::Dots(_))) => + { + // Varargs bind only in the nearest function; they cannot be captured. + if literal + .ancestors::() + .next() + .and_then(|closure| closure.get_params_list()) + .is_some_and(|params| { + params + .get_params() + .any(|param| param.is_dots() && param.get_name_token().is_none()) + }) + { + self.name_uses.push(NameUse { + syntax: literal.get_syntax_id(), + name: SmolStr::new("..."), + }); + } + } LuaAst::LuaIndexExpr(index_expr) => { self.member_uses.push(index_expr.get_syntax_id()); } diff --git a/crates/emmylua_code_analysis/src/semantic_db/query.rs b/crates/emmylua_code_analysis/src/semantic_db/query.rs index 60621d31a..389bf1aae 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/query.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/query.rs @@ -2497,7 +2497,7 @@ pub(crate) fn resolve_name( .or_else(|| global_decl_by_name(db, name_use.name.clone())) } -/// All references to a declaration (name use sites). +/// All references to a declaration (name use sites or vararg literals). pub(crate) fn decl_references( db: &SemanticDatabase, file: FileId, @@ -2594,10 +2594,19 @@ pub(crate) fn signature_returns( let Some(closure) = LuaClosureExpr::cast(node) else { return Vec::new(); }; - let multi_path_returns = closure.descendants::().count() > 1; + let returns: Vec<_> = closure + .descendants::() + .filter(|ret| { + ret.ancestors::() + .next() + .is_some_and(|owner| owner.get_syntax_id() == closure_syntax) + }) + .collect(); + let multi_path_returns = returns.len() > 1; let mut slots: Vec = Vec::new(); - for ret in closure.descendants::() { - for (index, expr) in ret.get_expr_list().enumerate() { + for ret in returns { + let exprs: Vec<_> = ret.get_expr_list().collect(); + for (index, expr) in exprs.iter().enumerate() { let shell = match &expr { LuaExpr::NameExpr(name) if name.get_name_text().as_deref() == Some("self") => { method_self_return_shell(&facts, closure_syntax) @@ -2618,6 +2627,11 @@ pub(crate) fn signature_returns( _ => None, } .unwrap_or_else(|| expr_type_of(db, file, config, expr.get_syntax_id())); + let shell = if index + 1 < exprs.len() { + shell.first_result() + } else { + shell + }; if let Some(slot) = slots.get_mut(index) { slot.merge(&shell); } else { @@ -2956,7 +2970,7 @@ fn expr_type_chain( ChainFrame::Index(index) => { shell = expr_type_index(db, facts, file, config, index.clone(), shell); } - ChainFrame::Paren => {} + ChainFrame::Paren => shell = shell.first_result(), } } Some(shell) @@ -2970,6 +2984,18 @@ fn expr_type_node( expr: LuaExpr, ) -> TypeShell { match expr { + LuaExpr::LiteralExpr(literal) + if matches!(literal.get_literal(), Some(LuaLiteralToken::Dots(_))) => + { + let shell = resolve_name(db, file, literal.get_position()) + .map(|decl| decl_type(db, file, config, decl)) + .unwrap_or_else(TypeShell::unknown); + if matches!(shell.candidates.as_slice(), [TypeCandidate::Variadic(_)]) { + shell + } else { + TypeShell::from_repeat(shell) + } + } LuaExpr::LiteralExpr(literal) => literal_type(&literal), LuaExpr::TableExpr(table) => { TypeShell::from_table(TableId::from_range(file, table.get_range())) @@ -3025,8 +3051,8 @@ fn expr_type_node( match op { Some(BinaryOperator::OpOr) | Some(BinaryOperator::OpAnd) => { if let Some((left, right)) = binary.get_exprs() { - let mut shell = expr_type(db, facts, file, config, left); - shell.merge(&expr_type(db, facts, file, config, right)); + let mut shell = expr_type(db, facts, file, config, left).first_result(); + shell.merge(&expr_type(db, facts, file, config, right).first_result()); return shell; } TypeShell::unknown() @@ -3059,13 +3085,13 @@ fn expr_type_node( } else { unary .get_expr() - .map(|expr| expr_type(db, facts, file, config, expr)) + .map(|expr| expr_type(db, facts, file, config, expr).first_result()) .unwrap_or_else(TypeShell::unknown) } } LuaExpr::ParenExpr(paren) => paren .get_expr() - .map(|expr| expr_type(db, facts, file, config, expr)) + .map(|expr| expr_type(db, facts, file, config, expr).first_result()) .unwrap_or_else(TypeShell::unknown), _ => TypeShell::unknown(), } @@ -3281,6 +3307,11 @@ fn substitute_generics( base, params, args, ))); } + TypeCandidate::Repeat(base) => { + out.merge(&TypeShell::from_repeat(substitute_generics( + base, params, args, + ))); + } TypeCandidate::Tuple(types) => { out.merge(&TypeShell::from_tuple( types diff --git a/crates/emmylua_code_analysis/src/semantic_db/types.rs b/crates/emmylua_code_analysis/src/semantic_db/types.rs index 316080809..29daba2de 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/types.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/types.rs @@ -100,6 +100,8 @@ pub enum TypeCandidate { Array(Arc), /// Variadic: `T...` → base type. Variadic(Arc), + /// Repeated runtime values; tuple values do not expand into their fields. + Repeat(Arc), /// Tuple: `[number, string]` → per-slot types. Tuple(Vec), Function(Arc), @@ -211,6 +213,39 @@ impl TypeShell { } } + pub fn from_repeat(base: TypeShell) -> Self { + Self { + candidates: vec![TypeCandidate::Repeat(Arc::new(base))], + } + } + + /// Reduce repeated result types to one value; ordinary value types stay unchanged. + pub(crate) fn first_result(&self) -> Self { + let mut out = Self::unknown(); + for candidate in &self.candidates { + match candidate { + TypeCandidate::Variadic(base) => { + for candidate in &base.candidates { + if let TypeCandidate::Tuple(types) = candidate { + if let Some(first) = types.first() { + out.merge(first); + } + } else { + out.merge(&Self { + candidates: vec![candidate.clone()], + }); + } + } + } + TypeCandidate::Repeat(base) => out.merge(base), + _ => out.merge(&Self { + candidates: vec![candidate.clone()], + }), + } + } + out + } + /// Tuple candidate: `[number, string]`. pub fn from_tuple(types: Vec) -> Self { Self { diff --git a/crates/emmylua_code_analysis/src/semantic_model/access.rs b/crates/emmylua_code_analysis/src/semantic_model/access.rs index 9711b1814..5e719e848 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/access.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/access.rs @@ -227,11 +227,8 @@ impl<'db> SemanticModel<'db> { { return Some(def.id); } - // 4. Name use point (right side of `x = 1`, etc.) -> Decl. - if kind == LuaSyntaxKind::NameExpr { - return self.resolve_name(offset); - } - None + // 4. Binding use (name expression or vararg literal) -> Decl. + self.resolve_name(offset) } /// Syntax location (node / token) -> semantic info (type + declaration identity). /// Shared query for LSP features such as hover / semantic_token and checkers. @@ -321,13 +318,9 @@ impl<'db> SemanticModel<'db> { }); } - // 5. Expression -> type (name expressions carry declaration identity). + // 5. Expression -> type and declaration identity for binding uses. if let Some(expr) = LuaExpr::cast(parent) { - let decl = if let LuaExpr::NameExpr(name_expr) = &expr { - self.resolve_name(name_expr.get_position()) - } else { - None - }; + let decl = self.resolve_name(offset); let typ = if matches!(&expr, LuaExpr::NameExpr(_)) { self.assignment_target_value_type(&expr) .unwrap_or_else(|| self.type_of_expr(expr.get_syntax_id())) diff --git a/crates/emmylua_code_analysis/src/semantic_model/flow.rs b/crates/emmylua_code_analysis/src/semantic_model/flow.rs index 98c05e290..52bbf078b 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/flow.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/flow.rs @@ -455,7 +455,9 @@ pub fn type_of_expr_at( if let Some(resolved) = model.resolve_member(index_expr) && let Some(member_id) = resolved.member_id { - let mut member_ty = type_of_member_at(model, &member_id, offset); + let mut member_ty = type_of_member_at(model, &member_id, offset) + .get_result_slot_type(0) + .unwrap_or(LuaType::Nil); // Old semantics use `LuaType::Signature` to represent the closure member's definition site; // flow-sensitive member values also keep that identity, matching the VM's member access path. if matches!(member_ty, LuaType::Function) @@ -2857,6 +2859,7 @@ fn widen_const_type(ty: LuaType) -> LuaType { LuaType::Variadic(variadic) => { let widened = match variadic.as_ref() { VariadicType::Base(base) => VariadicType::Base(widen_const_type(base.clone())), + VariadicType::Repeat(base) => VariadicType::Repeat(widen_const_type(base.clone())), VariadicType::Multi(types) => { VariadicType::Multi(types.iter().map(|t| widen_const_type(t.clone())).collect()) } diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/mod.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/mod.rs index c89f614a7..60f17c30d 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/mod.rs @@ -149,7 +149,10 @@ pub fn infer_expr_list_types( break; } - let expr_type = infer_expr(model, expr.get_syntax_id()); + let mut expr_type = infer_expr(model, expr.get_syntax_id()); + if idx + 1 < exprs.len() { + expr_type = expr_type.get_result_slot_type(0).unwrap_or(LuaType::Nil); + } if let Some(var_count) = var_count && expr_type.contain_multi_return() { @@ -168,7 +171,7 @@ pub fn infer_expr_list_types( match &expr_type { LuaType::Variadic(variadic) => { match variadic.as_ref() { - VariadicType::Base(base) => { + VariadicType::Base(base) | VariadicType::Repeat(base) => { value_types.push((base.clone(), expr.get_range())); } VariadicType::Multi(types) => { diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/overload.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/overload.rs index f6d6c261a..176c05a00 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/overload.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/overload.rs @@ -361,7 +361,7 @@ fn type_contains_tpl_ref(ty: &LuaType) -> bool { Generic(generic) => generic.get_params().iter().any(type_contains_tpl_ref), TableGeneric(generic) => generic.iter().any(type_contains_tpl_ref), Variadic(variadic) => match variadic.as_ref() { - VariadicType::Base(base) => type_contains_tpl_ref(base), + VariadicType::Base(base) | VariadicType::Repeat(base) => type_contains_tpl_ref(base), VariadicType::Multi(types) => types.iter().any(type_contains_tpl_ref), }, DocFunction(fun) => { @@ -686,39 +686,23 @@ pub fn pcall_return_type(callback_ret: LuaType, include_error_string: bool) -> L } } - let slots = match callback_ret { - LuaType::Variadic(variadic) => match variadic.as_ref() { - VariadicType::Multi(types) => { - let mut slots = vec![LuaType::Boolean]; - if let Some(first) = types.first() { - let slot = if include_error_string { - LuaType::from_vec(vec![first.clone(), LuaType::String]) - } else { - first.clone() - }; - slots.push(with_nil_for_variadic(slot)); - } - slots.extend(types.iter().skip(1).cloned().map(with_nil_for_variadic)); - slots - } - VariadicType::Base(base) => { - let slot = if include_error_string { - LuaType::from_vec(vec![base.clone(), LuaType::String]) - } else { - base.clone() - }; - vec![LuaType::Boolean, with_nil_for_variadic(slot)] - } - }, - other => { - let slot = if include_error_string { - LuaType::from_vec(vec![other, LuaType::String]) - } else { - other - }; - vec![LuaType::Boolean, with_nil_for_variadic(slot)] - } + // Only Multi lists separate result slots. Keep a repeated tail as a sequence. + let callback_slots = if let LuaType::Variadic(variadic) = &callback_ret + && let VariadicType::Multi(types) = variadic.as_ref() + { + types.clone() + } else { + vec![callback_ret] }; + let mut slots = vec![LuaType::Boolean]; + for (index, ty) in callback_slots.into_iter().enumerate() { + let slot = if index == 0 && include_error_string { + LuaType::from_vec(vec![ty, LuaType::String]) + } else { + ty + }; + slots.push(with_nil_for_variadic(slot)); + } LuaType::Variadic(Rc::new(VariadicType::Multi(slots))) } diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs index e740ed37b..07910c2b3 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs @@ -466,6 +466,433 @@ fn token_at( } } +#[test] +fn test_vararg_semantic_info_keeps_function_ownership() { + let source = r#" + ---@param ... integer + local function outer(...) + do print(...) end + ---@param ... string + local function inner(...) + print(...) + end + inner('hello') + print(...) + end + outer(1) + "#; + let env = model_of(source); + let model = env.model(); + let declarations: Vec<_> = model + .decls() + .unwrap() + .iter() + .filter(|decl| decl.name == "...") + .collect(); + assert_eq!(declarations.len(), 2); + for (decl, (occurrences, expected)) in declarations + .iter() + .zip([(vec![2, 6], LuaType::Integer), (vec![5], LuaType::String)]) + { + for n in occurrences { + let token = token_at(&model, source, "...", n); + let info = model.semantic_info(token.into()).unwrap(); + assert_eq!(info.decl, Some(decl.id.clone())); + assert_eq!(info.typ.get_result_slot_type(0), Some(expected.clone())); + } + } +} + +#[test] +fn test_vararg_references_do_not_cross_non_variadic_functions() { + let source = r#" + local function outer(...) + local function inner() + print(...) + end + inner() + end + print(...) + "#; + let env = model_of(source); + let model = env.model(); + for occurrence in [1, 2] { + let token = token_at(&model, source, "...", occurrence); + assert_eq!(model.find_decl(token.clone().into()), None); + assert_eq!(model.semantic_info(token.into()).unwrap().decl, None); + } +} + +#[test] +fn test_vararg_expression_lists_expand_only_the_last_value() { + let env = model_of( + r#" + ---@param ... integer + local function values(...) + local first, second = ..., 'last' + local third, fourth = 1, ... + local fifth, sixth = (...) + end + "#, + ); + let model = env.model(); + let locals: Vec<_> = model + .chunk() + .unwrap() + .descendants::() + .collect(); + let actual: Vec> = locals + .iter() + .map(|local| { + let exprs = local.get_value_exprs().collect::>(); + model + .infer_expr_list_types(&exprs, Some(2)) + .into_iter() + .map(|(ty, _)| ty) + .collect() + }) + .collect(); + assert_eq!( + actual, + vec![ + vec![ + LuaType::Integer, + LuaType::StringConst(smol_str::SmolStr::new("last").into()) + ], + vec![LuaType::IntegerConst(1), LuaType::Integer], + vec![LuaType::Integer], + ] + ); +} + +#[test] +fn test_vararg_operators_take_the_first_value() { + let env = model_of( + r#" + ---@param ... integer + local function values(...) + local negative = -... + local choice = ... or false + end + "#, + ); + let model = env.model(); + for name in ["negative", "choice"] { + let facts = model.file_facts().unwrap(); + let decl = facts.decl_by_id(&decl_of(&model, name)).unwrap(); + assert_eq!( + model.type_of_expr(decl.value_expr_syntax.unwrap()), + LuaType::Integer + ); + } +} + +#[test] +fn test_vararg_packing_distinguishes_tuple_values_and_sequences() { + let env = model_of( + r#" + ---@param ... [string, integer] + local function tuples(...) + local packed = { ... } + local tuple_first = packed[1] + local tuple_third = packed[3] + local tuple_count = select('#', ...) + end + ---@param ... [string, integer]... + local function sequence(...) + local packed = { ... } + local sequence_first = packed[1] + local sequence_second = packed[2] + local sequence_third = packed[3] + local selected = { select(2, ...) } + local selected_sequence = selected[1] + local sequence_count = select('#', ...) + end + ---@return boolean, [string, integer]... + local function returned_sequence() error('stub') end + local returned = { returned_sequence() } + local returned_first = returned[2] + local returned_second = returned[3] + ---@param ... [string, integer] + local function forward(...) return true, ... end + local forwarded = { forward({'a', 1}, {'b', 2}) } + local forwarded_second = forwarded[3] + local alias = forward + local aliased = { alias({'a', 1}, {'b', 2}) } + local aliased_second = aliased[3] + ---@param ... [string, integer]... + local function first(...) local value = ...; return value end + local first_value = first('a', 1) + "#, + ); + let model = env.model(); + let tuple = LuaType::Tuple(Rc::new(crate::LuaTupleType::new( + vec![LuaType::String, LuaType::Integer], + crate::LuaTupleStatus::DocResolve, + ))); + for name in [ + "tuple_first", + "tuple_third", + "forwarded_second", + "aliased_second", + ] { + assert_eq!( + model.type_of_decl(&decl_of(&model, name)), + Some(tuple.clone()), + "{name}" + ); + } + for (name, expected) in [ + ("sequence_first", LuaType::String), + ("sequence_second", LuaType::Integer), + ("sequence_third", LuaType::Nil), + ("tuple_count", LuaType::Integer), + ("selected_sequence", LuaType::Integer), + ("sequence_count", LuaType::IntegerConst(2)), + ("returned_first", LuaType::String), + ("returned_second", LuaType::Integer), + ("first_value", LuaType::String), + ] { + assert_eq!( + model.type_of_decl(&decl_of(&model, name)), + Some(expected), + "{name}" + ); + } +} + +#[test] +fn test_vararg_packing_expands_only_the_last_unkeyed_field() { + let env = model_of( + r#" + ---@param ... integer + local function values(...) + local final = { 0, ... } + local repeated = final[3] + local nonfinal = { ..., 'last' } + local parenthesized = { (...) } + local keyed = { [1] = ... } + local last = nonfinal[2] + local missing = parenthesized[2] + local also_missing = keyed[2] + end + "#, + ); + let model = env.model(); + assert_eq!( + model.type_of_decl(&decl_of(&model, "repeated")), + Some(LuaType::Integer) + ); + assert_eq!( + model.type_of_decl(&decl_of(&model, "last")), + Some(LuaType::StringConst(smol_str::SmolStr::new("last").into())) + ); + for name in ["missing", "also_missing"] { + assert_eq!( + model.type_of_decl(&decl_of(&model, name)), + Some(LuaType::Nil) + ); + } +} + +#[test] +fn test_vararg_return_lists_preserve_value_positions() { + let mut ws = crate::VirtualWorkspace::new(); + ws.def( + r#" + ---@param ... integer + local function forward(...) return ... end + ---@param ... integer + local function prefix(...) return false, ... end + ---@param ... integer + local function suffix(...) return ..., 'last' end + ---@param ... integer + local function single(...) return (...) end + forward_first, forward_second = forward(1, 2) + prefix_first, prefix_second, prefix_third = prefix(1, 2) + suffix_first, suffix_second = suffix(1, 2) + single_first = single(1, 2) + single_count = select('#', single(1, 2)) + "#, + ); + for name in [ + "forward_first", + "forward_second", + "prefix_second", + "prefix_third", + "suffix_first", + "single_first", + ] { + assert_eq!(ws.expr_ty(name), ws.ty("integer"), "{name}"); + } + assert_eq!(ws.expr_ty("prefix_first"), ws.ty("boolean")); + assert_eq!(ws.expr_ty("suffix_second"), ws.ty("string")); + assert_eq!(ws.expr_ty("single_count"), LuaType::IntegerConst(1)); +} + +#[test] +fn test_vararg_argument_counts_respect_single_value_contexts() { + let env = model_of( + r#" + ---@param ... integer + local function values(...) + local all = select('#', ...) + local one = select('#', (...)) + local two = select('#', ..., 'last') + end + "#, + ); + let model = env.model(); + for (name, expected) in [ + ("all", LuaType::Integer), + ("one", LuaType::IntegerConst(1)), + ("two", LuaType::IntegerConst(2)), + ] { + assert_eq!( + model.type_of_decl(&decl_of(&model, name)), + Some(expected), + "{name}" + ); + } +} + +#[test] +fn test_vararg_pcall_keeps_repeated_result_types() { + let env = model_of( + r#" + ---@param ... [string, integer] + local function forward(...) return ... end + local selected_tuple = select(3, pcall(forward, {'a', 1}, {'b', 2})) + local protected_count = select('#', pcall(forward, {'a', 1}, {'b', 2})) + local extended = {xpcall(forward, tostring, {'a', 1}, {'b', 2})} + local extended_tuple = extended[3] + "#, + ); + let model = env.model(); + let pair = LuaType::Tuple(Rc::new(crate::LuaTupleType::new( + vec![LuaType::String, LuaType::Integer], + crate::LuaTupleStatus::DocResolve, + ))); + for name in ["selected_tuple", "extended_tuple"] { + assert_eq!( + model.type_of_decl(&decl_of(&model, name)), + Some(LuaType::from_vec(vec![pair.clone(), LuaType::Nil])), + "{name}" + ); + } + assert_eq!( + model.type_of_decl(&decl_of(&model, "protected_count")), + Some(LuaType::Integer) + ); +} + +#[test] +fn test_vararg_select_keeps_nested_result_types() { + let env = model_of( + r#" + ---@param ... integer + local function values(...) return true, ... end + local count = select('#', values(1, 2, 3)) + ---@param ... integer + local function prefixed(...) + local prefix, tail = select(1, 'head', ...) + end + ---@return boolean, [string, integer]... + local function finite() error('stub') end + local finite_count = select('#', finite()) + local finite_third = select(3, finite()) + "#, + ); + let model = env.model(); + for (name, expected) in [ + ("count", LuaType::Integer), + ("prefix", LuaType::String), + ("tail", LuaType::Integer), + ("finite_count", LuaType::IntegerConst(3)), + ("finite_third", LuaType::Integer), + ] { + assert_eq!( + model.type_of_decl(&decl_of(&model, name)), + Some(expected), + "{name}" + ); + } +} + +#[test] +fn test_vararg_generic_callback_keeps_result_tail() { + let env = model_of( + r#" + ---@generic First, Rest + ---@param callback fun(): First, Rest... + ---@return First, Rest... + local function wrap(callback) return callback() end + ---@param ... [string, integer] + local function tuples(...) return ... end + local pairs = {wrap(function() return tuples({'a', 1}, {'b', 2}, {'c', 3}) end)} + local third_pair = pairs[3] + ---@return boolean, string, integer + local function finite() error('stub') end + local fixed = {wrap(finite)} + local finite_text = fixed[2] + local finite_number = fixed[3] + "#, + ); + let model = env.model(); + let pair = LuaType::Tuple(Rc::new(crate::LuaTupleType::new( + vec![LuaType::String, LuaType::Integer], + crate::LuaTupleStatus::DocResolve, + ))); + for (name, expected) in [ + ("third_pair", pair), + ("finite_text", LuaType::String), + ("finite_number", LuaType::Integer), + ] { + assert_eq!( + model.type_of_decl(&decl_of(&model, name)), + Some(expected), + "{name}" + ); + } +} + +#[test] +fn test_vararg_index_keys_take_the_first_value() { + let env = model_of( + r#" + ---@param ... 'key' + local function values(...) + local fields = { key = 'value' } + local value = fields[...] + end + "#, + ); + let model = env.model(); + assert_eq!( + model.type_of_decl(&decl_of(&model, "value")), + Some(LuaType::StringConst(smol_str::SmolStr::new("value").into())) + ); +} + +#[test] +fn test_vararg_nested_returns_do_not_change_outer_result_types() { + let mut ws = crate::VirtualWorkspace::new(); + ws.def( + r#" + ---@param ... integer + local function outer(...) + ---@param ... string + local function inner(...) return ... end + inner('ignored') + return ... + end + first, second = outer(1, 2) + "#, + ); + assert_eq!(ws.expr_ty("first"), ws.ty("integer")); + assert_eq!(ws.expr_ty("second"), ws.ty("integer")); +} + /// Name-use point -> declaration. #[test] fn test_find_decl_name_use() { diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/unify.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/unify.rs index 17b954fa7..30cd7f76b 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/unify.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/unify.rs @@ -217,14 +217,20 @@ pub fn unify_bindings(param: &LuaType, arg: &LuaType, bindings: &mut TplBindings // Variadic types: `T...` can unify with a single value or a structurally // matching Variadic. Variadic(param_variadic) => match (param_variadic.as_ref(), arg) { - (VariadicType::Base(p), Variadic(arg_variadic)) => match arg_variadic.as_ref() { - VariadicType::Base(a) => unify_bindings(p, a, bindings), - VariadicType::Multi(_) => false, - }, - (VariadicType::Base(p), _) if !matches!(arg, Unknown | Any) => { + (VariadicType::Base(p) | VariadicType::Repeat(p), Variadic(arg_variadic)) => { + match arg_variadic.as_ref() { + VariadicType::Base(a) | VariadicType::Repeat(a) => { + unify_bindings(p, a, bindings) + } + VariadicType::Multi(_) => false, + } + } + (VariadicType::Base(p) | VariadicType::Repeat(p), _) + if !matches!(arg, Unknown | Any) => + { unify_bindings(p, arg, bindings) } - (VariadicType::Base(_), _) => true, + (VariadicType::Base(_) | VariadicType::Repeat(_), _) => true, (VariadicType::Multi(p_types), Variadic(arg_variadic)) => { if let VariadicType::Multi(a_types) = arg_variadic.as_ref() && p_types.len() == a_types.len() @@ -407,6 +413,7 @@ pub fn substitute(ty: &LuaType, bindings: &TplBindings) -> LuaType { } VariadicType::Base(sub) } + VariadicType::Repeat(base) => VariadicType::Repeat(substitute(base, bindings)), VariadicType::Multi(types) => { VariadicType::Multi(types.iter().map(|t| substitute(t, bindings)).collect()) } diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs index 087ed5ec5..9c92dbb08 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs @@ -61,6 +61,8 @@ pub enum Instr { /// Concrete value type (string constants / anonymous table identity etc.). PushValue(LuaType), PushUnknown, + /// Reduce a multiple-value expression to its first value (nil when empty). + AdjustToOne, /// Closure reference (marks syntax for Call to back-infer params). PushClosure { syntax: LuaSyntaxId, @@ -121,6 +123,7 @@ impl Value { enum CTask { Expr(LuaExpr), + AdjustToOne, Index(IndexKey), Call { arg_count: usize, @@ -151,6 +154,7 @@ pub fn compile(expr: LuaExpr, file_id: FileId, out: &mut Vec) { while let Some(task) = stack.pop() { match task { CTask::Expr(expr) => compile_expr(&expr, file_id, out, &mut stack), + CTask::AdjustToOne => out.push(Instr::AdjustToOne), CTask::Index(key) => out.push(Instr::IndexMember(key)), CTask::Call { arg_count, @@ -264,6 +268,7 @@ fn compile_expr(expr: &LuaExpr, file_id: FileId, out: &mut Vec, stack: &m } } LuaExpr::IndexExpr(index_expr) => { + stack.push(CTask::AdjustToOne); let index_key = index_expr.get_index_key(); let key = index_key.as_ref().and_then(index_key_to_member_key); if let Some(prefix) = index_expr.get_prefix_expr() { @@ -337,6 +342,7 @@ fn compile_expr(expr: &LuaExpr, file_id: FileId, out: &mut Vec, stack: &m } LuaExpr::ParenExpr(paren) => { if let Some(inner) = paren.get_expr() { + stack.push(CTask::AdjustToOne); stack.push(CTask::Expr(inner)); } else { out.push(Instr::PushUnknown); @@ -458,6 +464,11 @@ impl<'a> InferVm<'a> { } Instr::PushValue(ty) => self.stack.push(Value::plain(ty)), Instr::PushUnknown => self.stack.push(Value::plain(LuaType::Unknown)), + Instr::AdjustToOne => { + if let Some(value) = self.stack.last_mut() { + value.ty = value.ty.get_result_slot_type(0).unwrap_or(LuaType::Nil); + } + } Instr::PushClosure { syntax } => self.stack.push(Value { ty: LuaType::Function, owner: None, @@ -485,6 +496,7 @@ impl<'a> InferVm<'a> { if !matches!(flow_ty, LuaType::Unknown | LuaType::Any) { key_value.ty = flow_ty; } + key_value.ty = key_value.ty.get_result_slot_type(0).unwrap_or(LuaType::Nil); let value = self.index_member_dynamic(owner_value, key_value); self.stack.push(value); } @@ -519,6 +531,7 @@ impl<'a> InferVm<'a> { // Inline `--[[@as T]]` and other flow casts are attached to arg expression // nodes; use flow-sensitive expression types to override bare VM types so // generic calls can back-infer from the cast type. + let arg_count = args.len(); for (index, arg) in args.iter_mut().enumerate() { if let Some(syntax) = arg_syntaxes.get(index) { let flow_ty = self @@ -529,6 +542,10 @@ impl<'a> InferVm<'a> { arg.ty = flow_ty; } } + // Only the last argument can supply multiple values. + if index + 1 < arg_count { + arg.ty = arg.ty.get_result_slot_type(0).unwrap_or(LuaType::Nil); + } } let callee = self.stack.pop().unwrap_or(Value::plain(LuaType::Unknown)); let value = self.call(callee, &args, colon_call, &generic_type_syntaxes); @@ -536,7 +553,8 @@ impl<'a> InferVm<'a> { } Instr::UnaryOp { op } => { let operand = self.stack.pop().unwrap_or(Value::plain(LuaType::Unknown)); - let ty = unary_type(self.model, op, &operand.ty); + let operand_ty = operand.ty.get_result_slot_type(0).unwrap_or(LuaType::Nil); + let ty = unary_type(self.model, op, &operand_ty); self.stack.push(Value::plain(ty)); } Instr::Binary { @@ -546,6 +564,8 @@ impl<'a> InferVm<'a> { } => { let mut right = self.stack.pop().unwrap_or(Value::plain(LuaType::Unknown)); let mut left = self.stack.pop().unwrap_or(Value::plain(LuaType::Unknown)); + left.ty = left.ty.get_result_slot_type(0).unwrap_or(LuaType::Nil); + right.ty = right.ty.get_result_slot_type(0).unwrap_or(LuaType::Nil); if matches!(op, BinaryOperator::OpAnd | BinaryOperator::OpOr) { if let Some(left_syntax) = left_syntax { if let Some(nullable_extra) = @@ -570,6 +590,12 @@ impl<'a> InferVm<'a> { } fn load_name(&mut self, name: &str, offset: rowan::TextSize) -> Value { + // A vararg use carries the whole sequence, even though it resolves to a parameter. + if name == "..." { + return Value::plain(self.enclosing_variadic_type(offset).unwrap_or_else(|| { + LuaType::Variadic(Rc::new(VariadicType::Repeat(LuaType::Unknown))) + })); + } // Local declaration. if let Some(decl) = self.model.resolve_name(offset) { // Closure param: look up the environment. @@ -615,12 +641,6 @@ impl<'a> InferVm<'a> { { return value; } - // Variadic args `...`: take the enclosing closure's variadic type. - if name == "..." - && let Some(ty) = self.enclosing_variadic_type(offset) - { - return Value::plain(ty); - } // Global name. let owner = SemanticId::name(SmolStr::new(name)); let ty = self.global_type(&owner); @@ -726,8 +746,9 @@ impl<'a> InferVm<'a> { if name == "..." { let base = ty.clone().unwrap_or(LuaType::Unknown); return Some(match base { - LuaType::Variadic(_) => base, - _ => LuaType::Variadic(Rc::new(VariadicType::Base(base))), + // A declared `T...` expands tuple slots; a plain `T` repeats whole values. + LuaType::Variadic(_) => self.expand_unpack_return(base), + _ => LuaType::Variadic(Rc::new(VariadicType::Repeat(base))), }); } } @@ -1527,7 +1548,7 @@ impl<'a> InferVm<'a> { } else { base }; - return Value::plain(LuaType::Variadic(Rc::new(VariadicType::Base( + return Value::plain(LuaType::Variadic(Rc::new(VariadicType::Repeat( element, )))); } @@ -1535,25 +1556,20 @@ impl<'a> InferVm<'a> { } // select(n, ...) / select('#', ...) "select" => { - // `select('#', ...)`: returns the number of remaining args. + let values = VariadicType::Multi( + args.iter() + .skip(1) + .map(|arg| widen_const(&arg.ty)) + .collect(), + ); + // Nested result sequences can have an unbounded tail. if let Some(first) = args.first() { if let LuaType::StringConst(s) | LuaType::DocStringConst(s) = &first.ty { if s.as_str() == "#" { - let mut count = 0usize; - let mut has_unbounded = false; - for arg in args.iter().skip(1) { - match &arg.ty { - LuaType::Variadic(variadic) => match variadic.as_ref() { - VariadicType::Multi(types) => count += types.len(), - VariadicType::Base(_) => has_unbounded = true, - }, - _ => count += 1, - } - } - if has_unbounded { - return Value::plain(LuaType::Integer); - } - return Value::plain(LuaType::IntegerConst(count as i64)); + return Value::plain(match values.get_max_len() { + Some(count) => LuaType::IntegerConst(count as i64), + None => LuaType::Integer, + }); } } } @@ -1568,48 +1584,11 @@ impl<'a> InferVm<'a> { }) .unwrap_or(1) .max(1); - let mut tail: Vec = Vec::new(); - let mut has_unbounded = false; - let mut remaining = n.saturating_sub(1); - for arg in args.iter().skip(1) { - match &arg.ty { - LuaType::Variadic(variadic) => match variadic.as_ref() { - VariadicType::Multi(types) => { - for ty in types { - if remaining > 0 { - remaining -= 1; - } else { - tail.push(widen_const(ty)); - } - } - } - VariadicType::Base(base) => { - // Unbounded variadic: once entered, all later values are - // the base type. - if remaining > 0 { - remaining = 0; - } - tail.push(widen_const(base)); - has_unbounded = true; - } - }, - ty => { - if remaining > 0 { - remaining -= 1; - } else { - tail.push(widen_const(ty)); - } - } - } - } - if has_unbounded { - let base = tail.first().cloned().unwrap_or(LuaType::Unknown); - return Value::plain(LuaType::Variadic(Rc::new(VariadicType::Base(base)))); - } - if tail.is_empty() { + let tail = values.get_new_variadic_from(n - 1); + if tail.get_max_len() == Some(0) { return Value::plain(LuaType::Nil); } - return Value::plain(LuaType::Variadic(Rc::new(VariadicType::Multi(tail)))); + return Value::plain(LuaType::Variadic(Rc::new(tail))); } _ => {} } @@ -1727,6 +1706,7 @@ impl<'a> InferVm<'a> { } if let LuaType::Variadic(variadic) = &substituted { let slot_types: Vec = match variadic.as_ref() { + VariadicType::Repeat(base) => vec![base.clone()], VariadicType::Base(base) => { if let LuaType::Tuple(tuple) = base { tuple.get_types().to_vec() @@ -2004,58 +1984,13 @@ impl<'a> InferVm<'a> { LuaType::Variadic(v) => { if let VariadicType::Base(LuaType::TplRef(tpl)) = v.as_ref() { bindings.insert(tpl.get_tpl_id(), callback_ret.clone()); - } else if let VariadicType::Multi(types) = v.as_ref() { - // For `R1, R...` ("fixed first return + variadic tail return"), do not - // unify the actual callback's entire return list with the param return - // structure directly; first take the fixed slot from the actual callback's - // first return, then bind the remaining returns as the tail R. - let Some(LuaType::Variadic(tail_v)) = types.last() else { - let _ = unify_call_bindings( - self.model, - substituted_callback_ret, - callback_ret, - bindings, - ); - return; - }; - let VariadicType::Base(tail_base) = tail_v.as_ref() else { - let _ = unify_call_bindings( - self.model, - substituted_callback_ret, - callback_ret, - bindings, - ); - return; - }; - let callback_types: Vec = match callback_ret { - LuaType::Variadic(callback_variadic) => match callback_variadic.as_ref() { - VariadicType::Multi(callback_types) => callback_types.clone(), - VariadicType::Base(base) => vec![base.clone()], - }, - other => vec![other.clone()], - }; - let fixed_count = types.len() - 1; - let mut ok = true; - for (param, actual) in types[..fixed_count].iter().zip(callback_types.iter()) { - if !unify_call_bindings(self.model, param, actual, bindings) { - ok = false; - break; - } - } - if ok { - let rest = if callback_types.len() > fixed_count { - if callback_types.len() == fixed_count + 1 { - callback_types[fixed_count].clone() - } else { - LuaType::Variadic(Rc::new(VariadicType::Multi( - callback_types[fixed_count..].to_vec(), - ))) - } - } else { - LuaType::Variadic(Rc::new(VariadicType::Base(LuaType::Nil))) - }; - let _ = unify_call_bindings(self.model, tail_base, &rest, bindings); - } + } else if matches!(v.as_ref(), VariadicType::Multi(_)) { + let _ = unify_call_bindings( + self.model, + original_callback_ret, + callback_ret, + bindings, + ); } else { let _ = unify_call_bindings( self.model, @@ -2155,6 +2090,11 @@ impl<'a> InferVm<'a> { }; let return_exprs: Vec = closure .descendants::() + .filter(|ret| { + ret.ancestors::() + .next() + .is_some_and(|owner| owner.get_syntax_id() == closure_syntax) + }) .flat_map(|ret| ret.get_expr_list()) .collect(); if return_exprs.is_empty() { @@ -2193,6 +2133,15 @@ impl<'a> InferVm<'a> { // For `T...` where T is a tuple/mapped tuple: expand the single tuple slot into // multi-return slots. if let LuaType::Variadic(variadic) = &ty { + if let VariadicType::Multi(types) = variadic.as_ref() { + return LuaType::Variadic(Rc::new(VariadicType::Multi( + types + .iter() + .cloned() + .map(|ty| self.expand_unpack_return(ty)) + .collect(), + ))); + } if let VariadicType::Base(base) = variadic.as_ref() { let slots: Option> = match base { LuaType::Tuple(tuple) => Some(tuple.get_types().to_vec()), @@ -2249,7 +2198,7 @@ impl<'a> InferVm<'a> { } LuaType::Array(array) => { let element = LuaType::from_vec(vec![array.get_base().clone(), LuaType::Nil]); - LuaType::Variadic(Rc::new(VariadicType::Base(element))) + LuaType::Variadic(Rc::new(VariadicType::Repeat(element))) } LuaType::Tuple(tuple) => { LuaType::Variadic(Rc::new(VariadicType::Multi(tuple.get_types().to_vec()))) @@ -2330,7 +2279,7 @@ impl<'a> InferVm<'a> { } _ => arg_fun.get_params().to_vec(), }, - VariadicType::Multi(_) => return ret, + VariadicType::Multi(_) | VariadicType::Repeat(_) => return ret, }; let mut ret_ty = sanitize_unresolved_generics(fun.get_ret(), &HashSet::new()); // After degradation, an unbound `R...` should not retain the "unknown variadic return" @@ -2710,6 +2659,22 @@ impl<'a> InferVm<'a> { } let mut ret = self.return_overload_types_lua(file_id, docs, generic_params, None); + // Parameter docs must not hide inferred return slots. + if matches!(ret, LuaType::Unknown) + && docs.returns.is_empty() + && docs.return_overloads.is_empty() + { + let generic_names: Vec<_> = generic_params + .iter() + .map(|param| param.name.clone()) + .collect(); + ret = self.model.signature_return_type( + file_id, + signature.closure_syntax, + signature, + &generic_names, + ); + } let generic_tpls = self.generic_tpls_with_metadata(file_id, generic_params); ret = bind_signature_generics(&ret, &generic_tpls); let async_state = if docs.is_async { @@ -2771,7 +2736,7 @@ impl<'a> InferVm<'a> { for ty in types { match ty { LuaType::Variadic(variadic) => match variadic.as_ref() { - VariadicType::Base(base) => { + VariadicType::Base(base) | VariadicType::Repeat(base) => { if tail.is_none() { *tail = Some(base.clone()); } @@ -3594,6 +3559,7 @@ pub(crate) fn replace_self_type(ty: &LuaType, self_ty: &LuaType) -> LuaType { )), Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { VariadicType::Base(base) => VariadicType::Base(replace_self_type(base, self_ty)), + VariadicType::Repeat(base) => VariadicType::Repeat(replace_self_type(base, self_ty)), VariadicType::Multi(types) => VariadicType::Multi( types .iter() @@ -3665,6 +3631,9 @@ fn resolve_function_ret_defaults(ty: &LuaType, fun: &LuaFunctionType) -> LuaType VariadicType::Base(base) => { VariadicType::Base(resolve_function_ret_defaults(base, fun)) } + VariadicType::Repeat(base) => { + VariadicType::Repeat(resolve_function_ret_defaults(base, fun)) + } VariadicType::Multi(types) => VariadicType::Multi( types .iter() @@ -3879,9 +3848,12 @@ pub(crate) fn unify_call_bindings( true } Variadic(param_variadic) => match param_variadic.as_ref() { - VariadicType::Base(base) => match arg { + VariadicType::Base(base) | VariadicType::Repeat(base) => match arg { + Variadic(_) if matches!(param_variadic.as_ref(), VariadicType::Base(TplRef(_))) => { + unify_call_bindings(model, base, arg, bindings) + } Variadic(arg_variadic) => match arg_variadic.as_ref() { - VariadicType::Base(arg_base) => { + VariadicType::Base(arg_base) | VariadicType::Repeat(arg_base) => { unify_call_bindings(model, base, arg_base, bindings) } VariadicType::Multi(arg_types) => { @@ -3899,33 +3871,57 @@ pub(crate) fn unify_call_bindings( Unknown | Any => true, _ => unify_call_bindings(model, base, arg, bindings), }, - VariadicType::Multi(param_types) => match arg { - Variadic(arg_variadic) => match arg_variadic.as_ref() { - VariadicType::Multi(arg_types) if param_types.len() == arg_types.len() => { - for (p, a) in param_types.iter().zip(arg_types.iter()) { - if !unify_call_bindings(model, p, a, bindings) { - return false; + VariadicType::Multi(param_types) => { + if let Some(Variadic(tail)) = param_types.last() + && let VariadicType::Base(base @ TplRef(_)) = tail.as_ref() + { + if matches!(arg, Unknown | Any) { + return true; + } + let fixed_count = param_types.len() - 1; + for (index, param) in param_types[..fixed_count].iter().enumerate() { + let actual = arg.get_result_slot_type(index).unwrap_or(Nil); + if !unify_call_bindings(model, param, &actual, bindings) { + return false; + } + } + // For `First, Rest...`, slice result slots without losing repeated values. + let rest = match arg { + Variadic(variadic) => variadic.get_new_variadic_from(fixed_count), + _ => VariadicType::Multi(vec![arg.clone()]) + .get_new_variadic_from(fixed_count), + }; + let rest = Variadic(Rc::new(rest)); + return unify_call_bindings(model, base, &rest, bindings); + } + match arg { + Variadic(arg_variadic) => match arg_variadic.as_ref() { + VariadicType::Multi(arg_types) if param_types.len() == arg_types.len() => { + for (p, a) in param_types.iter().zip(arg_types.iter()) { + if !unify_call_bindings(model, p, a, bindings) { + return false; + } } + true } - true + _ => false, + }, + Unknown | Any => true, + _ if param_types.len() == 1 => { + unify_call_bindings(model, ¶m_types[0], arg, bindings) + } + // A `R1, R...` param return is compatible with an actual function that returns + // only `R1`: the trailing variadic return may be empty. + _ if matches!( + param_types.last(), + Some(Variadic(v)) if matches!(v.as_ref(), VariadicType::Base(_)) + ) && param_types.len() == 2 => + { + unify_call_bindings(model, ¶m_types[0], arg, bindings) } _ => false, - }, - Unknown | Any => true, - _ if param_types.len() == 1 => { - unify_call_bindings(model, ¶m_types[0], arg, bindings) - } - // A `R1, R...` param return is compatible with an actual function that returns - // only `R1`: the trailing variadic return may be empty. - _ if matches!( - param_types.last(), - Some(Variadic(v)) if matches!(v.as_ref(), VariadicType::Base(_)) - ) && param_types.len() == 2 => - { - unify_call_bindings(model, ¶m_types[0], arg, bindings) } - _ => false, - }, + } }, Generic(param_generic) => { if let Generic(arg_generic) = arg { @@ -4009,7 +4005,7 @@ pub(crate) fn unify_call_bindings( Tuple(tuple) => tuple.get_types().len(), Variadic(v) => match v.as_ref() { VariadicType::Multi(types) => types.len(), - VariadicType::Base(_) => 1, + VariadicType::Base(_) | VariadicType::Repeat(_) => 1, }, _ => 1, } @@ -4229,7 +4225,7 @@ pub(crate) fn score_param_match(param_ty: &LuaType, arg_ty: &LuaType) -> i32 { pub(crate) fn variadic_base_type(ty: &LuaType) -> Option { match ty { LuaType::Variadic(variadic) => match variadic.as_ref() { - VariadicType::Base(base) => Some(base.clone()), + VariadicType::Base(base) | VariadicType::Repeat(base) => Some(base.clone()), VariadicType::Multi(types) => types.first().cloned(), }, _ => Some(ty.clone()), @@ -4910,6 +4906,9 @@ pub(crate) fn bind_signature_generics(ty: &LuaType, generics: &[GenericTpl]) -> VariadicType::Base(base) => { VariadicType::Base(bind_signature_generics(base, generics)) } + VariadicType::Repeat(base) => { + VariadicType::Repeat(bind_signature_generics(base, generics)) + } VariadicType::Multi(types) => VariadicType::Multi( types .iter() @@ -5042,6 +5041,9 @@ fn remove_nil_from_type(ty: LuaType) -> LuaType { VariadicType::Base(base) => LuaType::Variadic(Rc::new(VariadicType::Base( remove_nil_from_type(base.clone()), ))), + VariadicType::Repeat(base) => LuaType::Variadic(Rc::new(VariadicType::Repeat( + remove_nil_from_type(base.clone()), + ))), }, _ => ty, } diff --git a/crates/emmylua_code_analysis/src/semantic_model/member.rs b/crates/emmylua_code_analysis/src/semantic_model/member.rs index e619068c3..dabfdbf08 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/member.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/member.rs @@ -339,7 +339,7 @@ pub(crate) fn member_info_impl( is_method: first.as_ref().is_some_and(|info| info.is_method), }); } - if let Some(info) = member_infos(model, prefix_type) + if let Some(info) = member_infos_with_key(model, prefix_type, key) .into_iter() .find(|info| &info.key == key) { @@ -704,7 +704,7 @@ fn collect_members( for member_ref in owner_member_refs(model, &owner, filter) { if let Some(info) = member_info_of(model, &member_ref, bindings, name_bindings) { if let Some(expanded) = - expand_table_multi_return_member(model, table, &member_ref, &info) + expand_table_multi_return_member(model, table, &member_ref, &info, filter) { for info in expanded { push_member_info(out, filter, info); @@ -817,13 +817,14 @@ fn collect_members( } } -/// If the last unkeyed field of a Lua table constructor is a multi-return function call, +/// If the last unkeyed field of a Lua table constructor supplies multiple values, /// all returned values expand consecutively into integer-keyed fields (`{ coroutine.resume(...) }` → `[1]`, `[2]`, ...). fn expand_table_multi_return_member( model: &SemanticModel, table: &InFiled, member_ref: &MemberRef, info: &MemberInfo, + filter: Option<&LuaMemberKey>, ) -> Option> { let LuaType::Variadic(variadic) = &info.typ else { return None; @@ -850,7 +851,7 @@ fn expand_table_multi_return_member( { return None; } - // Lua expands only the function call in the last field. + // Lua expands calls and varargs only in the last unkeyed field. let is_last = table_expr .get_fields() .last() @@ -858,7 +859,13 @@ fn expand_table_multi_return_member( if !is_last { return None; } - let types = flatten_table_multi_return(variadic); + // A repeated tail cannot be enumerated. Resolve the requested slot directly. + let (start, types) = if let Some(LuaMemberKey::Integer(index)) = filter { + let offset = usize::try_from(index.checked_sub(start)?).ok()?; + (*index, vec![info.typ.get_result_slot_type(offset)?]) + } else { + (start, flatten_table_multi_return(variadic)) + }; if types.is_empty() { return None; } @@ -884,13 +891,13 @@ fn flatten_table_multi_return(variadic: &VariadicType) -> Vec { .iter() .flat_map(|ty| match ty { LuaType::Variadic(inner) => match inner.as_ref() { - VariadicType::Base(base) => vec![base.clone()], + VariadicType::Base(base) | VariadicType::Repeat(base) => vec![base.clone()], VariadicType::Multi(inner_types) => inner_types.clone(), }, other => vec![other.clone()], }) .collect(), - VariadicType::Base(base) => vec![base.clone()], + VariadicType::Base(base) | VariadicType::Repeat(base) => vec![base.clone()], } } diff --git a/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs b/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs index 18b765648..97ba07313 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/render/mod.rs @@ -554,7 +554,7 @@ impl<'a> TypeHumanizer<'a> { fn write_variadic(&mut self, variadic: &VariadicType, w: &mut W) -> fmt::Result { match variadic { - VariadicType::Base(base) => { + VariadicType::Base(base) | VariadicType::Repeat(base) => { self.write_type(base, w)?; w.write_str("...") } @@ -715,6 +715,9 @@ impl<'a> TypeHumanizer<'a> { VariadicType::Base(base) => { VariadicType::Base(self.substitute_named_refs(base, map)) } + VariadicType::Repeat(base) => { + VariadicType::Repeat(self.substitute_named_refs(base, map)) + } VariadicType::Multi(types) => VariadicType::Multi( types .iter() diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs index 0b0fe05ea..52ef88e7f 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs @@ -6,7 +6,7 @@ use crate::{LuaMemberKey, LuaType, SemanticId, TypeDef, TypeScope}; use crate::semantic_model::SemanticModel; -use super::context::TypeCheckContext; +use super::context::{TypeCheckContext, TypeCheckMode}; use super::guard::TypeCheckGuard; use super::{TypeCheckResult, check_general_type_compact}; @@ -22,7 +22,9 @@ pub(crate) fn check_special( if let Some(result) = check_named_intersection(context, source, target, guard) { return Some(result); } - if let Some(result) = check_function_assign(context, source, target, guard) { + if context.mode == TypeCheckMode::Assignment + && let Some(result) = check_function_assign(context, source, target, guard) + { return Some(result); } None diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs index 8894ccbb2..f25927e0b 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs @@ -22,7 +22,7 @@ pub fn check_complex_type_compact( compact_array.get_base(), check_guard.next_level()?, ), - LuaType::Object(object) if context.assign_mode => { + LuaType::Object(object) if context.mode.uses_assignment_rules() => { let array_base = source_array.get_base(); for (key_ty, expected) in object.get_index_access() { if matches!( diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs index 080964d88..83dc5d885 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs @@ -12,12 +12,37 @@ use super::fail_reason::TypeCheckFailReason; use crate::semantic_model::SemanticModel; use crate::semantic_model::render::humanize_type; +/// Rules for comparing a value's type with the expected type. +/// Call argument counts are checked separately. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum TypeCheckMode { + /// General compatibility checks, such as checking function arguments. + /// If `Dog` extends `Animal`, a `Dog` can supply an `Animal` parameter. + Compatibility, + + /// Check assignments to declared variables and table entries. + /// For example, a `string` can be assigned to `string | nil`. + /// Function types use the assignment-specific parameter rules. + Assignment, + + /// Check result sequences during a compatibility check. + /// For example, `integer...` can supply `number...`, but not the reverse. + /// Use assignment rules for result values and `Compatibility` rules + /// for function parameters. + Result, +} + +impl TypeCheckMode { + pub(crate) fn uses_assignment_rules(self) -> bool { + self != Self::Compatibility + } +} + #[derive(Clone)] pub struct TypeCheckContext<'db> { pub detail: bool, pub model: &'db SemanticModel<'db>, - /// Assignment semantic mode (`number -> integer`, any matching union target component, etc.). - pub assign_mode: bool, + pub mode: TypeCheckMode, /// Strict subtype mode (legacy `type_check_subtype`: all components of a union target, object field level). pub strict_union: bool, pub strict_object: bool, @@ -30,7 +55,7 @@ impl<'db> TypeCheckContext<'db> { Self { detail, model, - assign_mode: false, + mode: TypeCheckMode::Compatibility, strict_union: false, strict_object: false, strict_generic: false, diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs index c7e97ac6e..acb672442 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs @@ -24,7 +24,7 @@ mod test; use std::ops::Deref; -pub use context::TypeCheckContext; +pub use context::{TypeCheckContext, TypeCheckMode}; pub use fail_reason::TypeCheckFailReason; use crate::semantic_model::SemanticModel; @@ -86,7 +86,7 @@ pub fn check_assign_type_detail( target: &LuaType, ) -> TypeCheckResult { let mut context = TypeCheckContext::new(model, true); - context.assign_mode = true; + context.mode = TypeCheckMode::Assignment; check_general_type_compact(&mut context, source, target, guard::TypeCheckGuard::new()) } @@ -156,7 +156,7 @@ fn check_general_type_compact( } // Assignment mode: integer is assignable to number; number is not assignable to integer. - if context.assign_mode { + if context.mode.uses_assignment_rules() { match (source, compact_type) { ( LuaType::Integer | LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_), @@ -210,7 +210,7 @@ fn check_general_type_compact( } // Assignment-specific checks: semantics for object/intersection/function assignment not yet in the general path. - if context.assign_mode + if context.mode.uses_assignment_rules() && let Some(result) = assign::check_special(context, source, compact_type, check_guard) { return result; @@ -328,7 +328,7 @@ fn check_union_intersection_source( match source { LuaType::Union(union) => { // Assignment mode: every member of the source union must be assignable to the target. - if context.assign_mode { + if context.mode.uses_assignment_rules() { for member in union.iter() { check_general_type_compact( context, diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs index 84b9f47f4..c76613cc0 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs @@ -2,11 +2,12 @@ //! `semantic::type_check::simple_type`, with db access going through the semantic context). use std::ops::Deref; +use std::rc::Rc; use crate::{LuaMemberKey, LuaType, LuaTypeDeclId, VariadicType}; use super::TypeCheckResult; -use super::context::TypeCheckContext; +use super::context::{TypeCheckContext, TypeCheckMode}; use super::guard::TypeCheckGuard; use super::sub_type::{base_type_name, get_base_type_id, is_sub_type_of}; @@ -27,7 +28,10 @@ pub fn check_simple_type_compact( if let (LuaType::TableConst(_), LuaType::Tuple(tuple)) = (source, compact_type) { // Table entries use assignment rules, including optional and union elements. let mut element_context = TypeCheckContext { - assign_mode: true, + mode: match context.mode { + TypeCheckMode::Compatibility => TypeCheckMode::Assignment, + mode => mode, + }, ..context.clone() }; let next_guard = check_guard.next_level()?; @@ -158,13 +162,13 @@ pub fn check_simple_type_compact( }, LuaType::IntegerConst(value) => match compact_type { LuaType::IntegerConst(other) => { - if context.assign_mode || value == other { + if context.mode.uses_assignment_rules() || value == other { return Ok(()); } return Err(context.mismatch(source, compact_type)); } LuaType::DocIntegerConst(other) => { - if context.assign_mode || value == other { + if context.mode.uses_assignment_rules() || value == other { return Ok(()); } return Err(context.mismatch(source, compact_type)); @@ -349,19 +353,45 @@ fn check_variadic_type_compact( compact_type: &LuaType, check_guard: TypeCheckGuard, ) -> TypeCheckResult { + let mut element_context = TypeCheckContext { + mode: match context.mode { + TypeCheckMode::Compatibility => TypeCheckMode::Result, + mode => mode, + }, + ..context.clone() + }; match &source_type { - VariadicType::Base(source_base) => match compact_type { + VariadicType::Base(source_base) | VariadicType::Repeat(source_base) => match compact_type { LuaType::Variadic(compact_variadic) => match compact_variadic.deref() { - VariadicType::Base(compact_base) => { - if source_base == compact_base { - return Ok(()); + VariadicType::Base(_) | VariadicType::Repeat(_) => { + let count = compact_variadic + .get_max_len() + .or_else(|| source_type.get_max_len()) + .unwrap_or(1); + for index in 0..count { + let source = source_type.get_type(index).cloned().unwrap_or(LuaType::Nil); + let compact = compact_variadic + .get_type(index) + .cloned() + .unwrap_or(LuaType::Nil); + super::check_general_type_compact( + &mut element_context, + &source, + &compact, + check_guard.next_level()?, + )?; } } VariadicType::Multi(compact_multi) => { - for compact_type in compact_multi { - check_simple_type_compact( - context, - source_base, + for (index, compact_type) in compact_multi.iter().enumerate() { + let source = if compact_type.contain_multi_return() { + LuaType::Variadic(Rc::new(source_type.get_new_variadic_from(index))) + } else { + source_type.get_type(index).cloned().unwrap_or(LuaType::Nil) + }; + super::check_general_type_compact( + &mut element_context, + &source, compact_type, check_guard.next_level()?, )?; diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs b/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs index 75e1b9b9a..8b4f95c6d 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs @@ -273,6 +273,9 @@ fn bind_mapped_param_ref(ty: &LuaType, param_id: GenericTplId, param_name: &str) VariadicType::Base(base) => { VariadicType::Base(bind_mapped_param_ref(base, param_id, param_name)) } + VariadicType::Repeat(base) => { + VariadicType::Repeat(bind_mapped_param_ref(base, param_id, param_name)) + } VariadicType::Multi(types) => VariadicType::Multi( types .iter() @@ -372,6 +375,9 @@ pub fn sanitize_unresolved_generics( VariadicType::Base(base) => { VariadicType::Base(sanitize_unresolved_generics(base, allowed)) } + VariadicType::Repeat(base) => { + VariadicType::Repeat(sanitize_unresolved_generics(base, allowed)) + } VariadicType::Multi(types) => VariadicType::Multi( types .iter() @@ -462,6 +468,9 @@ pub fn sanitize_unresolved_generics_with_model( VariadicType::Base(base) => VariadicType::Base( sanitize_unresolved_generics_with_model(model, base, allowed), ), + VariadicType::Repeat(base) => VariadicType::Repeat( + sanitize_unresolved_generics_with_model(model, base, allowed), + ), VariadicType::Multi(types) => VariadicType::Multi( types .iter() @@ -536,6 +545,7 @@ fn sanitize_generic_param(model: &SemanticModel, ty: &LuaType) -> LuaType { )), Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { VariadicType::Base(base) => VariadicType::Base(sanitize_generic_param(model, base)), + VariadicType::Repeat(base) => VariadicType::Repeat(sanitize_generic_param(model, base)), VariadicType::Multi(types) => VariadicType::Multi( types .iter() @@ -686,6 +696,7 @@ pub fn substitute_named_refs(ty: &LuaType, map: &HashMap) -> Lu )), Variadic(variadic) => Variadic(Rc::new(match variadic.as_ref() { VariadicType::Base(base) => VariadicType::Base(substitute_named_refs(base, map)), + VariadicType::Repeat(base) => VariadicType::Repeat(substitute_named_refs(base, map)), VariadicType::Multi(types) => VariadicType::Multi( types .iter() @@ -890,6 +901,9 @@ fn expand_alias_generic_inner( VariadicType::Base(base) => { VariadicType::Base(expand_alias_generic_inner(model, base, visited)) } + VariadicType::Repeat(base) => { + VariadicType::Repeat(expand_alias_generic_inner(model, base, visited)) + } VariadicType::Multi(types) => VariadicType::Multi( types .iter() @@ -1113,6 +1127,9 @@ fn eval_conditionals_inner( VariadicType::Base(base) => { VariadicType::Base(eval_conditionals_inner(model, base, visiting)) } + VariadicType::Repeat(base) => { + VariadicType::Repeat(eval_conditionals_inner(model, base, visiting)) + } VariadicType::Multi(types) => VariadicType::Multi( types .iter() @@ -1461,7 +1478,9 @@ pub fn contains_conditional_infer(ty: &LuaType) -> bool { Generic(generic) => generic.get_params().iter().any(contains_conditional_infer), TableGeneric(generic) => generic.iter().any(contains_conditional_infer), Variadic(variadic) => match variadic.as_ref() { - VariadicType::Base(base) => contains_conditional_infer(base), + VariadicType::Base(base) | VariadicType::Repeat(base) => { + contains_conditional_infer(base) + } VariadicType::Multi(types) => types.iter().any(contains_conditional_infer), }, Call(call) => call.get_operands().iter().any(contains_conditional_infer), @@ -1507,7 +1526,9 @@ fn contains_unbound_function_tpl(ty: &LuaType) -> bool { .any(contains_unbound_function_tpl), TableGeneric(generic) => generic.iter().any(contains_unbound_function_tpl), Variadic(variadic) => match variadic.as_ref() { - VariadicType::Base(base) => contains_unbound_function_tpl(base), + VariadicType::Base(base) | VariadicType::Repeat(base) => { + contains_unbound_function_tpl(base) + } VariadicType::Multi(types) => types.iter().any(contains_unbound_function_tpl), }, Call(call) => call diff --git a/crates/emmylua_doc_cli/src/doc_model.rs b/crates/emmylua_doc_cli/src/doc_model.rs index fc8bd828e..8d615c2f4 100644 --- a/crates/emmylua_doc_cli/src/doc_model.rs +++ b/crates/emmylua_doc_cli/src/doc_model.rs @@ -709,7 +709,7 @@ fn expand_returns(ret: &LuaType) -> Vec { match ret { LuaType::Variadic(variadic) => match variadic.as_ref() { VariadicType::Multi(types) => types.clone(), - VariadicType::Base(base) => vec![base.clone()], + VariadicType::Base(base) | VariadicType::Repeat(base) => vec![base.clone()], }, _ => vec![ret.clone()], } @@ -812,7 +812,9 @@ impl TypeTextRenderer<'_> { .join(", "); format!("({parts})") } - VariadicType::Base(base) => format!("{}...", self.render(base)), + VariadicType::Base(base) | VariadicType::Repeat(base) => { + format!("{}...", self.render(base)) + } }, LuaType::Signature(_) => "fun(...)".to_string(), LuaType::Instance(instance) => self.render(instance.get_base()), diff --git a/crates/emmylua_ls/src/handlers/hover/build_hover.rs b/crates/emmylua_ls/src/handlers/hover/build_hover.rs index 3950f2d9e..65c742b32 100644 --- a/crates/emmylua_ls/src/handlers/hover/build_hover.rs +++ b/crates/emmylua_ls/src/handlers/hover/build_hover.rs @@ -650,7 +650,7 @@ fn render_function_params_and_ret_mode( .map(|ty| humanize(model, ty)) .collect::>() .join(", "), - VariadicType::Base(ty) => humanize(model, ty), + VariadicType::Base(ty) | VariadicType::Repeat(ty) => humanize(model, ty), }, ty => humanize(model, ty), }; diff --git a/crates/emmylua_ls/src/handlers/test/definition_test.rs b/crates/emmylua_ls/src/handlers/test/definition_test.rs index a76ba2397..3088e3041 100644 --- a/crates/emmylua_ls/src/handlers/test/definition_test.rs +++ b/crates/emmylua_ls/src/handlers/test/definition_test.rs @@ -6,6 +6,26 @@ mod tests { type Expected = VirtualLocation; + #[gtest] + fn test_vararg_definition_uses_the_nearest_function() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!(ws.check_definition( + r#" + local function outer(...) + local function inner(...) + return ... + end + return inner(...) + end + "#, + vec![Expected { + file: "".to_string(), + line: 2 + }] + )); + Ok(()) + } + #[gtest] fn test_basic_definition() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); diff --git a/crates/emmylua_ls/src/handlers/test/references_test.rs b/crates/emmylua_ls/src/handlers/test/references_test.rs index 4cf820472..9470941da 100644 --- a/crates/emmylua_ls/src/handlers/test/references_test.rs +++ b/crates/emmylua_ls/src/handlers/test/references_test.rs @@ -6,6 +6,33 @@ mod tests { use crate::handlers::test_lib::{ProviderVirtualWorkspace, VirtualLocation, check}; use googletest::prelude::*; + #[gtest] + fn test_vararg_references_stay_in_the_declaring_function() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!( + ws.check_references( + r#" + local function outer(...) + print(...) + local function inner(...) + print(...) + end + return ... + end + "#, + vec![], + vec![1, 2, 6] + .into_iter() + .map(|line| VirtualLocation { + file: "".to_string(), + line, + }) + .collect() + ) + ); + Ok(()) + } + // M3 batch 2 retirement: module export alias following (`export.flush = flush` → `require("mod").flush` // binding chain) requires cross-file alias chain queries at the semantic layer; see migration notes §M3. #[gtest]