fix: add Union branch to check_generic_type_compact to prevent param-type-mismatch false positives#1107
Conversation
When a method has both @field declaration and function implementation, resolve_member_type merges them into a Union type. check_generic_type_compact lacked a LuaType::Union branch, causing param-type-mismatch / assign-type-mismatch false positives even when all union members satisfy the generic constraint. Add a Union arm that iterates all members and checks each against the source generic type, following standard covariant semantics.
There was a problem hiding this comment.
Code Review: generic_type.rs Changes
Issue Found: Potential Infinite Recursion
Problem: The new Union case calls check_generic_type_compact recursively for each member of the union. If a union member is itself a Union type, this could lead to infinite recursion or stack overflow.
Example scenario:
// If union_type contains nested unions
Union([TypeA, Union([TypeB, TypeC])])
// This would recursively call check_generic_type_compact on the inner union
// which would then iterate over its members againRecommendation
Add a guard to prevent processing the same union type multiple times, or flatten nested unions before processing:
LuaType::Union(union_type) => {
// Flatten nested unions to prevent infinite recursion
let flat_members = flatten_union(union_type);
for member_type in flat_members {
check_generic_type_compact(
context,
source_generic,
&member_type,
check_guard.next_level()?,
)?;
}
Ok(())
}Where flatten_union would recursively extract all non-union types from nested unions.
Additional Consideration
- Performance: For large unions, this could be expensive. Consider adding a depth limit or early exit if a type mismatch is found (current implementation does exit early via
?operator, which is good). - Error Messages: When a union member fails, the error won't indicate which member failed. Consider wrapping errors with context about which union member caused the failure.
There was a problem hiding this comment.
Code Review
This pull request adds support for checking union types against generic types in check_generic_type_compact. However, the current implementation recursively calls check_generic_type_compact directly on union members, which bypasses critical type-checking logic like alias resolution and wildcard checks. It is recommended to wrap the generic type and delegate to check_general_type_compact instead, and to add a corresponding test case to verify this behavior.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| LuaType::Union(union_type) => { | ||
| // When compact_type is a Union, every member of the union must be | ||
| // assignable to the source generic type for the assignment to be safe. | ||
| for member_type in union_type.into_vec() { | ||
| check_generic_type_compact( | ||
| context, | ||
| source_generic, | ||
| &member_type, | ||
| check_guard.next_level()?, | ||
| )?; | ||
| } | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Calling check_generic_type_compact directly on union members bypasses critical type-checking logic defined in check_general_type_compact, such as is_like_any (which handles any and unknown), escape_type (which resolves aliases), and fast_eq_check.
If a union contains unknown or an alias type, calling check_generic_type_compact directly will result in a false positive TypeNotMatch error because those types are not explicitly handled in the match statement of check_generic_type_compact.
To fix this, we should construct a LuaType::Generic from source_generic and call check_general_type_compact for each member of the union.
Additionally, it is highly recommended to add a test case to crates/emmylua_code_analysis/src/semantic/type_check/test.rs to verify this behavior:
#[test]
fn test_issue_1106() {
let mut ws = VirtualWorkspace::new();
ws.def(
r#"
---@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>
"#,
);
assert!(ws.has_no_diagnostic(
DiagnosticCode::ParamTypeMismatch,
r#"
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
"#
));
}| LuaType::Union(union_type) => { | |
| // When compact_type is a Union, every member of the union must be | |
| // assignable to the source generic type for the assignment to be safe. | |
| for member_type in union_type.into_vec() { | |
| check_generic_type_compact( | |
| context, | |
| source_generic, | |
| &member_type, | |
| check_guard.next_level()?, | |
| )?; | |
| } | |
| Ok(()) | |
| } | |
| LuaType::Union(union_type) => { | |
| // When compact_type is a Union, every member of the union must be | |
| // assignable to the source generic type for the assignment to be safe. | |
| let source_type = LuaType::Generic(source_generic.clone().into()); | |
| for member_type in union_type.into_vec() { | |
| check_general_type_compact( | |
| context, | |
| &source_type, | |
| &member_type, | |
| check_guard.next_level()?, | |
| )?; | |
| } | |
| Ok(()) | |
| } |
Summary
Fixes #1106
When a method returning a generic type has both
@fielddeclaration andfunctionimplementation, the return value is internally wrapped in a Union type.check_generic_type_compactlacks aLuaType::Unionbranch, causing falseparam-type-mismatch/assign-type-mismatchwarnings even when types are identical.Root Cause
In
check_generic_type_compact(crates/emmylua_code_analysis/src/semantic/type_check/generic_type.rs, line 99), thematchstatement handlesGenericType,TableConst,Ref/Def, but has noLuaType::Unionbranch. Whencompact_typeis a Union (produced by merging@fieldandfunctionsources inresolve_member_type), it falls through to_ => Err(TypeCheckFailReason::TypeNotMatch).Changes
crates/emmylua_code_analysis/src/semantic/type_check/generic_type.rsLuaType::Unionbranch incheck_generic_type_compactthat iterates all union members and checks each against the source generic type (+13 lines)Code Change
Insert before the
_ => Err(TypeCheckFailReason::TypeNotMatch)arm incheck_generic_type_compact:Rationale
A Union type
A | Bassigned to a generic parameterTrequires all members to satisfyT's constraints for the assignment to be safe. This is standard covariant semantics — the Union is a "wider" type, so each member must individually be valid.Example
Verification
Before fix (commit
d7d5dcbf):After fix: