Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion crates/emmylua_code_analysis/src/semantic_db/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1980,7 +1980,18 @@ pub(crate) fn decl_type(
}

if let Some(value_expr_syntax) = decl.value_expr_syntax {
let shell = expr_type_of(db, file, config, value_expr_syntax);
let mut shell = expr_type_of(db, file, config, value_expr_syntax);
// `local a, b = f()`: each target takes its own result slot, not the whole list. A
// target past a single value's only slot is an undeclared missing value: `any`, as the
// VM types it.
if let Some(index) = decl.multi_return_index {
let slot = shell.result_slot(index);
shell = if slot.is_unknown() && !shell.is_unknown() {
TypeShell::from_name("any")
} else {
slot
};
}
if !shell.is_unknown() {
// Globals cannot carry generic parameters not instantiated inside the function body (the `a` in `function f(x) a = x end` is unknown outside).
if matches!(decl.kind, DeclKind::Global) {
Expand Down
55 changes: 55 additions & 0 deletions crates/emmylua_code_analysis/src/semantic_db/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,61 @@ impl TypeShell {
out
}

/// The `index`th value of a result list (0-based): the `b` in `local a, b = f()` is
/// `f()`'s `result_slot(1)`. A plain value has only slot 0; past a finite list's end is nil.
pub(crate) fn result_slot(&self, index: usize) -> Self {
if index == 0 {
return self.first_result();
}
let mut out = Self::unknown();
for candidate in &self.candidates {
match candidate {
TypeCandidate::Variadic(base) => {
for candidate in &base.candidates {
match candidate {
TypeCandidate::Tuple(types) => {
if let Some(slot) = types.get(index) {
out.merge(slot);
}
}
_ => out.merge(&Self {
candidates: vec![candidate.clone()],
}),
}
}
}
TypeCandidate::Repeat(base) => out.merge(base),
TypeCandidate::Multi(slots) => {
let Some(last) = slots.len().checked_sub(1) else {
continue;
};
if index < last {
out.merge(&slots[index].first_result());
continue;
}
let tail = &slots[last];
let expands = tail.candidates.iter().any(|candidate| {
matches!(
candidate,
TypeCandidate::Variadic(_)
| TypeCandidate::Repeat(_)
| TypeCandidate::Multi(_)
)
});
if expands {
out.merge(&tail.result_slot(index - last));
} else if index == last {
out.merge(tail);
} else {
out.merge(&Self::from_primitive(PrimitiveType::Nil));
}
}
_ => {}
}
}
out
}

/// Tuple candidate: `[number, string]`.
pub fn from_tuple(types: Vec<TypeShell>) -> Self {
Self {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,121 @@ mod test {
let expected = ws.ty("integer");
assert_eq!(e_ty, expected);
}

/// A call whose argument mismatches its parameter still assigns its returns slot by slot,
/// never the whole return list to every target.
#[test]
fn test_mismatched_call_assigns_return_slots() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@param y number
---@return string text
---@return number count
local function two(y) return 'a', 1 end

---@param y number
---@return string text
local function one(y) return 'a' end

local a1, b1 = two('x')
local a2, b2 = one('x')
A1, B1, A2, B2 = a1, b1, a2, b2
"#,
);

assert_eq!(ws.expr_ty("A1"), ws.ty("string"));
assert_eq!(ws.expr_ty("B1"), ws.ty("number"));
assert_eq!(ws.expr_ty("A2"), ws.ty("string"));
assert_eq!(ws.expr_ty("B2"), ws.ty("any"));
}

/// A field assigned from a multi-return local has that local's slot type, not the whole list.
#[test]
fn test_field_from_multi_return_local_has_slot_type() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@return any result
---@return string? err
---@return string output
local function captured() end

local result, err, output = captured()
local payload = {}
payload.output = output
P = payload.output
"#,
);

assert_eq!(ws.expr_ty("P"), ws.ty("string"));
}

/// The database's slot rule for every shape of result list, reached through mismatched calls.
#[test]
fn test_mismatched_call_slots_by_result_shape() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@param y number
---@return string a
---@return number b
---@return boolean c
local function three(y) end

---@param y number
---@return string a
---@return number b
local function two(y) end

---@param y number
---@return string a
---@return number ...
local function open_tail(y) end

---@param y number
---@return number ...
local function repeated(y) end

---@param y number
---@return number...
local function variadic(y) end

---@param y number
---@return [string, number]...
local function tuple_variadic(y) end

---@param y number
---@return [string, number]
local function tuple(y) end

---@param y number
---@param ... number
local function forwards(y, ...) return true, ... end

local _, middle, last = three('x')
local _, _, past_end = two('x')
local _, tail1, tail2 = open_tail('x')
local _, repeated2 = repeated('x')
local _, variadic2 = variadic('x')
local _, tuple_variadic2 = tuple_variadic('x')
local _, past_tuple = tuple('x')
local _, _, forwarded = forwards('x')
MIDDLE, LAST, PAST_END, TAIL1, TAIL2 = middle, last, past_end, tail1, tail2
REPEATED2, VARIADIC2, TUPLE_VARIADIC2 = repeated2, variadic2, tuple_variadic2
PAST_TUPLE, FORWARDED = past_tuple, forwarded
"#,
);

assert_eq!(ws.expr_ty("MIDDLE"), ws.ty("number"));
assert_eq!(ws.expr_ty("LAST"), ws.ty("boolean"));
assert_eq!(ws.expr_ty("PAST_END"), ws.ty("nil"));
assert_eq!(ws.expr_ty("TAIL1"), ws.ty("number"));
assert_eq!(ws.expr_ty("TAIL2"), ws.ty("number"));
assert_eq!(ws.expr_ty("REPEATED2"), ws.ty("number"));
assert_eq!(ws.expr_ty("VARIADIC2"), ws.ty("number"));
assert_eq!(ws.expr_ty("TUPLE_VARIADIC2"), ws.ty("number"));
assert_eq!(ws.expr_ty("PAST_TUPLE"), ws.ty("any"));
assert_eq!(ws.expr_ty("FORWARDED"), ws.ty("number"));
}
}
Loading