EmmyLua Version
Latest (built from source, main branch)
Description
When a table<K,V> field has a generic value type (e.g. Box<unknown>?) and the key type K is not string or integer (e.g. thread, boolean), indexing the table with a correctly-typed key falsely reports undefined-field.
If the value type is non-generic (e.g. string?) or the key type is string/integer, no warning is reported.
Minimal Reproduction
---@class Box<T>
---@field value T
-- ✅ Case 1: Non-generic value type + thread key → OK
---@class Container1
---@field items table<thread, string?>
local C1 = {}
---@param co thread
function C1:get(co)
local item = self.items[co] -- no warning ✅
return item
end
-- ❌ Case 2: Generic value type + thread key → false undefined-field
---@class Container2
---@field items table<thread, Box<unknown>?>
local C2 = {}
---@param co thread
function C2:get(co)
local item = self.items[co] -- ⚠️ Undefined field `[co]` ← BUG
return item
end
-- ✅ Case 3: Generic value type + string key → OK
---@class Container3
---@field items table<string, Box<unknown>?>
local C3 = {}
---@param key string
function C3:get(key)
local item = self.items[key] -- no warning ✅
return item
end
-- ✅ Case 4: Generic value type + integer key → OK
---@class Container4
---@field items table<integer, Box<unknown>?>
local C4 = {}
---@param key integer
function C4:get(key)
local item = self.items[key] -- no warning ✅
return item
end
-- ❌ Case 5: Generic value type + boolean key → false undefined-field
---@class Container5
---@field items table<boolean, Box<unknown>?>
local C5 = {}
---@param key boolean
function C5:get(key)
local item = self.items[key] -- ⚠️ Undefined field `[key]` ← BUG
return item
end
Expected Behavior
All five cases should report no undefined-field warning. The key type matches the table's declared key type in every case.
Actual Behavior
- Case 1 (non-generic value, thread key): ✅ No warning
- Case 2 (generic value, thread key): ❌
Undefined field [co]
- Case 3 (generic value, string key): ✅ No warning
- Case 4 (generic value, integer key): ✅ No warning
- Case 5 (generic value, boolean key): ❌
Undefined field [key]
Analysis
The bug has two layers in check_field.rs:
Layer 1: get_key_types() drops non-string/integer types
The get_key_types() function (around line 355) collects valid key types from the index expression's type. It only recognizes String, Integer, Union, StrTplRef, Ref, DocStringConst, DocIntegerConst, and Call. Other built-in types like Thread, Boolean, Number, Userdata fall into the _ => {} catch-all and are silently dropped.
When the key type is thread, get_key_types() returns an empty set → key_types.is_empty() is true → the function returns early without reaching the member matching logic at all.
Layer 2: ExprType matching only handles string and integer
Even if Layer 1 is fixed, the ExprType member key matching (around line 266) only handles string and integer:
LuaMemberKey::ExprType(typ) => {
if typ.is_string() { ... } // string → pass
else if typ.is_integer() { ... } // integer → pass
// thread, boolean, etc. → NOT HANDLED → falls through
}
Why it only triggers when the value type is generic
When the value type is non-generic (e.g. string?), the earlier check via get_semantic_info / get_index_decl_type successfully resolves the member before reaching the buggy code. But when the value type is generic (e.g. Box<unknown>?), semantic_decl is None and the code falls through to the member matching — where both bugs are.
Additional note: in_conditional_statement in check_field.rs masks the bug
There is a separate check at line 170 that unconditionally passes []-style access when the expression is inside a conditional statement (e.g. if not self.items[co] then). This masks the bug for usages inside if/while/for conditions, making it appear inconsistent.
EmmyLua Version
Latest (built from source, main branch)
Description
When a
table<K,V>field has a generic value type (e.g.Box<unknown>?) and the key typeKis notstringorinteger(e.g.thread,boolean), indexing the table with a correctly-typed key falsely reportsundefined-field.If the value type is non-generic (e.g.
string?) or the key type isstring/integer, no warning is reported.Minimal Reproduction
Expected Behavior
All five cases should report no
undefined-fieldwarning. The key type matches the table's declared key type in every case.Actual Behavior
Undefined field [co]Undefined field [key]Analysis
The bug has two layers in
check_field.rs:Layer 1:
get_key_types()drops non-string/integer typesThe
get_key_types()function (around line 355) collects valid key types from the index expression's type. It only recognizesString,Integer,Union,StrTplRef,Ref,DocStringConst,DocIntegerConst, andCall. Other built-in types likeThread,Boolean,Number,Userdatafall into the_ => {}catch-all and are silently dropped.When the key type is
thread,get_key_types()returns an empty set →key_types.is_empty()is true → the function returns early without reaching the member matching logic at all.Layer 2:
ExprTypematching only handlesstringandintegerEven if Layer 1 is fixed, the
ExprTypemember key matching (around line 266) only handlesstringandinteger:Why it only triggers when the value type is generic
When the value type is non-generic (e.g.
string?), the earlier check viaget_semantic_info/get_index_decl_typesuccessfully resolves the member before reaching the buggy code. But when the value type is generic (e.g.Box<unknown>?),semantic_declisNoneand the code falls through to the member matching — where both bugs are.Additional note:
in_conditional_statementincheck_field.rsmasks the bugThere is a separate check at line 170 that unconditionally passes
[]-style access when the expression is inside a conditional statement (e.g.if not self.items[co] then). This masks the bug for usages insideif/while/forconditions, making it appear inconsistent.