Skip to content

param-type-mismatch / assign-type-mismatch false positive when a method returning a generic type has both @field declaration and function implementation #1106

@ShenzhenGopher

Description

@ShenzhenGopher

Summary

When a method that returns a generic type (e.g., Box<Item>) has both an @field fun():Generic<X> declaration and a function implementation, the return value is internally wrapped in a Union type. The generic constraint checker check_generic_type_compact lacks a LuaType::Union branch, causing it to reject the Union-wrapped type as incompatible — even though the underlying types are identical. This produces param-type-mismatch or assign-type-mismatch false positives.

Minimal Reproduction

---@class Item
---@field id integer

---@class Box<T>
---@field value T

---@class Service
---@field execute fun(self:Service, box:Box<Item>)
---@field fetch fun(self:Service):Box<Item>
local Service = {}

function Service:execute(_box)
end

function Service:fetch()
    return {} --[[@as Box<Item>]]
end

function Service:run()
    local b = self:fetch()
    self:execute(b)  -- ❌ False positive: expected `Box<Item>` but found `Box<Item>` [param-type-mismatch]
end

Key observations:

  • The bug only triggers when the value-producing method (fetch) has both @field declaration and function implementation. The consuming method (execute) is irrelevant.
  • The bug occurs with both parameter passing (param-type-mismatch) and assignment (assign-type-mismatch).
  • If fetch has only @field (no implementation) or only function + @return (no @field), the bug does not occur.
  • The bug does not occur when the return type is non-generic (e.g., string).

Expected Behavior

self:execute(b) should produce no warnings because b's type Box<Item> matches the parameter declaration exactly.

Actual Behavior

warning: expected `Box<Item>` but found `Box<Item>`. [param-type-mismatch]

The error message shows identical types, but internally the actual type is Box<Item> | Box<Item> (a Union wrapping Box<Item>), which the generic checker cannot handle.

Reproduction Steps

  1. Create a minimal reproduction file bug1_repro.lua:

    ---@class Item
    ---@field id integer
    ---@class Box<T>
    ---@field value T
    ---@class Service
    ---@field execute fun(self:Service, box:Box<Item>)
    ---@field fetch fun(self:Service):Box<Item>
    local Service = {}
    function Service:execute(_box) end
    function Service:fetch() return {} --[[@as Box<Item>]] end
    function Service:run()
        local b = self:fetch()
        self:execute(b)
    end
  2. Run the check:

    emmylua_check bug1_repro.lua
  3. Observe the false positive:

    ---  [1 warning]
    warning: expected `Box<Item>` but found `Box<Item>`. [param-type-mismatch]
      --> :17:18
    
     16 |     local b = self:fetch()
     17 |     self:execute(b)
    
    Summary
      1 warning
    
    Check completed with warnings
    Check finished
    

Root Cause

The issue has two layers:

Layer 1: Union creation. When fetch has both @field fun():Box<Item> (marked as FileFieldDecl) and function Service:fetch() (marked as FileMethodDecl), resolve_member_type in lua_member_item.rs treats both as is_file_decl() == true and merges them via union_all, producing fun():Box<Item> | fun():Box<Item>. Calling self:fetch() then returns a Union-wrapped Box<Item>.

Layer 2: Missing Union branch. In check_generic_type_compact (generic_type.rs), the match statement handles GenericType, TableConst, Ref/Def, but has no LuaType::Union branch. The Union-wrapped type falls through to _ => Err(TypeCheckFailReason::TypeNotMatch), producing the false positive.

Environment

  • emmylua_check version: based on commit d7d5dcbf
  • OS: Linux

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