Area: glua_check (expected to affect the language server too; the repro and diagnostics below are from glua_check)
Summary
Assigning an undeclared field to a value typed as a container's element (table<_, T> or T[]), inside a loop or conditional block, promotes that field to a required member of the class T. Every table literal that constructs a T then fails with Missing required fields in type T: X - even though T never declared X, and it was only attached at runtime in an unrelated loop.
These are warnings, so this fails glua_check --warnings-as-errors with exit 1.
Minimal repro
Repro: bug7-undeclared-field-promoted-required.zip
Standalone - no workspace.library and no GLua annotations. .luarc.json is only {"runtime":{"version":"LuaJIT"}}.
---@class Fires
---@field ID string
---@type table<string, Fires>
local storeF = {}
---@param t Fires
local function makeF(t) return t.ID end
makeF({ ID = "a" }) -- Missing required fields in type `Fires`: `X`
for _, v in pairs(storeF) do v.X = 5 end -- attaching undeclared `X` here is what promotes it
glua_check --warnings-as-errors . reports two warnings, both on the constructor at line 28 of repro.lua (Fires never declares X):
warning: Missing required fields in type `Fires`: `X` [missing-fields]
--> repro.lua:28:7
warning: expected `Fires` but found `{ ID = "a" }`. missing member X, in table [param-type-mismatch]
--> repro.lua:28:7
Removing the v.X = 5 line clears both. The constructor and the loop are otherwise unrelated - the loop merely stashes a runtime value on each element.
What the trigger needs (controls, each its own class so they don't cross-contaminate)
All four scenarios are in the repro file; only the first fires. Each control drops one ingredient and goes clean:
- A declared field -
for _, v in pairs(store) do v.ID = "z" end (assigning the declared ID) promotes nothing. Only an undeclared field is promoted.
- A plain param, not a container element -
---@param a T ... a.X = 5 does not promote. The value has to come from indexing a table<_, T> / T[].
- Top level, not a nested block - the identical
store["k"].X = 5 at file scope is clean; wrapping it in if ... then ... end (or a for) is what triggers it.
That last one is the clearest sign it is an implementation artefact: the same container-element assignment promotes the field to required only when it sits inside a block.
Real-world impact
Sonic-Screwdriver keeps its sonic definitions in a table<string, sonicsd_sonic> and, when building the options menu, iterates them to stash a combo-box handle on each:
for _, v in pairs(SonicSD.sonics) do
v.OptionID = comboBox:AddChoice(v.Name, v.ID)
end
OptionID is not declared on sonicsd_sonic, so on 1.2.0 it was promoted to a required field and every sonic literal reported Missing required fields: OptionID. We worked around it by declaring a sonicsd_sonic_complete : sonicsd_sonic subclass with ---@field OptionID number? and retyping the store to it - fine here because the field genuinely exists at runtime, but the promotion is still wrong: a field attached in a loop should not become mandatory for constructors.
Expected
X (attached only by a runtime assignment) is at most an optional member of Fires, never a required one, so makeF({ ID = "a" }) type-checks - matching the declared-field, param, and top-level 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 all three controls were run before filing. The Fires / X reduction keeps the real Sonic-Screwdriver structure - a table<string, T> store, a loop attaching an undeclared field to each element, and a T literal elsewhere.
Area:
glua_check(expected to affect the language server too; the repro and diagnostics below are fromglua_check)Summary
Assigning an undeclared field to a value typed as a container's element (
table<_, T>orT[]), inside a loop or conditional block, promotes that field to a required member of the classT. Every table literal that constructs aTthen fails withMissing required fields in type T: X- even thoughTnever declaredX, and it was only attached at runtime in an unrelated loop.These are warnings, so this fails
glua_check --warnings-as-errorswith exit 1.Minimal repro
Repro: bug7-undeclared-field-promoted-required.zip
Standalone - no
workspace.libraryand no GLua annotations..luarc.jsonis only{"runtime":{"version":"LuaJIT"}}.glua_check --warnings-as-errors .reports two warnings, both on the constructor at line 28 ofrepro.lua(Firesnever declaresX):Removing the
v.X = 5line clears both. The constructor and the loop are otherwise unrelated - the loop merely stashes a runtime value on each element.What the trigger needs (controls, each its own class so they don't cross-contaminate)
All four scenarios are in the repro file; only the first fires. Each control drops one ingredient and goes clean:
for _, v in pairs(store) do v.ID = "z" end(assigning the declaredID) promotes nothing. Only an undeclared field is promoted.---@param a T ... a.X = 5does not promote. The value has to come from indexing atable<_, T>/T[].store["k"].X = 5at file scope is clean; wrapping it inif ... then ... end(or afor) is what triggers it.That last one is the clearest sign it is an implementation artefact: the same container-element assignment promotes the field to required only when it sits inside a block.
Real-world impact
Sonic-Screwdriver keeps its sonic definitions in a
table<string, sonicsd_sonic>and, when building the options menu, iterates them to stash a combo-box handle on each:OptionIDis not declared onsonicsd_sonic, so on 1.2.0 it was promoted to a required field and every sonic literal reportedMissing required fields: OptionID. We worked around it by declaring asonicsd_sonic_complete : sonicsd_sonicsubclass with---@field OptionID number?and retyping the store to it - fine here because the field genuinely exists at runtime, but the promotion is still wrong: a field attached in a loop should not become mandatory for constructors.Expected
X(attached only by a runtime assignment) is at most an optional member ofFires, never a required one, somakeF({ ID = "a" })type-checks - matching the declared-field, param, and top-level forms, which already are clean.Environment
glua_check1.2.0gluals-annotations-prerelease@aae7341(not needed for the repro)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 all three controls were run before filing. The
Fires/Xreduction keeps the real Sonic-Screwdriver structure - atable<string, T>store, a loop attaching an undeclared field to each element, and aTliteral elsewhere.