Skip to content
Open
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
22 changes: 22 additions & 0 deletions crates/emmylua_code_analysis/src/diagnostic/checker/check_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ fn is_invalid_prefix_type(typ: &LuaType) -> bool {
LuaType::Instance(instance_typ) => {
current_typ = instance_typ.get_base();
}
LuaType::Intersection(intersection) => {
return intersection.get_types().iter().any(is_invalid_prefix_type);
}
_ => return false,
}
}
Expand All @@ -141,6 +144,25 @@ pub(super) fn is_valid_member(
return None;
}
}
LuaType::Intersection(intersection) => {
// If any component of the intersection would pass the member check on its own,
// the intersection should also pass (e.g. unknown[] & { n: integer }).
for component in intersection.get_types() {
if is_valid_member(semantic_model, component, index_expr, index_key, code).is_some()
{
return Some(());
}
}
// Even if no single component passes the early checks, the intersection's
// member lookup may still succeed (e.g. Tuple([Unknown]) & Object).
// Try inferring the index expression type directly.
if semantic_model
.get_index_decl_type(index_expr.clone())
.is_some()
{
return Some(());
}
}
_ => {}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -818,4 +818,30 @@ mod test {
"#
));
}

#[test]
fn test_intersection_array_index_access() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();

// Accessing [1] on an intersection type containing an array should not report undefined-field
assert!(ws.check_code_for(
DiagnosticCode::UndefinedField,
r#"
function test(...)
local values = table.pack(...)
local e = values[1]
end
"#
));

// Explicit intersection type annotation
assert!(ws.check_code_for(
DiagnosticCode::UndefinedField,
r#"
---@type integer[] & { n: integer }
local values
local e = values[1]
"#
));
}
}
19 changes: 19 additions & 0 deletions crates/emmylua_code_analysis/src/semantic/type_check/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,25 @@ fn check_general_type_compact(
);
}

// When compact_type is an Intersection, the value satisfies all components simultaneously.
// So it can be assigned to any target that accepts at least one component.
// This must be handled before the source-based dispatch, because individual source branches
// (e.g. Array, Ref) do not know how to decompose an intersection compact_type.
if let LuaType::Intersection(compact_intersection) = compact_type {
// Skip if source is also Intersection — that case is handled symmetrically in
// check_intersection_type_compact.
if !matches!(source, LuaType::Intersection(_)) {
for component in compact_intersection.get_types() {
if check_general_type_compact(context, source, component, check_guard.next_level()?)
.is_ok()
{
return Ok(());
}
}
return Err(TypeCheckFailReason::TypeNotMatch);
}
}

match source {
LuaType::Unknown | LuaType::Any => Ok(()),
// simple type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ pub fn get_base_type_id(typ: &LuaType) -> Option<LuaTypeDeclId> {
| LuaType::TableGeneric(_)
| LuaType::TableConst(_)
| LuaType::Tuple(_)
| LuaType::Array(_) => Some(LuaTypeDeclId::global("table")),
| LuaType::Array(_)
| LuaType::Object(_) => Some(LuaTypeDeclId::global("table")),
LuaType::Intersection(intersection) => {
intersection.get_types().iter().find_map(get_base_type_id)
}
LuaType::DocFunction(_) | LuaType::Function | LuaType::Signature(_) => {
Some(LuaTypeDeclId::global("function"))
}
Expand Down
88 changes: 88 additions & 0 deletions crates/emmylua_code_analysis/src/semantic/type_check/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,94 @@ mod test {
));
}

#[test]
fn test_intersection_is_table_subtype() {
let mut ws = VirtualWorkspace::new();

// [integer] & { n: integer } should be assignable to table
let intersection_ty = ws.ty("integer[] & { n: integer }");
let table_ty = ws.ty("table");
assert!(
ws.check_type(&table_ty, &intersection_ty),
"integer[] & {{ n: integer }} should be a subtype of table"
);

// Verify via diagnostic: passing intersection type to a table parameter should not error
assert!(ws.check_code_for(
DiagnosticCode::ParamTypeMismatch,
r#"
---@param t table
local function foo(t) end

---@type integer[] & { n: integer }
local packed
foo(packed)
"#
));

// Also verify: assigning intersection to table should not error
assert!(ws.check_code_for(
DiagnosticCode::AssignTypeMismatch,
r#"
---@type integer[] & { n: integer }
local packed

---@type table
local t = packed
"#
));

// Intersection type should be assignable to an array type (non-generic)
let array_ty = ws.ty("integer[]");
assert!(
ws.check_type(&array_ty, &intersection_ty),
"integer[] & {{ n: integer }} should be assignable to integer[]"
);

// Intersection type should be assignable to an array parameter (non-generic)
assert!(ws.check_code_for(
DiagnosticCode::ParamTypeMismatch,
r#"
---@param t integer[]
local function foo2(t) end

---@type integer[] & { n: integer }
local packed
foo2(packed)
"#
));

// Intersection type should be assignable to a generic array parameter
assert!(ws.check_code_for(
DiagnosticCode::ParamTypeMismatch,
r#"
---@generic V
---@param t V[]
---@return fun(): integer, V
local function my_ipairs(t) end

---@type integer[] & { n: integer }
local packed
my_ipairs(packed)
"#
));

// Intersection type should be assignable to table<int, V>
assert!(ws.check_code_for(
DiagnosticCode::ParamTypeMismatch,
r#"
---@generic V
---@param t table<integer, V>
---@return fun(): integer, V
local function my_iter(t) end

---@type integer[] & { n: integer }
local packed
my_iter(packed)
"#
));
}

#[test]
fn test_set_index_expr_owner_prefers_declared_global_type() {
let mut ws = VirtualWorkspace::new_with_init_std_lib();
Expand Down
Loading