Skip to content

[Bug]: A truthiness guard if x.f then x.f:m() end does not strip nil from an unknown-typed field #81

Description

@AmyJeanes

Area: glua_check (expected to affect the language server too; the repro and diagnostics below are from glua_check)


Summary

A bare truthiness guard - if x.f then x.f:m() end - fails to narrow x.f to non-nil inside the guarded block when f has no resolvable type (an undeclared field, so unknown?). The method call is flagged x.f may be nil. This is a warning, so it fails glua_check --warnings-as-errors with exit 1.

Ordinary nil-narrowing is fine; it is specifically the unknown-typed field that the truthiness guard does not narrow. The three controls below each stay clean.

Minimal repro

Repro: bug2-truthiness-unknown-field.zip

Standalone - no workspace.library and no GLua annotations. .luarc.json is only {"runtime":{"version":"LuaJIT"}}.

---@class hFire
---@param h hFire
local function fires(h)
    if h.snd then h.snd:Stop() end            -- warning: h.snd may be nil
end

glua_check --warnings-as-errors . on the repro reports exactly one diagnostic (line 23 is the if line in repro.lua, below the header comment):

warning: h.snd may be nil [unchecked-nil-access]
  --> repro.lua:23:19

snd is never declared on hFire, so it resolves to unknown?. The if h.snd then guard should make h.snd non-nil inside the block, but the :Stop() call is still flagged.

Three controls that isolate it

All three are in the repro file, and all three are clean - each shows a form that narrows correctly:

  • if h.snd ~= nil then ... instead of the bare truthiness test - clean.
  • Cache to a local first (local s = h.snd; if s then s:Stop() end) - clean.
  • Same bare if h.snd guard, but the field has a declared nilable type (---@field snd snd_obj?) - clean.

The third is the key one: with a declared nilable field, the identical guard narrows fine. So this is not a general nil-narrowing failure; it is specific to the field being unknown-typed.

One further point that rules out an intentional policy: a truthiness test excludes both nil and false - a superset of what ~= nil excludes. So truthiness should never narrow less than ~= nil. Here it narrows less (it fails where ~= nil, control 1, succeeds), which is backwards regardless of what the intended narrowing rule is.

Real-world impact

TARDIS templates_default_halloween.lua stores a sound handle on an undeclared field of the interior entity (int.halloween_corridor_sound, so unknown?). The natural guarded stop -

if int.halloween_corridor_sound then int.halloween_corridor_sound:Stop() end
  • fires this warning, which fails our --warnings-as-errors build. We cache the field to a local first (control 2) purely to silence it.

Expected

The if h.snd then guard narrows h.snd to non-nil inside the block, and the :Stop() call is not reported - matching the ~= nil, local-alias, and declared-field forms, which already are clean.

Environment

  • glua_check 1.2.0
  • annotations gluals-annotations-prerelease @ aae7341 (not needed for the repro)
  • Windows 11

Disclosure: this report was researched and written by Claude Code working in my repositories. Every claim in it was reproduced against a real workspace on 1.2.0 rather than asserted, and the three controls were run before filing. The hFire / snd names are placeholders standing in for the real interior entity and its sound-handle field; the reduction keeps the exact structure of the real site.

Activity

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions