Skip to content

Commit 2357628

Browse files
committed
fix(generic): treat keyof instantiation as union
1 parent 10960e4 commit 2357628

4 files changed

Lines changed: 212 additions & 5 deletions

File tree

crates/emmylua_code_analysis/src/diagnostic/checker/generic/call_constraint.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use emmylua_parser::{LuaAstToken, LuaCallExpr};
44
use rowan::TextRange;
55

66
use crate::{
7-
DbIndex, LuaFunctionType, LuaType, LuaTypeNode, SemanticModel, TypeSubstitutor,
8-
build_call_generic_substitutor, collect_callable_overload_groups, instantiate_type_generic,
7+
DbIndex, LuaAliasCallKind, LuaFunctionType, LuaType, LuaTypeNode, SemanticModel,
8+
TypeSubstitutor, build_call_generic_substitutor, collect_callable_overload_groups,
9+
instantiate_type_generic,
910
};
1011

1112
// 泛型约束上下文
@@ -188,6 +189,16 @@ fn generic_arg_count(
188189
pub fn normalize_constraint_type(db: &DbIndex, ty: LuaType) -> LuaType {
189190
match ty {
190191
LuaType::Tuple(tuple) if tuple.is_infer_resolve() => tuple.collapse_to_union(db),
192+
LuaType::Call(alias_call)
193+
if alias_call.get_call_kind() == LuaAliasCallKind::KeyOf
194+
&& !LuaType::Call(alias_call.clone()).contains_tpl_node() =>
195+
{
196+
let call_type = LuaType::Call(alias_call);
197+
normalize_constraint_type(
198+
db,
199+
instantiate_type_generic(db, &call_type, &TypeSubstitutor::new()),
200+
)
201+
}
191202
_ => ty,
192203
}
193204
}

crates/emmylua_code_analysis/src/diagnostic/test/generic_constraint_mismatch_test.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,118 @@ mod test {
247247
));
248248
}
249249

250+
#[test]
251+
fn test_alias_keyof_constraint_accepts_keyof_type() {
252+
let mut ws = VirtualWorkspace::new();
253+
assert!(ws.has_no_diagnostic(
254+
DiagnosticCode::GenericConstraintMismatch,
255+
r#"
256+
---@class A
257+
---@field one 1
258+
---@field two 2
259+
---@field three 3
260+
261+
---@alias C<K extends keyof A> any
262+
263+
---@type C<keyof A>
264+
local tmp
265+
"#,
266+
));
267+
}
268+
269+
#[test]
270+
fn test_alias_keyof_constraint_rejects_keyof_type_with_invalid_key() {
271+
let mut ws = VirtualWorkspace::new();
272+
assert!(!ws.has_no_diagnostic(
273+
DiagnosticCode::GenericConstraintMismatch,
274+
r#"
275+
---@class A
276+
---@field one 1
277+
---@field two 2
278+
279+
---@class B
280+
---@field one 1
281+
---@field missing 3
282+
283+
---@alias C<K extends keyof A> any
284+
285+
---@type C<keyof B>
286+
local tmp
287+
"#,
288+
));
289+
}
290+
291+
#[test]
292+
fn test_alias_keyof_constraint_rejects_invalid_union_for_keyof_type() {
293+
let mut ws = VirtualWorkspace::new();
294+
assert!(!ws.has_no_diagnostic(
295+
DiagnosticCode::GenericConstraintMismatch,
296+
r#"
297+
---@class A
298+
---@field one 1
299+
---@field two 2
300+
301+
---@alias C<K extends keyof A> any
302+
303+
---@type C<'one' | 'missing'>
304+
local tmp
305+
"#,
306+
));
307+
}
308+
309+
#[test]
310+
fn test_alias_dependent_keyof_constraint_uses_explicit_type_arg() {
311+
let mut ws = VirtualWorkspace::new();
312+
assert!(ws.has_no_diagnostic(
313+
DiagnosticCode::GenericConstraintMismatch,
314+
r#"
315+
---@class Base
316+
---@field common 1
317+
318+
---@class Exact: Base
319+
---@field one 1
320+
---@field two 2
321+
322+
---@alias PickFrom<T extends Base, K extends keyof T> any
323+
324+
---@type PickFrom<Exact, keyof Exact>
325+
local tmp
326+
"#,
327+
));
328+
329+
assert!(!ws.has_no_diagnostic(
330+
DiagnosticCode::GenericConstraintMismatch,
331+
r#"
332+
---@type PickFrom<Exact, 'error'>
333+
local tmp
334+
"#,
335+
));
336+
}
337+
338+
#[test]
339+
fn test_alias_dependent_keyof_constraint_rejects_keyof_other_type() {
340+
let mut ws = VirtualWorkspace::new();
341+
assert!(!ws.has_no_diagnostic(
342+
DiagnosticCode::GenericConstraintMismatch,
343+
r#"
344+
---@class Base
345+
---@field common 1
346+
347+
---@class Exact: Base
348+
---@field one 1
349+
350+
---@class Extra: Base
351+
---@field one 1
352+
---@field missing 2
353+
354+
---@alias PickFrom<T extends Base, K extends keyof T> any
355+
356+
---@type PickFrom<Exact, keyof Extra>
357+
local tmp
358+
"#,
359+
));
360+
}
361+
250362
#[test]
251363
fn test_alias_keyof_constraint_rejects_union_with_invalid_key() {
252364
let mut ws = VirtualWorkspace::new();
@@ -345,6 +457,40 @@ mod test {
345457
));
346458
}
347459

460+
#[test]
461+
fn test_dependent_keyof_default_must_satisfy_any_valid_type_arg() {
462+
let mut ws = VirtualWorkspace::new();
463+
assert!(!ws.has_no_diagnostic(
464+
DiagnosticCode::GenericConstraintMismatch,
465+
r#"
466+
---@class Base
467+
---@field common 1
468+
469+
---@class Exact: Base
470+
---@field one 1
471+
472+
---@alias PickFrom<T extends Base = Exact, K extends keyof T = keyof Exact> any
473+
"#,
474+
));
475+
}
476+
477+
#[test]
478+
fn test_dependent_keyof_default_can_reference_same_type_param() {
479+
let mut ws = VirtualWorkspace::new();
480+
assert!(ws.has_no_diagnostic(
481+
DiagnosticCode::GenericConstraintMismatch,
482+
r#"
483+
---@class Base
484+
---@field common 1
485+
486+
---@class Exact: Base
487+
---@field one 1
488+
489+
---@alias PickFrom<T extends Base = Exact, K extends keyof T = keyof T> any
490+
"#,
491+
));
492+
}
493+
348494
#[test]
349495
fn test_alias_generic_default_constraint_mismatch() {
350496
let mut ws = VirtualWorkspace::new();

crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/instantiate_special_generic.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
DbIndex, LuaAliasCallKind, LuaAliasCallType, LuaMemberInfo, LuaMemberKey, LuaObjectType,
3-
LuaTupleStatus, LuaTupleType, LuaType, LuaTypeNode, TypeOps, VariadicType, get_member_map,
3+
LuaType, LuaTypeNode, TypeOps, VariadicType, get_member_map,
44
semantic::{
55
generic::key_type_to_member_key,
66
member::{find_members, infer_raw_member_type},
@@ -45,6 +45,7 @@ pub(super) fn instantiate_alias_call(
4545
}
4646

4747
let members = get_keyof_members(context.db, &operands[0]).unwrap_or_default();
48+
// keyof 表示可取键的联合类型, 不是按位置展开的 tuple.
4849
let member_key_types = members
4950
.iter()
5051
.filter_map(|m| match &m.key {
@@ -53,7 +54,7 @@ pub(super) fn instantiate_alias_call(
5354
_ => None,
5455
})
5556
.collect::<Vec<_>>();
56-
LuaType::Tuple(LuaTupleType::new(member_key_types, LuaTupleStatus::InferResolve).into())
57+
TypeOps::union_all(context.db, member_key_types)
5758
}
5859
// 条件类型不在此处理
5960
LuaAliasCallKind::Extends => {

crates/emmylua_code_analysis/src/semantic/generic/test.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#[cfg(test)]
22
mod test {
3-
use crate::{DiagnosticCode, LuaType, VirtualWorkspace};
3+
use crate::{
4+
DiagnosticCode, LuaType, RenderLevel, TypeSubstitutor, VirtualWorkspace, humanize_type,
5+
};
46

57
#[test]
68
fn test_variadic_func() {
@@ -272,6 +274,53 @@ result = {
272274
assert_eq!(a, expected);
273275
}
274276

277+
#[test]
278+
fn test_keyof_generic_instantiates_to_union() {
279+
let mut ws = VirtualWorkspace::new();
280+
ws.def(
281+
r#"
282+
---@class A
283+
---@field one 1
284+
---@field two 2
285+
---@field three 3
286+
287+
---@alias B<T> T extends any and keyof T or never
288+
"#,
289+
);
290+
291+
let ty = ws.ty("B<A>");
292+
let db = ws.analysis.compilation.get_db();
293+
let origin = match ty {
294+
LuaType::Generic(generic) => {
295+
let type_decl = db
296+
.get_type_index()
297+
.get_type_decl(&generic.get_base_type_id())
298+
.expect("B must resolve to an alias declaration");
299+
let substitutor = TypeSubstitutor::from_type_array(generic.get_params().clone());
300+
type_decl
301+
.get_alias_origin(&db, Some(&substitutor))
302+
.expect("B<A> must expand to its instantiated alias origin")
303+
}
304+
ty => ty,
305+
};
306+
307+
let LuaType::Union(union) = &origin else {
308+
panic!(
309+
"keyof generic should instantiate to union, got {}",
310+
humanize_type(&db, &origin, RenderLevel::Detailed)
311+
);
312+
};
313+
314+
let mut keys = union
315+
.into_vec()
316+
.iter()
317+
.map(|ty| humanize_type(&db, ty, RenderLevel::Brief))
318+
.collect::<Vec<_>>();
319+
keys.sort();
320+
321+
assert_eq!(keys, vec!["\"one\"", "\"three\"", "\"two\""]);
322+
}
323+
275324
#[test]
276325
fn test_generic_alias_instantiation2() {
277326
let mut ws = VirtualWorkspace::new();

0 commit comments

Comments
 (0)