Skip to content

Commit 66b0434

Browse files
committed
fix: separate unreachable conditions from never types
Truthiness checks for call conditions treated an impossible branch as `Result(Never)`. That made the queried symbol look like `never` while walking the branch, so unrelated callable values inside impossible `if`/`else` bodies could be narrowed to non-callable. Add an explicit unreachable condition action and handle it by query mode. Normal point queries continue past an unreachable condition edge so unrelated symbols keep their declared types. Merge-branch queries finish with `never`, so unreachable branch predecessors still drop out of the merged type. Carry branch reachability as an internal flow-query result instead of inferring it from `LuaType::Never`. Assignment and cast continuations can then distinguish an unreachable predecessor from an ordinary type result that happens to be `never`. Preserve merge-branch mode through assignment, tag-cast, and correlated condition subqueries. That prevents assignments, annotated assignments, and casts inside impossible branches from contributing their statement-local type effects to the final merge. Rename flow modes around their actual use and keep single-predecessor branch labels out of merge mode. Add coverage for plain call conditions, always-false and always-true call-condition branches, and unreachable assignment, annotated-assignment, and cast merge contributions. Assisted-by: Codex
1 parent d7d5dcb commit 66b0434

4 files changed

Lines changed: 332 additions & 52 deletions

File tree

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

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,134 @@ 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_tag_cast_does_not_contribute_to_merge() {
393+
let mut ws = VirtualWorkspace::new();
394+
ws.def(
395+
r#"
396+
---@return false
397+
local function always_false()
398+
return false
399+
end
400+
401+
local value = "before"
402+
if always_false() then
403+
---@cast value integer
404+
end
405+
after = value
406+
"#,
407+
);
408+
409+
let after = ws.expr_ty("after");
410+
assert_eq!(ws.humanize_type(after), r#""before""#);
411+
}
412+
413+
#[test]
414+
fn test_false_call_condition_doc_assignment_does_not_contribute_to_merge() {
415+
let mut ws = VirtualWorkspace::new();
416+
ws.def(
417+
r#"
418+
---@return false
419+
local function always_false()
420+
return false
421+
end
422+
423+
local value = "before"
424+
if always_false() then
425+
---@type integer
426+
value = 1
427+
end
428+
after = value
429+
"#,
430+
);
431+
432+
assert_eq!(ws.expr_ty("after"), ws.ty("string"));
433+
}
434+
307435
#[test]
308436
fn test_branch_join_keeps_union_when_only_one_side_narrows() {
309437
let mut ws = VirtualWorkspace::new();

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,32 @@ 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+
Unreachable,
41+
}
42+
43+
impl FlowQueryResult {
44+
pub(in crate::semantic) fn into_type(self) -> LuaType {
45+
match self {
46+
Self::Type(typ) => typ,
47+
Self::Unreachable => LuaType::Never,
48+
}
3649
}
3750
}
3851

3952
#[derive(Debug, Default)]
4053
pub(in crate::semantic) struct FlowVarCache {
41-
pub type_cache: HashMap<(FlowId, FlowMode), CacheEntry<LuaType>>,
54+
pub type_cache: HashMap<(FlowId, FlowMode), CacheEntry<FlowQueryResult>>,
4255
pub condition_cache: HashMap<(FlowId, InferConditionFlow), CacheEntry<ConditionFlowAction>>,
4356
}
4457

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)