Skip to content

Commit 77cc687

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 d7d5dcb commit 77cc687

4 files changed

Lines changed: 430 additions & 64 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
@@ -304,6 +304,180 @@ mod test {
304304
assert_eq!(ws.expr_ty("after_guard"), ws.ty("string"));
305305
}
306306

307+
#[test]
308+
fn test_plain_call_condition_keeps_inner_call_prefix_type() {
309+
let mut ws = VirtualWorkspace::new();
310+
let code = r#"
311+
local function a() end
312+
local function b() end
313+
314+
b()
315+
if a() then
316+
b()
317+
inner = b
318+
end
319+
"#;
320+
ws.def(code);
321+
322+
let ty = ws.expr_ty("inner");
323+
assert!(ty.is_function());
324+
325+
let mut diag_ws = VirtualWorkspace::new();
326+
assert!(diag_ws.has_no_diagnostic(DiagnosticCode::CallNonCallable, code));
327+
}
328+
329+
#[test]
330+
fn test_false_call_condition_keeps_inner_unrelated_type() {
331+
let mut ws = VirtualWorkspace::new();
332+
ws.def(
333+
r#"
334+
---@return false
335+
local function always_false()
336+
return false
337+
end
338+
339+
---@type string
340+
local value = "ok"
341+
if always_false() then
342+
inner = value
343+
end
344+
"#,
345+
);
346+
347+
assert_eq!(ws.expr_ty("inner"), ws.ty("string"));
348+
}
349+
350+
#[test]
351+
fn test_true_call_condition_keeps_else_call_prefix_type() {
352+
let mut ws = VirtualWorkspace::new();
353+
let code = r#"
354+
---@return true
355+
local function always_true()
356+
return true
357+
end
358+
359+
local function b() end
360+
if always_true() then
361+
else
362+
b()
363+
end
364+
"#;
365+
366+
assert!(ws.has_no_diagnostic(DiagnosticCode::CallNonCallable, code));
367+
}
368+
369+
#[test]
370+
fn test_false_call_condition_assignment_does_not_contribute_to_merge() {
371+
let mut ws = VirtualWorkspace::new();
372+
ws.def(
373+
r#"
374+
---@return false
375+
local function always_false()
376+
return false
377+
end
378+
379+
local value = "before"
380+
if always_false() then
381+
value = 1
382+
end
383+
after = value
384+
"#,
385+
);
386+
387+
let after = ws.expr_ty("after");
388+
assert_eq!(ws.humanize_type(after), "string");
389+
}
390+
391+
#[test]
392+
fn test_false_call_condition_missing_field_assignment_does_not_contribute_to_merge() {
393+
let mut ws = VirtualWorkspace::new();
394+
ws.def(
395+
r#"
396+
---@return false
397+
local function is_windows()
398+
return false
399+
end
400+
401+
local command = "ls"
402+
local config = {}
403+
if is_windows() then
404+
command = config.windows_command
405+
end
406+
after = command
407+
"#,
408+
);
409+
410+
let after = ws.expr_ty("after");
411+
assert_eq!(ws.humanize_type(after), "string");
412+
}
413+
414+
#[test]
415+
fn test_reachable_assignment_over_never_value_contributes_to_merge() {
416+
let mut ws = VirtualWorkspace::new();
417+
ws.def(
418+
r#"
419+
---@class NeverBox
420+
---@field value never
421+
422+
---@return NeverBox
423+
local function make_box() end
424+
425+
local value = make_box().value
426+
local cond ---@type boolean
427+
if cond then
428+
value = 1
429+
end
430+
after = value
431+
"#,
432+
);
433+
434+
assert_eq!(ws.expr_ty("after"), LuaType::IntegerConst(1));
435+
}
436+
437+
#[test]
438+
fn test_false_call_condition_tag_cast_does_not_contribute_to_merge() {
439+
let mut ws = VirtualWorkspace::new();
440+
ws.def(
441+
r#"
442+
---@return false
443+
local function always_false()
444+
return false
445+
end
446+
447+
local value = "before"
448+
if always_false() then
449+
---@cast value integer
450+
end
451+
after = value
452+
"#,
453+
);
454+
455+
let after = ws.expr_ty("after");
456+
assert_eq!(ws.humanize_type(after), r#""before""#);
457+
}
458+
459+
#[test]
460+
fn test_false_call_condition_doc_assignment_does_not_contribute_to_merge() {
461+
let mut ws = VirtualWorkspace::new();
462+
ws.def(
463+
r#"
464+
---@return false
465+
local function always_false()
466+
return false
467+
end
468+
469+
local value = "before"
470+
if always_false() then
471+
---@type integer
472+
value = 1
473+
end
474+
after = value
475+
"#,
476+
);
477+
478+
assert_eq!(ws.expr_ty("after"), ws.ty("string"));
479+
}
480+
307481
#[test]
308482
fn test_branch_join_keeps_union_when_only_one_side_narrows() {
309483
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: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ pub(in crate::semantic) enum CorrelatedDiscriminantNarrow {
121121
#[derive(Debug, Clone)]
122122
pub(in crate::semantic) enum ConditionFlowAction {
123123
Continue,
124+
Unreachable,
124125
Result(LuaType),
125126
Pending(PendingConditionNarrow),
126127
NeedExprType {
@@ -712,12 +713,12 @@ pub(in crate::semantic::infer::narrow) fn resolve_expr_type_continuation(
712713
condition_flow,
713714
),
714715
ExprTypeContinuation::Truthiness { condition_flow } => Ok(match condition_flow {
715-
_ if expr_type.is_never() => ConditionFlowAction::Result(LuaType::Never),
716+
_ if expr_type.is_never() => ConditionFlowAction::Unreachable,
716717
InferConditionFlow::TrueCondition if expr_type.is_always_falsy() => {
717-
ConditionFlowAction::Result(LuaType::Never)
718+
ConditionFlowAction::Unreachable
718719
}
719720
InferConditionFlow::FalseCondition if expr_type.is_always_truthy() => {
720-
ConditionFlowAction::Result(LuaType::Never)
721+
ConditionFlowAction::Unreachable
721722
}
722723
_ => ConditionFlowAction::Continue,
723724
}),

0 commit comments

Comments
 (0)