From 6fe0014219039bd5624afdb9623a40471d771a1e Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 6 Feb 2026 20:32:03 +0000 Subject: [PATCH] fix(typecheck): prefer declared globals for index member ownership When setting a member owner for an index expression with a name prefix (e.g. `table.unpack`), prefer the declared global type cache over the inferred prefix type. This keeps stdlib members attached to their type defs and avoids losing them after casts (like `---@cast table unknown`). Skip the override when the name is shadowed by a local. --- .../src/compilation/analyzer/lua/stats.rs | 38 +++++++++++++++++++ .../src/semantic/type_check/test.rs | 25 +++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/stats.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/stats.rs index 02aabe390..5cc87dd6a 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/lua/stats.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/lua/stats.rs @@ -207,6 +207,44 @@ fn set_index_expr_owner(analyzer: &mut LuaAnalyzer, var_expr: LuaVarExpr) -> Opt match analyzer.infer_expr(&prefix_expr.clone()) { Ok(prefix_type) => { + // Prefer declared global types for name prefixes when choosing a member owner. + // This keeps stdlib members (like table.unpack) attached to their type defs. + let prefix_type = if let LuaExpr::NameExpr(name_expr) = &prefix_expr { + let mut explicit_type = None; + if let Some(name) = name_expr.get_name_text() { + // Avoid attaching members to stdlib globals when a local shadows the name. + let is_shadowed = analyzer + .db + .get_decl_index() + .get_decl_tree(&file_id) + .and_then(|tree| tree.find_local_decl(&name, name_expr.get_position())) + .map(|decl| decl.is_local() || decl.is_implicit_self()) + .unwrap_or(false); + if !is_shadowed + && let Some(decl_ids) = + analyzer.db.get_global_index().get_global_decl_ids(&name) + { + // Pick the first resolvable global type cache as the owner type. + for decl_id in decl_ids { + if let Some(type_cache) = analyzer + .db + .get_type_index() + .get_type_cache(&(*decl_id).into()) + { + explicit_type = Some(type_cache.as_type().clone()); + break; + } + } + } + } + + // Fall back to the inferred prefix type when no explicit type exists. + explicit_type.unwrap_or(prefix_type) + } else { + // Non-name prefixes keep the inferred prefix type. + prefix_type + }; + index_expr.get_index_key()?; let member_id = LuaMemberId::new(index_expr.get_syntax_id(), file_id); let member_owner = match prefix_type { diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/test.rs b/crates/emmylua_code_analysis/src/semantic/type_check/test.rs index 12e0723fd..8aad34fea 100644 --- a/crates/emmylua_code_analysis/src/semantic/type_check/test.rs +++ b/crates/emmylua_code_analysis/src/semantic/type_check/test.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod test { - use crate::{DiagnosticCode, VirtualWorkspace}; + use crate::{DiagnosticCode, LuaType, VirtualWorkspace}; #[test] fn test_string() { @@ -208,4 +208,27 @@ mod test { "# )); } + + #[test] + fn test_set_index_expr_owner_prefers_declared_global_type() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + + ws.def_file( + "def.lua", + r#" + table = table + + ---@cast table unknown + AFTER_CAST = table + + ---@return integer + function table.__sentinel() + return 1 + end + "#, + ); + + assert_eq!(ws.expr_ty("AFTER_CAST"), LuaType::Unknown); + assert_eq!(ws.expr_ty("table.__sentinel()"), ws.ty("integer")); + } }