Skip to content

Commit ccab61c

Browse files
committed
fix(type): preserve unknown unions and narrow pcall(any)
Preserve unknown as a real union member in type unions and doc-type parsing instead of collapsing unknown|T to T. Update call sites that previously used Unknown as an empty union accumulator to use Never or explicit first-value handling, and avoid merging missing closure param annotations as unknown. Keep the pcall(any) flow fixes so the payload stays unknown|string outside the branch, narrows to unknown on success, and narrows to string on failure.
1 parent 827e0b3 commit ccab61c

24 files changed

Lines changed: 393 additions & 109 deletions

File tree

crates/emmylua_code_analysis/src/compilation/analyzer/doc/infer_type.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -370,13 +370,6 @@ fn infer_binary_type(analyzer: &mut DocAnalyzer, binary_type: &LuaDocBinaryType)
370370
if let Some((left, right)) = binary_type.get_types() {
371371
let left_type = infer_type(analyzer, left);
372372
let right_type = infer_type(analyzer, right);
373-
if left_type.is_unknown() {
374-
return right_type;
375-
}
376-
if right_type.is_unknown() {
377-
return left_type;
378-
}
379-
380373
if let Some(op) = binary_type.get_op_token() {
381374
match op.get_op() {
382375
LuaTypeBinaryOperator::Union => match (left_type, right_type) {

crates/emmylua_code_analysis/src/compilation/analyzer/lua/closure.rs

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -208,31 +208,33 @@ fn analyze_lambda_returns(
208208
pub fn analyze_return_point(
209209
db: &DbIndex,
210210
cache: &mut LuaInferCache,
211-
return_points: &Vec<LuaReturnPoint>,
211+
return_points: &[LuaReturnPoint],
212212
) -> Result<Vec<LuaDocReturnInfo>, InferFailReason> {
213-
let mut return_type = LuaType::Unknown;
213+
let mut return_type = None;
214214
for point in return_points {
215-
match point {
216-
LuaReturnPoint::Expr(expr) => {
217-
let expr_type = infer_expr(db, cache, expr.clone())?;
218-
return_type = union_return_expr(db, return_type, expr_type);
219-
}
215+
let point_type = match point {
216+
LuaReturnPoint::Expr(expr) => Some(infer_expr(db, cache, expr.clone())?),
220217
LuaReturnPoint::MuliExpr(exprs) => {
221-
let mut multi_return = vec![];
218+
let mut multi_return = Vec::with_capacity(exprs.len());
222219
for expr in exprs {
223-
let expr_type = infer_expr(db, cache, expr.clone())?;
224-
multi_return.push(expr_type);
220+
multi_return.push(infer_expr(db, cache, expr.clone())?);
225221
}
226-
let typ = LuaType::Variadic(VariadicType::Multi(multi_return).into());
227-
return_type = union_return_expr(db, return_type, typ);
228-
}
229-
LuaReturnPoint::Nil => {
230-
return_type = union_return_expr(db, return_type, LuaType::Nil);
222+
Some(LuaType::Variadic(VariadicType::Multi(multi_return).into()))
231223
}
232-
_ => {}
224+
LuaReturnPoint::Nil => Some(LuaType::Nil),
225+
_ => None,
226+
};
227+
228+
if let Some(point_type) = point_type {
229+
return_type = Some(match return_type {
230+
Some(return_type) => union_return_expr(db, return_type, point_type),
231+
None => point_type,
232+
});
233233
}
234234
}
235235

236+
let return_type = return_type.unwrap_or(LuaType::Unknown);
237+
236238
Ok(vec![LuaDocReturnInfo {
237239
type_ref: return_type,
238240
description: None,
@@ -242,10 +244,6 @@ pub fn analyze_return_point(
242244
}
243245

244246
fn union_return_expr(db: &DbIndex, left: LuaType, right: LuaType) -> LuaType {
245-
if left == LuaType::Unknown {
246-
return right;
247-
}
248-
249247
match (&left, &right) {
250248
(LuaType::Variadic(left_variadic), LuaType::Variadic(right_variadic)) => {
251249
match (&left_variadic.deref(), &right_variadic.deref()) {

crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/find_decl_function.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,15 @@ fn find_custom_type_function_member(
205205
let key = LuaMemberKey::from_index_key(db, cache, &index_key)?;
206206
if let Some(member_item) = db.get_member_index().get_member_item(&owner, &key) {
207207
let index_member_id = get_member_id(cache, &index_expr);
208-
let mut result_type = LuaType::Unknown;
208+
let mut result_type = LuaType::Never;
209209
for member_id in member_item.get_member_ids() {
210210
if index_member_id != member_id
211211
&& let Some(type_cache) = db.get_type_index().get_type_cache(&member_id.into())
212212
{
213213
result_type = TypeOps::Union.apply(db, &result_type, type_cache.as_type());
214214
}
215215
}
216-
if !result_type.is_unknown() {
216+
if !result_type.is_never() {
217217
return Ok(result_type);
218218
}
219219
}
@@ -540,7 +540,7 @@ fn find_member_by_index_table(
540540
.get_member_index()
541541
.get_members(&LuaMemberOwner::Element(table_range.clone()));
542542
if let Some(members) = members {
543-
let mut result_type = LuaType::Unknown;
543+
let mut result_type = LuaType::Never;
544544
for member in members {
545545
let member_key_type = match member.get_key() {
546546
LuaMemberKey::Name(s) => LuaType::StringConst(s.clone().into()),
@@ -558,7 +558,7 @@ fn find_member_by_index_table(
558558
}
559559
}
560560

561-
if !result_type.is_unknown() {
561+
if !result_type.is_never() {
562562
if matches!(
563563
key_type,
564564
LuaType::String | LuaType::Number | LuaType::Integer
@@ -698,7 +698,7 @@ fn find_member_by_index_union(
698698
infer_guard: &InferGuardRef,
699699
deep_guard: &mut DeepLevel,
700700
) -> FunctionTypeResult {
701-
let mut member_type = LuaType::Unknown;
701+
let mut member_type = LuaType::Never;
702702
for member in union.into_vec() {
703703
let result = find_function_type_by_operator(
704704
db,
@@ -719,7 +719,7 @@ fn find_member_by_index_union(
719719
}
720720
}
721721

722-
if member_type.is_unknown() {
722+
if member_type.is_never() {
723723
return Err(InferFailReason::FieldNotFound);
724724
}
725725

crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/resolve_closure.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,8 @@ fn resolve_closure_member_type(
307307
.get(&closure_params.signature_id)
308308
.ok_or(InferFailReason::None)?;
309309
let mut final_params = signature.get_type_params().to_vec();
310-
let mut final_ret = LuaType::Unknown;
310+
let mut final_ret = LuaType::Never;
311+
let mut has_final_ret = false;
311312

312313
let mut multi_function_type = Vec::new();
313314
for typ in union_types.into_vec() {
@@ -369,17 +370,21 @@ fn resolve_closure_member_type(
369370

370371
break;
371372
}
372-
let new_type = TypeOps::Union.apply(
373-
db,
374-
final_param.1.as_ref().unwrap_or(&LuaType::Unknown),
375-
param.1.as_ref().unwrap_or(&LuaType::Unknown),
376-
);
377-
final_params[idx] = (final_param.0.clone(), Some(new_type));
373+
let new_type = match (&final_param.1, &param.1) {
374+
(Some(final_type), Some(param_type)) => {
375+
Some(TypeOps::Union.apply(db, final_type, param_type))
376+
}
377+
(Some(final_type), None) => Some(final_type.clone()),
378+
(None, Some(param_type)) => Some(param_type.clone()),
379+
(None, None) => None,
380+
};
381+
final_params[idx] = (final_param.0.clone(), new_type);
378382
} else {
379383
final_params.push((param.0.clone(), param.1.clone()));
380384
}
381385
}
382386

387+
has_final_ret = true;
383388
final_ret = TypeOps::Union.apply(db, &final_ret, doc_func.get_ret());
384389
}
385390

@@ -389,6 +394,12 @@ fn resolve_closure_member_type(
389394
param.1 = Some(variadic_type);
390395
}
391396

397+
let final_ret = if !has_final_ret {
398+
LuaType::Unknown
399+
} else {
400+
final_ret
401+
};
402+
392403
resolve_doc_function(
393404
db,
394405
closure_params,

crates/emmylua_code_analysis/src/compilation/test/closure_return_test.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,49 @@ mod test {
8080
"#,
8181
));
8282
}
83+
84+
#[test]
85+
fn test_inferred_return_preserves_never() {
86+
let mut ws = VirtualWorkspace::new();
87+
88+
ws.def(
89+
r#"
90+
---@return { y: number } & { y: string }
91+
local function impossible() end
92+
93+
local function f()
94+
return impossible().y
95+
end
96+
97+
result = f()
98+
"#,
99+
);
100+
101+
assert_eq!(ws.expr_ty("result"), ws.ty("never"));
102+
}
103+
104+
#[test]
105+
fn test_member_doc_return_preserves_never() {
106+
let mut ws = VirtualWorkspace::new();
107+
108+
ws.def(
109+
r#"
110+
---@return { y: number } & { y: string }
111+
local function impossible() end
112+
113+
---@class ClosureTest
114+
---@field e fun(): never
115+
---@field e fun(): never
116+
local Test
117+
118+
function Test.e()
119+
return impossible().y
120+
end
121+
122+
result = Test.e()
123+
"#,
124+
);
125+
126+
assert_eq!(ws.expr_ty("result"), ws.ty("never"));
127+
}
83128
}

crates/emmylua_code_analysis/src/compilation/test/member_infer_test.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,48 @@ mod test {
324324
value_ty
325325
);
326326
}
327+
328+
#[test]
329+
fn test_union_member_access_preserves_never() {
330+
let mut ws = VirtualWorkspace::new();
331+
332+
ws.def(
333+
r#"
334+
---@class A
335+
---@field y never
336+
337+
---@class B
338+
---@field y never
339+
340+
---@return A|B
341+
local function make() end
342+
343+
local value = make()
344+
345+
result = value.y
346+
"#,
347+
);
348+
349+
assert_eq!(ws.expr_ty("result"), ws.ty("never"));
350+
}
351+
352+
#[test]
353+
fn test_table_expr_index_preserves_never() {
354+
let mut ws = VirtualWorkspace::new();
355+
356+
ws.def(
357+
r#"
358+
---@return { y: number } & { y: string }
359+
local function impossible() end
360+
361+
local t = {
362+
a = impossible().y,
363+
}
364+
365+
result = t["a"]
366+
"#,
367+
);
368+
369+
assert_eq!(ws.expr_ty("result"), ws.ty("never"));
370+
}
327371
}

crates/emmylua_code_analysis/src/compilation/test/pcall_test.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,28 @@ mod test {
175175
);
176176
assert_eq!(ws.expr_ty("narrowed"), ws.ty("integer"));
177177
}
178+
179+
#[test]
180+
fn test_pcall_any_callable_splits_success_unknown_and_failure_string() {
181+
let mut ws = VirtualWorkspace::new_with_init_std_lib();
182+
183+
ws.def(
184+
r#"
185+
---@type any
186+
local x
187+
188+
local ok, result = pcall(x)
189+
outside = result
190+
if ok then
191+
success = result
192+
else
193+
failure = result
194+
end
195+
"#,
196+
);
197+
198+
assert_eq!(ws.expr_ty("outside"), ws.ty("unknown|string"));
199+
assert_eq!(ws.expr_ty("success"), ws.ty("unknown"));
200+
assert_eq!(ws.expr_ty("failure"), ws.ty("string"));
201+
}
178202
}

crates/emmylua_code_analysis/src/compilation/test/return_overload_flow_test.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,71 @@ mod test {
7777
assert_eq!(ws.expr_ty("d"), ws.ty("boolean"));
7878
}
7979

80+
#[test]
81+
fn test_return_overload_narrow_with_overlapping_target_union() {
82+
let mut ws = VirtualWorkspace::new();
83+
84+
ws.def(
85+
r#"
86+
---@param ok boolean
87+
---@return boolean
88+
---@return string|number
89+
---@return_overload true, string
90+
---@return_overload false, string|number
91+
local function pick(ok)
92+
if ok then
93+
return true, "value"
94+
end
95+
return false, 1
96+
end
97+
98+
local cond ---@type boolean
99+
local ok, result = pick(cond)
100+
101+
if ok then
102+
success_branch = result
103+
else
104+
failure_branch = result
105+
end
106+
"#,
107+
);
108+
109+
assert_eq!(ws.expr_ty("success_branch"), ws.ty("string"));
110+
assert_eq!(ws.expr_ty("failure_branch"), ws.ty("string|number"));
111+
}
112+
113+
#[test]
114+
fn test_return_overload_narrow_with_overlapping_supertype_target() {
115+
let mut ws = VirtualWorkspace::new();
116+
117+
ws.def(
118+
r#"
119+
---@param ok boolean
120+
---@return boolean
121+
---@return number
122+
---@return_overload true, integer
123+
---@return_overload false, number
124+
local function pick(ok)
125+
if ok then
126+
return true, 1
127+
end
128+
return false, 1.5
129+
end
130+
131+
local cond ---@type boolean
132+
local ok, result = pick(cond)
133+
134+
if ok then
135+
success_branch = result
136+
else
137+
failure_branch = result
138+
end
139+
"#,
140+
);
141+
142+
assert_eq!(ws.expr_ty("success_branch"), ws.ty("integer"));
143+
}
144+
80145
#[test]
81146
fn test_return_overload_reassign_clears_multi_return_mapping() {
82147
let mut ws = VirtualWorkspace::new();

0 commit comments

Comments
 (0)