Skip to content

Commit 78af2e0

Browse files
committed
fix: separate unreachable conditions from never types
Truthiness checks for call conditions treated impossible branches as `never` values. That let unreachable branch facts leak into point queries and merges: unrelated symbols inside impossible bodies could look non-callable, while assignment and cast effects from impossible branches could still contribute to joined types. Track unreachable condition edges explicitly in flow-query results. Point queries continue through unreachable condition edges, while merge-branch queries contribute `never` for unreachable predecessors. Preserve merge-branch mode through assignments, tag casts, correlated condition subqueries, and recovered assignment fallbacks. This prevents assignments, annotated assignments, casts, and missing-field RHS fallback values inside impossible branches from contributing to final merge types. Add coverage for plain call conditions, always-false and always-true call-condition branches, reachable `never` assignments, and unreachable assignment, annotated-assignment, cast, and fallback merge contributions. Assisted-by: Codex
1 parent dca540c commit 78af2e0

4 files changed

Lines changed: 428 additions & 62 deletions

File tree

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

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,180 @@ mod test {
305305
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
306306
}
307307

308+
#[test]
309+
fn test_plain_call_condition_keeps_inner_call_prefix_type() {
310+
let mut ws = VirtualWorkspace::new();
311+
let code = r#"
312+
local function a() end
313+
local function b() end
314+
315+
b()
316+
if a() then
317+
b()
318+
inner = b
319+
end
320+
"#;
321+
ws.def(code);
322+
323+
let ty = ws.expr_ty("inner");
324+
assert!(ty.is_function());
325+
326+
let mut diag_ws = VirtualWorkspace::new();
327+
assert!(diag_ws.has_no_diagnostic(DiagnosticCode::CallNonCallable, code));
328+
}
329+
330+
#[test]
331+
fn test_false_call_condition_keeps_inner_unrelated_type() {
332+
let mut ws = VirtualWorkspace::new();
333+
ws.def(
334+
r#"
335+
---@return false
336+
local function always_false()
337+
return false
338+
end
339+
340+
---@type string
341+
local value = "ok"
342+
if always_false() then
343+
inner = value
344+
end
345+
"#,
346+
);
347+
348+
assert_eq!(ws.expr_ty("inner"), ws.ty("string"));
349+
}
350+
351+
#[test]
352+
fn test_true_call_condition_keeps_else_call_prefix_type() {
353+
let mut ws = VirtualWorkspace::new();
354+
let code = r#"
355+
---@return true
356+
local function always_true()
357+
return true
358+
end
359+
360+
local function b() end
361+
if always_true() then
362+
else
363+
b()
364+
end
365+
"#;
366+
367+
assert!(ws.has_no_diagnostic(DiagnosticCode::CallNonCallable, code));
368+
}
369+
370+
#[test]
371+
fn test_false_call_condition_assignment_does_not_contribute_to_merge() {
372+
let mut ws = VirtualWorkspace::new();
373+
ws.def(
374+
r#"
375+
---@return false
376+
local function always_false()
377+
return false
378+
end
379+
380+
local value = "before"
381+
if always_false() then
382+
value = 1
383+
end
384+
after = value
385+
"#,
386+
);
387+
388+
let after = ws.expr_ty("after");
389+
assert_eq!(ws.humanize_type(after), "string");
390+
}
391+
392+
#[test]
393+
fn test_false_call_condition_missing_field_assignment_does_not_contribute_to_merge() {
394+
let mut ws = VirtualWorkspace::new();
395+
ws.def(
396+
r#"
397+
---@return false
398+
local function is_windows()
399+
return false
400+
end
401+
402+
local command = "ls"
403+
local config = {}
404+
if is_windows() then
405+
command = config.windows_command
406+
end
407+
after = command
408+
"#,
409+
);
410+
411+
let after = ws.expr_ty("after");
412+
assert_eq!(ws.humanize_type(after), "string");
413+
}
414+
415+
#[test]
416+
fn test_reachable_assignment_over_never_value_contributes_to_merge() {
417+
let mut ws = VirtualWorkspace::new();
418+
ws.def(
419+
r#"
420+
---@class NeverBox
421+
---@field value never
422+
423+
---@return NeverBox
424+
local function make_box() end
425+
426+
local value = make_box().value
427+
local cond ---@type boolean
428+
if cond then
429+
value = 1
430+
end
431+
after = value
432+
"#,
433+
);
434+
435+
assert_eq!(ws.expr_ty("after"), LuaType::IntegerConst(1));
436+
}
437+
438+
#[test]
439+
fn test_false_call_condition_tag_cast_does_not_contribute_to_merge() {
440+
let mut ws = VirtualWorkspace::new();
441+
ws.def(
442+
r#"
443+
---@return false
444+
local function always_false()
445+
return false
446+
end
447+
448+
local value = "before"
449+
if always_false() then
450+
---@cast value integer
451+
end
452+
after = value
453+
"#,
454+
);
455+
456+
let after = ws.expr_ty("after");
457+
assert_eq!(ws.humanize_type(after), r#""before""#);
458+
}
459+
460+
#[test]
461+
fn test_false_call_condition_doc_assignment_does_not_contribute_to_merge() {
462+
let mut ws = VirtualWorkspace::new();
463+
ws.def(
464+
r#"
465+
---@return false
466+
local function always_false()
467+
return false
468+
end
469+
470+
local value = "before"
471+
if always_false() then
472+
---@type integer
473+
value = 1
474+
end
475+
after = value
476+
"#,
477+
);
478+
479+
assert_eq!(ws.expr_ty("after"), ws.ty("string"));
480+
}
481+
308482
#[test]
309483
fn test_branch_join_keeps_union_when_only_one_side_narrows() {
310484
let mut ws = VirtualWorkspace::new();

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,35 @@ pub(in crate::semantic) struct FlowAssignmentInfo {
2626

2727
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2828
pub(in crate::semantic) enum FlowMode {
29-
WithConditions,
30-
WithoutConditions,
29+
Normal,
30+
// Query one predecessor of a merge label; if that walk reaches an
31+
// unreachable condition edge, it contributes `never` to the merged type.
32+
MergeBranch,
33+
// Re-query assignment antecedents without applying condition narrows.
34+
IgnoreConditions,
3135
}
3236

33-
impl FlowMode {
34-
pub fn uses_conditions(self) -> bool {
35-
matches!(self, Self::WithConditions)
37+
#[derive(Debug, Clone)]
38+
pub(in crate::semantic) enum FlowQueryResult {
39+
Type(LuaType),
40+
// A merge contribution crossed an impossible condition edge. Keep this
41+
// separate from `Type(Never)` because reachable code can still have a real
42+
// never-typed value and assign over it before the merge.
43+
Unreachable,
44+
}
45+
46+
impl FlowQueryResult {
47+
pub(in crate::semantic) fn into_type(self) -> LuaType {
48+
match self {
49+
Self::Type(typ) => typ,
50+
Self::Unreachable => LuaType::Never,
51+
}
3652
}
3753
}
3854

3955
#[derive(Debug, Default)]
4056
pub(in crate::semantic) struct FlowVarCache {
41-
pub type_cache: HashMap<(FlowId, FlowMode), CacheEntry<LuaType>>,
57+
pub type_cache: HashMap<(FlowId, FlowMode), CacheEntry<FlowQueryResult>>,
4258
pub condition_cache: HashMap<(FlowId, InferConditionFlow), CacheEntry<ConditionFlowAction>>,
4359
}
4460

crates/emmylua_code_analysis/src/semantic/infer/narrow/condition_flow/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ pub(in crate::semantic) enum CorrelatedDiscriminantNarrow {
119119
#[derive(Debug, Clone)]
120120
pub(in crate::semantic) enum ConditionFlowAction {
121121
Continue,
122+
Unreachable,
122123
Result(LuaType),
123124
Pending(PendingConditionNarrow),
124125
NeedExprType {
@@ -610,7 +611,7 @@ pub(super) fn get_type_at_condition_flow(
610611
};
611612

612613
if unreachable_branch {
613-
ConditionFlowAction::Result(LuaType::Never)
614+
ConditionFlowAction::Unreachable
614615
} else {
615616
ConditionFlowAction::Continue
616617
}

0 commit comments

Comments
 (0)