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
-
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
-
Run the check:
emmylua_check bug1_repro.lua
-
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
Summary
When a method that returns a generic type (e.g.,
Box<Item>) has both an@field fun():Generic<X>declaration and afunctionimplementation, the return value is internally wrapped in a Union type. The generic constraint checkercheck_generic_type_compactlacks aLuaType::Unionbranch, causing it to reject the Union-wrapped type as incompatible — even though the underlying types are identical. This producesparam-type-mismatchorassign-type-mismatchfalse positives.Minimal Reproduction
Key observations:
fetch) has both@fielddeclaration andfunctionimplementation. The consuming method (execute) is irrelevant.param-type-mismatch) and assignment (assign-type-mismatch).fetchhas only@field(no implementation) or onlyfunction+@return(no@field), the bug does not occur.string).Expected Behavior
self:execute(b)should produce no warnings becauseb's typeBox<Item>matches the parameter declaration exactly.Actual Behavior
The error message shows identical types, but internally the actual type is
Box<Item> | Box<Item>(a Union wrappingBox<Item>), which the generic checker cannot handle.Reproduction Steps
Create a minimal reproduction file
bug1_repro.lua:Run the check:
Observe the false positive:
Root Cause
The issue has two layers:
Layer 1: Union creation. When
fetchhas both@field fun():Box<Item>(marked asFileFieldDecl) andfunction Service:fetch()(marked asFileMethodDecl),resolve_member_typeinlua_member_item.rstreats both asis_file_decl() == trueand merges them viaunion_all, producingfun():Box<Item> | fun():Box<Item>. Callingself:fetch()then returns a Union-wrappedBox<Item>.Layer 2: Missing Union branch. In
check_generic_type_compact(generic_type.rs), thematchstatement handlesGenericType,TableConst,Ref/Def, but has noLuaType::Unionbranch. The Union-wrapped type falls through to_ => Err(TypeCheckFailReason::TypeNotMatch), producing the false positive.Environment
d7d5dcbf