diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f1b8192c..0ca7cc142 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,3 +38,7 @@ repos: - id: check-symlinks - id: destroyed-symlinks - id: check-vcs-permalinks + - repo: https://github.com/crate-ci/typos + rev: master + hooks: + - id: typos diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs index 3a1c8a821..c6ab2ab35 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs @@ -182,10 +182,9 @@ pub fn analyze_param(analyzer: &mut DocAnalyzer, tag: LuaDocTagParam) -> Option< }; let nullable = tag.is_nullable(); - let mut type_ref = if let Some(lua_doc_type) = tag.get_type() { + let mut type_ref = { + let lua_doc_type = tag.get_type()?; infer_type(&mut analyzer.type_context, lua_doc_type) - } else { - return None; }; if nullable && !type_ref.is_nullable() { diff --git a/crates/emmylua_code_analysis/src/compilation/test/generic_test.rs b/crates/emmylua_code_analysis/src/compilation/test/generic_test.rs index 384742822..ad14ee7a9 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/generic_test.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/generic_test.rs @@ -903,6 +903,131 @@ mod test { assert_eq!(ws.humanize_type(result_ty), "integer"); } + #[test] + fn test_call_operator_self_infer_on_index() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class Factory + ---@overload fun(): self + + ---@class Mod + ---@field Factory Factory + ---@type Mod + local Mod + + result = Mod.Factory() + "#, + ); + + let result_ty = ws.expr_ty("result"); + assert_eq!(ws.humanize_type(result_ty), "Factory"); + } + + #[test] + fn test_call_operator_self_infer_filters_union_receiver() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class Callable + ---@overload fun(): self + + ---@type Callable|string + local value + + result = value() + "#, + ); + + let result_ty = ws.expr_ty("result"); + assert_eq!(ws.humanize_type(result_ty), "Callable"); + } + + #[test] + fn test_call_operator_self_infer_through_generic_alias() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class Callable + ---@overload fun(): self + + ---@alias Box T + + ---@type Box + local value + + result = value() + "#, + ); + + let result_ty = ws.expr_ty("result"); + assert_eq!(ws.humanize_type(result_ty), "Callable"); + } + + #[test] + fn test_call_operator_self_infer_through_intersection() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class Callable + ---@overload fun(): self + + ---@class Extra + + ---@type Callable & Extra + local value + + result = value() + "#, + ); + + let result_ty = ws.expr_ty("result"); + assert_eq!(ws.humanize_type(result_ty), "(Callable & Extra)"); + } + + #[test] + fn test_colon_call_generic_uses_receiver_when_member_is_callable() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class Owner + ---@field run CallableMethod + + ---@class CallableMethod + ---@overload fun(owner: T): T + + ---@type Owner + local owner + + result = owner:run() + "#, + ); + + let result_ty = ws.expr_ty("result"); + assert_eq!(ws.humanize_type(result_ty), "Owner"); + } + + #[test] + fn test_plain_table_metatable_call_return_self() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def( + r#" + factory = setmetatable({}, { + ---@return self + __call = function(self) + return self + end, + }) + + result = factory() + "#, + ); + + let result_ty = ws.expr_ty("result"); + let humanized = ws.humanize_type(result_ty); + assert_eq!(humanized, "table"); + } + #[test] fn test_function_generic_constraint_is_fallback() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/config/config_loader.rs b/crates/emmylua_code_analysis/src/config/config_loader.rs index e54e101ba..7901105cd 100644 --- a/crates/emmylua_code_analysis/src/config/config_loader.rs +++ b/crates/emmylua_code_analysis/src/config/config_loader.rs @@ -28,7 +28,7 @@ pub fn load_configs_raw(config_files: Vec, partial_emmyrcs: Option { log::error!( "Failed to parse lua config file: {:?}, error: {:?}", - &config_file, + config_file, e ); continue; @@ -40,7 +40,7 @@ pub fn load_configs_raw(config_files: Vec, partial_emmyrcs: Option { log::error!( "Failed to parse config file: {:?}, error: {:?}", - &config_file, + config_file, e ); continue; diff --git a/crates/emmylua_code_analysis/src/db_index/module/mod.rs b/crates/emmylua_code_analysis/src/db_index/module/mod.rs index e8b947603..73f84d025 100644 --- a/crates/emmylua_code_analysis/src/db_index/module/mod.rs +++ b/crates/emmylua_code_analysis/src/db_index/module/mod.rs @@ -164,9 +164,9 @@ impl LuaModuleIndex { let node = self.module_nodes.get_mut(&parent_node_id)?; node.file_ids.push(file_id); - let module_name = match module_parts.last() { - Some(name) => name.to_string(), - None => return None, + let module_name = { + let name = module_parts.last()?; + name.to_string() }; let module_info = ModuleInfo { file_id, @@ -274,9 +274,9 @@ impl LuaModuleIndex { let mut parent_node_id = self.module_root_id; for part in module_parts { let parent_node = self.module_nodes.get(&parent_node_id)?; - let child_id = match parent_node.children.get(*part) { - Some(id) => *id, - None => return None, + let child_id = { + let id = parent_node.children.get(*part)?; + *id }; parent_node_id = child_id; } diff --git a/crates/emmylua_code_analysis/src/db_index/type/humanize_type.rs b/crates/emmylua_code_analysis/src/db_index/type/humanize_type.rs index 53faef02b..cb59b778f 100644 --- a/crates/emmylua_code_analysis/src/db_index/type/humanize_type.rs +++ b/crates/emmylua_code_analysis/src/db_index/type/humanize_type.rs @@ -5,9 +5,9 @@ use itertools::Itertools; use crate::{ AsyncState, DbIndex, LuaAliasCallType, LuaConditionalType, LuaFunctionType, LuaGenericType, - LuaIntersectionType, LuaMemberKey, LuaMemberOwner, LuaObjectType, LuaSignatureId, - LuaStringTplType, LuaTupleType, LuaType, LuaTypeDeclId, LuaUnionType, TypeSubstitutor, - VariadicType, + LuaIntersectionType, LuaMappedType, LuaMemberKey, LuaMemberOwner, LuaObjectType, + LuaSignatureId, LuaStringTplType, LuaTupleType, LuaType, LuaTypeDeclId, LuaUnionType, + TypeSubstitutor, VariadicType, }; use super::{LuaAliasCallKind, LuaMultiLineUnion}; @@ -217,9 +217,9 @@ impl<'a> TypeHumanizer<'a> { } LuaType::Language(s) => w.write_str(s), LuaType::Conditional(c) => self.write_conditional_type(c, w), + LuaType::Mapped(mapped) => self.write_mapped_type(mapped, w), LuaType::Never => w.write_str("never"), LuaType::ModuleRef(file_id) => self.write_module_ref(*file_id, w), - _ => w.write_str("unknown"), } } @@ -1082,6 +1082,36 @@ impl<'a> TypeHumanizer<'a> { Ok(()) } + // ─── Mapped ───────────────────────────────────────────────────── + + fn write_mapped_type(&mut self, mapped: &LuaMappedType, w: &mut W) -> fmt::Result { + w.write_str("{ ")?; + if mapped.is_readonly { + w.write_str("readonly ")?; + } + + w.write_char('[')?; + w.write_str(mapped.param.1.name.as_str())?; + w.write_str(" in ")?; + + let saved = self.level; + self.level = self.child_level(); + if let Some(constraint) = &mapped.param.1.constraint { + self.write_type(constraint, w)?; + } else { + w.write_str("unknown")?; + } + w.write_char(']')?; + if mapped.is_optional { + w.write_char('?')?; + } + w.write_str(": ")?; + self.write_type(&mapped.value, w)?; + self.level = saved; + + w.write_str("; }") + } + // ─── ModuleRef ────────────────────────────────────────────────── fn write_module_ref(&mut self, file_id: crate::FileId, w: &mut W) -> fmt::Result { diff --git a/crates/emmylua_code_analysis/src/db_index/type/type_decl.rs b/crates/emmylua_code_analysis/src/db_index/type/type_decl.rs index 9824f23d2..546bd4829 100644 --- a/crates/emmylua_code_analysis/src/db_index/type/type_decl.rs +++ b/crates/emmylua_code_analysis/src/db_index/type/type_decl.rs @@ -317,11 +317,11 @@ impl Serialize for LuaTypeDeclId { match self.id.as_ref() { LuaTypeIdentifier::Global(name) => serializer.serialize_str(name.as_ref()), LuaTypeIdentifier::Internal(workspace_id, name) => { - let s = format!("ws:{}|{}", workspace_id.id, &name); + let s = format!("ws:{}|{}", workspace_id.id, name); serializer.serialize_str(&s) } LuaTypeIdentifier::File(file_id, name) => { - let s = format!("{}|{}", file_id.id, &name); + let s = format!("{}|{}", file_id.id, name); serializer.serialize_str(&s) } } diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/generic/call_constraint.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/generic/call_constraint.rs index 90960a262..6a15aea0a 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/generic/call_constraint.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/generic/call_constraint.rs @@ -60,7 +60,7 @@ pub(super) fn build_call_constraint_context( params.insert(0, ("self".into(), Some(LuaType::SelfInfer))); } (true, false) => { - let self_type = semantic_model.infer_call_self_type(call_expr)?; + let self_type = semantic_model.resolve_call_self_type(call_expr)?; args.insert( 0, CallConstraintArg { diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs index b5eded625..46779aa2e 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs @@ -1,6 +1,6 @@ use hashbrown::{HashMap, HashSet}; -use emmylua_parser::{LuaAst, LuaAstNode, LuaSyntaxId, LuaTableExpr}; +use emmylua_parser::{LuaAst, LuaAstNode, LuaExpr, LuaSyntaxId, LuaTableExpr}; use rowan::NodeOrToken; use crate::{ @@ -63,6 +63,13 @@ fn check_table_expr( let table_type = match semantic_model.infer_table_should_be(expr.clone())? { LuaType::Union(union) => { let mut check_type = None; + let array_like_expr_type = if expr.is_array() || expr.is_empty() { + semantic_model + .infer_expr(LuaExpr::TableExpr(expr.clone())) + .ok() + } else { + None + }; for ty in union.into_vec() { match &ty { LuaType::Ref(_) @@ -74,10 +81,14 @@ fn check_table_expr( } check_type = Some(ty); } - LuaType::Table | LuaType::Userdata => { + LuaType::Table | LuaType::Userdata | LuaType::TableGeneric(_) => { return Some(()); } - LuaType::TableGeneric(_) => { + LuaType::Array(_) | LuaType::Tuple(_) + if array_like_expr_type.as_ref().is_some_and(|expr_type| { + semantic_model.type_check(&ty, expr_type).is_ok() + }) => + { return Some(()); } _ => {} diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/param_check/type_mismatch.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/param_check/type_mismatch.rs index c1b0af12e..cf39b8e10 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/param_check/type_mismatch.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/param_check/type_mismatch.rs @@ -30,7 +30,7 @@ pub(super) fn check_param_types( .into_iter() .unzip(); - let self_type = semantic_model.infer_call_self_type(&facts.call_expr); + let self_type = semantic_model.resolve_call_self_type(&facts.call_expr); let colon_range = facts .call_expr .get_colon_token() diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/assign_type_mismatch_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/assign_type_mismatch_test.rs index b58a3f5fe..4eb2c9162 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/assign_type_mismatch_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/assign_type_mismatch_test.rs @@ -1421,6 +1421,40 @@ return t )); } + #[test] + fn test_function_parameter_contravariance_assignment() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class A + ---@class B + ---@class C + ---@class D + + ---@param a A | B | C + ---@return boolean + function condition(a) + return true + end + "#, + ); + assert!(!ws.has_no_diagnostic( + DiagnosticCode::AssignTypeMismatch, + r#" + ---@type fun(a: A | B | C | D): boolean + local tmp = condition + "#, + )); + + assert!(ws.has_no_diagnostic( + DiagnosticCode::AssignTypeMismatch, + r#" + ---@type fun(a: A | B): boolean + local tmp = condition + "#, + )); + } + #[test] fn test_generic_extends_table() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/generic_constraint_mismatch_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/generic_constraint_mismatch_test.rs index a59903efd..9da33b25a 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/generic_constraint_mismatch_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/generic_constraint_mismatch_test.rs @@ -531,6 +531,26 @@ mod test { )); } + #[test] + fn test_colon_call_constraint_uses_receiver_when_member_is_callable() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.has_no_diagnostic( + DiagnosticCode::GenericConstraintMismatch, + r#" + ---@class Owner + ---@field run CallableMethod + + ---@class CallableMethod + ---@overload fun(owner: T) + + ---@type Owner + local owner + + owner:run() + "#, + )); + } + #[test] fn test_extend_string() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/missing_fields_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/missing_fields_test.rs index b6b246dbe..d543789b8 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/missing_fields_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/missing_fields_test.rs @@ -260,6 +260,61 @@ foo({}) )); } + #[test] + fn test_union_enum_array_does_not_report_missing_fields() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.has_no_diagnostic( + DiagnosticCode::MissingFields, + r#" + ---@enum NiceEnum + local GOODGUYS = { + superman = 1 + } + + ---@alias Evil string | NiceEnum + + ---@param evils Evil | (Evil[]) + local function do_evil(evils) end + + do_evil({ "hi", "dead" }) + "# + )); + } + + #[test] + fn test_union_array_named_table_still_reports_missing_fields() { + let mut ws = VirtualWorkspace::new(); + assert!(!ws.has_no_diagnostic( + DiagnosticCode::MissingFields, + r#" + ---@class Foo + ---@field name string + + ---@param foo Foo | Foo[] + local function use_foo(foo) end + + use_foo({ typo = 1 }) + "# + )); + } + + #[test] + fn test_union_array_empty_table_does_not_report_missing_fields() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.has_no_diagnostic( + DiagnosticCode::MissingFields, + r#" + ---@class Foo + ---@field name string + + ---@param foo Foo | Foo[] + local function use_foo(foo) end + + use_foo({}) + "# + )); + } + #[test] fn test_multiline_union_nil_field_is_optional() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs index e9655f10b..b2f879270 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs @@ -986,18 +986,15 @@ mod test { assert!(ws.has_no_diagnostic( DiagnosticCode::ParamTypeMismatch, r#" - ---@class D23.A + ---@class A - ---@generic Extends: string - ---@param init? fun(self: self, super: Extends) - local function extends(init) + ---@param init fun(self: self) + local function test(init) end - ---@generic Super: string - ---@param super? `Super` - ---@param superInit? fun(self: D23.A, super: Super, ...) - local function declare(super, superInit) - extends(superInit) + ---@param superInit fun(self: A) + local function declare(superInit) + test(superInit) end "# )); @@ -1642,6 +1639,27 @@ mod test { )); } + #[test] + fn test_colon_call_param_check_uses_receiver_when_member_is_callable() { + let mut ws = VirtualWorkspace::new(); + + assert!(ws.has_no_diagnostic( + DiagnosticCode::ParamTypeMismatch, + r#" + ---@class Owner + ---@field run CallableMethod + + ---@class CallableMethod + ---@overload fun(owner: Owner) + + ---@type Owner + local owner + + owner:run() + "#, + )); + } + #[test] fn test_generic_infer_function_2() { let mut ws = VirtualWorkspace::new(); @@ -1752,4 +1770,45 @@ mod test { "# )); } + + #[test] + fn test_generic_callback_parameter_contravariance() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class A + ---@class B + ---@class C + ---@class D + + ---@param a A | B | C + ---@return boolean + function condition(a) + return true + end + + ---@generic T + ---@param a T[] + ---@param b fun(a: T): boolean + function test(a, b) end + "#, + ); + assert!(!ws.has_no_diagnostic( + DiagnosticCode::ParamTypeMismatch, + r#" + ---@type (A | B | C | D)[] + local ABCD + test(ABCD, condition) + "# + )); + + assert!(ws.has_no_diagnostic( + DiagnosticCode::ParamTypeMismatch, + r#" + ---@type (A | B)[] + local AB + test(AB, condition) + "# + )); + } } diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/return_type_mismatch_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/return_type_mismatch_test.rs index 3c07abd0f..cd6a97936 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/return_type_mismatch_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/return_type_mismatch_test.rs @@ -838,4 +838,30 @@ mod tests { "# )); } + + #[test] + fn test_recursive_alias_field_return_before_class() { + let mut ws = VirtualWorkspace::new(); + + assert!(ws.has_no_diagnostic( + DiagnosticCode::ReturnTypeMismatch, + r#" + ---@alias Recursive string | (Recursive[]) + + ---@class Container + ---@field recurse? Recursive + + local A = {} + + ---@param container? Container + ---@return Recursive + function A.return_recurse(container) + if container and container.recurse then + return container.recurse + end + return A.return_recurse(container) + end + "# + )); + } } diff --git a/crates/emmylua_code_analysis/src/semantic/generic/infer_call_generic.rs b/crates/emmylua_code_analysis/src/semantic/generic/infer_call_generic.rs index 99a2d80ad..d72a00809 100644 --- a/crates/emmylua_code_analysis/src/semantic/generic/infer_call_generic.rs +++ b/crates/emmylua_code_analysis/src/semantic/generic/infer_call_generic.rs @@ -1,11 +1,11 @@ -use emmylua_parser::{LuaAstNode, LuaDocTypeList}; -use emmylua_parser::{LuaCallExpr, LuaExpr}; +use emmylua_parser::{LuaAstNode, LuaCallExpr, LuaDocTypeList, LuaExpr, LuaIndexExpr}; use hashbrown::HashSet; use std::{ops::Deref, sync::Arc}; use crate::semantic::infer::{InferResult, infer_expr_list_types}; use crate::{ - DocTypeInferContext, FileId, GenericTplId, LuaFunctionType, LuaGenericType, LuaTypeNode, + DocTypeInferContext, FileId, GenericTplId, LuaFunctionType, LuaGenericType, LuaMemberId, + LuaTypeNode, db_index::{DbIndex, LuaType}, infer_doc_type, semantic::{ @@ -19,11 +19,14 @@ use crate::{ }, infer::InferFailReason, infer_expr, - overload_resolve::{callable_accepts_args, resolve_signature_by_args}, + member::find_member_origin_owner, + overload_resolve::{ + call_operator_self_type, callable_accepts_args, resolve_signature_by_args, + }, }, }; use crate::{ - LuaMemberOwner, LuaSemanticDeclId, LuaTypeOwner, SemanticDeclLevel, TypeVisitTrait, + LuaMemberOwner, LuaSemanticDeclId, SemanticDeclLevel, TypeVisitTrait, collect_callable_overload_groups, infer_node_semantic_decl, tpl_pattern_match_args_skip_unknown, }; @@ -87,12 +90,9 @@ pub fn build_call_generic_substitutor( } } - let self_type = if func.any_nested_type(|ty| matches!(ty, LuaType::SelfInfer)) { - infer_self_type(db, cache, call_expr, &substitutor) - } else { - None - }; - if let Some(self_type) = self_type { + if func.any_nested_type(|ty| matches!(ty, LuaType::SelfInfer)) + && let Some(self_type) = instantiate_call_self_type(db, cache, call_expr, &substitutor) + { substitutor.add_self_type(self_type); } @@ -436,8 +436,12 @@ fn infer_generic_types_from_call( (false, true) => { if let Some(self_param) = func_params.first().cloned() && self_param.contains_tpl_node() - && let Some(self_type) = - infer_self_type(context.db, context.cache, call_expr, context.substitutor) + && let Some(self_type) = instantiate_call_self_type( + context.db, + context.cache, + call_expr, + context.substitutor, + ) { // 点定义被冒号调用时, 隐式 self 仍然会传给第一个参数. tpl_pattern_match(context, &self_param, &self_type)?; @@ -535,13 +539,92 @@ fn infer_generic_types_from_call( Ok(()) } -pub(crate) fn infer_self_type( +/// 解析调用点 self 的实际类型 (不做泛型实参展开). +pub(crate) fn resolve_call_self_type( + db: &DbIndex, + cache: &mut LuaInferCache, + call_expr: &LuaCallExpr, +) -> Option { + match call_expr.get_prefix_expr()? { + LuaExpr::IndexExpr(index) => { + // 点调用可调用成员时 self 是 callee (例如 Mod.Factory()); + // 冒号调用则隐式 self 仍是 receiver (例如 owner:run()). + if !call_expr.is_colon_call() + && let Ok(callee_ty) = infer_expr(db, cache, LuaExpr::IndexExpr(index.clone())) + && let Some(self_ty) = call_operator_self_type(db, &callee_ty) + { + return Some(self_ty); + } + + // 成员赋值链上的定义点 prefix (例如 A.foo = B.foo) + if let Some(LuaSemanticDeclId::Member(member_id)) = infer_node_semantic_decl( + db, + cache, + index.syntax().clone(), + SemanticDeclLevel::default(), + ) && let Some(LuaSemanticDeclId::Member(origin_id)) = + find_member_origin_owner(db, cache, member_id) + && let Some(ty) = infer_member_index_prefix_type(db, cache, origin_id) + { + return Some(ty); + } + + let self_expr = index.get_prefix_expr()?; + Some(infer_expr(db, cache, self_expr).unwrap_or(LuaType::SelfInfer)) + } + LuaExpr::NameExpr(name) => { + if let Ok(name_ty) = infer_expr(db, cache, LuaExpr::NameExpr(name.clone())) + && let Some(self_ty) = call_operator_self_type(db, &name_ty) + { + return Some(self_ty); + } + + let LuaSemanticDeclId::Member(member_id) = infer_node_semantic_decl( + db, + cache, + name.syntax().clone(), + SemanticDeclLevel::default(), + )? + else { + return None; + }; + + if let Some(LuaSemanticDeclId::Member(origin_id)) = + find_member_origin_owner(db, cache, member_id) + && let Some(ty) = infer_member_index_prefix_type(db, cache, origin_id) + { + return Some(ty); + } + + if let Some(LuaMemberOwner::Type(id)) = + db.get_member_index().get_current_owner(&member_id) + { + return Some(LuaType::Ref(id.clone())); + } + + None + } + _ => None, + } +} + +/// 解析并实例化 self +pub(crate) fn instantiate_call_self_type( db: &DbIndex, cache: &mut LuaInferCache, call_expr: &LuaCallExpr, call_substitutor: &TypeSubstitutor, ) -> Option { - let build_self_type = |self_type: &LuaType| match self_type { + let raw = resolve_call_self_type(db, cache, call_expr)?; + Some(expand_self_generic_params(db, &raw, call_substitutor)) +} + +fn expand_self_generic_params( + db: &DbIndex, + self_type: &LuaType, + call_substitutor: &TypeSubstitutor, +) -> LuaType { + match self_type { LuaType::Def(id) | LuaType::Ref(id) => match db.get_type_index().get_generic_params(id) { Some(generic) => { let mut params = Vec::with_capacity(generic.len()); @@ -576,50 +659,22 @@ pub(crate) fn infer_self_type( None => self_type.clone(), }, _ => self_type.clone(), - }; - - let prefix_expr = call_expr.get_prefix_expr()?; - match prefix_expr { - LuaExpr::IndexExpr(index) => { - let self_expr = index.get_prefix_expr()?; - let self_type = infer_expr(db, cache, self_expr).ok()?; - let self_type = build_self_type(&self_type); - return Some(self_type); - } - LuaExpr::NameExpr(name) => { - let semantic_decl_id = infer_node_semantic_decl( - db, - cache, - name.syntax().clone(), - SemanticDeclLevel::default(), - )?; - match semantic_decl_id { - LuaSemanticDeclId::Member(member_id) => { - let owner = db.get_member_index().get_current_owner(&member_id)?; - if let LuaMemberOwner::Type(id) = owner { - let typ = LuaType::Ref(id.clone()); - let self_type = build_self_type(&typ); - return Some(self_type); - } - return None; - } - LuaSemanticDeclId::LuaDecl(decl_id) => { - let typ = db - .get_type_index() - .get_type_cache(&LuaTypeOwner::Decl(decl_id)) - .map(|cache| cache.as_type()) - .unwrap_or(&LuaType::Unknown) - .clone(); - let self_type = build_self_type(&typ); - return Some(self_type); - } - _ => return None, - } - } - _ => {} } +} - None +fn infer_member_index_prefix_type( + db: &DbIndex, + cache: &mut LuaInferCache, + member_id: LuaMemberId, +) -> Option { + let root = db + .get_vfs() + .get_syntax_tree(&member_id.file_id)? + .get_red_root(); + let cur_node = member_id.get_syntax_id().to_node_from_root(&root)?; + let index_expr = LuaIndexExpr::cast(cur_node)?; + let prefix_expr = index_expr.get_prefix_expr()?; + Some(infer_expr(db, cache, prefix_expr).unwrap_or(LuaType::SelfInfer)) } fn check_expr_can_later_infer_with_doc_func( diff --git a/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/mod.rs b/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/mod.rs index facc9ec7e..f129b0c24 100644 --- a/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/mod.rs @@ -478,7 +478,6 @@ fn instantiate_generic_type( generic: &LuaGenericType, ) -> LuaType { let generic_params = generic.get_params(); - let new_params = instantiate_types(context, generic_params.iter()); let base = generic.get_base_type(); let type_decl_id = if let LuaType::Ref(id) = base { @@ -487,10 +486,32 @@ fn instantiate_generic_type( return LuaType::Unknown; }; + // todo: 我们应该建立 scope 以解决这个问题, 但暂时没想好怎么处理 owner, 所以先暂时这样. + // 只有当前 substitutor 能消费参数里的模板时, 才递归实例化泛型实参. + // 否则像 `Promise>` 这样的嵌套 alias 会在 U 尚未推导完成时被过早展开. + let should_instantiate_params = generic_params.iter().any(|param| { + param.any_type(|ty| match ty { + LuaType::TplRef(tpl) => context.substitutor.get(tpl.get_tpl_id()).is_some(), + LuaType::StrTplRef(str_tpl) => context.substitutor.get(str_tpl.get_tpl_id()).is_some(), + LuaType::SelfInfer => context.substitutor.get_self_type().is_some(), + _ => false, + }) + }); + let new_params = if should_instantiate_params { + instantiate_types(context, generic_params.iter()) + } else { + generic_params.to_vec() + }; + if !context.substitutor.check_recursion(&type_decl_id) && let Some(type_decl) = context.db.get_type_index().get_type_decl(&type_decl_id) && type_decl.is_alias() { + // 参数已经被本轮实例化过但仍有模板残留时, 保留 alias 形态等待后续推导补齐. + if should_instantiate_params && new_params.iter().any(LuaTypeNode::contains_tpl_node) { + return LuaType::Generic(LuaGenericType::new(type_decl_id, new_params).into()); + } + let new_substitutor = TypeSubstitutor::from_alias(new_params.clone(), type_decl_id.clone()); if let Some(origin) = type_decl.get_alias_origin(context.db, Some(&new_substitutor)) { return origin; diff --git a/crates/emmylua_code_analysis/src/semantic/generic/mod.rs b/crates/emmylua_code_analysis/src/semantic/generic/mod.rs index fd8028ff1..e1e33b699 100644 --- a/crates/emmylua_code_analysis/src/semantic/generic/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/generic/mod.rs @@ -6,8 +6,8 @@ mod tpl_pattern; mod type_substitutor; mod widening; -pub(crate) use infer_call_generic::infer_self_type; pub use infer_call_generic::{build_call_generic_substitutor, infer_call_generic}; +pub(crate) use infer_call_generic::{instantiate_call_self_type, resolve_call_self_type}; pub use instantiate_type::get_keyof_members; pub use instantiate_type::*; pub use tpl_context::TplContext; diff --git a/crates/emmylua_code_analysis/src/semantic/generic/test.rs b/crates/emmylua_code_analysis/src/semantic/generic/test.rs index 56389443a..a0990f7ba 100644 --- a/crates/emmylua_code_analysis/src/semantic/generic/test.rs +++ b/crates/emmylua_code_analysis/src/semantic/generic/test.rs @@ -396,4 +396,123 @@ result = { let expected = ws.ty("string[]"); assert_eq!(ty, expected); } + + #[test] + fn test_colon_call_infers_generic_self_and_callback_return() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class Promise + local M = {} + + ---@alias Unwrap T extends Promise and U or T + + ---@generic T + ---@return Promise + function M.new() return M end + + ---@generic U + ---@param on_resolved fun(value: T): U + ---@return Promise> + function M:then1(on_resolved) return self end + + p1 = M.new():then1(function() + return {} ---@as Promise + end) + + "#, + ); + + let expected = ws.ty("Promise"); + assert_eq!(ws.expr_ty("p1"), expected); + // assert_eq!(ws.expr_ty("p2"), expected); + } + + #[test] + fn test_simple_alias_param_still_infers_function_generic() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@alias Id T + + ---@generic T + ---@param value Id + ---@return T + function id(value) + end + + result = id("value") + "#, + ); + + assert_eq!(ws.expr_ty("result"), ws.ty("string")); + } + + #[test] + fn test_function_and_alias_generic_same_name_do_not_collide() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@alias Id U + + ---@generic U + ---@param value Id + ---@return U + function id(value) + end + + result = id("value") + "#, + ); + + assert_eq!(ws.expr_ty("result"), ws.ty("string")); + } + + #[test] + fn test_nested_callback_return_alias_waits_for_function_generic() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class Promise + local M = {} + + ---@alias Unwrap T extends Promise and U or T + ---@alias Awaited Unwrap + + ---@generic T + ---@return Promise + function M.new() return M end + + ---@generic U + ---@param on_resolved fun(value: T): U + ---@return Promise> + function M:then_nested(on_resolved) return self end + + result = M.new():then_nested(function() + return {} ---@as Promise + end) + "#, + ); + + assert_eq!(ws.expr_ty("result"), ws.ty("Promise")); + } + + #[test] + fn test_nested_function_generic_shadows_outer_function_generic() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@generic T + ---@param value T + ---@return fun(value: T): T + function make(value) + end + + local fn = make("outer") + result = fn(1) + "#, + ); + + assert_eq!(ws.expr_ty("result"), ws.ty("integer")); + } } diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs index 3ec4bb745..68b1c0a49 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_call/mod.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use emmylua_parser::{LuaAstNode, LuaCallExpr, LuaExpr, LuaIndexExpr, LuaSyntaxKind}; +use emmylua_parser::{LuaAstNode, LuaCallExpr, LuaExpr, LuaSyntaxKind}; use rowan::TextRange; use super::{ @@ -9,17 +9,14 @@ use super::{ }; use crate::{ AsyncState, CacheEntry, DbIndex, InFiled, LuaFunctionType, LuaGenericType, LuaInstanceType, - LuaIntersectionType, LuaOperatorMetaMethod, LuaOperatorOwner, LuaSemanticDeclId, LuaSignature, - LuaSignatureId, LuaType, LuaTypeDeclId, LuaUnionType, SemanticDeclLevel, TypeOps, - TypeVisitTrait, VariadicType, + LuaIntersectionType, LuaOperatorMetaMethod, LuaOperatorOwner, LuaSignature, LuaSignatureId, + LuaType, LuaTypeDeclId, LuaTypeNode, LuaUnionType, TypeOps, TypeVisitTrait, VariadicType, }; use crate::{ InferGuardRef, semantic::{ - generic::{TypeSubstitutor, infer_self_type}, + generic::{TypeSubstitutor, instantiate_call_self_type}, infer::narrow::get_type_at_call_expr_inline_cast, - infer_node_semantic_decl, - member::find_member_origin_owner, overload_resolve::{collect_callable_overload_groups, match_callable_by_arg_types}, }, }; @@ -332,22 +329,9 @@ fn infer_type_doc_function( has_generic_tpl }; - if has_generic_tpl { + if has_generic_tpl || f.any_nested_type(|ty| matches!(ty, LuaType::SelfInfer)) { let result = infer_call_generic(db, cache, &f, call_expr.clone())?; overloads.push(Arc::new(result)); - } else if f.contain_self() { - let mut substitutor = TypeSubstitutor::new(); - if let Some(self_type) = infer_self_type(db, cache, &call_expr, &substitutor) { - substitutor.add_self_type(self_type); - let func_type = LuaType::DocFunction(f.clone()); - if let LuaType::DocFunction(f) = - instantiate_type_generic(db, &func_type, &substitutor) - { - overloads.push(f); - } - } else { - overloads.push(f.clone()); - } } else { overloads.push(f.clone()); } @@ -678,7 +662,8 @@ fn unwrapp_return_type( } LuaType::SelfInfer => { let substitutor = TypeSubstitutor::new(); - if let Some(self_type) = infer_self_type(db, cache, &call_expr, &substitutor) { + if let Some(self_type) = instantiate_call_self_type(db, cache, &call_expr, &substitutor) + { return Ok(self_type); } } @@ -815,67 +800,6 @@ fn signature_is_generic( } } -/// 推断调用表达式中用于 self 参数的类型. -pub fn infer_call_self_type( - db: &DbIndex, - cache: &mut LuaInferCache, - call_expr: &LuaCallExpr, -) -> Option { - match call_expr.get_prefix_expr()? { - LuaExpr::IndexExpr(index_expr) => { - let decl = infer_node_semantic_decl( - db, - cache, - index_expr.syntax().clone(), - SemanticDeclLevel::default(), - )?; - - if let LuaSemanticDeclId::Member(member_id) = decl - && let Some(LuaSemanticDeclId::Member(member_id)) = - find_member_origin_owner(db, cache, member_id) - { - let root = db - .get_vfs() - .get_syntax_tree(&member_id.file_id)? - .get_red_root(); - let cur_node = member_id.get_syntax_id().to_node_from_root(&root)?; - let index_expr = LuaIndexExpr::cast(cur_node)?; - - return index_expr.get_prefix_expr().map(|prefix_expr| { - infer_expr(db, cache, prefix_expr).unwrap_or(LuaType::SelfInfer) - }); - } - - index_expr - .get_prefix_expr() - .map(|prefix_expr| infer_expr(db, cache, prefix_expr).unwrap_or(LuaType::SelfInfer)) - } - LuaExpr::NameExpr(name_expr) => { - let decl = infer_node_semantic_decl( - db, - cache, - name_expr.syntax().clone(), - SemanticDeclLevel::default(), - )?; - if let LuaSemanticDeclId::Member(member_id) = decl { - let root = db - .get_vfs() - .get_syntax_tree(&member_id.file_id)? - .get_red_root(); - let cur_node = member_id.get_syntax_id().to_node_from_root(&root)?; - let index_expr = LuaIndexExpr::cast(cur_node)?; - - return index_expr.get_prefix_expr().map(|prefix_expr| { - infer_expr(db, cache, prefix_expr).unwrap_or(LuaType::SelfInfer) - }); - } - - None - } - _ => None, - } -} - #[cfg(test)] mod tests { use crate::{ diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_index/mod.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_index/mod.rs index 9cd904850..747a375b1 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_index/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_index/mod.rs @@ -140,9 +140,9 @@ pub fn get_index_expr_var_ref_id( cache: &mut LuaInferCache, index_expr: &LuaIndexExpr, ) -> Option { - let access_path = match index_expr.get_access_path() { - Some(path) => ArcIntern::new(SmolStr::new(&path)), - None => return None, + let access_path = { + let path = index_expr.get_access_path()?; + ArcIntern::new(SmolStr::new(&path)) }; let mut prefix_expr = index_expr.get_prefix_expr()?; diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_table.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_table.rs index 12e44a895..31171a94a 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_table.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_table.rs @@ -258,20 +258,50 @@ fn infer_table_type_by_callee( /// 移除掉一些非`table`类型 fn union_remove_non_table_type(db: &DbIndex, union: &Arc) -> LuaType { - let mut result = None; - for typ in union.into_set().into_iter() { - match typ { - LuaType::Signature(_) | LuaType::DocFunction(_) => {} - _ if typ.is_string() || typ.is_number() || typ.is_boolean() => {} - _ => { - result = Some(match result { - Some(result) => TypeOps::Union.apply(db, &result, &typ), - None => typ, - }); + let result = TypeOps::union_all( + db, + union + .into_vec() + .into_iter() + .filter(|typ| may_accept_table_literal(db, typ, 0)), + ); + if matches!(result, LuaType::Never) { + LuaType::Unknown + } else { + result + } +} + +fn may_accept_table_literal(db: &DbIndex, typ: &LuaType, depth: usize) -> bool { + if depth >= 16 { + return false; + } + + match typ { + LuaType::Ref(type_id) => { + let Some(type_decl) = db.get_type_index().get_type_decl(type_id) else { + return true; + }; + if !type_decl.is_alias() { + return true; } + + type_decl + .get_alias_ref() + .is_none_or(|alias_ref| may_accept_table_literal(db, alias_ref, depth + 1)) } + LuaType::Union(union) => union + .into_vec() + .iter() + .any(|typ| may_accept_table_literal(db, typ, depth + 1)), + LuaType::MultiLineUnion(union) => union + .get_unions() + .iter() + .any(|(typ, _)| may_accept_table_literal(db, typ, depth + 1)), + LuaType::Nil | LuaType::Never => false, + _ if typ.is_function() || typ.is_string() || typ.is_number() || typ.is_boolean() => false, + _ => true, } - result.unwrap_or(LuaType::Unknown) } fn infer_table_field_type_by_parent( diff --git a/crates/emmylua_code_analysis/src/semantic/infer/mod.rs b/crates/emmylua_code_analysis/src/semantic/infer/mod.rs index 14fa612f6..46a4ba32f 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/mod.rs @@ -17,7 +17,7 @@ use emmylua_parser::{ }; use infer_binary::infer_binary_expr; use infer_call::infer_call_expr; -pub use infer_call::{infer_call_expr_func, infer_call_self_type}; +pub use infer_call::infer_call_expr_func; pub use infer_doc_type::{DocTypeInferContext, infer_doc_type}; pub use infer_fail_reason::InferFailReason; pub use infer_index::infer_index_expr; diff --git a/crates/emmylua_code_analysis/src/semantic/member/find_members.rs b/crates/emmylua_code_analysis/src/semantic/member/find_members.rs index 1e089d6e3..d3e26c881 100644 --- a/crates/emmylua_code_analysis/src/semantic/member/find_members.rs +++ b/crates/emmylua_code_analysis/src/semantic/member/find_members.rs @@ -663,7 +663,7 @@ fn find_namespace_members( overload_index: None, }); } else { - let ns_type = LuaType::Namespace(SmolStr::new(format!("{}.{}", ns, &name)).into()); + let ns_type = LuaType::Namespace(SmolStr::new(format!("{}.{}", ns, name)).into()); members.push(LuaMemberInfo { property_owner_id: None, key: member_key, diff --git a/crates/emmylua_code_analysis/src/semantic/mod.rs b/crates/emmylua_code_analysis/src/semantic/mod.rs index badd86408..5ec790fb7 100644 --- a/crates/emmylua_code_analysis/src/semantic/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/mod.rs @@ -20,11 +20,9 @@ use emmylua_parser::{ LuaCallExpr, LuaChunk, LuaExpr, LuaIndexExpr, LuaIndexKey, LuaParseError, LuaSyntaxNode, LuaSyntaxToken, LuaTableExpr, }; +use generic::resolve_call_self_type; pub use infer::infer_index_expr; -use infer::{ - apply_assignment_target_casts, infer_bind_value_type, infer_call_self_type, - infer_expr_list_types, -}; +use infer::{apply_assignment_target_casts, infer_bind_value_type, infer_expr_list_types}; pub use infer::{infer_table_field_value_should_be, infer_table_should_be}; use lsp_types::Uri; pub use member::LuaMemberInfo; @@ -350,8 +348,8 @@ impl<'a> SemanticModel<'a> { find_member_origin_owner(self.db, &mut self.infer_cache.borrow_mut(), member_id) } - pub fn infer_call_self_type(&self, call_expr: &LuaCallExpr) -> Option { - infer_call_self_type(self.db, &mut self.infer_cache.borrow_mut(), call_expr) + pub fn resolve_call_self_type(&self, call_expr: &LuaCallExpr) -> Option { + resolve_call_self_type(self.db, &mut self.infer_cache.borrow_mut(), call_expr) } pub fn get_index_decl_type(&self, index_expr: LuaIndexExpr) -> Option { diff --git a/crates/emmylua_code_analysis/src/semantic/overload_resolve/collect_overloads.rs b/crates/emmylua_code_analysis/src/semantic/overload_resolve/collect_overloads.rs index 3f206e68a..d5d6a3a81 100644 --- a/crates/emmylua_code_analysis/src/semantic/overload_resolve/collect_overloads.rs +++ b/crates/emmylua_code_analysis/src/semantic/overload_resolve/collect_overloads.rs @@ -2,7 +2,7 @@ use hashbrown::HashSet; use std::sync::Arc; use crate::{ - DbIndex, LuaOperatorMetaMethod, LuaOperatorOwner, LuaTypeDeclId, + DbIndex, LuaOperatorMetaMethod, LuaOperatorOwner, LuaTypeDeclId, LuaUnionType, db_index::{LuaFunctionType, LuaType}, semantic::{ generic::{TypeSubstitutor, instantiate_type_generic}, @@ -166,3 +166,97 @@ fn push_call_operator_overload_group( groups.push(overloads); } } + +fn owner_has_call_operator(db: &DbIndex, owner: &LuaOperatorOwner) -> bool { + db.get_operator_index() + .get_operators(owner, LuaOperatorMetaMethod::Call) + .is_some_and(|ops| !ops.is_empty()) +} + +/// 如果类型可通过 `__call` 作为调用目标 (不含 signature/function 本身), 则返回 self. +pub(crate) fn call_operator_self_type(db: &DbIndex, ty: &LuaType) -> Option { + let mut visiting_aliases = HashSet::new(); + call_operator_self_type_inner(db, ty, &mut visiting_aliases) +} + +fn call_operator_self_type_inner( + db: &DbIndex, + ty: &LuaType, + visiting_aliases: &mut HashSet, +) -> Option { + match ty { + LuaType::Ref(type_id) | LuaType::Def(type_id) => { + call_operator_self_for_type_id(db, ty, type_id, None, visiting_aliases) + } + LuaType::Generic(generic) => { + let type_id = generic.get_base_type_id(); + let substitutor = TypeSubstitutor::from_type_array(generic.get_params().to_vec()); + call_operator_self_for_type_id(db, ty, &type_id, Some(&substitutor), visiting_aliases) + } + LuaType::Union(union) => { + let mut callable = Vec::new(); + for member in union.into_vec() { + if let Some(projected) = + call_operator_self_type_inner(db, &member, visiting_aliases) + { + callable.push(projected); + } + } + match callable.len() { + 0 => None, + 1 => callable.pop(), + _ => Some(LuaType::Union(LuaUnionType::from_vec(callable).into())), + } + } + // intersection 任一成员可调用则整体可调用 + LuaType::Intersection(intersection) => intersection + .get_types() + .iter() + .any(|member| call_operator_self_type_inner(db, member, visiting_aliases).is_some()) + .then(|| ty.clone()), + LuaType::Instance(instance) => { + call_operator_self_type_inner(db, instance.get_base(), visiting_aliases) + .map(|_| ty.clone()) + } + LuaType::TableConst(table) => { + // setmetatable 产生的 __call 挂在 metatable owner 上. + db.get_metatable_index() + .get(table) + .is_some_and(|meta_table| { + owner_has_call_operator(db, &LuaOperatorOwner::Table(meta_table.clone())) + }) + .then(|| ty.clone()) + } + _ => None, + } +} + +/// alias 的可调用性来自 origin, self 也按 origin 的可调用部分解析; 非 alias 则看自身是否挂了 __call. +fn call_operator_self_for_type_id( + db: &DbIndex, + ty: &LuaType, + type_id: &LuaTypeDeclId, + substitutor: Option<&TypeSubstitutor>, + visiting_aliases: &mut HashSet, +) -> Option { + if !visiting_aliases.insert(type_id.clone()) { + return None; + } + let Some(type_decl) = db.get_type_index().get_type_decl(type_id) else { + visiting_aliases.remove(type_id); + return None; + }; + + if let Some(origin_type) = type_decl.get_alias_origin(db, substitutor) + && let Some(projected) = call_operator_self_type_inner(db, &origin_type, visiting_aliases) + { + visiting_aliases.remove(type_id); + return Some(projected); + } + + let has_call = !type_decl.is_alias() + && !type_decl.is_enum() + && owner_has_call_operator(db, &type_id.clone().into()); + visiting_aliases.remove(type_id); + has_call.then(|| ty.clone()) +} diff --git a/crates/emmylua_code_analysis/src/semantic/overload_resolve/mod.rs b/crates/emmylua_code_analysis/src/semantic/overload_resolve/mod.rs index bd7b0cf12..654985f53 100644 --- a/crates/emmylua_code_analysis/src/semantic/overload_resolve/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/overload_resolve/mod.rs @@ -14,6 +14,7 @@ use super::{ infer::{InferCallFuncResult, InferFailReason, infer_expr_list_types, try_infer_expr_no_flow}, }; +pub(crate) use collect_overloads::call_operator_self_type; pub use collect_overloads::collect_callable_overload_groups; pub(crate) use filter_overloads::match_callable_by_arg_types; pub use filter_overloads::{filter_callable_overloads, find_callable_overload}; diff --git a/crates/emmylua_code_analysis/src/semantic/semantic_info/infer_expr_semantic_decl.rs b/crates/emmylua_code_analysis/src/semantic/semantic_info/infer_expr_semantic_decl.rs index 119d41a88..3ed6558a0 100644 --- a/crates/emmylua_code_analysis/src/semantic/semantic_info/infer_expr_semantic_decl.rs +++ b/crates/emmylua_code_analysis/src/semantic/semantic_info/infer_expr_semantic_decl.rs @@ -129,13 +129,10 @@ fn infer_require_module_semantic_decl( let first_arg = call_expr.get_args_list()?.get_args().next()?; let module_path = match first_arg { LuaExpr::LiteralExpr(literal_expr) => { - if let Some(literal_token) = literal_expr.get_literal() { - match literal_token { - LuaLiteralToken::String(string_token) => string_token.get_value(), - _ => return None, - } - } else { - return None; + let literal_token = literal_expr.get_literal()?; + match literal_token { + LuaLiteralToken::String(string_token) => string_token.get_value(), + _ => return None, } } _ => return None, diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/func_type.rs b/crates/emmylua_code_analysis/src/semantic/type_check/func_type.rs index d8c6e067b..e115d8ea5 100644 --- a/crates/emmylua_code_analysis/src/semantic/type_check/func_type.rs +++ b/crates/emmylua_code_analysis/src/semantic/type_check/func_type.rs @@ -89,10 +89,11 @@ fn check_doc_func_type_compact_for_params( let compact_param_type = &compact_param.1; if let (Some(source_type), Some(compact_type)) = (source_param_type, compact_param_type) { + // 函数赋值时, 被赋值函数必须接受目标类型可能传入的所有参数. match check_general_type_compact( context, - source_type, compact_type, + source_type, check_guard.next_level()?, ) { Ok(()) => {} @@ -127,8 +128,8 @@ fn check_doc_func_type_compact_for_varargs( if let Some(compact_param_type) = compact_param_type { check_general_type_compact( context, - varargs_type, compact_param_type, + varargs_type, check_guard.next_level()?, )?; } diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/generic_type.rs b/crates/emmylua_code_analysis/src/semantic/type_check/generic_type.rs index 65509d5ce..0203bfcea 100644 --- a/crates/emmylua_code_analysis/src/semantic/type_check/generic_type.rs +++ b/crates/emmylua_code_analysis/src/semantic/type_check/generic_type.rs @@ -18,6 +18,31 @@ pub fn check_generic_type_compact( check_guard: TypeCheckGuard, ) -> TypeCheckResult { if let Some(alias_origin) = instantiate_generic_alias_origin(context.db, source_generic) { + // Generic alias 期望类型需要接受实际 union 的每个分支. + if let LuaType::Union(compact_union) = compact_type { + for compact_sub_type in compact_union.into_vec() { + check_generic_type_compact( + context, + source_generic, + &compact_sub_type, + check_guard.next_level()?, + )?; + } + return Ok(()); + } + + // 递归 generic alias 的直接成员已经属于 alias 本身, 提前通过避免继续展开自引用. + let origin_contains_compact = match &alias_origin { + LuaType::Union(origin_union) => origin_union + .into_vec() + .iter() + .any(|origin_sub_type| origin_sub_type == compact_type), + _ => alias_origin == *compact_type, + }; + if origin_contains_compact { + return Ok(()); + } + return check_general_type_compact( context, &alias_origin, diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/ref_type.rs b/crates/emmylua_code_analysis/src/semantic/type_check/ref_type.rs index fb79a41b4..4021269c7 100644 --- a/crates/emmylua_code_analysis/src/semantic/type_check/ref_type.rs +++ b/crates/emmylua_code_analysis/src/semantic/type_check/ref_type.rs @@ -36,7 +36,32 @@ pub fn check_ref_type_compact( })?; if type_decl.is_alias() { + // Alias 期望类型需要接受实际 union 的每个分支. + if let LuaType::Union(compact_union) = compact_type { + for compact_sub_type in compact_union.into_vec() { + check_ref_type_compact( + context, + source_id, + &compact_sub_type, + check_guard.next_level()?, + )?; + } + return Ok(()); + } + if let Some(origin_type) = type_decl.get_alias_origin(context.db, None) { + // 递归 alias 的直接成员已经属于 alias 本身, 提前通过避免继续展开自引用. + let origin_contains_compact = match &origin_type { + LuaType::Union(origin_union) => origin_union + .into_vec() + .iter() + .any(|origin_sub_type| origin_sub_type == compact_type), + _ => origin_type == *compact_type, + }; + if origin_contains_compact { + return Ok(()); + } + let result = check_general_type_compact( context, &origin_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 a07aa2c7d..fb24c3244 100644 --- a/crates/emmylua_code_analysis/src/semantic/type_check/test.rs +++ b/crates/emmylua_code_analysis/src/semantic/type_check/test.rs @@ -69,6 +69,40 @@ mod test { assert!(ws.check_type(&ty_union3, &ty_union3)); } + #[test] + fn test_recursive_alias_accepts_expanded_origin_members() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@alias Recursive string | (Recursive[]) + "#, + ); + + let recursive_ty = ws.ty("Recursive"); + let expanded_ty = ws.ty("string | Recursive[]"); + let invalid_ty = ws.ty("boolean | Recursive[]"); + + assert!(ws.check_type(&recursive_ty, &expanded_ty)); + assert!(!ws.check_type(&recursive_ty, &invalid_ty)); + } + + #[test] + fn test_generic_recursive_alias_accepts_expanded_origin_members() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@alias Recursive T | (Recursive[]) + "#, + ); + + let recursive_ty = ws.ty("Recursive"); + let expanded_ty = ws.ty("string | Recursive[]"); + let invalid_ty = ws.ty("boolean | Recursive[]"); + + assert!(ws.check_type(&recursive_ty, &expanded_ty)); + assert!(!ws.check_type(&recursive_ty, &invalid_ty)); + } + #[test] fn test_object_types() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_ls/src/context/file_diagnostic.rs b/crates/emmylua_ls/src/context/file_diagnostic.rs index 521d391cf..ced8826b5 100644 --- a/crates/emmylua_ls/src/context/file_diagnostic.rs +++ b/crates/emmylua_ls/src/context/file_diagnostic.rs @@ -124,7 +124,7 @@ impl FileDiagnostic { #[allow(unused)] pub async fn cancel_all(&self) { let mut tokens = self.diagnostic_tokens.lock().await; - for (_, token) in tokens.iter() { + for token in tokens.values() { token.cancel(); } tokens.clear(); diff --git a/crates/emmylua_ls/src/handlers/call_hierarchy/build_call_hierarchy.rs b/crates/emmylua_ls/src/handlers/call_hierarchy/build_call_hierarchy.rs index 892086c80..b8996c525 100644 --- a/crates/emmylua_ls/src/handlers/call_hierarchy/build_call_hierarchy.rs +++ b/crates/emmylua_ls/src/handlers/call_hierarchy/build_call_hierarchy.rs @@ -103,7 +103,7 @@ pub fn build_incoming_hierarchy( search_decl_references(semantic_model, compilation, decl_id, &mut locations); } LuaSemanticDeclId::Member(member_id) => { - search_member_references(semantic_model, compilation, member_id, &mut locations); + search_member_references(semantic_model, compilation, member_id, true, &mut locations); } _ => return None, } diff --git a/crates/emmylua_ls/src/handlers/code_lens/resolve_code_lens.rs b/crates/emmylua_ls/src/handlers/code_lens/resolve_code_lens.rs index 0418e6d1b..e0f2541a7 100644 --- a/crates/emmylua_ls/src/handlers/code_lens/resolve_code_lens.rs +++ b/crates/emmylua_ls/src/handlers/code_lens/resolve_code_lens.rs @@ -26,7 +26,7 @@ pub fn resolve_code_lens( let file_id = member_id.file_id; let semantic_model = compilation.get_semantic_model(file_id)?; let mut results = Vec::new(); - search_member_references(&semantic_model, compilation, member_id, &mut results); + search_member_references(&semantic_model, compilation, member_id, true, &mut results); let mut ref_count = results.len(); ref_count = ref_count.saturating_sub(1); let uri = semantic_model.get_document().get_uri(); diff --git a/crates/emmylua_ls/src/handlers/references/mod.rs b/crates/emmylua_ls/src/handlers/references/mod.rs index 15e50e63e..e591b5432 100644 --- a/crates/emmylua_ls/src/handlers/references/mod.rs +++ b/crates/emmylua_ls/src/handlers/references/mod.rs @@ -23,13 +23,19 @@ pub async fn on_references_handler( let file_id = analysis.get_file_id(&uri)?; let position = params.text_document_position.position; - references(&analysis, file_id, position) + references( + &analysis, + file_id, + position, + params.context.include_declaration, + ) } pub fn references( analysis: &EmmyLuaAnalysis, file_id: FileId, position: Position, + include_declaration: bool, ) -> Option> { let semantic_model = analysis.compilation.get_semantic_model(file_id)?; if !semantic_model.get_emmyrc().references.enable { @@ -60,7 +66,12 @@ pub fn references( } }; - search_references(&semantic_model, &analysis.compilation, token) + search_references( + &semantic_model, + &analysis.compilation, + token, + include_declaration, + ) } pub struct ReferencesCapabilities; diff --git a/crates/emmylua_ls/src/handlers/references/reference_searcher.rs b/crates/emmylua_ls/src/handlers/references/reference_searcher.rs index 9528c9840..0a0842c54 100644 --- a/crates/emmylua_ls/src/handlers/references/reference_searcher.rs +++ b/crates/emmylua_ls/src/handlers/references/reference_searcher.rs @@ -14,19 +14,37 @@ use emmylua_parser::{ }; use lsp_types::Location; -#[derive(Default)] struct ReferenceSearchContext { visited_module_exports: HashSet, visited_semantic_ids: HashSet, + include_declaration: bool, +} + +impl ReferenceSearchContext { + fn new(include_declaration: bool) -> Self { + Self { + visited_module_exports: HashSet::new(), + visited_semantic_ids: HashSet::new(), + include_declaration, + } + } } pub fn search_references( semantic_model: &SemanticModel, compilation: &LuaCompilation, token: LuaSyntaxToken, + include_declaration: bool, ) -> Option> { let mut result = Vec::new(); - if search_label_references(semantic_model, token.clone(), &mut result).is_some() { + if search_label_references( + semantic_model, + token.clone(), + include_declaration, + &mut result, + ) + .is_some() + { return Some(result); } @@ -40,15 +58,26 @@ pub fn search_references( compilation, decl_id, token, + include_declaration, &mut result, ); } LuaSemanticDeclId::Member(member_id) => { - let _ = - search_member_references(semantic_model, compilation, member_id, &mut result); + let _ = search_member_references( + semantic_model, + compilation, + member_id, + include_declaration, + &mut result, + ); } LuaSemanticDeclId::TypeDecl(type_decl_id) => { - let _ = search_type_decl_references(semantic_model, type_decl_id, &mut result); + let _ = search_type_decl_references( + semantic_model, + type_decl_id, + include_declaration, + &mut result, + ); } _ => {} } @@ -68,6 +97,7 @@ pub fn search_references( fn search_label_references( semantic_model: &SemanticModel, token: LuaSyntaxToken, + include_declaration: bool, result: &mut Vec, ) -> Option<()> { let name_token = LuaNameToken::cast(token.clone())?; @@ -78,12 +108,23 @@ fn search_label_references( let closure_id = LuaClosureId::from_node(&parent); let label_name = name_token.get_name_text(); - let ranges = semantic_model - .get_db() - .get_reference_index() - .get_label_references(&semantic_model.get_file_id(), closure_id, label_name)?; + let reference_index = semantic_model.get_db().get_reference_index(); + let ranges = reference_index.get_label_references( + &semantic_model.get_file_id(), + closure_id, + label_name, + )?; + let declaration = if include_declaration { + None + } else { + reference_index.get_label_definition(&semantic_model.get_file_id(), closure_id, label_name) + }; let document = semantic_model.get_document(); for range in ranges { + if declaration == Some(range) { + continue; + } + let location = document.to_lsp_location(range)?; result.push(location); } @@ -96,9 +137,10 @@ pub fn search_decl_references_with_token( compilation: &LuaCompilation, decl_id: LuaDeclId, token: LuaSyntaxToken, + include_declaration: bool, result: &mut Vec, ) -> Option<()> { - let mut ctx = ReferenceSearchContext::default(); + let mut ctx = ReferenceSearchContext::new(include_declaration); let mut semantic_cache = HashMap::new(); let previous_result = result.len(); let ret = search_semantic_references_with_ctx( @@ -136,7 +178,7 @@ pub fn search_decl_references( decl_id: LuaDeclId, result: &mut Vec, ) -> Option<()> { - let mut ctx = ReferenceSearchContext::default(); + let mut ctx = ReferenceSearchContext::new(true); let mut semantic_cache = HashMap::new(); search_semantic_references_with_ctx( &mut ctx, @@ -166,8 +208,9 @@ fn search_decl_references_with_ctx<'a>( .get_reference_index() .get_decl_references(&decl_id.file_id, &decl_id)?; let document = semantic_model.get_document(); - // 加入自己 - if let Some(location) = document.to_lsp_location(decl.get_range()) { + if ctx.include_declaration + && let Some(location) = document.to_lsp_location(decl.get_range()) + { result.push(location); } let typ = semantic_model.get_type(decl.get_id().into()); @@ -181,6 +224,10 @@ fn search_decl_references_with_ctx<'a>( ); for decl_ref in &decl_refs.cells { + if !ctx.include_declaration && decl_ref.range == decl.get_range() { + continue; + } + let location = document.to_lsp_location(decl_ref.range)?; result.push(location); if should_follow_value_alias { @@ -206,6 +253,13 @@ fn search_decl_references_with_ctx<'a>( .get_reference_index() .get_global_references(name)?; for in_filed_syntax_id in global_references { + if !ctx.include_declaration + && in_filed_syntax_id.file_id == decl.get_file_id() + && in_filed_syntax_id.value.get_range() == decl.get_range() + { + continue; + } + let document = semantic_model.get_document_by_file_id(in_filed_syntax_id.file_id)?; let location = document.to_lsp_location(in_filed_syntax_id.value.get_range())?; result.push(location); @@ -219,9 +273,10 @@ pub fn search_member_references( _semantic_model: &SemanticModel, compilation: &LuaCompilation, member_id: LuaMemberId, + include_declaration: bool, result: &mut Vec, ) -> Option<()> { - let mut ctx = ReferenceSearchContext::default(); + let mut ctx = ReferenceSearchContext::new(include_declaration); let mut semantic_cache = HashMap::new(); search_semantic_references_with_ctx( &mut ctx, @@ -264,6 +319,13 @@ fn search_member_references_with_ctx<'a>( ) { let document = reference_semantic_model.get_document(); let range = in_filed_syntax_id.value.get_range(); + if !ctx.include_declaration + && in_filed_syntax_id.file_id == member.get_file_id() + && range == member.get_range() + { + continue; + } + let location = document.to_lsp_location(range)?; result.push(location); let _ = search_member_secondary_references( @@ -376,9 +438,11 @@ fn search_member_secondary_references( let var = vars.get(idx)?; let decl_id = LuaDeclId::new(semantic_model.get_file_id(), var.get_position()); enqueue_semantic_id(ctx, worklist, LuaSemanticDeclId::LuaDecl(decl_id)); - let document = semantic_model.get_document(); - let range = document.to_lsp_location(var.get_range())?; - result.push(range); + if ctx.include_declaration { + let document = semantic_model.get_document(); + let range = document.to_lsp_location(var.get_range())?; + result.push(range); + } } LuaAst::LuaLocalStat(local_stat) => { let local_names = local_stat.get_local_name_list().collect::>(); @@ -387,9 +451,11 @@ fn search_member_secondary_references( let name = local_names.get(idx)?; let decl_id = LuaDeclId::new(semantic_model.get_file_id(), name.get_position()); enqueue_semantic_id(ctx, worklist, LuaSemanticDeclId::LuaDecl(decl_id)); - let document = semantic_model.get_document(); - let range = document.to_lsp_location(name.get_range())?; - result.push(range); + if ctx.include_declaration { + let document = semantic_model.get_document(); + let range = document.to_lsp_location(name.get_range())?; + result.push(range); + } } _ => {} } @@ -452,14 +518,32 @@ fn fuzzy_search_references( fn search_type_decl_references( semantic_model: &SemanticModel, type_decl_id: LuaTypeDeclId, + include_declaration: bool, result: &mut Vec, ) -> Option<()> { let refs = semantic_model .get_db() .get_reference_index() .get_type_references(&type_decl_id)?; + let type_decl = if include_declaration { + None + } else { + semantic_model + .get_db() + .get_type_index() + .get_type_decl(&type_decl_id) + }; let mut document_cache = HashMap::new(); for in_filed_reference_range in refs { + if type_decl.is_some_and(|type_decl| { + type_decl.get_locations().iter().any(|location| { + location.file_id == in_filed_reference_range.file_id + && location.range == in_filed_reference_range.value + }) + }) { + continue; + } + let document = if let Some(document) = document_cache.get(&in_filed_reference_range.file_id) { document @@ -653,52 +737,6 @@ fn find_require_call_binding_semantic( None } -#[allow(unused)] -fn filter_duplicate_and_covered_locations(locations: Vec) -> Vec { - if locations.is_empty() { - return locations; - } - let mut sorted_locations = locations; - sorted_locations.sort_by(|a, b| { - a.uri - .to_string() - .cmp(&b.uri.to_string()) - .then_with(|| a.range.start.line.cmp(&b.range.start.line)) - .then_with(|| b.range.end.line.cmp(&a.range.end.line)) - }); - - let mut result = Vec::new(); - let mut seen_lines_by_uri: HashMap> = HashMap::new(); - - for location in sorted_locations { - let uri_str = location.uri.to_string(); - let seen_lines = seen_lines_by_uri.entry(uri_str).or_default(); - - let start_line = location.range.start.line; - let end_line = location.range.end.line; - - let is_covered = (start_line..=end_line).any(|line| seen_lines.contains(&line)); - - if !is_covered { - for line in start_line..=end_line { - seen_lines.insert(line); - } - result.push(location); - } - } - - // 最终按位置排序 - result.sort_by(|a, b| { - a.uri - .to_string() - .cmp(&b.uri.to_string()) - .then_with(|| a.range.start.line.cmp(&b.range.start.line)) - .then_with(|| a.range.start.character.cmp(&b.range.start.character)) - }); - - result -} - fn enqueue_semantic_id( ctx: &mut ReferenceSearchContext, worklist: &mut Vec, diff --git a/crates/emmylua_ls/src/handlers/rename/rename_decl.rs b/crates/emmylua_ls/src/handlers/rename/rename_decl.rs index 191807747..33f5d29d7 100644 --- a/crates/emmylua_ls/src/handlers/rename/rename_decl.rs +++ b/crates/emmylua_ls/src/handlers/rename/rename_decl.rs @@ -108,10 +108,9 @@ fn rename_doc_param( let closure_expr = param_node.ancestors::().next()?; let comments = if let Some(table_field) = closure_expr.get_parent::() { table_field.get_comments() - } else if let Some(stat) = closure_expr.ancestors::().next() { - stat.get_comments() } else { - return None; + let stat = closure_expr.ancestors::().next()?; + stat.get_comments() }; let document = semantic_model.get_document(); diff --git a/crates/emmylua_ls/src/handlers/test/completion_test.rs b/crates/emmylua_ls/src/handlers/test/completion_test.rs index df395207e..90f140330 100644 --- a/crates/emmylua_ls/src/handlers/test/completion_test.rs +++ b/crates/emmylua_ls/src/handlers/test/completion_test.rs @@ -446,6 +446,67 @@ mod tests { Ok(()) } + #[gtest] + fn test_union_array_literal_param_completion() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new_with_init_std_lib(); + check!(ws.check_completion( + r#" + ---@alias Name 'foo1' | 'bar' | 'baz' + + ---@param name Name | Name[] + function foo(name) end + + foo({'foo'}) + "#, + vec![ + VirtualCompletionItem { + label: "foo1".to_string(), + kind: CompletionItemKind::ENUM_MEMBER, + ..Default::default() + }, + VirtualCompletionItem { + label: "bar".to_string(), + kind: CompletionItemKind::ENUM_MEMBER, + ..Default::default() + }, + VirtualCompletionItem { + label: "baz".to_string(), + kind: CompletionItemKind::ENUM_MEMBER, + ..Default::default() + }, + ], + )); + Ok(()) + } + + #[gtest] + fn test_recursive_union_array_literal_param_completion() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new_with_init_std_lib(); + check!(ws.check_completion( + r#" + ---@alias RecursiveName RecursiveName | 'foo1' | 'bar' + + ---@param name RecursiveName | RecursiveName[] + function foo(name) end + + foo({'foo1'}) + "#, + vec![ + VirtualCompletionItem { + label: "foo1".to_string(), + kind: CompletionItemKind::ENUM_MEMBER, + ..Default::default() + }, + VirtualCompletionItem { + label: "bar".to_string(), + kind: CompletionItemKind::ENUM_MEMBER, + ..Default::default() + }, + ], + )); + Ok(()) + } + #[gtest] fn test_enum() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new_with_init_std_lib(); diff --git a/crates/emmylua_ls/src/handlers/test/hover_test.rs b/crates/emmylua_ls/src/handlers/test/hover_test.rs index 88c63b3d0..ea542d971 100644 --- a/crates/emmylua_ls/src/handlers/test/hover_test.rs +++ b/crates/emmylua_ls/src/handlers/test/hover_test.rs @@ -361,6 +361,18 @@ mod tests { }, )); + check!( + ws.check_hover( + r#" + ---@alias Copy { readonly [K in keyof T]?: T[K] } + "#, + VirtualHoverResult { + value: "```lua\n(alias) Copy = { readonly [K in keyof T]?: T[K]; }\n```" + .to_string(), + }, + ) + ); + check!( ws.check_hover( r#" diff --git a/crates/emmylua_ls/src/handlers/test/references_test.rs b/crates/emmylua_ls/src/handlers/test/references_test.rs index ccb5aa839..564442865 100644 --- a/crates/emmylua_ls/src/handlers/test/references_test.rs +++ b/crates/emmylua_ls/src/handlers/test/references_test.rs @@ -244,6 +244,31 @@ mod tests { Ok(()) } + #[gtest] + fn test_references_excludes_declaration_when_requested() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + + let (main_content, position) = check!(ProviderVirtualWorkspace::handle_file_content( + r#" + local target = 1 + print(target) + target = 2 + "#, + )); + let file_id = ws.def(&main_content); + + let result = references(&ws.analysis, file_id, position, false) + .ok_or("failed to get references") + .or_fail()?; + + let mut lines = result + .iter() + .map(|location| location.range.start.line) + .collect::>(); + lines.sort(); + verify_eq!(lines, vec![2, 3]) + } + #[gtest] fn test_member_references_alias_cycle_does_not_stack_overflow() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); @@ -258,7 +283,7 @@ mod tests { )); let file_id = ws.def(&main_content); - let result = references(&ws.analysis, file_id, position) + let result = references(&ws.analysis, file_id, position, true) .ok_or("failed to get references") .or_fail()?; diff --git a/crates/emmylua_ls/src/handlers/test_lib/mod.rs b/crates/emmylua_ls/src/handlers/test_lib/mod.rs index 8b32a3193..0c38a6de0 100644 --- a/crates/emmylua_ls/src/handlers/test_lib/mod.rs +++ b/crates/emmylua_ls/src/handlers/test_lib/mod.rs @@ -641,7 +641,7 @@ impl ProviderVirtualWorkspace { .map(|(file_name, content)| (file_name.as_str(), content.as_str())) .collect(), ); - let result = references(&self.analysis, file_id, position) + let result = references(&self.analysis, file_id, position, true) .ok_or("failed to get references") .or_fail()?; Self::assert_locations(result, expected) diff --git a/crates/emmylua_ls/src/logger/mod.rs b/crates/emmylua_ls/src/logger/mod.rs index eb0658e10..f54a52b85 100644 --- a/crates/emmylua_ls/src/logger/mod.rs +++ b/crates/emmylua_ls/src/logger/mod.rs @@ -92,7 +92,7 @@ pub fn init_logger(root: Option<&str>, cmd_args: &CmdArgs) { } let uri = file_path_to_uri(&log_file_path).unwrap(); - info!("init logger success with file: {}", uri.as_str()); + eprintln!("init logger success with file: {}", uri.as_str()); info!("{} v{}", CRATE_NAME, CRATE_VERSION); } diff --git a/crates/emmylua_parser/src/grammar/doc/test.rs b/crates/emmylua_parser/src/grammar/doc/test.rs index e2b5e00b7..b6e176eae 100644 --- a/crates/emmylua_parser/src/grammar/doc/test.rs +++ b/crates/emmylua_parser/src/grammar/doc/test.rs @@ -3268,6 +3268,14 @@ Syntax(Chunk)@0..110 assert_ast_eq!(code, result); } + #[test] + fn test_mapped_type_omits_trailing_semicolon_before_close_brace() { + let code = r#"---@alias Copy { [K in keyof T]: T[K] }"#; + let tree = LuaParser::parse(code, ParserConfig::default()); + + assert_eq!(tree.get_errors(), &[]); + } + #[test] fn test_alias_conditional_infer_dots() { let code = r#" diff --git a/crates/emmylua_parser/src/grammar/doc/types.rs b/crates/emmylua_parser/src/grammar/doc/types.rs index 44cbbf572..cbe05f845 100644 --- a/crates/emmylua_parser/src/grammar/doc/types.rs +++ b/crates/emmylua_parser/src/grammar/doc/types.rs @@ -248,7 +248,11 @@ fn parse_mapped_type(p: &mut LuaDocParser, m: Marker) -> DocParseResult { parse_type(p)?; - expect_token(p, LuaTokenKind::TkSemicolon)?; + if p.current_token() == LuaTokenKind::TkSemicolon { + p.bump(); + } else if p.current_token() != LuaTokenKind::TkRightBrace { + expect_token(p, LuaTokenKind::TkSemicolon)?; + } expect_token(p, LuaTokenKind::TkRightBrace)?; p.set_parser_state(LuaDocParserState::Normal);