Skip to content

Commit 431dea2

Browse files
author
efve.zff
committed
fix: handle intersection type as table subtype, to allow types like [T...] & { n: integer } to be assignable to table
1 parent eeb90e3 commit 431dea2

4 files changed

Lines changed: 123 additions & 1 deletion

File tree

crates/emmylua_code_analysis/src/semantic/type_check/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,25 @@ fn check_general_type_compact(
8181
);
8282
}
8383

84+
// When compact_type is an Intersection, the value satisfies all components simultaneously.
85+
// So it can be assigned to any target that accepts at least one component.
86+
// This must be handled before the source-based dispatch, because individual source branches
87+
// (e.g. Array, Ref) do not know how to decompose an intersection compact_type.
88+
if let LuaType::Intersection(compact_intersection) = compact_type {
89+
// Skip if source is also Intersection — that case is handled symmetrically in
90+
// check_intersection_type_compact.
91+
if !matches!(source, LuaType::Intersection(_)) {
92+
for component in compact_intersection.get_types() {
93+
if check_general_type_compact(context, source, component, check_guard.next_level()?)
94+
.is_ok()
95+
{
96+
return Ok(());
97+
}
98+
}
99+
return Err(TypeCheckFailReason::TypeNotMatch);
100+
}
101+
}
102+
84103
match source {
85104
LuaType::Unknown | LuaType::Any => Ok(()),
86105
// simple type

crates/emmylua_code_analysis/src/semantic/type_check/simple_type.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub fn check_simple_type_compact(
4141
| LuaType::Global
4242
| LuaType::Userdata
4343
| LuaType::Instance(_)
44+
| LuaType::Intersection(_)
4445
| LuaType::Any
4546
) {
4647
return Ok(());

crates/emmylua_code_analysis/src/semantic/type_check/sub_type.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,21 @@ pub fn get_base_type_id(typ: &LuaType) -> Option<LuaTypeDeclId> {
8888
| LuaType::TableGeneric(_)
8989
| LuaType::TableConst(_)
9090
| LuaType::Tuple(_)
91-
| LuaType::Array(_) => Some(LuaTypeDeclId::global("table")),
91+
| LuaType::Array(_)
92+
| LuaType::Object(_) => Some(LuaTypeDeclId::global("table")),
93+
LuaType::Intersection(intersection) => {
94+
let all_table = intersection.get_types().iter().all(|t| {
95+
matches!(
96+
get_base_type_id(t),
97+
Some(id) if id == LuaTypeDeclId::global("table")
98+
)
99+
});
100+
if all_table {
101+
Some(LuaTypeDeclId::global("table"))
102+
} else {
103+
None
104+
}
105+
}
92106
LuaType::DocFunction(_) | LuaType::Function | LuaType::Signature(_) => {
93107
Some(LuaTypeDeclId::global("function"))
94108
}

crates/emmylua_code_analysis/src/semantic/type_check/test.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,94 @@ mod test {
209209
));
210210
}
211211

212+
#[test]
213+
fn test_intersection_is_table_subtype() {
214+
let mut ws = VirtualWorkspace::new();
215+
216+
// [integer] & { n: integer } should be assignable to table
217+
let intersection_ty = ws.ty("integer[] & { n: integer }");
218+
let table_ty = ws.ty("table");
219+
assert!(
220+
ws.check_type(&table_ty, &intersection_ty),
221+
"integer[] & {{ n: integer }} should be a subtype of table"
222+
);
223+
224+
// Verify via diagnostic: passing intersection type to a table parameter should not error
225+
assert!(ws.check_code_for(
226+
DiagnosticCode::ParamTypeMismatch,
227+
r#"
228+
---@param t table
229+
local function foo(t) end
230+
231+
---@type integer[] & { n: integer }
232+
local packed
233+
foo(packed)
234+
"#
235+
));
236+
237+
// Also verify: assigning intersection to table should not error
238+
assert!(ws.check_code_for(
239+
DiagnosticCode::AssignTypeMismatch,
240+
r#"
241+
---@type integer[] & { n: integer }
242+
local packed
243+
244+
---@type table
245+
local t = packed
246+
"#
247+
));
248+
249+
// Intersection type should be assignable to an array type (non-generic)
250+
let array_ty = ws.ty("integer[]");
251+
assert!(
252+
ws.check_type(&array_ty, &intersection_ty),
253+
"integer[] & {{ n: integer }} should be assignable to integer[]"
254+
);
255+
256+
// Intersection type should be assignable to an array parameter (non-generic)
257+
assert!(ws.check_code_for(
258+
DiagnosticCode::ParamTypeMismatch,
259+
r#"
260+
---@param t integer[]
261+
local function foo2(t) end
262+
263+
---@type integer[] & { n: integer }
264+
local packed
265+
foo2(packed)
266+
"#
267+
));
268+
269+
// Intersection type should be assignable to a generic array parameter
270+
assert!(ws.check_code_for(
271+
DiagnosticCode::ParamTypeMismatch,
272+
r#"
273+
---@generic V
274+
---@param t V[]
275+
---@return fun(): integer, V
276+
local function my_ipairs(t) end
277+
278+
---@type integer[] & { n: integer }
279+
local packed
280+
my_ipairs(packed)
281+
"#
282+
));
283+
284+
// Intersection type should be assignable to table<int, V>
285+
assert!(ws.check_code_for(
286+
DiagnosticCode::ParamTypeMismatch,
287+
r#"
288+
---@generic V
289+
---@param t table<integer, V>
290+
---@return fun(): integer, V
291+
local function my_iter(t) end
292+
293+
---@type integer[] & { n: integer }
294+
local packed
295+
my_iter(packed)
296+
"#
297+
));
298+
}
299+
212300
#[test]
213301
fn test_set_index_expr_owner_prefers_declared_global_type() {
214302
let mut ws = VirtualWorkspace::new_with_init_std_lib();

0 commit comments

Comments
 (0)