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]