Skip to content

fix(flow): the false branch of a member test keeps owners whose member can be falsy - #1361

Draft
mareklangiewicz wants to merge 1 commit into
EmmyLuaLs:mainfrom
mareklangiewicz:up-member-falsy-branch
Draft

mareklangiewicz wants to merge 1 commit into
EmmyLuaLs:mainfrom
mareklangiewicz:up-member-falsy-branch

Conversation

@mareklangiewicz

Copy link
Copy Markdown
Contributor

Summary

The false branch of a member test drops an owner whose member can be falsy:

---@class Res
---@field ok boolean

---@param res Res?
local function f(res)
  if res and res.ok then return end
  -- `res` is nil here, but it can also be a `Res` whose `ok` is false
end

filter_type_by_member_truthiness asks "can the member be truthy?" for both branches, so a boolean member counts as truthy and the false branch removes its owner:

-let truthy = type_is_truthy(&member_ty);
-if truthy == keep_matching {
+let possible = if keep_matching {
+    type_is_truthy(&member_ty)
+} else {
+    !member_ty.is_always_truthy()
+};
+if possible {
     kept.push(component);
 }

Evidence

test_member_false_branch_keeps_owner_whose_member_can_be_falsy, on res after the guard:

Guard main This PR
if res and res.ok then return end nil Res?
nested if res then if res.ok ... nil Res?
if not (res and res.ok) then nil Res?
boolean? member nil Res?
string member, always truthy nil nil
  • Before (plain main de89376): the test fails, Nil ≠ Res?.
  • After: it passes. Replacing either side of the new if with true fails a test. Full emmylua_code_analysis suite passes. clippy reports nothing new.

Merge Danger

Door: two-way

Blast Radius: narrowing

emmylua_check from main and from this branch on three real Lua codebases:

Diagnostics on main Removed Added
~4.9k 1 1
~1.2k 0 2
~9.1k 0 0

The 2 added in the second codebase look false: a module field read after if M.flag then return end reports may be nil and undefined-field. I haven't found why, and they stay with the other PRs applied. The other added one, an unnecessary-if, is gone once #1360 is applied too.

This is one of two fixes #1359 needs: see that PR.


Written with help from Claude Code.

🤖 Generated with Claude Code

https://claude.ai/code/session_018NxUUQmc3YFNEXrUcrVPxV

…r can be falsy

`filter_type_by_member_truthiness` asked one question for both branches: can
the member be truthy? The false branch then dropped every owner whose member
can be truthy, even a `boolean` that can also be false. After
`if res and res.ok then return end` with `res: Res?`, `res` was `nil`.

The false branch now keeps an owner unless its member is always truthy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NxUUQmc3YFNEXrUcrVPxV

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Summary

The change fixes a real semantic bug: the false branch of a member truthiness test previously used type_is_truthy (which asks "can this be truthy?") and compared it against keep_matching, so a boolean member was incorrectly dropped from the false branch. The new logic correctly distinguishes "can be truthy" vs. "can be falsy". The fix is sound and well-tested.

Issues & Suggestions

1. Asymmetry between the two branches (minor, readability)

let possible = if keep_matching {
    type_is_truthy(&member_ty)          // "can be truthy"
} else {
    !member_ty.is_always_truthy()       // "can be falsy"
};

The two branches use different helper functions (type_is_truthy vs. !is_always_truthy). This is correct, but the asymmetry makes it harder to verify at a glance that the two are exact logical duals. Consider making the intent explicit, e.g.:

let possible = if keep_matching {
    type_is_truthy(&member_ty)
} else {
    type_is_falsy(&member_ty)   // if such a helper exists / can be added
};

or add a short comment noting that !is_always_truthy() is the "can be falsy" predicate. This guards against future divergence if one helper's semantics change.

2. Verify is_always_truthy semantics for nil/missing members
The unwrap_or(LuaType::Nil) path relies on Nil.is_always_truthy() == false, so missing members are kept in the false branch. That is the desired behavior, but it's worth confirming is_always_truthy is conservative (returns false for unions containing nil, boolean, false, etc.) rather than optimistic. If it ever returns true for a type that can actually be falsy, the false branch would silently drop valid components. A unit test covering a union member type (e.g. string | nil) in the false branch would harden this.

3. Test coverage gaps
The added test is good but could be extended:

  • No case exercises the true branch (keep_matching == true) with a boolean member to confirm it is still kept (regression guard for the other side of the change).
  • No case with a union member type (e.g. ---@field flag string|nil) to validate the is_always_truthy boundary.
  • ALWAYS_TRUTHY_MEMBER asserting nil is a nice negative case; consider also asserting the positive counterpart (if res and res.name then ... end keeps Res).

4. Comment accuracy
The comment says "the false branch one whose member can be falsy (a boolean member can be either)." This is accurate, but the phrase "can be either" is slightly ambiguous — it means the member may be truthy or falsy, hence kept in both branches. Minor wording suggestion:

"...the false branch keeps a component whose member can be falsy. A boolean member can be either, so it is kept in both branches."

Verdict

The logic fix is correct and the test demonstrates the intended behavior. The main actionable items are (a) making the two-branch predicate symmetry explicit and (b) adding a true-branch / union-member regression test to lock in the behavior. No blocking issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant