Skip to content

[Bug]: Regression - a cross-file field on an aliased X.sub = X.sub or {} namespace reports Undefined field on 1.2.0 (clean on 1.1.2) #83

Description

@AmyJeanes

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


Summary

A function attached to a global sub-table in one file is reported Undefined field when read from another file, on 1.2.0. The identical workspace is clean on 1.1.2 - so this is a regression, and given the timing it looks like a side effect of the cross-file analysis/indexing rework in #75.

The pattern is the ordinary multi-file namespace idiom: every file opens with MCP.wp = MCP.wp or {}, one file attaches functions to it, others call them.

Underneath (see the type dump below), the cause is that 1.2.0 no longer unifies the type across the repeated MCP.wp = MCP.wp or {} assignments in different files. Each assignment produces its own wp member; on 1.1.2 their types merged, so fields attached in any file were visible from all of them; on 1.2.0 they do not merge, so a file that re-guards the namespace sees only its own fresh empty {} and the field attached elsewhere is dropped.

Regression - same workspace, two versions

$ glua_check --version
glua_check 1.1.2
$ glua_check --warnings-as-errors .
No issues found                       <- clean

$ glua_check --version
glua_check 1.2.0
$ glua_check --warnings-as-errors .
warning: Undefined field `Foo`.  [undefined-field]
  --> lua/02_read.lua:23:15          <- fires

It is a warning, so on 1.2.0 it fails --warnings-as-errors with exit 1.

What the type resolves to (root cause)

Adding ---@type integer local _ = MCP to the reader dumps MCP's resolved type in the mismatch message (the two files' MCP.wp = MCP.wp or {} each show up as a wp member, so MCP displays two of them on both versions - that duplication is not the bug):

1.1.2:  Cannot assign `{ wp = { Foo = fun() -> 1 }, wp = { Foo = fun() -> 1 } }` to `integer`.
1.2.0:  Cannot assign `{ wp = table, wp = table }` to `integer`.

On 1.1.2 both wp members carry Foo; on 1.2.0 both are bare table and Foo is gone. So the regression is the loss of Foo from the type, not the duplicate member.

And it is specifically the reader re-guarding the namespace that drops it. With the reader reading MCP.wp without its own MCP.wp = MCP.wp or {}, both versions resolve a single { wp = { Foo = fun() -> 1 } } and the read is clean. The moment the reader repeats the = MCP.wp or {} guard, 1.2.0 resolves its reads against a fresh, field-less wp.

Minimal repro

Repro: bug4-crossfile-undefined-field.zip

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

lua/01_define.lua:

MCP = MCP or {}
MCP.wp = MCP.wp or {}
local wp_ = MCP.wp
function wp_.Foo() return 1 end

lua/02_read.lua:

MCP.wp = MCP.wp or {}
local wp_ = MCP.wp
local a = wp_.Foo()   -- 1.2.0: Undefined field `Foo`
return a

What the trigger needs (controls I reduced against)

Each of these, applied to the repro, makes 1.2.0 go clean - so all are load-bearing:

  • 01_define.lua must attach through the local alias. Attaching directly - function MCP.wp.Foo() ... end, no local wp_ - is clean.
  • 01_define.lua must re-assign the namespace itself (MCP.wp = MCP.wp or {} in that file). If the guard lives in a third file and 01_define.lua only aliases a pre-existing MCP.wp, it is clean.
  • 02_read.lua must repeat MCP.wp = MCP.wp or {}. Drop that one line - read MCP.wp without re-guarding it - and it is clean.
  • The two must be in separate files. The identical sequence in a single file is clean.

It is not order-dependent: renaming the files so the reader sorts first fires just the same (unlike #72, where sort order was the whole difference).

Real-world impact

world-portals is built this way: ~18 files open with the MCP.wp = MCP.wp or {} guard and take local wp_ = MCP.wp, and one file attaches the API onto that alias. On 1.2.0 every cross-file wp_.X() call became Undefined field and failed the build; on 1.1.2 the same tree was clean. We worked around it by declaring a ---@class for the namespace so the fields resolve regardless, but the underlying cross-file resolution regressed.

This is a distinct symptom from the cross-file issues #75 closed (#46 / #72 / #74 were infer-unknown and nil-resolution); I am filing it separately rather than reopening any of them, but flagging the shared area in case the cause is common.

Expected

Foo, attached to MCP.wp in 01_define.lua, resolves when read through MCP.wp (or an alias of it) in 02_read.lua - as it did on 1.1.2.

Environment

  • glua_check 1.2.0 (regressed) vs 1.1.2 (clean) - both run against the same workspace
  • 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 rather than asserted - including the 1.1.2-vs-1.2.0 comparison above, run with both cached binaries against the same folder, each of the four controls, and the MCP type dumps (via a throwaway ---@type integer mismatch, left commented in the repro). The MCP.wp / Foo names are a reduction of the real world-portals namespace and its API; the structure (per-file namespace guard, alias, cross-file call) is the same.

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