Skip to content

Commit 675eef4

Browse files
committed
refactor(generic): centralize literal widening policy
fix #1138
1 parent a3806a7 commit 675eef4

12 files changed

Lines changed: 643 additions & 282 deletions

File tree

crates/emmylua_code_analysis/src/compilation/test/generic_test.rs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,89 @@ mod test {
389389
));
390390
}
391391

392+
#[test]
393+
fn test_type_mapped_pick_single_key() {
394+
let mut ws = VirtualWorkspace::new();
395+
396+
ws.def(
397+
r#"
398+
---@class A
399+
---@field one 1
400+
---@field two 2
401+
---@field three 3
402+
403+
---@alias Pick<T, K extends keyof T> {[P in K]: T[P];}
404+
405+
---@type Pick<A, 'one'>
406+
Tmp = nil
407+
"#,
408+
);
409+
410+
let tmp_ty = ws.expr_ty("Tmp");
411+
assert_eq!(
412+
ws.humanize_type_detailed(tmp_ty),
413+
"Pick<A,\"one\"> = { one: 1 }"
414+
);
415+
}
416+
417+
#[test]
418+
fn test_type_mapped_pick_literal_union_keys() {
419+
let mut ws = VirtualWorkspace::new();
420+
421+
ws.def(
422+
r#"
423+
---@class A
424+
---@field one 1
425+
---@field two 2
426+
---@field three 3
427+
428+
---@alias Pick<T, K extends keyof T> {[P in K]: T[P];}
429+
---@alias Value<T, K extends keyof T> T[K]
430+
431+
---@type Pick<A, 'one' | 'two'>
432+
Picked = nil
433+
434+
---@type Value<A, 'one' | 'two'>
435+
Value = nil
436+
"#,
437+
);
438+
439+
let picked_ty = ws.expr_ty("Picked");
440+
assert_eq!(
441+
ws.humanize_type_detailed(picked_ty),
442+
"Pick<A,(\"one\"|\"two\")> = { one: 1, two: 2 }"
443+
);
444+
445+
let value_ty = ws.expr_ty("Value");
446+
assert_eq!(
447+
ws.humanize_type_detailed(value_ty),
448+
"Value<A,(\"one\"|\"two\")> = (1|2)"
449+
);
450+
}
451+
452+
#[test]
453+
fn test_explicit_call_generic_literal_used_as_index_key() {
454+
let mut ws = VirtualWorkspace::new();
455+
456+
ws.def(
457+
r#"
458+
---@class A
459+
---@field one 1
460+
---@field two 2
461+
462+
---@generic T, K extends keyof T
463+
---@return T[K]
464+
function get_explicit()
465+
end
466+
467+
Result = get_explicit--[[@<A, "one">]]()
468+
"#,
469+
);
470+
471+
let result_ty = ws.expr_ty("Result");
472+
assert_eq!(ws.humanize_type(result_ty), "1");
473+
}
474+
392475
#[test]
393476
fn test_alias_generic_constraint_references_later_param() {
394477
let mut ws = VirtualWorkspace::new();
@@ -1345,6 +1428,44 @@ mod test {
13451428
assert_eq!(ws.humanize_type(inferred_result), "integer");
13461429
}
13471430

1431+
#[test]
1432+
fn test_non_const_generic_call_inference_widens_literal_return() {
1433+
let mut ws = VirtualWorkspace::new();
1434+
ws.def(
1435+
r#"
1436+
---@generic T
1437+
---@param value T
1438+
---@return T
1439+
function identity(value)
1440+
end
1441+
1442+
Result = identity("literal")
1443+
"#,
1444+
);
1445+
1446+
let result_ty = ws.expr_ty("Result");
1447+
assert_eq!(ws.humanize_type(result_ty), "string");
1448+
}
1449+
1450+
#[test]
1451+
fn test_const_generic_call_inference_preserves_literal_return() {
1452+
let mut ws = VirtualWorkspace::new();
1453+
ws.def(
1454+
r#"
1455+
---@generic const T
1456+
---@param value T
1457+
---@return T
1458+
function identity(value)
1459+
end
1460+
1461+
Result = identity("literal")
1462+
"#,
1463+
);
1464+
1465+
let result_ty = ws.expr_ty("Result");
1466+
assert_eq!(ws.humanize_type(result_ty), "\"literal\"");
1467+
}
1468+
13481469
#[test]
13491470
fn test_function_generic_default_can_reference_earlier_param_at_call_sites() {
13501471
let mut ws = VirtualWorkspace::new();
@@ -1547,6 +1668,32 @@ mod test {
15471668
));
15481669
}
15491670

1671+
#[test]
1672+
fn test_conditional_generic_literal_check_operand_preserved() {
1673+
let mut ws = VirtualWorkspace::new();
1674+
ws.def(
1675+
r#"
1676+
---@alias IsKnown<T> T extends ("one" | "two") and 1 or 2
1677+
1678+
---@generic T
1679+
---@return IsKnown<T>
1680+
function check_explicit()
1681+
end
1682+
1683+
---@type IsKnown<"one">
1684+
Direct = nil
1685+
1686+
Result = check_explicit--[[@<"one">]]()
1687+
"#,
1688+
);
1689+
1690+
let direct_ty = ws.expr_ty("Direct");
1691+
assert_eq!(ws.humanize_type_detailed(direct_ty), "IsKnown<\"one\"> = 1");
1692+
1693+
let result_ty = ws.expr_ty("Result");
1694+
assert_eq!(ws.humanize_type(result_ty), "1");
1695+
}
1696+
15501697
#[test]
15511698
fn test_issue_986() {
15521699
let mut ws = VirtualWorkspace::new();
@@ -1988,4 +2135,23 @@ mod test {
19882135

19892136
assert_eq!(ws.expr_ty("A"), ws.ty("string|integer"));
19902137
}
2138+
2139+
#[test]
2140+
fn test_conditional_infer_preserves_literal_from_type_level_source() {
2141+
let mut ws = VirtualWorkspace::new();
2142+
ws.def(
2143+
r#"
2144+
---@alias ValueOf<T> T extends { value: infer P } and P or never
2145+
2146+
---@type ValueOf<{ value: "one" }>
2147+
A = nil
2148+
"#,
2149+
);
2150+
2151+
let a_ty = ws.expr_ty("A");
2152+
assert_eq!(
2153+
ws.humanize_type_detailed(a_ty),
2154+
"ValueOf<{ value: \"one\" }> = \"one\""
2155+
);
2156+
}
19912157
}

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

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ use super::call_constraint::{
1111
};
1212
use crate::diagnostic::{checker::Checker, lua_diagnostic::DiagnosticContext};
1313
use crate::{
14-
DiagnosticCode, DocTypeInferContext, GenericParam, GenericTplId, LuaArrayType, LuaGenericType,
15-
LuaIntersectionType, LuaObjectType, LuaSignatureId, LuaStringTplType, LuaTupleType, LuaType,
16-
LuaUnionType, RenderLevel, SemanticModel, TypeCheckFailReason, TypeCheckResult,
17-
TypeSubstitutor, VariadicType, humanize_type, infer_doc_type, instantiate_type_generic,
14+
DiagnosticCode, DocTypeInferContext, GenericParam, GenericResolveMode, GenericTplId,
15+
LuaArrayType, LuaGenericType, LuaIntersectionType, LuaObjectType, LuaSignatureId,
16+
LuaStringTplType, LuaTupleType, LuaType, LuaUnionType, RenderLevel, SemanticModel,
17+
TypeCheckFailReason, TypeCheckResult, TypeSubstitutor, VariadicType, humanize_type,
18+
infer_doc_type, instantiate_type_generic_full,
1819
};
1920

2021
pub struct GenericConstraintMismatchChecker;
@@ -608,7 +609,12 @@ fn check_doc_tag_type(
608609
};
609610
let extend_type = normalize_constraint_type(
610611
semantic_model.get_db(),
611-
instantiate_type_generic(semantic_model.get_db(), extend_type, &substitutor),
612+
instantiate_type_generic_full(
613+
semantic_model.get_db(),
614+
extend_type,
615+
&substitutor,
616+
GenericResolveMode::Literal,
617+
),
612618
);
613619
let param_type = normalize_constraint_type(semantic_model.get_db(), param_type.clone());
614620
let result = semantic_model.type_check_detail(&extend_type, &param_type);
@@ -649,7 +655,12 @@ fn check_call_arg(
649655
let extend_type = str_tpl_ref.get_constraint().map(|ty| {
650656
normalize_constraint_type(
651657
semantic_model.get_db(),
652-
instantiate_type_generic(semantic_model.get_db(), ty, substitutor),
658+
instantiate_type_generic_full(
659+
semantic_model.get_db(),
660+
ty,
661+
substitutor,
662+
GenericResolveMode::Literal,
663+
),
653664
)
654665
});
655666
check_str_tpl_ref(
@@ -666,7 +677,12 @@ fn check_call_arg(
666677
if let Some(extend_type) = tpl_ref.get_constraint().map(|ty| {
667678
normalize_constraint_type(
668679
semantic_model.get_db(),
669-
instantiate_type_generic(semantic_model.get_db(), ty, substitutor),
680+
instantiate_type_generic_full(
681+
semantic_model.get_db(),
682+
ty,
683+
substitutor,
684+
GenericResolveMode::Literal,
685+
),
670686
)
671687
}) {
672688
let result = check_generic_default_satisfies_constraint(

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
mod test {
33

44
use crate::{DiagnosticCode, VirtualWorkspace};
5+
use lsp_types::NumberOrString;
6+
use tokio_util::sync::CancellationToken;
57

68
#[test]
79
fn test_1() {
@@ -263,6 +265,42 @@ mod test {
263265
));
264266
}
265267

268+
#[test]
269+
fn test_alias_keyof_constraint_reports_invalid_literal_key() {
270+
let mut ws = VirtualWorkspace::new();
271+
ws.enable_check(DiagnosticCode::GenericConstraintMismatch);
272+
let file_id = ws.def(
273+
r#"
274+
---@class A
275+
---@field one 1
276+
277+
---@alias Pick<T, K extends keyof T> nil
278+
279+
---@type Pick<A, 'missing'>
280+
local tmp
281+
"#,
282+
);
283+
284+
let diagnostics = ws
285+
.analysis
286+
.diagnose_file(file_id, CancellationToken::new())
287+
.unwrap();
288+
let code = Some(NumberOrString::String(
289+
DiagnosticCode::GenericConstraintMismatch
290+
.get_name()
291+
.to_string(),
292+
));
293+
let diagnostic = diagnostics
294+
.iter()
295+
.find(|diagnostic| diagnostic.code == code)
296+
.expect("expected generic constraint mismatch diagnostic");
297+
assert!(
298+
diagnostic.message.contains("\"missing\""),
299+
"{}",
300+
diagnostic.message
301+
);
302+
}
303+
266304
#[test]
267305
fn test_class_generic_default_constraint_match() {
268306
let mut ws = VirtualWorkspace::new();

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

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ use crate::{
2828
tpl_pattern_match_args_skip_unknown,
2929
};
3030

31+
use super::type_substitutor::{
32+
GenericCandidate, GenericResolveMode, LiteralPolicy, SubstitutorValue,
33+
};
3134
use crate::semantic::generic::{TypeSubstitutor, instantiate_type::instantiate_type_generic};
3235

3336
pub fn infer_call_generic(
@@ -129,9 +132,10 @@ fn apply_call_generic_type_list(
129132
let doc_ctx = DocTypeInferContext::new(db, file_id);
130133
for (i, doc_type) in type_list.get_types().enumerate() {
131134
let typ = infer_doc_type(doc_ctx, &doc_type);
132-
context
133-
.substitutor
134-
.insert_type(GenericTplId::Func(i as u32), typ, true);
135+
context.substitutor.insert_value(
136+
GenericTplId::Func(i as u32),
137+
SubstitutorValue::Type(GenericCandidate::new(typ, LiteralPolicy::Preserve)),
138+
);
135139
}
136140
}
137141

@@ -350,7 +354,13 @@ fn instantiate_callable_from_arg_types(
350354
}
351355

352356
for tpl_id in callback_return_tpls {
353-
callable_substitutor.insert_type(tpl_id, LuaType::Unknown, true);
357+
callable_substitutor.insert_value(
358+
tpl_id,
359+
SubstitutorValue::Type(GenericCandidate::new(
360+
LuaType::Unknown,
361+
LiteralPolicy::Widen,
362+
)),
363+
);
354364
}
355365
match instantiate_type_generic(context.db, &callable_type, &callable_substitutor) {
356366
LuaType::DocFunction(func) => Some(func),
@@ -539,7 +549,7 @@ pub(crate) fn infer_self_type(
539549
for (i, generic_param) in generic.iter().enumerate() {
540550
let tpl_id = GenericTplId::Type(i as u32);
541551
let param = call_substitutor
542-
.get_type(tpl_id)
552+
.resolve_type(tpl_id, GenericResolveMode::Value, generic_param.is_const)
543553
.cloned()
544554
.unwrap_or_else(|| {
545555
match generic_param
@@ -551,7 +561,13 @@ pub(crate) fn infer_self_type(
551561
None => LuaType::Unknown,
552562
}
553563
});
554-
substitutor.insert_type(tpl_id, param.clone(), true);
564+
substitutor.insert_value(
565+
tpl_id,
566+
SubstitutorValue::Type(GenericCandidate::new(
567+
param.clone(),
568+
LiteralPolicy::Preserve,
569+
)),
570+
);
555571
params.push(param);
556572
}
557573
let generic = LuaGenericType::new(id.clone(), params);
@@ -634,9 +650,13 @@ fn fill_call_prefix_substitutor(context: &mut TplContext, call_expr: &LuaCallExp
634650
let self_type = infer_expr(context.db, context.cache, self_expr).ok()?;
635651
if let LuaType::Generic(generic) = self_type {
636652
for (i, param) in generic.get_params().iter().enumerate() {
637-
context
638-
.substitutor
639-
.insert_type(GenericTplId::Type(i as u32), param.clone(), true);
653+
context.substitutor.insert_value(
654+
GenericTplId::Type(i as u32),
655+
SubstitutorValue::Type(GenericCandidate::new(
656+
param.clone(),
657+
LiteralPolicy::Preserve,
658+
)),
659+
);
640660
}
641661
return Some(());
642662
}

0 commit comments

Comments
 (0)