Skip to content

Commit 88a6194

Browse files
authored
Merge pull request #1025 from EfveZombie/fix.intersection_table_subtype
fix: handle intersection type as table subtype
2 parents a3cc920 + 6e97840 commit 88a6194

5 files changed

Lines changed: 160 additions & 1 deletion

File tree

crates/emmylua_code_analysis/src/diagnostic/checker/check_field.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ fn is_invalid_prefix_type(typ: &LuaType) -> bool {
116116
LuaType::Instance(instance_typ) => {
117117
current_typ = instance_typ.get_base();
118118
}
119+
LuaType::Intersection(intersection) => {
120+
return intersection.get_types().iter().any(is_invalid_prefix_type);
121+
}
119122
_ => return false,
120123
}
121124
}
@@ -141,6 +144,25 @@ pub(super) fn is_valid_member(
141144
return None;
142145
}
143146
}
147+
LuaType::Intersection(intersection) => {
148+
// If any component of the intersection would pass the member check on its own,
149+
// the intersection should also pass (e.g. unknown[] & { n: integer }).
150+
for component in intersection.get_types() {
151+
if is_valid_member(semantic_model, component, index_expr, index_key, code).is_some()
152+
{
153+
return Some(());
154+
}
155+
}
156+
// Even if no single component passes the early checks, the intersection's
157+
// member lookup may still succeed (e.g. Tuple([Unknown]) & Object).
158+
// Try inferring the index expression type directly.
159+
if semantic_model
160+
.get_index_decl_type(index_expr.clone())
161+
.is_some()
162+
{
163+
return Some(());
164+
}
165+
}
144166
_ => {}
145167
}
146168

crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,4 +815,30 @@ mod test {
815815
"#
816816
));
817817
}
818+
819+
#[test]
820+
fn test_intersection_array_index_access() {
821+
let mut ws = VirtualWorkspace::new_with_init_std_lib();
822+
823+
// Accessing [1] on an intersection type containing an array should not report undefined-field
824+
assert!(ws.check_code_for(
825+
DiagnosticCode::UndefinedField,
826+
r#"
827+
function test(...)
828+
local values = table.pack(...)
829+
local e = values[1]
830+
end
831+
"#
832+
));
833+
834+
// Explicit intersection type annotation
835+
assert!(ws.check_code_for(
836+
DiagnosticCode::UndefinedField,
837+
r#"
838+
---@type integer[] & { n: integer }
839+
local values
840+
local e = values[1]
841+
"#
842+
));
843+
}
818844
}

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/sub_type.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ 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+
intersection.get_types().iter().find_map(get_base_type_id)
95+
}
9296
LuaType::DocFunction(_) | LuaType::Function | LuaType::Signature(_) => {
9397
Some(LuaTypeDeclId::global("function"))
9498
}

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)