Skip to content

Bug: table<K,V> with generic value type reports false undefined-field when key is not string/integer #1108

@ShenzhenGopher

Description

@ShenzhenGopher

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 setkey_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions