Skip to content

Commit 17e4c3f

Browse files
committed
fix: call type
fix #1164
1 parent c35588f commit 17e4c3f

5 files changed

Lines changed: 119 additions & 7 deletions

File tree

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,4 +1584,22 @@ return t
15841584
"#
15851585
));
15861586
}
1587+
1588+
#[test]
1589+
fn test_keyof_nested_doc_index_access_accepts_member_name() {
1590+
let mut ws = VirtualWorkspace::new();
1591+
assert!(ws.has_no_diagnostic(
1592+
DiagnosticCode::AssignTypeMismatch,
1593+
r#"
1594+
---@class A
1595+
---@field test number
1596+
1597+
---@class B
1598+
---@field test A
1599+
1600+
---@type keyof B["test"]
1601+
local tmp = "test"
1602+
"#
1603+
));
1604+
}
15871605
}

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,4 +1046,49 @@ mod test {
10461046
"#
10471047
));
10481048
}
1049+
1050+
#[test]
1051+
fn test_doc_index_access_type_resolves_object_field() {
1052+
let mut ws = VirtualWorkspace::new();
1053+
let code = r#"
1054+
---@class A
1055+
---@field b { test: number }
1056+
1057+
---@type A['b']
1058+
local tmp
1059+
local out = tmp.test
1060+
result = out
1061+
"#;
1062+
1063+
let has_no_diagnostic = ws.has_no_diagnostic(DiagnosticCode::UndefinedField, code);
1064+
let result_ty = ws.expr_ty("result");
1065+
let expected_ty = ws.ty("number");
1066+
let result_type_text = ws.humanize_type(result_ty.clone());
1067+
1068+
assert!(
1069+
has_no_diagnostic && result_ty == expected_ty,
1070+
"undefined-field diagnostic: {}; result type: {}",
1071+
!has_no_diagnostic,
1072+
result_type_text
1073+
);
1074+
}
1075+
1076+
#[test]
1077+
fn test_doc_index_access_type_cycle_does_not_overflow() {
1078+
let mut ws = VirtualWorkspace::new();
1079+
assert!(!ws.has_no_diagnostic(
1080+
DiagnosticCode::UndefinedField,
1081+
r#"
1082+
---@class A
1083+
---@field b B['c']
1084+
1085+
---@class B
1086+
---@field c A['b']
1087+
1088+
---@type A['b']
1089+
local tmp
1090+
local out = tmp.test
1091+
"#
1092+
));
1093+
}
10491094
}

crates/emmylua_code_analysis/src/semantic/guard.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use std::{cell::RefCell, collections::HashSet, rc::Rc};
22

3-
use crate::{InferFailReason, LuaTypeDeclId};
3+
use crate::{InferFailReason, LuaType, LuaTypeDeclId};
44

55
pub type InferGuardRef = Rc<InferGuard>;
66

77
/// Guard to prevent infinite recursion with optimized lazy allocation
88
///
99
/// This guard uses a lazy allocation strategy:
1010
/// - Fork is zero-cost (no HashSet allocation)
11-
/// - `current` HashSet is only created when needed (write-on-create)
11+
/// - Visited declaration and instantiated type sets are only created when needed
1212
/// - Most child guards never allocate memory if they only read from parents
1313
///
1414
/// # Memory Layout
@@ -23,9 +23,10 @@ pub type InferGuardRef = Rc<InferGuard>;
2323
/// ```
2424
#[derive(Debug, Clone)]
2525
pub struct InferGuard {
26-
/// Current level's visited types (lazily allocated)
27-
/// Only created when we need to add a new type not in parent chain
26+
/// Current level's visited type declarations (lazily allocated)
2827
current: RefCell<Option<HashSet<LuaTypeDeclId>>>,
28+
/// Current level's visited instantiated types (lazily allocated)
29+
current_types: RefCell<Option<HashSet<LuaType>>>,
2930
/// Parent guard (shared reference)
3031
parent: Option<Rc<InferGuard>>,
3132
}
@@ -34,6 +35,7 @@ impl InferGuard {
3435
pub fn new() -> Rc<Self> {
3536
Rc::new(Self {
3637
current: RefCell::new(None),
38+
current_types: RefCell::new(None),
3739
parent: None,
3840
})
3941
}
@@ -44,6 +46,7 @@ impl InferGuard {
4446
pub fn fork(self: &Rc<Self>) -> Rc<Self> {
4547
Rc::new(Self {
4648
current: RefCell::new(None), // Lazy allocation
49+
current_types: RefCell::new(None),
4750
parent: Some(Rc::clone(self)),
4851
})
4952
}
@@ -70,6 +73,20 @@ impl InferGuard {
7073
Ok(())
7174
}
7275

76+
pub fn check_type(&self, typ: &LuaType) -> Result<(), InferFailReason> {
77+
if self.contains_type_in_parents(typ) {
78+
return Err(InferFailReason::RecursiveInfer);
79+
}
80+
81+
let mut current_types = self.current_types.borrow_mut();
82+
let current_types = current_types.get_or_insert_with(HashSet::default);
83+
if !current_types.insert(typ.clone()) {
84+
return Err(InferFailReason::RecursiveInfer);
85+
}
86+
87+
Ok(())
88+
}
89+
7390
/// Check if a type has been visited in parent chain
7491
fn contains_in_parents(&self, type_id: &LuaTypeDeclId) -> bool {
7592
let mut current_parent = self.parent.as_ref();
@@ -84,6 +101,19 @@ impl InferGuard {
84101
false
85102
}
86103

104+
fn contains_type_in_parents(&self, typ: &LuaType) -> bool {
105+
let mut current_parent = self.parent.as_ref();
106+
while let Some(parent) = current_parent {
107+
if let Some(ref types) = *parent.current_types.borrow()
108+
&& types.contains(typ)
109+
{
110+
return true;
111+
}
112+
current_parent = parent.parent.as_ref();
113+
}
114+
false
115+
}
116+
87117
/// Check if a type has been visited (without modifying the guard)
88118
pub fn contains(&self, type_id: &LuaTypeDeclId) -> bool {
89119
// Check current level
@@ -99,6 +129,11 @@ impl InferGuard {
99129
/// Get the depth of current level
100130
pub fn current_depth(&self) -> usize {
101131
self.current.borrow().as_ref().map_or(0, |set| set.len())
132+
+ self
133+
.current_types
134+
.borrow()
135+
.as_ref()
136+
.map_or(0, |set| set.len())
102137
}
103138

104139
/// Get the total depth of the entire guard chain
@@ -127,7 +162,7 @@ impl InferGuard {
127162
/// Useful for debugging and performance analysis
128163
#[cfg(test)]
129164
pub fn has_allocated(&self) -> bool {
130-
self.current.borrow().is_some()
165+
self.current.borrow().is_some() || self.current_types.borrow().is_some()
131166
}
132167
}
133168

crates/emmylua_code_analysis/src/semantic/infer/infer_index/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,19 @@ fn infer_member_by_lookup(
336336
LuaType::TableGeneric(table_generic) => {
337337
infer_table_generic_member_by_key_type(db, table_generic, &lookup.key_type)
338338
}
339+
LuaType::Call(alias_call)
340+
if alias_call.get_call_kind() == LuaAliasCallKind::Index
341+
&& !prefix_type.contain_tpl() =>
342+
{
343+
let index_guard = infer_guard.fork();
344+
index_guard.check_type(prefix_type)?;
345+
let resolved = instantiate_type_generic(db, prefix_type, &TypeSubstitutor::new());
346+
if resolved == *prefix_type {
347+
Err(InferFailReason::FieldNotFound)
348+
} else {
349+
infer_member_by_lookup(db, cache, &resolved, lookup, &index_guard)
350+
}
351+
}
339352
LuaType::TplRef(tpl) => infer_tpl_ref_member(db, cache, tpl, lookup, infer_guard),
340353
LuaType::ModuleRef(file_id) => {
341354
let module_info = db.get_module_index().get_module(*file_id);

crates/emmylua_code_analysis/src/semantic/type_check/complex_type/call_type_check.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::sync::Arc;
22

33
use crate::{
44
LuaAliasCallKind, LuaAliasCallType, LuaMemberKey, LuaType, LuaUnionType, TypeCheckFailReason,
5-
TypeCheckResult, get_keyof_members,
5+
TypeCheckResult, TypeSubstitutor, get_keyof_members, instantiate_type_generic,
66
semantic::type_check::{
77
check_general_type_compact, type_check_context::TypeCheckContext,
88
type_check_guard::TypeCheckGuard,
@@ -69,7 +69,8 @@ pub fn check_call_type_compact(
6969
}
7070

7171
fn get_keyof_keys(context: &TypeCheckContext, prefix_type: &LuaType) -> Vec<LuaType> {
72-
let members = get_keyof_members(context.db, prefix_type).unwrap_or_default();
72+
let prefix_type = instantiate_type_generic(context.db, prefix_type, &TypeSubstitutor::new());
73+
let members = get_keyof_members(context.db, &prefix_type).unwrap_or_default();
7374
let key_types = members
7475
.iter()
7576
.filter_map(|m| match &m.key {

0 commit comments

Comments
 (0)