diff --git a/.gitignore b/.gitignore index 8016f3f70..cd41e8f73 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,10 @@ dhat-heap.json # AI +.agents .cursor .claude .codex openspec +.trellis +AGENTS.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b97cee76e..d279cdb64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ *All notable changes to the EmmyLua Analyzer Rust project will be documented in this file.* +## [Unreleased] + +### 🗑️ Removed + +- **`lsp_optimization`**: Removed the `skip_table_fields_check` optimization (including the old `check_table_field` alias). Table field diagnostics are no longer skipped via this attribute. + ## [0.24.0] - 2026-7-10 ### ✨ Added diff --git a/crates/emmylua_code_analysis/resources/std/builtin.lua b/crates/emmylua_code_analysis/resources/std/builtin.lua index 3a8e2b242..814b6b64d 100644 --- a/crates/emmylua_code_analysis/resources/std/builtin.lua +++ b/crates/emmylua_code_analysis/resources/std/builtin.lua @@ -181,11 +181,10 @@ --- Language Server Optimization Items. --- --- Parameters: ---- - `skip_table_fields_check`: Skip table field diagnostics. It is recommended to use this option for all large configuration tables. --- - `delayed_definition`: Indicates that the type of the variable is determined by the first assignment. --- Only valid for `local` declarations with no initial value. --- @class lsp_optimization: Attribute ---- @overload fun(code: "skip_table_fields_check"|"delayed_definition") +--- @overload fun(code: "delayed_definition") --- --- Index field alias, will be displayed in `hint` and `completion`. 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 c6ab2ab35..36167e277 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 @@ -213,6 +213,7 @@ pub fn analyze_param(analyzer: &mut DocAnalyzer, tag: LuaDocTagParam) -> Option< .get_db() .get_signature_index_mut() .get_or_create(id); + signature.has_explicit_docs = true; let param_info = LuaDocParamInfo { name: name.clone(), type_ref: type_ref.clone(), @@ -261,6 +262,7 @@ pub fn analyze_return(analyzer: &mut DocAnalyzer, tag: LuaDocTagReturn) -> Optio .collect::>(); bind_signature_return_docs(analyzer, &tag, |signature| { + signature.has_explicit_docs = true; signature.return_docs.extend(return_infos); }) } diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/find_decl_function.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/find_decl_function.rs index abe30c473..29ab702a5 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/find_decl_function.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/unresolve/find_decl_function.rs @@ -4,12 +4,12 @@ use smol_str::SmolStr; use crate::{ InFiled, InferFailReason, InferGuardRef, LuaInferCache, LuaInstanceType, LuaMemberId, - LuaMemberOwner, LuaOperatorOwner, TypeOps, TypeSubstitutor, check_type_compact, + LuaMemberOwner, LuaOperatorOwner, TypeOps, TypeSubstitutor, db_index::{ DbIndex, LuaGenericType, LuaIntersectionType, LuaMemberKey, LuaObjectType, LuaOperatorMetaMethod, LuaTupleType, LuaType, LuaTypeDeclId, LuaUnionType, }, - infer_expr, instantiate_type_generic, + infer_expr, instantiate_type_generic, is_assignable, semantic::InferGuard, }; @@ -316,7 +316,7 @@ fn find_index_metamethod( LuaIndexKey::Expr(expr) => infer_expr(db, cache, expr.clone())?, }; - if check_type_compact(db, key_type, &access_key_type).is_ok() { + if is_assignable(db, &access_key_type, key_type) { return Ok(value_type.clone()); } @@ -548,7 +548,7 @@ fn find_member_by_index_table( LuaMemberKey::Integer(i) => LuaType::IntegerConst(*i), _ => continue, }; - if check_type_compact(db, &key_type, &member_key_type).is_ok() { + if is_assignable(db, &member_key_type, &key_type) { let member_type = db .get_type_index() .get_type_cache(&member.get_id().into()) @@ -663,7 +663,7 @@ fn infer_member_by_index_array( } else if member_key.is_expr() { let expr = member_key.get_expr().ok_or(InferFailReason::None)?; let expr_type = infer_expr(db, cache, expr.clone())?; - if check_type_compact(db, &LuaType::Number, &expr_type).is_ok() { + if is_assignable(db, &expr_type, &LuaType::Number) { return Ok(base.clone()); } } @@ -683,7 +683,7 @@ fn infer_member_by_index_object( let expr = member_key.get_expr().ok_or(InferFailReason::None)?; let expr_type = infer_expr(db, cache, expr.clone())?; for (key, field) in access_member_type { - if check_type_compact(db, key, &expr_type).is_ok() { + if is_assignable(db, &expr_type, key) { return Ok(field.clone()); } } diff --git a/crates/emmylua_code_analysis/src/compilation/test/attribute_test.rs b/crates/emmylua_code_analysis/src/compilation/test/attribute_test.rs index 2342cd462..b9e835896 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/attribute_test.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/attribute_test.rs @@ -34,19 +34,6 @@ mod test { assert_eq!(ws.humanize_type(ty), "A"); } - #[test] - fn test_def_attribute() { - let mut ws = VirtualWorkspace::new_with_init_std_lib(); - - ws.has_no_diagnostic( - DiagnosticCode::AssignTypeMismatch, - r#" - ---@[lsp_optimization("skip_table_fields_check")] - local config = {} - "#, - ); - } - #[test] fn test_attribute_overload_uses_arg_type_for_diagnostic() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/compilation/test/closure_return_test.rs b/crates/emmylua_code_analysis/src/compilation/test/closure_return_test.rs index fc790bfea..19057d88c 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/closure_return_test.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/closure_return_test.rs @@ -10,8 +10,8 @@ mod test { let ty = ws.expr_ty("result"); let expected = ws.ty("integer"); let nil = ws.ty("nil"); - assert!(ws.check_type(&ty, &expected)); - assert!(!ws.check_type(&ty, &nil)); + assert!(ws.check_type(&expected, &ty)); + assert!(!ws.check_type(&nil, &ty)); } #[test] @@ -364,8 +364,8 @@ mod test { let ty = ws.expr_ty("result"); let expected = ws.ty("integer|string"); let nil = ws.ty("nil"); - assert!(ws.check_type(&ty, &expected)); - assert!(!ws.check_type(&ty, &nil)); + assert!(ws.check_type(&expected, &ty)); + assert!(!ws.check_type(&nil, &ty)); } #[test] @@ -393,7 +393,7 @@ mod test { let ty = ws.expr_ty("result"); let expected = ws.ty("integer|string"); let nil = ws.ty("nil"); - assert!(ws.check_type(&ty, &expected)); - assert!(!ws.check_type(&ty, &nil)); + assert!(ws.check_type(&expected, &ty)); + assert!(!ws.check_type(&nil, &ty)); } } diff --git a/crates/emmylua_code_analysis/src/compilation/test/member_infer_test.rs b/crates/emmylua_code_analysis/src/compilation/test/member_infer_test.rs index b8d53f0c5..39d0492e2 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/member_infer_test.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/member_infer_test.rs @@ -72,7 +72,7 @@ mod test { let result_ty = ws.expr_ty("Result"); let expected_ty = ws.ty("string?"); - assert!(ws.check_type(&result_ty, &expected_ty)); + assert!(ws.check_type(&expected_ty, &result_ty)); } #[test] diff --git a/crates/emmylua_code_analysis/src/compilation/test/module_test.rs b/crates/emmylua_code_analysis/src/compilation/test/module_test.rs index bed255017..47a36d912 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/module_test.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/module_test.rs @@ -139,8 +139,8 @@ mod test { let ty = ws.expr_ty(r#"require("virtual_0")"#); let integer = ws.ty("integer"); let nil = ws.ty("nil"); - assert!(ws.check_type(&ty, &integer)); - assert!(!ws.check_type(&ty, &nil)); + assert!(ws.check_type(&integer, &ty)); + assert!(!ws.check_type(&nil, &ty)); } #[test] diff --git a/crates/emmylua_code_analysis/src/compilation/test/return_overload_generic_test.rs b/crates/emmylua_code_analysis/src/compilation/test/return_overload_generic_test.rs index 2b508128f..0e0ed8f26 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/return_overload_generic_test.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/return_overload_generic_test.rs @@ -161,7 +161,7 @@ mod test { let status_ty = ws.expr_ty("status"); let boolean_ty = ws.ty("boolean"); - assert!(ws.check_type(&status_ty, &boolean_ty)); + assert!(ws.check_type(&boolean_ty, &status_ty)); assert!(!status_ty.is_always_truthy()); assert!(!status_ty.is_always_falsy()); @@ -169,9 +169,9 @@ mod test { let integer_or_string_ty = ws.ty("integer|string"); let integer_ty = ws.ty("integer"); let string_ty = ws.ty("string"); - assert!(ws.check_type(&value_ty, &integer_or_string_ty)); - assert!(ws.check_type(&value_ty, &integer_ty)); - assert!(ws.check_type(&value_ty, &string_ty)); + assert!(ws.check_type(&integer_or_string_ty, &value_ty)); + assert!(ws.check_type(&integer_ty, &value_ty)); + assert!(ws.check_type(&string_ty, &value_ty)); } #[test] diff --git a/crates/emmylua_code_analysis/src/db_index/member/lua_owner_members.rs b/crates/emmylua_code_analysis/src/db_index/member/lua_owner_members.rs index 0b26d365a..28471d909 100644 --- a/crates/emmylua_code_analysis/src/db_index/member/lua_owner_members.rs +++ b/crates/emmylua_code_analysis/src/db_index/member/lua_owner_members.rs @@ -42,6 +42,10 @@ impl LuaOwnerMembers { self.members.values() } + pub fn iter(&self) -> impl Iterator { + self.members.iter() + } + pub fn iter_mut(&mut self) -> impl Iterator { self.members.iter_mut() } diff --git a/crates/emmylua_code_analysis/src/db_index/member/mod.rs b/crates/emmylua_code_analysis/src/db_index/member/mod.rs index f5bed478a..971c770f0 100644 --- a/crates/emmylua_code_analysis/src/db_index/member/mod.rs +++ b/crates/emmylua_code_analysis/src/db_index/member/mod.rs @@ -191,7 +191,13 @@ impl LuaMemberIndex { Some(members) } - #[allow(unused)] + pub fn get_member_items( + &self, + owner: &LuaMemberOwner, + ) -> Option> { + Some(self.owner_members.get(owner)?.iter()) + } + pub fn get_member_item_by_member_id( &self, member_id: LuaMemberId, diff --git a/crates/emmylua_code_analysis/src/db_index/property/builtin_attribute.rs b/crates/emmylua_code_analysis/src/db_index/property/builtin_attribute.rs index a7b2089e6..4e56a54cd 100644 --- a/crates/emmylua_code_analysis/src/db_index/property/builtin_attribute.rs +++ b/crates/emmylua_code_analysis/src/db_index/property/builtin_attribute.rs @@ -189,9 +189,6 @@ impl LuaAttributeUse { } let code = match self.get_string_param("code")? { - "skip_table_fields_check" | "check_table_field" => { - LuaLspOptimizationCode::SkipTableFieldsCheck - } "delayed_definition" => LuaLspOptimizationCode::DelayedDefinition, _ => return None, }; @@ -252,14 +249,12 @@ pub struct LuaDeprecatedAttribute<'a> { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum LuaLspOptimizationCode { - SkipTableFieldsCheck, DelayedDefinition, } impl LuaLspOptimizationCode { pub const fn as_str(self) -> &'static str { match self { - Self::SkipTableFieldsCheck => "skip_table_fields_check", Self::DelayedDefinition => "delayed_definition", } } @@ -271,10 +266,6 @@ pub struct LuaLspOptimizationAttribute { } impl LuaLspOptimizationAttribute { - pub fn is_skip_table_fields_check(self) -> bool { - self.code == LuaLspOptimizationCode::SkipTableFieldsCheck - } - pub fn is_delayed_definition(self) -> bool { self.code == LuaLspOptimizationCode::DelayedDefinition } @@ -408,20 +399,4 @@ mod tests { LuaLspOptimizationCode::DelayedDefinition ); } - - #[test] - fn lsp_optimization_accepts_skip_table_fields_check_aliases() { - for code in ["skip_table_fields_check", "check_table_field"] { - let attribute = LuaAttributeUse::new( - LuaTypeDeclId::global("lsp_optimization"), - vec![("code".into(), Some(doc_string(code)))], - ); - - let lsp_optimization = attribute.as_lsp_optimization().unwrap(); - assert_eq!( - lsp_optimization.code, - LuaLspOptimizationCode::SkipTableFieldsCheck - ); - } - } } diff --git a/crates/emmylua_code_analysis/src/db_index/signature/signature.rs b/crates/emmylua_code_analysis/src/db_index/signature/signature.rs index 0271c3213..2bbd64225 100644 --- a/crates/emmylua_code_analysis/src/db_index/signature/signature.rs +++ b/crates/emmylua_code_analysis/src/db_index/signature/signature.rs @@ -21,6 +21,7 @@ use crate::{ pub struct LuaSignature { pub generic_params: Vec, pub overloads: Vec>, + pub has_explicit_docs: bool, pub param_docs: HashMap, pub params: Vec, pub return_docs: Vec, @@ -49,6 +50,7 @@ impl LuaSignature { Self { generic_params: Vec::new(), overloads: Vec::new(), + has_explicit_docs: false, param_docs: HashMap::new(), params: Vec::new(), return_docs: Vec::new(), @@ -151,9 +153,7 @@ impl LuaSignature { return false; } - semantic_model - .type_check(owner_type, ¶m_info.type_ref) - .is_ok() + semantic_model.is_assignable(¶m_info.type_ref, owner_type) } None => param_info.name == "self", } diff --git a/crates/emmylua_code_analysis/src/db_index/type/types/complex.rs b/crates/emmylua_code_analysis/src/db_index/type/types/complex.rs index 477178353..cc206bd72 100644 --- a/crates/emmylua_code_analysis/src/db_index/type/types/complex.rs +++ b/crates/emmylua_code_analysis/src/db_index/type/types/complex.rs @@ -196,10 +196,10 @@ impl LuaFunctionType { { return false; } - if semantic_model.type_check(owner_type, t).is_ok() { + if semantic_model.is_assignable(t, owner_type) { return true; } - name == "self" && semantic_model.type_check(t, owner_type).is_ok() + name == "self" && semantic_model.is_assignable(owner_type, t) } None => name == "self", } diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs index 124a34fc0..cbbf8acbf 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs @@ -2,14 +2,14 @@ use std::ops::Deref; use emmylua_parser::{ LuaAssignStat, LuaAst, LuaAstNode, LuaAstToken, LuaExpr, LuaIndexExpr, LuaLocalStat, - LuaNameExpr, LuaSyntaxNode, LuaSyntaxToken, LuaTableExpr, LuaVarExpr, + LuaNameExpr, LuaTableExpr, LuaVarExpr, }; use rowan::{NodeOrToken, TextRange}; use crate::{ - DbIndex, DiagnosticCode, LuaBuiltinAttributeKind, LuaDeclExtra, LuaDeclId, LuaMemberKey, - LuaSemanticDeclId, LuaType, SemanticDeclLevel, SemanticModel, TypeCheckFailReason, - TypeCheckResult, VariadicType, get_real_type, infer_index_expr, + DbIndex, DiagnosticCode, LuaDeclExtra, LuaDeclId, LuaMemberKey, LuaSemanticDeclId, LuaType, + SemanticDeclLevel, SemanticModel, TypeMismatch, VariadicType, get_real_type, infer_index_expr, + render_type_mismatch, }; use super::{Checker, DiagnosticContext, humanize_lint_type}; @@ -117,13 +117,7 @@ fn check_name_expr( false, ); if let Some(expr) = expr { - check_table_expr( - context, - semantic_model, - NodeOrToken::Node(name_expr.syntax().clone()), - &expr, - source_type.as_ref(), - ); + check_table_expr(context, semantic_model, &expr, source_type.as_ref()); } Some(()) @@ -157,13 +151,7 @@ fn check_index_expr( true, ); if let Some(expr) = expr { - check_table_expr( - context, - semantic_model, - NodeOrToken::Node(index_expr.syntax().clone()), - &expr, - source_type.as_ref(), - ); + check_table_expr(context, semantic_model, &expr, source_type.as_ref()); } Some(()) } @@ -200,13 +188,7 @@ fn check_local_stat( false, ); if let Some(expr) = value_exprs.get(idx) { - check_table_expr( - context, - semantic_model, - NodeOrToken::Node(var.syntax().clone()), - expr, - Some(&var_type), - ); + check_table_expr(context, semantic_model, expr, Some(&var_type)); } } Some(()) @@ -216,27 +198,9 @@ fn check_local_stat( pub fn check_table_expr( context: &mut DiagnosticContext, semantic_model: &SemanticModel, - decl_node: NodeOrToken, table_expr: &LuaExpr, table_type: Option<&LuaType>, // 记录的类型 ) -> Option { - // 检查是否附加了元数据以跳过诊断 - if let Some(semantic_decl) = semantic_model.find_decl(decl_node, SemanticDeclLevel::default()) { - if let Some(property) = semantic_model - .get_db() - .get_property_index() - .get_property(&semantic_decl) - { - if property - .find_builtin_attribute(LuaBuiltinAttributeKind::LspOptimization) - .and_then(|attribute_use| attribute_use.as_lsp_optimization()) - .is_some_and(|attribute| attribute.is_skip_table_fields_check()) - { - return Some(false); - } - } - } - let table_type = table_type?; let Some(table_expr) = LuaTableExpr::cast(table_expr.syntax().clone()) else { return Some(false); @@ -414,15 +378,14 @@ fn check_assign_type_mismatch( _ => {} } - let result = semantic_model.type_check_detail(source_type, value_type); - if result.is_err() { + if let Err(mismatch) = semantic_model.check_assignable(value_type, source_type) { add_type_check_diagnostic( context, semantic_model, range, source_type, value_type, - result, + &mismatch, ); return Some(true); } @@ -435,32 +398,21 @@ fn add_type_check_diagnostic( range: TextRange, source_type: &LuaType, value_type: &LuaType, - result: TypeCheckResult, + mismatch: &TypeMismatch, ) { let db = semantic_model.get_db(); - match result { - Ok(_) => (), - Err(reason) => { - let reason_message = match reason { - TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason, - TypeCheckFailReason::TypeRecursion => t!("type recursion").to_string(), - _ => "".to_string(), - }; - - context.add_diagnostic( - DiagnosticCode::AssignTypeMismatch, - range, - t!( - "Cannot assign `%{value}` to `%{source}`. %{reason}", - value = humanize_lint_type(db, value_type), - source = humanize_lint_type(db, source_type), - reason = reason_message - ) - .to_string(), - None, - ); - } - } + context.add_diagnostic( + DiagnosticCode::AssignTypeMismatch, + range, + t!( + "Cannot assign `%{value}` to `%{source}`. %{reason}", + value = humanize_lint_type(db, value_type), + source = humanize_lint_type(db, source_type), + reason = render_type_mismatch(db, mismatch) + ) + .to_string(), + None, + ); } fn get_real_type_or_self<'a>(db: &'a DbIndex, ty: &'a LuaType) -> &'a LuaType { diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/attribute_check.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/attribute_check.rs index f3ab11a7e..edd407b5d 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/attribute_check.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/attribute_check.rs @@ -1,7 +1,7 @@ use crate::{ - DiagnosticCode, DocTypeInferContext, LuaType, SemanticModel, TypeCheckFailReason, - TypeCheckResult, diagnostic::checker::humanize_lint_type, get_attribute_constructor_params, - infer_doc_type, is_attribute_class, + DiagnosticCode, DocTypeInferContext, LuaType, SemanticModel, TypeMismatch, + diagnostic::checker::humanize_lint_type, get_attribute_constructor_params, infer_doc_type, + is_attribute_class, render_type_mismatch, }; use emmylua_parser::{ LuaAstNode, LuaDocAttributeUse, LuaDocTagAttributeUse, LuaDocType, LuaExpr, LuaLiteralExpr, @@ -144,15 +144,15 @@ fn check_param( } if let Some(variadic_type) = param.1.as_ref() { for (arg_idx, arg_type) in call_arg_types[idx..].iter().enumerate() { - let result = semantic_model.type_check_detail(variadic_type, arg_type); - if result.is_err() { + if let Err(mismatch) = semantic_model.check_assignable(arg_type, variadic_type) + { add_type_check_diagnostic( context, semantic_model, args.get(idx + arg_idx)?.get_range(), variadic_type, arg_type, - result, + &mismatch, ); } } @@ -161,15 +161,14 @@ fn check_param( } if let Some(param_type) = param.1.as_ref() { let arg_type = call_arg_types.get(idx).unwrap_or(&LuaType::Any); - let result = semantic_model.type_check_detail(param_type, arg_type); - if result.is_err() { + if let Err(mismatch) = semantic_model.check_assignable(arg_type, param_type) { add_type_check_diagnostic( context, semantic_model, args.get(idx)?.get_range(), param_type, arg_type, - result, + &mismatch, ); } } @@ -183,31 +182,19 @@ fn add_type_check_diagnostic( range: TextRange, param_type: &LuaType, expr_type: &LuaType, - result: TypeCheckResult, + mismatch: &TypeMismatch, ) { let db = semantic_model.get_db(); - match result { - Ok(_) => (), - Err(reason) => { - let reason_message = match reason { - TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason, - TypeCheckFailReason::TypeNotMatch | TypeCheckFailReason::DonotCheck => { - "".to_string() - } - TypeCheckFailReason::TypeRecursion => "type recursion".to_string(), - }; - context.add_diagnostic( - DiagnosticCode::AttributeParamTypeMismatch, - range, - t!( - "expected `%{source}` but found `%{found}`. %{reason}", - source = humanize_lint_type(db, param_type), - found = humanize_lint_type(db, expr_type), - reason = reason_message - ) - .to_string(), - None, - ); - } - } + context.add_diagnostic( + DiagnosticCode::AttributeParamTypeMismatch, + range, + t!( + "expected `%{source}` but found `%{found}`. %{reason}", + source = humanize_lint_type(db, param_type), + found = humanize_lint_type(db, expr_type), + reason = render_type_mismatch(db, mismatch) + ) + .to_string(), + None, + ); } diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/cast_type_mismatch.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/cast_type_mismatch.rs index 220b3fab5..5ba1a717e 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/cast_type_mismatch.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/cast_type_mismatch.rs @@ -4,7 +4,7 @@ use rowan::TextRange; use crate::{ DbIndex, DiagnosticCode, DocTypeInferContext, LuaType, LuaUnionType, SemanticModel, - TypeCheckFailReason, TypeCheckResult, get_real_type, infer_doc_type, + TypeMismatch, get_real_type, infer_doc_type, render_type_mismatch, }; use super::{Checker, DiagnosticContext, humanize_lint_type}; @@ -83,7 +83,7 @@ fn check_cast_compatibility( return Some(()); } } - Err(TypeCheckFailReason::TypeNotMatch) + Err(CastCheckFailure::Mismatch(None)) } _ => cast_type_check(semantic_model, origin_type, target_type, 0), }; @@ -108,18 +108,18 @@ fn add_cast_type_mismatch_diagnostic( range: TextRange, origin_type: &LuaType, target_type: &LuaType, - result: TypeCheckResult, + result: Result<(), CastCheckFailure>, ) { let db = semantic_model.get_db(); match result { Ok(_) => (), Err(reason) => { let reason_message = match reason { - TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason, - TypeCheckFailReason::TypeNotMatch | TypeCheckFailReason::DonotCheck => { - "".to_string() - } - TypeCheckFailReason::TypeRecursion => t!("type recursion").to_string(), + CastCheckFailure::Mismatch(mismatch) => mismatch + .as_ref() + .map(|mismatch| render_type_mismatch(db, mismatch)) + .unwrap_or_default(), + CastCheckFailure::Recursion => t!("type recursion").to_string(), }; context.add_diagnostic( @@ -144,10 +144,10 @@ fn cast_type_check( origin_type: &LuaType, target_type: &LuaType, recursion_depth: u32, -) -> TypeCheckResult { +) -> Result<(), CastCheckFailure> { const MAX_RECURSION_DEPTH: u32 = 100; if recursion_depth >= MAX_RECURSION_DEPTH { - return Err(TypeCheckFailReason::TypeRecursion); + return Err(CastCheckFailure::Recursion); } if origin_type == target_type { @@ -199,17 +199,29 @@ fn cast_type_check( } else if origin_type.is_number() && target_type.is_number() { return Ok(()); } - match semantic_model.type_check_detail(target_type, origin_type) { - Ok(_) => Ok(()), - Err(_) => match semantic_model.type_check_detail(origin_type, target_type) { - Ok(_) => Ok(()), - Err(reason) => Err(reason), - }, + match semantic_model.check_assignable(origin_type, target_type) { + Ok(()) => Ok(()), + Err(mismatch) => { + if semantic_model + .check_assignable(target_type, origin_type) + .is_ok() + { + Ok(()) + } else { + Err(CastCheckFailure::Mismatch(Some(mismatch))) + } + } } } } } +#[derive(Debug, Clone, PartialEq, Eq)] +enum CastCheckFailure { + Mismatch(Option), + Recursion, +} + fn expand_type(db: &DbIndex, typ: &LuaType) -> Option { let mut visited = HashSet::new(); expand_type_recursive(db, typ, &mut visited) @@ -247,7 +259,8 @@ fn expand_type_recursive( } LuaType::Union(union_type) => { // 递归展开 union 中的每个类型 - let mut expanded_types = HashSet::new(); + let mut expanded_types = Vec::new(); + let mut expanded_type_set = HashSet::new(); let mut has_nil = false; for inner_type in union_type.into_vec() { if inner_type.is_nil() { @@ -258,14 +271,20 @@ fn expand_type_recursive( match expanded { LuaType::Union(inner_union) => { // 如果展开后还是 union,则将其成员类型添加到结果中 - expanded_types.extend(inner_union.into_vec().iter().cloned()); + for inner_type in inner_union.into_vec() { + if expanded_type_set.insert(inner_type.clone()) { + expanded_types.push(inner_type); + } + } } _ => { - expanded_types.insert(expanded); + if expanded_type_set.insert(expanded.clone()) { + expanded_types.push(expanded); + } } } - } else { - expanded_types.insert(inner_type.clone()); + } else if expanded_type_set.insert(inner_type.clone()) { + expanded_types.push(inner_type); } } @@ -278,11 +297,11 @@ fn expand_type_recursive( } } 1 => { - let single = expanded_types.iter().next().cloned()?; + let single = expanded_types.into_iter().next()?; Some(single) } _ => Some(LuaType::Union( - LuaUnionType::from_set(expanded_types).into(), + LuaUnionType::from_vec(expanded_types).into(), )), }; } diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs index f16135c5a..3230fbb9e 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs @@ -14,10 +14,18 @@ use crate::{ DiagnosticCode, DocTypeInferContext, GenericParam, GenericResolveMode, GenericTplId, LuaArrayType, LuaGenericType, LuaIntersectionType, LuaObjectType, LuaSignatureId, LuaStringTplType, LuaTupleType, LuaType, LuaUnionType, RenderLevel, SemanticModel, - TypeCheckFailReason, TypeCheckResult, TypeSubstitutor, VariadicType, humanize_type, - infer_doc_type, instantiate_type_generic_full, + TypeMismatch, TypeSubstitutor, VariadicType, humanize_type, infer_doc_type, + instantiate_type_generic_full, render_type_mismatch, }; +type ConstraintCheckResult = Result<(), ConstraintCheckFailure>; + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ConstraintCheckFailure { + Mismatch(Option), + Recursion, +} + pub struct GenericConstraintMismatchChecker; impl Checker for GenericConstraintMismatchChecker { @@ -193,7 +201,7 @@ fn check_generic_default_satisfies_constraint( semantic_model: &SemanticModel, constraint: &LuaType, default_type: &LuaType, -) -> TypeCheckResult { +) -> ConstraintCheckResult { check_generic_default_satisfies_constraint_inner(semantic_model, constraint, default_type, 0) } @@ -202,9 +210,9 @@ fn check_generic_default_satisfies_constraint_inner( constraint: &LuaType, default_type: &LuaType, depth: usize, -) -> TypeCheckResult { +) -> ConstraintCheckResult { if depth > 64 { - return Err(TypeCheckFailReason::TypeRecursion); + return Err(ConstraintCheckFailure::Recursion); } if constraint == default_type { @@ -239,7 +247,7 @@ fn check_generic_default_satisfies_constraint_inner( return Ok(()); } - return Err(TypeCheckFailReason::TypeNotMatch); + return Err(ConstraintCheckFailure::Mismatch(None)); } if let Some(default_constraint) = generic_tpl_constraint(default_type) { @@ -285,7 +293,7 @@ fn check_generic_default_satisfies_constraint_inner( if constraint_field.is_nullable() || constraint_field.is_any() { continue; } - return Err(TypeCheckFailReason::TypeNotMatch); + return Err(ConstraintCheckFailure::Mismatch(None)); }; check_generic_default_satisfies_constraint_inner( semantic_model, @@ -347,7 +355,7 @@ fn check_generic_default_satisfies_constraint_inner( return Ok(()); } } - return Err(TypeCheckFailReason::TypeNotMatch); + return Err(ConstraintCheckFailure::Mismatch(None)); } (_, LuaType::Union(union)) => { for member in union.into_vec() { @@ -384,14 +392,17 @@ fn check_generic_default_satisfies_constraint_inner( return Ok(()); } } - return Err(TypeCheckFailReason::TypeNotMatch); + return Err(ConstraintCheckFailure::Mismatch(None)); } _ => {} } let check_constraint = instantiate_decl_type_for_check(constraint, false); let check_default = instantiate_decl_type_for_check(default_type, true); - semantic_model.type_check_detail(&check_constraint, &check_default) + match semantic_model.check_assignable(&check_default, &check_constraint) { + Ok(()) => Ok(()), + Err(mismatch) => Err(ConstraintCheckFailure::Mismatch(Some(mismatch))), + } } fn check_variadic_default_satisfies_constraint( @@ -399,7 +410,7 @@ fn check_variadic_default_satisfies_constraint( constraint_variadic: &VariadicType, default_variadic: &VariadicType, depth: usize, -) -> TypeCheckResult { +) -> ConstraintCheckResult { match (constraint_variadic, default_variadic) { (VariadicType::Base(constraint_base), VariadicType::Base(default_base)) => { check_generic_default_satisfies_constraint_inner( @@ -422,7 +433,7 @@ fn check_variadic_default_satisfies_constraint( } Ok(()) } - _ => Err(TypeCheckFailReason::TypeNotMatch), + _ => Err(ConstraintCheckFailure::Mismatch(None)), } } @@ -617,7 +628,10 @@ fn check_doc_tag_type( ), ); let param_type = normalize_constraint_type(semantic_model.get_db(), param_type.clone()); - let result = semantic_model.type_check_detail(&extend_type, ¶m_type); + let result = match semantic_model.check_assignable(¶m_type, &extend_type) { + Ok(()) => Ok(()), + Err(mismatch) => Err(ConstraintCheckFailure::Mismatch(Some(mismatch))), + }; if result.is_err() { add_type_check_diagnostic( context, @@ -760,7 +774,10 @@ fn check_str_tpl_ref( { let type_id = type_decl.get_id(); let ref_type = LuaType::Ref(type_id); - let result = semantic_model.type_check_detail(&extend_type, &ref_type); + let result = match semantic_model.check_assignable(&ref_type, &extend_type) { + Ok(()) => Ok(()), + Err(mismatch) => Err(ConstraintCheckFailure::Mismatch(Some(mismatch))), + }; if result.is_err() { add_type_check_diagnostic( context, @@ -792,18 +809,18 @@ fn add_type_check_diagnostic( range: TextRange, extend_type: &LuaType, expr_type: &LuaType, - result: TypeCheckResult, + result: ConstraintCheckResult, ) { let db = semantic_model.get_db(); match result { Ok(_) => (), Err(reason) => { let reason_message = match reason { - TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason, - TypeCheckFailReason::TypeNotMatch | TypeCheckFailReason::DonotCheck => { - "".to_string() - } - TypeCheckFailReason::TypeRecursion => "type recursion".to_string(), + ConstraintCheckFailure::Mismatch(mismatch) => mismatch + .as_ref() + .map(|mismatch| render_type_mismatch(db, mismatch)) + .unwrap_or_default(), + ConstraintCheckFailure::Recursion => "type recursion".to_string(), }; context.add_diagnostic( DiagnosticCode::GenericConstraintMismatch, 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 46779aa2e..7f98b1e7b 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs @@ -1,12 +1,8 @@ use hashbrown::{HashMap, HashSet}; -use emmylua_parser::{LuaAst, LuaAstNode, LuaExpr, LuaSyntaxId, LuaTableExpr}; -use rowan::NodeOrToken; +use emmylua_parser::{LuaAstNode, LuaExpr, LuaTableExpr}; -use crate::{ - DbIndex, DiagnosticCode, LuaBuiltinAttributeKind, LuaMemberOwner, LuaType, SemanticDeclLevel, - SemanticModel, -}; +use crate::{DbIndex, DiagnosticCode, LuaMemberOwner, LuaType, SemanticModel}; use super::{Checker, DiagnosticContext, humanize_lint_type}; use itertools::Itertools; @@ -24,22 +20,7 @@ impl Checker for MissingFieldsChecker { let mut required_fields_cache = HashMap::new(); let mut optional_field_type_cache = HashMap::new(); - let mut skipped_table_exprs: HashSet = HashSet::new(); for expr in root.descendants::() { - let expr_syntax_id = expr.get_syntax_id(); - if skipped_table_exprs.contains(&expr_syntax_id) { - continue; - } - - if table_expr_has_skip_table_fields_check_optimization(semantic_model, &expr) { - skipped_table_exprs.insert(expr_syntax_id); - skipped_table_exprs.extend( - expr.descendants::() - .map(|expr| expr.get_syntax_id()), - ); - continue; - } - check_table_expr( context, semantic_model, @@ -86,7 +67,7 @@ fn check_table_expr( } 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() + semantic_model.is_assignable(expr_type, &ty) }) => { return Some(()); @@ -160,61 +141,6 @@ fn check_table_expr( Some(()) } -fn table_expr_has_skip_table_fields_check_optimization( - semantic_model: &SemanticModel, - expr: &LuaTableExpr, -) -> bool { - let Some(parent) = expr.syntax().parent().and_then(LuaAst::cast) else { - return false; - }; - - let decl_node = match parent { - LuaAst::LuaLocalStat(local) => { - let Some(idx) = local - .get_value_exprs() - .position(|value| value.get_position() == expr.get_position()) - else { - return false; - }; - let Some(local_name) = local.get_local_name_list().nth(idx) else { - return false; - }; - NodeOrToken::Node(local_name.syntax().clone()) - } - LuaAst::LuaAssignStat(assign) => { - let (vars, exprs) = assign.get_var_and_expr_list(); - let Some(idx) = exprs - .iter() - .position(|value| value.get_position() == expr.get_position()) - else { - return false; - }; - let Some(var) = vars.get(idx) else { - return false; - }; - NodeOrToken::Node(var.syntax().clone()) - } - _ => return false, - }; - - let Some(semantic_decl) = semantic_model.find_decl(decl_node, SemanticDeclLevel::default()) - else { - return false; - }; - let Some(property) = semantic_model - .get_db() - .get_property_index() - .get_property(&semantic_decl) - else { - return false; - }; - - property - .find_builtin_attribute(LuaBuiltinAttributeKind::LspOptimization) - .and_then(|attribute_use| attribute_use.as_lsp_optimization()) - .is_some_and(|attribute| attribute.is_skip_table_fields_check()) -} - fn get_required_fields<'a>( db: &DbIndex, table_type: &LuaType, 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 cf39b8e10..6b2ea72fb 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 @@ -1,12 +1,13 @@ use std::sync::Arc; use emmylua_parser::{LuaAstNode, LuaAstToken, LuaCallExpr}; -use rowan::{NodeOrToken, TextRange}; +use rowan::TextRange; use crate::{ - DiagnosticCode, LuaFunctionType, LuaType, RenderLevel, SemanticModel, TypeCheckFailReason, - TypeCheckResult, diagnostic::checker::assign_type_mismatch::check_table_expr, humanize_type, - semantic::get_func_param_type, + DiagnosticCode, LuaFunctionType, LuaType, RenderLevel, SemanticModel, TypeMismatch, + diagnostic::checker::assign_type_mismatch::check_table_expr, + humanize_type, render_type_mismatch, + semantic::{RelationKind, RelationOutcome, get_func_param_type, is_assignable_ex}, }; use super::{super::DiagnosticContext, call_facts::CallFacts}; @@ -54,7 +55,7 @@ pub(super) fn check_param_types( arg_index, ); - let (failed_arg, param_type, result) = match arg_index_result { + let (failed_arg, param_type) = match arg_index_result { ArgIndexCheckResult::NoDiagnostic => return Some(()), ArgIndexCheckResult::MatchedCandidates(next_candidates) => { candidates = next_candidates; @@ -64,33 +65,30 @@ pub(super) fn check_param_types( ArgIndexCheckResult::Mismatch { failed_arg, param_type, - result, - } => (failed_arg, param_type, result), + } => (failed_arg, param_type), }; // 表字段已经报错了, 则不添加参数不匹配的诊断避免干扰. if failed_arg.typ.is_table() && let Some(arg_expr_idx) = failed_arg.expr_index && let Some(arg_expr) = facts.arg_exprs.get(arg_expr_idx) - && let Some(add_diagnostic) = check_table_expr( - context, - semantic_model, - NodeOrToken::Node(arg_expr.syntax().clone()), - arg_expr, - Some(¶m_type), - ) + && let Some(add_diagnostic) = + check_table_expr(context, semantic_model, arg_expr, Some(¶m_type)) && add_diagnostic { return Some(()); } + let mismatch = semantic_model + .check_assignable(failed_arg.typ, ¶m_type) + .err(); add_diagnostic( context, semantic_model, failed_arg.range, ¶m_type, failed_arg.typ, - result, + mismatch.as_ref(), ); return Some(()); } @@ -104,7 +102,6 @@ enum ArgIndexCheckResult<'func, 'arg> { Mismatch { failed_arg: DiagnosticArg<'arg>, param_type: LuaType, - result: TypeCheckResult, }, } @@ -129,7 +126,6 @@ fn check_arg_index_candidates<'func, 'arg>( let mut next_candidates = Vec::with_capacity(candidates.len()); let mut failed_param_types = Vec::with_capacity(candidates.len()); let mut failed_arg = None; - let mut failed_result = None; // 按参数位置逐步收窄候选, 第一处全体失败的位置就是本次诊断的位置. for func in candidates.iter().copied() { @@ -171,8 +167,15 @@ fn check_arg_index_candidates<'func, 'arg>( continue; } - let type_check_result = semantic_model.type_check_detail(¶m_type, arg.typ); - if type_check_result.is_ok() { + if matches!( + is_assignable_ex( + semantic_model.get_db(), + arg.typ, + ¶m_type, + RelationKind::Assignable, + ), + RelationOutcome::Related + ) { next_candidates.push(func); continue; } @@ -181,9 +184,6 @@ fn check_arg_index_candidates<'func, 'arg>( if failed_arg.is_none() { failed_arg = Some(arg); } - if failed_result.is_none() { - failed_result = Some(type_check_result); - } } if !checked_call_arg { @@ -201,14 +201,9 @@ fn check_arg_index_candidates<'func, 'arg>( if failed_param_types.is_empty() { return ArgIndexCheckResult::NoDiagnostic; } - let Some(result) = failed_result else { - return ArgIndexCheckResult::NoDiagnostic; - }; - ArgIndexCheckResult::Mismatch { failed_arg, param_type: LuaType::from_vec(failed_param_types), - result, } } @@ -253,7 +248,7 @@ fn add_diagnostic( range: TextRange, param_type: &LuaType, expr_type: &LuaType, - result: TypeCheckResult, + mismatch: Option<&TypeMismatch>, ) { if let (LuaType::Integer, LuaType::FloatConst(f)) = (param_type, expr_type) && f.fract() == 0.0 @@ -261,28 +256,18 @@ fn add_diagnostic( return; } let db = semantic_model.get_db(); - match result { - Ok(_) => (), - Err(reason) => { - let reason_message = match reason { - TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason, - TypeCheckFailReason::TypeNotMatch | TypeCheckFailReason::DonotCheck => { - "".to_string() - } - TypeCheckFailReason::TypeRecursion => "type recursion".to_string(), - }; - context.add_diagnostic( - DiagnosticCode::ParamTypeMismatch, - range, - t!( - "expected `%{source}` but found `%{found}`. %{reason}", - source = humanize_type(db, param_type, RenderLevel::Simple), - found = humanize_type(db, expr_type, RenderLevel::Simple), - reason = reason_message - ) - .to_string(), - None, - ); - } - } + context.add_diagnostic( + DiagnosticCode::ParamTypeMismatch, + range, + t!( + "expected `%{source}` but found `%{found}`. %{reason}", + source = humanize_type(db, param_type, RenderLevel::Simple), + found = humanize_type(db, expr_type, RenderLevel::Simple), + reason = mismatch + .map(|mismatch| render_type_mismatch(db, mismatch)) + .unwrap_or_default() + ) + .to_string(), + None, + ); } diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs index 3b37026e2..cd0ca05db 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs @@ -5,8 +5,9 @@ use rowan::{NodeOrToken, TextRange}; use crate::{ DiagnosticCode, LuaSemanticDeclId, LuaSignatureId, LuaType, SemanticDeclLevel, SemanticModel, - SignatureReturnStatus, TypeCheckFailReason, TypeCheckResult, + SignatureReturnStatus, TypeMismatch, diagnostic::checker::{assign_type_mismatch::check_table_expr, humanize_lint_type}, + render_type_mismatch, }; use super::{Checker, DiagnosticContext, get_return_stats}; @@ -86,18 +87,12 @@ fn check_return_stat( check_type = self_type; } - let result = semantic_model.type_check_detail(check_type, return_expr_type); - if result.is_err() { + if let Err(mismatch) = semantic_model.check_assignable(return_expr_type, check_type) + { if return_expr_type.is_table() && let Some(return_expr) = return_exprs.get(index) { - check_table_expr( - context, - semantic_model, - NodeOrToken::Node(return_expr.syntax().clone()), - return_expr, - Some(check_type), - ); + check_table_expr(context, semantic_model, return_expr, Some(check_type)); } add_type_check_diagnostic( @@ -109,7 +104,7 @@ fn check_return_stat( .unwrap_or(&return_stat.get_range()), check_type, return_expr_type, - result, + &mismatch, ); } } @@ -123,19 +118,14 @@ fn check_return_stat( } let return_expr_type = &return_expr_types[0]; let return_expr_range = return_expr_ranges[0]; - let result = semantic_model.type_check_detail(check_type, return_expr_type); - if result.is_err() { + if let Err(mismatch) = semantic_model.check_assignable(return_expr_type, check_type) { if return_expr_type.is_table() && let Some(return_expr) = return_exprs.first() { // 表字段已经报错了, 则不添加返回值不匹配的诊断避免干扰 - if let Some(add_diagnostic) = check_table_expr( - context, - semantic_model, - NodeOrToken::Node(return_expr.syntax().clone()), - return_expr, - Some(return_type), - ) && add_diagnostic + if let Some(add_diagnostic) = + check_table_expr(context, semantic_model, return_expr, Some(return_type)) + && add_diagnostic { return Some(()); } @@ -147,7 +137,7 @@ fn check_return_stat( return_expr_range, return_type, return_expr_type, - result, + &mismatch, ); } } @@ -163,34 +153,22 @@ fn add_type_check_diagnostic( range: TextRange, param_type: &LuaType, expr_type: &LuaType, - result: TypeCheckResult, + mismatch: &TypeMismatch, ) { let db = semantic_model.get_db(); - match result { - Ok(_) => (), - Err(reason) => { - let reason_message = match reason { - TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason, - TypeCheckFailReason::TypeNotMatch | TypeCheckFailReason::DonotCheck => { - "".to_string() - } - TypeCheckFailReason::TypeRecursion => "type recursion".to_string(), - }; - context.add_diagnostic( - DiagnosticCode::ReturnTypeMismatch, - range, - t!( - "Annotations specify that return value %{index} has a type of `%{source}`, returning value of type `%{found}` here instead. %{reason}", - index = index + 1, - source = humanize_lint_type(db, param_type), - found = humanize_lint_type(db, expr_type), - reason = reason_message - ) - .to_string(), - None, - ); - } - } + context.add_diagnostic( + DiagnosticCode::ReturnTypeMismatch, + range, + t!( + "Annotations specify that return value %{index} has a type of `%{source}`, returning value of type `%{found}` here instead. %{reason}", + index = index + 1, + source = humanize_lint_type(db, param_type), + found = humanize_lint_type(db, expr_type), + reason = render_type_mismatch(db, mismatch) + ) + .to_string(), + None, + ); } fn has_setmetatable(semantic_model: &SemanticModel, return_stat: &LuaReturnStat) -> Option { 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 4eb2c9162..8140cf690 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 @@ -891,6 +891,70 @@ return t )); } + #[test] + fn test_literal_argument_selects_overload_return_type() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.has_no_diagnostic( + DiagnosticCode::AssignTypeMismatch, + r#" + ---@class A + ---@class B + local C + + ---@overload fun(): A + ---@overload fun(type: "fluid"): B + function C.name(type) end + + local tmp = C.name("fluid") + "# + )); + } + + #[test] + fn test_explicit_signature_param_keeps_main_return_priority() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.has_no_diagnostic( + DiagnosticCode::AssignTypeMismatch, + r#" + ---@class A + ---@class B + local C + local a ---@type A + + ---@overload fun(type: "fluid"): B + ---@param type "fluid" + function C.name(type) + return a + end + + local tmp = C.name("fluid") + "# + )); + } + + #[test] + fn test_table_field_explicit_signature_param_keeps_main_return_priority() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.has_no_diagnostic( + DiagnosticCode::AssignTypeMismatch, + r#" + ---@class A + ---@class B + local a ---@type A + + local C = { + ---@overload fun(type: "fluid"): B + ---@param type "fluid" + name = function(type) + return a + end + } + + local tmp = C.name("fluid") + "# + )); + } + #[test] fn test_table_pack_in_function() { let mut ws = VirtualWorkspace::new_with_init_std_lib(); @@ -1520,4 +1584,22 @@ return t "# )); } + + #[test] + fn test_keyof_nested_doc_index_access_accepts_member_name() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.has_no_diagnostic( + DiagnosticCode::AssignTypeMismatch, + r#" + ---@class A + ---@field test number + + ---@class B + ---@field test A + + ---@type keyof B["test"] + local tmp = "test" + "# + )); + } } 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 d543789b8..3f3a60f9c 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 @@ -1,5 +1,8 @@ #[cfg(test)] mod tests { + use lsp_types::NumberOrString; + use tokio_util::sync::CancellationToken; + use crate::{DiagnosticCode, VirtualWorkspace}; #[test] @@ -336,23 +339,48 @@ foo({}) } #[test] - fn test_lsp_optimization_skip_table_fields_check_skips_missing_fields() { - let mut ws = VirtualWorkspace::new_with_init_std_lib(); - assert!(ws.has_no_diagnostic( - DiagnosticCode::MissingFields, - r#" - ---@class D32.Child - ---@field name string - - ---@class D32.Config - ---@field child D32.Child - - ---@[lsp_optimization("skip_table_fields_check")] - ---@type D32.Config - local config = { - child = {}, - } - "# + fn test_call_argument_comment_does_not_shift_missing_fields_range() { + let mut ws = VirtualWorkspace::new(); + ws.analysis + .diagnostic + .enable_only(DiagnosticCode::MissingFields); + let file_id = ws.def( + r#"---@class A +---@field a 1 +---@class B +---@field b 2 +---@class C +---@field c 3 + +---@param a A +---@param b B +---@param c C +local function test(a, b, c) end + +test( + -- What + {}, + {}, + {} +)"#, + ); + let code = Some(NumberOrString::String( + DiagnosticCode::MissingFields.get_name().to_string(), )); + let diagnostics = ws + .analysis + .diagnose_file(file_id, CancellationToken::new()) + .unwrap_or_default() + .into_iter() + .filter(|diagnostic| diagnostic.code == code) + .collect::>(); + + assert_eq!(diagnostics.len(), 3, "{diagnostics:#?}"); + assert_eq!(diagnostics[0].range.start.line, 14, "{diagnostics:#?}"); + assert!(diagnostics[0].message.contains("`a`"), "{diagnostics:#?}"); + assert_eq!(diagnostics[1].range.start.line, 15, "{diagnostics:#?}"); + assert!(diagnostics[1].message.contains("`b`"), "{diagnostics:#?}"); + assert_eq!(diagnostics[2].range.start.line, 16, "{diagnostics:#?}"); + assert!(diagnostics[2].message.contains("`c`"), "{diagnostics:#?}"); } } diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs index 21d83b321..0533f8fe7 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs @@ -1046,4 +1046,49 @@ mod test { "# )); } + + #[test] + fn test_doc_index_access_type_resolves_object_field() { + let mut ws = VirtualWorkspace::new(); + let code = r#" + ---@class A + ---@field b { test: number } + + ---@type A['b'] + local tmp + local out = tmp.test + result = out + "#; + + let has_no_diagnostic = ws.has_no_diagnostic(DiagnosticCode::UndefinedField, code); + let result_ty = ws.expr_ty("result"); + let expected_ty = ws.ty("number"); + let result_type_text = ws.humanize_type(result_ty.clone()); + + assert!( + has_no_diagnostic && result_ty == expected_ty, + "undefined-field diagnostic: {}; result type: {}", + !has_no_diagnostic, + result_type_text + ); + } + + #[test] + fn test_doc_index_access_type_cycle_does_not_overflow() { + let mut ws = VirtualWorkspace::new(); + assert!(!ws.has_no_diagnostic( + DiagnosticCode::UndefinedField, + r#" + ---@class A + ---@field b B['c'] + + ---@class B + ---@field c A['b'] + + ---@type A['b'] + local tmp + local out = tmp.test + "# + )); + } } diff --git a/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/instantiate_conditional_generic.rs b/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/instantiate_conditional_generic.rs index 16e05ac36..8c0b690bf 100644 --- a/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/instantiate_conditional_generic.rs +++ b/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/instantiate_conditional_generic.rs @@ -3,9 +3,12 @@ use std::ops::Deref; use crate::{ DbIndex, GenericTpl, GenericTplId, LuaConditionalType, LuaTypeDeclId, LuaTypeNode, TypeOps, - check_type_compact, db_index::{LuaObjectType, LuaTupleType, LuaType}, - semantic::{member::find_members_with_key, type_check::check_type_compact_with_level}, + is_assignable, + semantic::{ + member::find_members_with_key, + type_check::{RelationKind, RelationOutcome, is_assignable_ex}, + }, }; use super::{get_default_constructor, instantiate_type_generic_inner}; @@ -236,14 +239,10 @@ fn check_conditional_extends(db: &DbIndex, source: &LuaType, target: &LuaType) - return ConditionalCheck::False; } - if check_type_compact_with_level( - db, - source, - target, - crate::semantic::type_check::TypeCheckCheckLevel::GenericConditional, - ) - .is_ok() - { + if matches!( + is_assignable_ex(db, source, target, RelationKind::ConditionalExtends), + RelationOutcome::Related + ) { ConditionalCheck::True } else { ConditionalCheck::False @@ -572,7 +571,7 @@ fn strict_type_match(db: &DbIndex, source: &LuaType, pattern: &LuaType) -> bool return true; } - check_type_compact(db, pattern, source).is_ok() + is_assignable(db, source, pattern) } fn is_optional_param_type(db: &DbIndex, ty: &LuaType) -> bool { diff --git a/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/instantiate_special_generic.rs b/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/instantiate_special_generic.rs index 461ef2f8a..73a3f043f 100644 --- a/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/instantiate_special_generic.rs +++ b/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type/instantiate_special_generic.rs @@ -70,8 +70,15 @@ pub(super) fn instantiate_alias_call( ); } - let compact = - type_check::check_type_compact(context.db, &operands[0], &operands[1]).is_ok(); + let compact = matches!( + type_check::is_assignable_ex( + context.db, + &operands[0], + &operands[1], + type_check::RelationKind::ConditionalExtends, + ), + type_check::RelationOutcome::Related + ); LuaType::BooleanConst(compact) } LuaAliasCallKind::Select => { diff --git a/crates/emmylua_code_analysis/src/semantic/generic/tpl_pattern/mod.rs b/crates/emmylua_code_analysis/src/semantic/generic/tpl_pattern/mod.rs index d93626f2e..8d983584b 100644 --- a/crates/emmylua_code_analysis/src/semantic/generic/tpl_pattern/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/generic/tpl_pattern/mod.rs @@ -11,9 +11,9 @@ use smol_str::SmolStr; use crate::{ InferFailReason, LuaFunctionType, LuaMemberInfo, LuaMemberKey, LuaMemberOwner, LuaObjectType, LuaSemanticDeclId, LuaTupleType, LuaTypeDeclId, LuaTypeNode, LuaUnionType, SemanticDeclLevel, - VariadicType, check_type_compact, + VariadicType, db_index::{DbIndex, LuaGenericType, LuaType}, - infer_node_semantic_decl, + infer_node_semantic_decl, is_assignable, semantic::{ generic::{ tpl_context::TplContext, @@ -245,9 +245,9 @@ fn object_tpl_pattern_match( let target_index_access = target_object.get_index_access(); for (origin_key, v) in origin_obj.get_index_access() { // 先匹配 key 类型进行转换 - let target_access = target_index_access.iter().find(|(target_key, _)| { - check_type_compact(context.db, origin_key, target_key).is_ok() - }); + let target_access = target_index_access + .iter() + .find(|(target_key, _)| is_assignable(context.db, target_key, origin_key)); if let Some(target_access) = target_access { tpl_pattern_match(context, origin_key, &target_access.0)?; tpl_pattern_match(context, v, &target_access.1)?; @@ -499,8 +499,7 @@ fn table_generic_tpl_pattern_member_owner_match( _ => continue, }; - if !target_key_type.is_generic() - && check_type_compact(context.db, &target_key_type, &key_type).is_err() + if !target_key_type.is_generic() && !is_assignable(context.db, &key_type, &target_key_type) { continue; } @@ -534,7 +533,7 @@ fn table_generic_tpl_pattern_member_owner_match( LuaMemberKey::TypeKey(typ) => typ.clone(), _ => return, }; - if check_type_compact(context.db, &target_key_type, &key_type).is_ok() { + if is_assignable(context.db, &key_type, &target_key_type) { keys.push(key_type); values.push(m.typ.clone()); } diff --git a/crates/emmylua_code_analysis/src/semantic/guard.rs b/crates/emmylua_code_analysis/src/semantic/guard.rs index 64b2bae16..bd1cb9e23 100644 --- a/crates/emmylua_code_analysis/src/semantic/guard.rs +++ b/crates/emmylua_code_analysis/src/semantic/guard.rs @@ -1,6 +1,6 @@ use std::{cell::RefCell, collections::HashSet, rc::Rc}; -use crate::{InferFailReason, LuaTypeDeclId}; +use crate::{InferFailReason, LuaType, LuaTypeDeclId}; pub type InferGuardRef = Rc; @@ -8,7 +8,7 @@ pub type InferGuardRef = Rc; /// /// This guard uses a lazy allocation strategy: /// - Fork is zero-cost (no HashSet allocation) -/// - `current` HashSet is only created when needed (write-on-create) +/// - Visited declaration and instantiated type sets are only created when needed /// - Most child guards never allocate memory if they only read from parents /// /// # Memory Layout @@ -23,9 +23,10 @@ pub type InferGuardRef = Rc; /// ``` #[derive(Debug, Clone)] pub struct InferGuard { - /// Current level's visited types (lazily allocated) - /// Only created when we need to add a new type not in parent chain + /// Current level's visited type declarations (lazily allocated) current: RefCell>>, + /// Current level's visited instantiated types (lazily allocated) + current_types: RefCell>>, /// Parent guard (shared reference) parent: Option>, } @@ -34,6 +35,7 @@ impl InferGuard { pub fn new() -> Rc { Rc::new(Self { current: RefCell::new(None), + current_types: RefCell::new(None), parent: None, }) } @@ -44,6 +46,7 @@ impl InferGuard { pub fn fork(self: &Rc) -> Rc { Rc::new(Self { current: RefCell::new(None), // Lazy allocation + current_types: RefCell::new(None), parent: Some(Rc::clone(self)), }) } @@ -70,6 +73,20 @@ impl InferGuard { Ok(()) } + pub fn check_type(&self, typ: &LuaType) -> Result<(), InferFailReason> { + if self.contains_type_in_parents(typ) { + return Err(InferFailReason::RecursiveInfer); + } + + let mut current_types = self.current_types.borrow_mut(); + let current_types = current_types.get_or_insert_with(HashSet::default); + if !current_types.insert(typ.clone()) { + return Err(InferFailReason::RecursiveInfer); + } + + Ok(()) + } + /// Check if a type has been visited in parent chain fn contains_in_parents(&self, type_id: &LuaTypeDeclId) -> bool { let mut current_parent = self.parent.as_ref(); @@ -84,6 +101,19 @@ impl InferGuard { false } + fn contains_type_in_parents(&self, typ: &LuaType) -> bool { + let mut current_parent = self.parent.as_ref(); + while let Some(parent) = current_parent { + if let Some(ref types) = *parent.current_types.borrow() + && types.contains(typ) + { + return true; + } + current_parent = parent.parent.as_ref(); + } + false + } + /// Check if a type has been visited (without modifying the guard) pub fn contains(&self, type_id: &LuaTypeDeclId) -> bool { // Check current level @@ -99,6 +129,11 @@ impl InferGuard { /// Get the depth of current level pub fn current_depth(&self) -> usize { self.current.borrow().as_ref().map_or(0, |set| set.len()) + + self + .current_types + .borrow() + .as_ref() + .map_or(0, |set| set.len()) } /// Get the total depth of the entire guard chain @@ -127,7 +162,7 @@ impl InferGuard { /// Useful for debugging and performance analysis #[cfg(test)] pub fn has_allocated(&self) -> bool { - self.current.borrow().is_some() + self.current.borrow().is_some() || self.current_types.borrow().is_some() } } diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/infer_binary_or.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/infer_binary_or.rs index 3494cf6be..12073dbb4 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/infer_binary_or.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/infer_binary_or.rs @@ -1,8 +1,9 @@ use emmylua_parser::LuaExpr; use crate::{ - BasicTypeKind, DbIndex, LuaType, LuaUnionType, TypeOps, check_type_compact, + BasicTypeKind, DbIndex, LuaType, LuaUnionType, TypeOps, db_index::{LuaMemberOwner, LuaTypeCache, LuaTypeDeclId}, + is_assignable, semantic::infer::{InferResult, narrow::remove_false_or_nil}, }; @@ -22,13 +23,13 @@ fn can_empty_table_satisfy_type(db: &DbIndex, ty: &LuaType) -> bool { LuaType::Table | LuaType::TableConst(_) => true, // For class/ref types, check if all fields (including inherited) are optional - LuaType::Ref(type_decl_id) => { + LuaType::Ref(type_decl_id) | LuaType::Def(type_decl_id) => { // Collect this type and all its super types (includes inheritance) let all_types = type_decl_id.collect_super_types_with_self(db, ty.clone()); // Check each type in the hierarchy for required fields for typ in all_types { - if let LuaType::Ref(decl_id) = typ { + if let LuaType::Ref(decl_id) | LuaType::Def(decl_id) = typ { if has_required_fields(db, &decl_id) { return false; // Found a required field somewhere in hierarchy } @@ -105,7 +106,7 @@ pub fn special_or_rule( let left_without_nil = remove_false_or_nil(left_type.clone()); if table_expr.is_empty() { // Remove nil/false from left type and check if result is table-compatible - if check_type_compact(db, &left_without_nil, &LuaType::Table).is_ok() { + if is_assignable(db, &LuaType::Table, &left_without_nil) { // Only narrow if empty table can actually satisfy the type // (i.e., the type has no required fields) if can_empty_table_satisfy_type(db, &left_without_nil) { @@ -113,7 +114,7 @@ pub fn special_or_rule( } // Otherwise, fall through to regular OR logic which will create a union } - } else if check_type_compact(db, &left_without_nil, right_type).is_ok() { + } else if is_assignable(db, right_type, &left_without_nil) { return Some(left_without_nil); } } @@ -127,7 +128,7 @@ pub fn special_or_rule( return None; } - if check_type_compact(db, left_type, right_type).is_ok() { + if is_assignable(db, right_type, left_type) { return Some(remove_false_or_nil(left_type.clone())); } } diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/mod.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/mod.rs index df3bf18b3..408f3acb4 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_binary/mod.rs @@ -7,9 +7,9 @@ use infer_binary_or::{infer_binary_expr_or, special_or_rule}; use smol_str::SmolStr; use crate::{ - LuaInferCache, TypeOps, check_type_compact, + LuaInferCache, TypeOps, db_index::{DbIndex, LuaOperatorMetaMethod, LuaType}, - get_real_type, + get_real_type, is_assignable, }; use super::{InferFailReason, InferResult, get_custom_type_operator, infer_expr}; @@ -137,7 +137,7 @@ fn infer_binary_custom_operator( { for operator in operators { let operand = operator.get_operand(db); - if check_type_compact(db, &operand, right).is_ok() { + if is_assignable(db, right, &operand) { return operator.get_result(db); } } @@ -149,7 +149,7 @@ fn infer_binary_custom_operator( { for operator in operators { let operand = operator.get_operand(db); - if check_type_compact(db, &operand, left).is_ok() { + if is_assignable(db, left, &operand) { return operator.get_result(db); } } diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_index/infer_array.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_index/infer_array.rs index aadd064da..bf746d533 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_index/infer_array.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_index/infer_array.rs @@ -5,7 +5,7 @@ use emmylua_parser::{ use crate::{ DbIndex, InferFailReason, LuaArrayLen, LuaArrayType, LuaInferCache, LuaMemberKey, LuaType, - TypeOps, check_type_compact, + TypeOps, is_assignable, semantic::infer::{infer_index::infer_expr_for_index, narrow::get_var_expr_var_ref_id}, }; @@ -69,7 +69,7 @@ fn key_type_matches(db: &DbIndex, expected: &LuaType, actual: &LuaType) -> bool !matches!( actual, LuaType::Any | LuaType::Unknown | LuaType::TplRef(_) | LuaType::StrTplRef(_) - ) && check_type_compact(db, expected, actual).is_ok() + ) && is_assignable(db, actual, expected) } pub(super) fn array_member_fallback(db: &DbIndex, base: &LuaType) -> LuaType { 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 747a375b1..a0551927b 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 @@ -29,7 +29,7 @@ use crate::{ }, member::get_buildin_type_map_type_id, member::intersect_member_types, - type_check::check_type_compact, + type_check::is_assignable, }, }; @@ -336,6 +336,19 @@ fn infer_member_by_lookup( LuaType::TableGeneric(table_generic) => { infer_table_generic_member_by_key_type(db, table_generic, &lookup.key_type) } + LuaType::Call(alias_call) + if alias_call.get_call_kind() == LuaAliasCallKind::Index + && !prefix_type.contain_tpl() => + { + let index_guard = infer_guard.fork(); + index_guard.check_type(prefix_type)?; + let resolved = instantiate_type_generic(db, prefix_type, &TypeSubstitutor::new()); + if resolved == *prefix_type { + Err(InferFailReason::FieldNotFound) + } else { + infer_member_by_lookup(db, cache, &resolved, lookup, &index_guard) + } + } LuaType::TplRef(tpl) => infer_tpl_ref_member(db, cache, tpl, lookup, infer_guard), LuaType::ModuleRef(file_id) => { let module_info = db.get_module_index().get_module(*file_id); @@ -475,7 +488,7 @@ fn infer_matching_member_key_type( continue; }; - if check_type_compact(db, key_type, &member_key_type).is_ok() { + if is_assignable(db, &member_key_type, key_type) { let member_type = db .get_type_index() .get_type_cache(&member.get_id().into()) @@ -604,7 +617,7 @@ fn infer_index_metamethod_by_key_type( key_type: &LuaType, value_type: &LuaType, ) -> InferResult { - if check_type_compact(db, key_type, access_key_type).is_ok() { + if is_assignable(db, access_key_type, key_type) { return Ok(value_type.clone()); } 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 31171a94a..6fdb49180 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_table.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_table.rs @@ -7,9 +7,9 @@ use emmylua_parser::{ use crate::{ InFiled, InferGuard, LuaArrayType, LuaDeclId, LuaInferCache, LuaMemberId, LuaTupleStatus, - LuaTupleType, LuaUnionType, TypeOps, VariadicType, check_type_compact, + LuaTupleType, LuaUnionType, TypeOps, VariadicType, db_index::{DbIndex, LuaType}, - infer_call_expr_func, infer_expr, + infer_call_expr_func, infer_expr, is_assignable, }; use super::{InferFailReason, InferResult, infer_index::infer_member}; @@ -91,7 +91,7 @@ fn infer_table_tuple_or_array( let field = fields.get(i).ok_or(InferFailReason::None)?; let value_expr = field.get_value_expr().ok_or(InferFailReason::None)?; let typ = infer_expr(db, cache, value_expr)?; - if check_type_compact(db, &non_nil_base, &typ).is_err() { + if !is_assignable(db, &typ, &non_nil_base) { all_can_accept_base = false; break; } @@ -224,7 +224,7 @@ fn infer_table_type_by_callee( )?; let param_types = func_type.get_params(); let mut call_arg_number = call_arg_list - .children::() + .get_args() .enumerate() .find(|(_, arg)| arg.get_position() == table_expr.get_position()) .ok_or(InferFailReason::None)? diff --git a/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs b/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs index 3363c46ef..1c85c2fef 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs @@ -8,7 +8,7 @@ use std::{rc::Rc, sync::Arc}; use crate::{ CacheEntry, DbIndex, FlowAntecedent, FlowId, FlowNode, FlowNodeKind, FlowTree, InferFailReason, - LuaDeclId, LuaInferCache, LuaMemberId, LuaSignatureId, LuaType, TypeOps, check_type_compact, + LuaDeclId, LuaInferCache, LuaMemberId, LuaSignatureId, LuaType, TypeOps, is_assignable, semantic::{ cache::{FlowAssignmentInfo, FlowMode, FlowQueryResult, FlowVarCache}, infer::{ @@ -1987,7 +1987,7 @@ fn is_partial_assignment_expr_compatible( source_type: &LuaType, expr_type: &LuaType, ) -> bool { - if check_type_compact(db, source_type, expr_type).is_ok() { + if is_assignable(db, expr_type, source_type) { return true; } diff --git a/crates/emmylua_code_analysis/src/semantic/infer/test.rs b/crates/emmylua_code_analysis/src/semantic/infer/test.rs index c01f7c9b7..5f46b8c74 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/test.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/test.rs @@ -99,8 +99,8 @@ mod test { let integer = ws.ty("integer"); let foo = ws.expr_ty("Foo"); let bar = ws.expr_ty("Bar"); - assert!(ws.check_type(&integer, &foo)); - assert!(ws.check_type(&integer, &bar)); + assert!(ws.check_type(&foo, &integer)); + assert!(ws.check_type(&bar, &integer)); } #[test] @@ -117,7 +117,7 @@ mod test { let integer = ws.ty("integer"); let foo = ws.expr_ty("Foo"); - assert!(!ws.check_type(&integer, &foo)); + assert!(!ws.check_type(&foo, &integer)); } #[test] diff --git a/crates/emmylua_code_analysis/src/semantic/member/infer_raw_member.rs b/crates/emmylua_code_analysis/src/semantic/member/infer_raw_member.rs index 503e23d65..26c17a70c 100644 --- a/crates/emmylua_code_analysis/src/semantic/member/infer_raw_member.rs +++ b/crates/emmylua_code_analysis/src/semantic/member/infer_raw_member.rs @@ -2,8 +2,7 @@ use std::sync::Arc; use crate::{ DbIndex, InferFailReason, InferGuard, InferGuardRef, LuaGenericType, LuaMemberKey, - LuaMemberOwner, LuaObjectType, LuaTupleType, LuaType, LuaTypeDeclId, TypeOps, - check_type_compact, + LuaMemberOwner, LuaObjectType, LuaTupleType, LuaType, LuaTypeDeclId, TypeOps, is_assignable, semantic::generic::{TypeSubstitutor, instantiate_type_generic}, }; @@ -107,7 +106,7 @@ fn infer_custom_type_raw_member_type( continue; }; - if check_type_compact(db, index_key_type, &access_key_type).is_err() { + if !is_assignable(db, &access_key_type, index_key_type) { continue; } @@ -175,7 +174,7 @@ fn infer_object_raw_member_type( continue; }; - if check_type_compact(db, key, &access_key_type).is_ok() { + if is_assignable(db, &access_key_type, key) { return Ok(value.clone()); } } @@ -220,7 +219,7 @@ fn infer_table_generic_raw_member_type( return Err(InferFailReason::FieldNotFound); }; - if check_type_compact(db, key_type, &access_key_type).is_ok() { + if is_assignable(db, &access_key_type, key_type) { return Ok(value_type.clone()); } diff --git a/crates/emmylua_code_analysis/src/semantic/mod.rs b/crates/emmylua_code_analysis/src/semantic/mod.rs index 5ec790fb7..49146d8d4 100644 --- a/crates/emmylua_code_analysis/src/semantic/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/mod.rs @@ -39,13 +39,11 @@ pub(crate) use semantic_info::{infer_node_semantic_decl, resolve_global_decl_id} use semantic_info::{ infer_node_semantic_info, infer_token_semantic_decl, infer_token_semantic_info, }; -pub(crate) use type_check::check_type_compact; -pub(crate) use type_check::is_sub_type_of; +pub(crate) use type_check::{RelationKind, RelationOutcome, is_assignable_ex, is_sub_type_of}; pub use visibility::check_module_visibility; use visibility::check_visibility; pub use crate::semantic::member::find_members_with_key; -use crate::semantic::type_check::check_type_compact_detail; use crate::{Emmyrc, LuaDocument, LuaSemanticDeclId, ModuleInfo, db_index::LuaTypeDeclId}; use crate::{ FileId, @@ -67,7 +65,10 @@ pub use overload_resolve::{ collect_callable_overload_groups, filter_callable_overloads, find_callable_overload, }; pub use semantic_info::SemanticDeclLevel; -pub use type_check::{TypeCheckFailReason, TypeCheckResult}; +pub use type_check::{ + OverflowKind, TypeMismatch, TypeMismatchKind, TypePathSegment, check_assignable, is_assignable, + render_type_mismatch, +}; pub use generic::get_keyof_members; pub use infer::{DocTypeInferContext, infer_doc_type}; @@ -175,12 +176,12 @@ impl<'a> SemanticModel<'a> { get_member_map_in_scope(self.db, self.file_id, prefix_type) } - pub fn type_check(&self, source: &LuaType, compact_type: &LuaType) -> TypeCheckResult { - check_type_compact(self.db, source, compact_type) + pub fn is_assignable(&self, source: &LuaType, target: &LuaType) -> bool { + is_assignable(self.db, source, target) } - pub fn type_check_detail(&self, source: &LuaType, compact_type: &LuaType) -> TypeCheckResult { - check_type_compact_detail(self.db, source, compact_type) + pub fn check_assignable(&self, source: &LuaType, target: &LuaType) -> Result<(), TypeMismatch> { + check_assignable(self.db, source, target) } pub fn infer_call_expr_func( 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 d5d6a3a81..af0ebb4b1 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 @@ -91,9 +91,14 @@ fn collect_callable_overload_groups_inner( let Some(signature) = db.get_signature_index().get(sig_id) else { return Ok(()); }; - // 主签名描述了函数实现本身, 当它和 overload 同时可匹配时应作为同等匹配下的优先候选. - let mut overloads = vec![signature.to_doc_func_type()]; - overloads.extend(signature.overloads.iter().cloned()); + let main_signature = signature.to_doc_func_type(); + let mut overloads = signature.overloads.clone(); + // 显式声明参数或返回值的主签名, 在匹配程度相同时优先于 overload. + if signature.has_explicit_docs { + overloads.insert(0, main_signature); + } else { + overloads.push(main_signature); + } groups.push(overloads); } LuaType::Instance(instance) => { @@ -136,7 +141,9 @@ fn push_call_operator_overload_group( }; // 同一个 owner 的 call operators 作为一个 overload group, 由调用方再做参数匹配. - let mut overloads = Vec::new(); + let mut declared_signatures = Vec::new(); + let mut doc_functions = Vec::new(); + let mut undeclared_signatures = Vec::new(); for operator_id in operator_ids { let Some(operator) = db.get_operator_index().get_operator(operator_id) else { continue; @@ -148,22 +155,29 @@ fn push_call_operator_overload_group( } match func_type { - LuaType::DocFunction(func) => overloads.push(func), + LuaType::DocFunction(func) => doc_functions.push(func), LuaType::Signature(signature_id) => { let Some(signature) = db.get_signature_index().get(&signature_id) else { continue; }; // 未解析返回的 signature 不能安全转换成候选, 这里先跳过. if signature.is_resolve_return() { - overloads.push(signature.to_call_operator_func_type()); + let function = signature.to_call_operator_func_type(); + if signature.has_explicit_docs { + declared_signatures.push(function); + } else { + undeclared_signatures.push(function); + } } } _ => {} } } - if !overloads.is_empty() { - groups.push(overloads); + declared_signatures.extend(doc_functions); + declared_signatures.extend(undeclared_signatures); + if !declared_signatures.is_empty() { + groups.push(declared_signatures); } } diff --git a/crates/emmylua_code_analysis/src/semantic/overload_resolve/resolve_signature_by_args.rs b/crates/emmylua_code_analysis/src/semantic/overload_resolve/resolve_signature_by_args.rs index 63575c1c5..7a8408f5e 100644 --- a/crates/emmylua_code_analysis/src/semantic/overload_resolve/resolve_signature_by_args.rs +++ b/crates/emmylua_code_analysis/src/semantic/overload_resolve/resolve_signature_by_args.rs @@ -2,8 +2,9 @@ use std::cmp::Ordering; use std::sync::Arc; use crate::{ - InferFailReason, check_type_compact, + InferFailReason, db_index::{DbIndex, LuaFunctionType, LuaType}, + is_assignable, semantic::infer::InferCallFuncResult, }; @@ -27,7 +28,7 @@ pub(crate) fn callable_accepts_args( return false; }; - if !param_type.is_any() && check_type_compact(db, ¶m_type, expr_type).is_err() { + if !param_type.is_any() && !is_assignable(db, expr_type, ¶m_type) { return false; } } @@ -98,7 +99,7 @@ pub fn resolve_signature_by_args( ParamMatchResult::Any } else if param_type.is_any() { ParamMatchResult::Any - } else if check_type_compact(db, ¶m_type, expr_type).is_ok() { + } else if is_assignable(db, expr_type, ¶m_type) { ParamMatchResult::Type } else { ParamMatchResult::Not @@ -383,8 +384,8 @@ fn compare_param_specificity( _ => {} } - let a_sub_b = check_type_compact(db, b, a).is_ok(); - let b_sub_a = check_type_compact(db, a, b).is_ok(); + let a_sub_b = is_assignable(db, a, b); + let b_sub_a = is_assignable(db, b, a); match (a_sub_b, b_sub_a) { (true, false) => Ordering::Greater, (false, true) => Ordering::Less, diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/callable.rs b/crates/emmylua_code_analysis/src/semantic/type_check/callable.rs new file mode 100644 index 000000000..bf809f6eb --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/callable.rs @@ -0,0 +1,348 @@ +use crate::{ + LuaFunctionType, LuaOperatorMetaMethod, LuaType, TypeSubstitutor, db_index::LuaTypeDeclId, + instantiate_type_generic, +}; +use hashbrown::HashSet; + +use super::{ + mismatch::{TypeMismatch, TypePathSegment}, + normalize_type, + relation::{ + IntersectionState, Relater, RelationFailure, RelationOutcome, RelationResult, + declared_super_types, + }, + sub_type::is_sub_type_of, +}; + +pub(crate) fn relate_callable( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + context: IntersectionState, +) -> Option { + if matches!(source, LuaType::Signature(signature_id) if relater.db().get_signature_index().get(signature_id).is_some_and(|signature| signature.is_generic())) + || matches!(target, LuaType::Signature(signature_id) if relater.db().get_signature_index().get(signature_id).is_some_and(|signature| signature.is_generic())) + { + relater.note_progress(); + return Some(Ok(())); + } + + if matches!(source, LuaType::Function) { + if let Some(candidates) = callable_candidates(relater, target) { + return Some(if candidates.is_empty() { + relater.unrelated(|| TypeMismatch::incompatible(source, target)) + } else { + relater.note_progress(); + Ok(()) + }); + } + } + + if matches!(target, LuaType::Function) { + return callable_candidates(relater, source).map(|candidates| { + if candidates.is_empty() { + relater.unrelated(|| TypeMismatch::incompatible(source, target)) + } else { + relater.note_progress(); + Ok(()) + } + }); + } + + if let (LuaType::DocFunction(source_func), LuaType::DocFunction(target_func)) = (source, target) + { + return Some(relate_function( + relater, + source, + source_func, + target, + target_func, + context, + )); + } + + let target_candidates = callable_candidates(relater, target)?; + let source_candidates = match callable_candidates(relater, source) { + Some(candidates) if !candidates.is_empty() => candidates, + _ => { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + } + }; + if target_candidates.is_empty() { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + } + + let mut best = None; + let mut indeterminate = None; + for (target_index, target_candidate) in target_candidates.iter().enumerate() { + for (source_index, source_candidate) in source_candidates.iter().enumerate() { + let (outcome, progress) = + relater.probe_relation(source_candidate, target_candidate, context); + match outcome { + RelationOutcome::Related => return Some(Ok(())), + RelationOutcome::Indeterminate(kind) => { + indeterminate.get_or_insert(kind); + } + RelationOutcome::Unrelated => { + if best + .map(|(_, _, best_progress)| progress > best_progress) + .unwrap_or(true) + { + best = Some((source_index, target_index, progress)); + } + } + } + } + } + if let Some(kind) = indeterminate { + return Some(Err(relater.indeterminate_failure(kind, source, target))); + } + let Some((source_index, target_index, _)) = best else { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + }; + if !relater.is_explain() { + return Some(Err(RelationFailure::Unrelated(None))); + } + Some( + relater + .relate( + &source_candidates[source_index], + &target_candidates[target_index], + context, + ) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at( + TypePathSegment::TargetUnionCandidate(target_index), + source, + target, + ) + }) + }), + ) +} + +fn relate_function( + relater: &mut Relater, + source: &LuaType, + source_func: &LuaFunctionType, + target: &LuaType, + target_func: &LuaFunctionType, + context: IntersectionState, +) -> RelationResult { + let target_self_offset = usize::from(target_func.is_colon_define()); + let source_self_offset = usize::from( + source_func.is_colon_define() + && (target_func.is_colon_define() + || target_func + .get_params() + .first() + .is_some_and(|(name, _)| name == "self")), + ); + let target_len = target_func.get_params().len() + target_self_offset; + for index in 0..target_len { + let (target_name, target_type) = if index < target_self_offset { + ("self", None) + } else { + let (name, typ) = &target_func.get_params()[index - target_self_offset]; + (name.as_str(), typ.as_ref()) + }; + let (source_name, source_type) = if index < source_self_offset { + ("self", None) + } else if let Some((name, typ)) = source_func.get_params().get(index - source_self_offset) { + (name.as_str(), typ.as_ref()) + } else { + break; + }; + if source_name == "..." { + if let Some(source_vararg) = source_type { + for remaining in index..target_len { + let remaining_target = if remaining < target_self_offset { + None + } else { + target_func.get_params()[remaining - target_self_offset] + .1 + .as_ref() + }; + if let Some(remaining_target) = remaining_target { + relater + .relate(remaining_target, source_vararg, context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at( + TypePathSegment::FunctionParameter(remaining), + source, + target, + ) + }) + })?; + } + } + } + break; + } + if target_name == "..." { + break; + } + if let (Some(source_type), Some(target_type)) = (source_type, target_type) { + if source_type.is_self_infer() || target_type.is_self_infer() { + relater.note_progress(); + continue; + } + let result = match nominal_union_assignable(relater, target_type, source_type) { + Some(true) => Ok(()), + Some(false) => { + relater.unrelated(|| TypeMismatch::incompatible(target_type, source_type)) + } + None => relater.relate(target_type, source_type, context), + }; + result.map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::FunctionParameter(index), source, target) + }) + })?; + relater.note_progress(); + } + } + + relater + .relate(source_func.get_ret(), target_func.get_ret(), context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::FunctionReturn(0), source, target) + }) + })?; + relater.note_progress(); + Ok(()) +} + +fn nominal_union_assignable(relater: &Relater, source: &LuaType, target: &LuaType) -> Option { + let (LuaType::Union(source_union), LuaType::Union(target_union)) = (source, target) else { + return None; + }; + let source_members = source_union.into_vec(); + let target_members = target_union.into_vec(); + let type_id = |typ: &LuaType| match typ { + LuaType::Ref(type_id) | LuaType::Def(type_id) => Some(type_id.clone()), + LuaType::Generic(generic) => Some(generic.get_base_type_id()), + _ => None, + }; + let source_ids = source_members + .iter() + .map(type_id) + .collect::>>()?; + let target_ids = target_members + .iter() + .map(type_id) + .collect::>>()?; + + Some(source_ids.iter().all(|source_id| { + target_ids.iter().any(|target_id| { + source_id == target_id || is_sub_type_of(relater.db(), source_id, target_id) + }) + })) +} + +fn callable_candidates(relater: &Relater, typ: &LuaType) -> Option> { + collect_callable_candidates(relater, typ, &mut HashSet::new()) +} + +fn collect_callable_candidates( + relater: &Relater, + typ: &LuaType, + visited: &mut HashSet, +) -> Option> { + if let Some(normalized) = normalize_type(relater.db(), typ) + && normalized != *typ + { + return collect_callable_candidates(relater, &normalized, visited); + } + match typ { + LuaType::Function => Some(vec![LuaType::Function]), + LuaType::DocFunction(_) => Some(vec![typ.clone()]), + LuaType::Signature(signature_id) => { + let signature = relater.db().get_signature_index().get(signature_id)?; + if signature.is_generic() { + return Some(vec![LuaType::DocFunction(signature.to_doc_func_type())]); + } + let mut candidates = signature + .overloads + .iter() + .cloned() + .map(LuaType::DocFunction) + .collect::>(); + candidates.push(LuaType::DocFunction(signature.to_doc_func_type())); + Some(candidates) + } + LuaType::Ref(type_id) | LuaType::Def(type_id) => { + if !visited.insert(type_id.clone()) { + return None; + } + let mut candidates = callable_operators(relater, type_id, None).unwrap_or_default(); + for super_type in declared_super_types(relater.db(), typ) { + if let Some(mut super_candidates) = + collect_callable_candidates(relater, &super_type, visited) + { + candidates.append(&mut super_candidates); + } + } + (!candidates.is_empty()).then_some(candidates) + } + LuaType::Generic(generic) => { + let type_id = generic.get_base_type_id(); + if !visited.insert(type_id.clone()) { + return None; + } + let substitutor = TypeSubstitutor::from_type_array(generic.get_params().clone()); + let mut candidates = + callable_operators(relater, &type_id, Some(&substitutor)).unwrap_or_default(); + for super_type in declared_super_types(relater.db(), typ) { + if let Some(mut super_candidates) = + collect_callable_candidates(relater, &super_type, visited) + { + candidates.append(&mut super_candidates); + } + } + (!candidates.is_empty()).then_some(candidates) + } + _ => None, + } +} + +fn callable_operators( + relater: &Relater, + type_id: &LuaTypeDeclId, + substitutor: Option<&TypeSubstitutor>, +) -> Option> { + let type_decl = relater.db().get_type_index().get_type_decl(type_id)?; + if !type_decl.is_class() { + return None; + } + let operator_ids = relater + .db() + .get_operator_index() + .get_operators(&type_id.clone().into(), LuaOperatorMetaMethod::Call)?; + let mut candidates = Vec::with_capacity(operator_ids.len()); + for operator_id in operator_ids { + let Some(operator) = relater.db().get_operator_index().get_operator(operator_id) else { + continue; + }; + let candidate = match operator.get_operator_func(relater.db()) { + LuaType::DocFunction(function) => LuaType::DocFunction(function), + LuaType::Signature(signature_id) => { + let Some(signature) = relater.db().get_signature_index().get(&signature_id) else { + continue; + }; + LuaType::DocFunction(signature.to_call_operator_func_type()) + } + _ => continue, + }; + let candidate = substitutor + .map(|substitutor| instantiate_type_generic(relater.db(), &candidate, substitutor)) + .unwrap_or(candidate); + if let LuaType::DocFunction(function) = candidate { + candidates.push(LuaType::DocFunction(function)); + } + } + Some(candidates) +} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/array_type_check.rs b/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/array_type_check.rs deleted file mode 100644 index fdb99d9df..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/array_type_check.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::{ - LuaMemberKey, LuaMemberOwner, LuaType, TypeCheckFailReason, TypeCheckResult, TypeOps, - find_index_operations, - semantic::type_check::{ - check_general_type_compact, type_check_context::TypeCheckContext, - type_check_guard::TypeCheckGuard, - }, -}; - -pub fn check_array_type_compact( - context: &mut TypeCheckContext, - source_base: &LuaType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let source_base = if context.db.get_emmyrc().strict.array_index { - TypeOps::Union.apply(context.db, source_base, &LuaType::Nil) - } else { - source_base.clone() - }; - - match compact_type { - LuaType::Array(compact_array_type) => { - return check_general_type_compact( - context, - &source_base, - compact_array_type.get_base(), - check_guard.next_level()?, - ); - } - LuaType::Tuple(tuple_type) => { - for element_type in tuple_type.get_types() { - check_general_type_compact( - context, - &source_base, - element_type, - check_guard.next_level()?, - )?; - } - - return Ok(()); - } - LuaType::TableConst(inst) => { - let table_member_owner = LuaMemberOwner::Element(inst.clone()); - return check_array_type_compact_table( - context, - &source_base, - table_member_owner, - check_guard.next_level()?, - ); - } - LuaType::Object(compact_object) => { - let compact_base = compact_object - .cast_down_array_base(context.db) - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - return check_general_type_compact( - context, - &source_base, - &compact_base, - check_guard.next_level()?, - ); - } - LuaType::Table => return Ok(()), - LuaType::TableGeneric(compact_types) => { - if compact_types.len() == 2 { - for typ in compact_types.iter() { - check_general_type_compact( - context, - &source_base, - typ, - check_guard.next_level()?, - )?; - } - - return Ok(()); - } - } - LuaType::Any => return Ok(()), - LuaType::Ref(_) | LuaType::Def(_) => { - return check_array_type_compact_ref_def( - context, - &source_base, - compact_type, - check_guard.next_level()?, - ); - } - _ => {} - } - - Err(TypeCheckFailReason::DonotCheck) -} - -fn check_array_type_compact_ref_def( - context: &mut TypeCheckContext, - source_base: &LuaType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let Some(members) = find_index_operations(context.db, compact_type) else { - return Err(TypeCheckFailReason::TypeNotMatch); - }; - - for member in &members { - if let LuaMemberKey::TypeKey(key_type) = &member.key - && key_type.is_integer() - && let Ok(()) = - check_general_type_compact(context, source_base, &member.typ, check_guard) - { - return Ok(()); - } - } - - Err(TypeCheckFailReason::TypeNotMatch) -} - -fn check_array_type_compact_table( - context: &mut TypeCheckContext, - source_base: &LuaType, - table_owner: LuaMemberOwner, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let member_index = context.db.get_member_index(); - - let member_len = member_index.get_member_len(&table_owner); - for i in 0..member_len { - let key = LuaMemberKey::Integer((i + 1) as i64); - if let Some(member_item) = member_index.get_member_item(&table_owner, &key) { - let member_type = member_item - .resolve_type(context.db) - .map_err(|_| TypeCheckFailReason::TypeNotMatch)?; - check_general_type_compact( - context, - source_base, - &member_type, - check_guard.next_level()?, - )?; - } else { - return Err(TypeCheckFailReason::TypeNotMatch); - } - } - - Ok(()) -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/call_type_check.rs b/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/call_type_check.rs deleted file mode 100644 index c096813b1..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/call_type_check.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::sync::Arc; - -use crate::{ - LuaAliasCallKind, LuaAliasCallType, LuaMemberKey, LuaType, LuaUnionType, TypeCheckFailReason, - TypeCheckResult, get_keyof_members, - semantic::type_check::{ - check_general_type_compact, type_check_context::TypeCheckContext, - type_check_guard::TypeCheckGuard, - }, -}; - -pub fn check_call_type_compact( - context: &mut TypeCheckContext, - source_call: &LuaAliasCallType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - if let LuaAliasCallKind::KeyOf = source_call.get_call_kind() { - let source_operands = source_call.get_operands().iter().collect::>(); - if source_operands.len() != 1 { - return Err(TypeCheckFailReason::TypeNotMatch); - } - match compact_type { - LuaType::Call(compact_call) => { - if compact_call.get_call_kind() == LuaAliasCallKind::KeyOf { - if compact_call.as_ref() == source_call { - return Ok(()); - } - let compact_operands = compact_call.get_operands().iter().collect::>(); - if compact_operands.len() != 1 { - return Err(TypeCheckFailReason::TypeNotMatch); - } - - let source_key_types = LuaType::Union(Arc::new(LuaUnionType::from_vec( - get_keyof_keys(context, &source_operands[0]), - ))); - let compact_key_types = LuaType::Union(Arc::new(LuaUnionType::from_vec( - get_keyof_keys(context, &compact_operands[0]), - ))); - return check_general_type_compact( - context, - &source_key_types, - &compact_key_types, - check_guard.next_level()?, - ); - } - } - _ => { - let key_types = get_keyof_keys(context, &source_operands[0]); - for key_type in &key_types { - match check_general_type_compact( - context, - &key_type, - compact_type, - check_guard.next_level()?, - ) { - Ok(_) => return Ok(()), - Err(e) if e.is_type_not_match() => {} - Err(e) => return Err(e), - } - } - return Err(TypeCheckFailReason::TypeNotMatch); - } - } - } - - // TODO: 实现其他 call 类型的检查 - Ok(()) -} - -fn get_keyof_keys(context: &TypeCheckContext, prefix_type: &LuaType) -> Vec { - let members = get_keyof_members(context.db, prefix_type).unwrap_or_default(); - let key_types = members - .iter() - .filter_map(|m| match &m.key { - LuaMemberKey::Integer(i) => Some(LuaType::DocIntegerConst(*i)), - LuaMemberKey::Name(s) => Some(LuaType::DocStringConst(s.clone().into())), - _ => None, - }) - .collect::>(); - key_types -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/intersection_type_check.rs b/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/intersection_type_check.rs deleted file mode 100644 index e27bf3a94..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/intersection_type_check.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::sync::Arc; - -use crate::{ - LuaIntersectionType, LuaMemberOwner, LuaType, TypeCheckFailReason, TypeCheckResult, - semantic::type_check::{ - check_general_type_compact, intersection_utils::intersection_to_object, - type_check_context::TypeCheckContext, type_check_guard::TypeCheckGuard, - }, -}; - -pub fn check_intersection_type_compact( - context: &mut TypeCheckContext, - source_intersection: &LuaIntersectionType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - match compact_type { - LuaType::TableConst(range) => check_intersection_type_compact_table( - context, - source_intersection, - LuaMemberOwner::Element(range.clone()), - check_guard.next_level()?, - ), - LuaType::Def(_) | LuaType::Ref(_) => { - if let Some(object_type) = intersection_to_object(context.db, source_intersection) { - let object_type = LuaType::Object(Arc::new(object_type)); - check_general_type_compact( - context, - &object_type, - compact_type, - check_guard.next_level()?, - ) - } else { - Err(TypeCheckFailReason::TypeNotMatch) - } - } - LuaType::Object(_) => { - // 检查对象是否满足交叉类型的所有组成部分 - for intersection_component in source_intersection.get_types() { - // NOTE: Use a fresh TypeCheckContext per component so `table_member_checked` (and - // similar state) doesn't leak across components. Otherwise `A & B` may check `A` - // first and then skip `B`'s same-key checks. - let mut component_context = - TypeCheckContext::new(context.db, context.detail, context.level.clone()); - check_general_type_compact( - &mut component_context, - intersection_component, - compact_type, - check_guard.next_level()?, - )?; - } - Ok(()) - } - LuaType::Intersection(compact_intersection) => { - // 交叉类型对交叉类型:检查所有组成部分 - check_intersection_type_compact_intersection( - context, - source_intersection, - compact_intersection, - check_guard.next_level()?, - ) - } - LuaType::Table => Ok(()), // 通用表类型可以匹配任何交叉类型 - _ => { - // 对于其他类型,检查是否至少满足一个组成部分 - for intersection_component in source_intersection.get_types() { - // NOTE: Use a fresh TypeCheckContext per component to avoid leaking check state. - let mut component_context = - TypeCheckContext::new(context.db, context.detail, context.level.clone()); - if check_general_type_compact( - &mut component_context, - intersection_component, - compact_type, - check_guard.next_level()?, - ) - .is_ok() - { - return Ok(()); - } - } - Err(TypeCheckFailReason::TypeNotMatch) - } - } -} - -fn check_intersection_type_compact_table( - context: &mut TypeCheckContext, - source_intersection: &LuaIntersectionType, - table_owner: LuaMemberOwner, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - // 交叉类型要求 TableConst 必须满足所有组成部分 - for intersection_component in source_intersection.get_types() { - // NOTE: Use a fresh TypeCheckContext per component to avoid leaking check state. - let mut component_context = - TypeCheckContext::new(context.db, context.detail, context.level.clone()); - check_general_type_compact( - &mut component_context, - intersection_component, - &LuaType::TableConst( - table_owner - .get_element_range() - .ok_or(TypeCheckFailReason::TypeNotMatch)? - .clone(), - ), - check_guard.next_level()?, - )?; - } - - Ok(()) -} - -fn check_intersection_type_compact_intersection( - context: &mut TypeCheckContext, - source_intersection: &LuaIntersectionType, - compact_intersection: &LuaIntersectionType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - // 检查源交叉类型的每个组成部分是否都能在目标交叉类型中找到匹配 - for source_component in source_intersection.get_types() { - let mut component_matched = false; - - for compact_component in compact_intersection.get_types() { - if check_general_type_compact( - context, - source_component, - compact_component, - check_guard.next_level()?, - ) - .is_ok() - { - component_matched = true; - break; - } - } - - if !component_matched { - return Err(TypeCheckFailReason::TypeNotMatch); - } - } - - Ok(()) -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/mod.rs b/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/mod.rs deleted file mode 100644 index 0cfff26d4..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/mod.rs +++ /dev/null @@ -1,154 +0,0 @@ -mod array_type_check; -mod call_type_check; -mod intersection_type_check; -mod object_type_check; -mod table_generic_check; -mod tuple_type_check; - -use array_type_check::check_array_type_compact; -use call_type_check::check_call_type_compact; -use intersection_type_check::check_intersection_type_compact; -use object_type_check::check_object_type_compact; -use table_generic_check::check_table_generic_type_compact; -use tuple_type_check::check_tuple_type_compact; - -use crate::{LuaType, LuaUnionType, semantic::type_check::type_check_context::TypeCheckContext}; - -use super::{ - TypeCheckResult, check_general_type_compact, type_check_fail_reason::TypeCheckFailReason, - type_check_guard::TypeCheckGuard, -}; - -// all is duck typing -pub fn check_complex_type_compact( - context: &mut TypeCheckContext, - source: &LuaType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - match source { - LuaType::Array(source_array_type) => { - match check_array_type_compact( - context, - source_array_type.get_base(), - compact_type, - check_guard, - ) { - Err(TypeCheckFailReason::DonotCheck) => {} - result => return result, - } - } - LuaType::Tuple(tuple) => { - match check_tuple_type_compact(context, tuple, compact_type, check_guard) { - Err(TypeCheckFailReason::DonotCheck) => {} - result => return result, - } - } - LuaType::Object(source_object) => { - match check_object_type_compact(context, source_object, compact_type, check_guard) { - Err(TypeCheckFailReason::DonotCheck) => {} - result => return result, - } - } - LuaType::TableGeneric(source_generic_param) => { - match check_table_generic_type_compact( - context, - source_generic_param, - compact_type, - check_guard, - ) { - Err(TypeCheckFailReason::DonotCheck) => {} - result => return result, - } - } - LuaType::Intersection(source_intersection) => { - match check_intersection_type_compact( - context, - source_intersection, - compact_type, - check_guard, - ) { - Err(TypeCheckFailReason::DonotCheck) => {} - result => return result, - } - } - LuaType::Union(union_type) => { - if let LuaType::Union(compact_union) = compact_type { - return check_union_type_compact_union( - context, - source, - compact_union, - check_guard.next_level()?, - ); - } - for sub_type in union_type.into_vec() { - match check_general_type_compact( - context, - &sub_type, - compact_type, - check_guard.next_level()?, - ) { - Ok(_) => return Ok(()), - Err(e) if e.is_type_not_match() => {} - Err(e) => return Err(e), - } - } - - return Err(TypeCheckFailReason::TypeNotMatch); - } - LuaType::Generic(_) => { - return Ok(()); - } - LuaType::Call(alias_call) => { - return check_call_type_compact(context, alias_call, compact_type, check_guard); - } - LuaType::MultiLineUnion(multi_union) => { - let union = multi_union.to_union(); - return check_complex_type_compact( - context, - &union, - compact_type, - check_guard.next_level()?, - ); - } - _ => {} - } - // Do I need to check union types? - if let LuaType::Union(union) = compact_type { - for sub_compact in union.into_vec() { - match check_complex_type_compact( - context, - source, - &sub_compact, - check_guard.next_level()?, - ) { - Ok(_) => {} - Err(e) => return Err(e), - } - } - - return Ok(()); - } - - Err(TypeCheckFailReason::TypeNotMatch) -} - -// too complex -fn check_union_type_compact_union( - context: &mut TypeCheckContext, - source: &LuaType, - compact_union: &LuaUnionType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let compact_types = compact_union.into_vec(); - for compact_sub_type in compact_types { - check_general_type_compact( - context, - source, - &compact_sub_type, - check_guard.next_level()?, - )?; - } - - Ok(()) -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/object_type_check.rs b/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/object_type_check.rs deleted file mode 100644 index 96859224b..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/object_type_check.rs +++ /dev/null @@ -1,409 +0,0 @@ -use std::collections::{HashMap, hash_map::Entry}; - -use crate::{ - LuaMemberKey, LuaMemberOwner, LuaObjectType, LuaTupleType, LuaType, RenderLevel, - TypeCheckFailReason, TypeCheckResult, humanize_type, - semantic::{ - member::find_members, - type_check::{ - check_general_type_compact, type_check_context::TypeCheckContext, - type_check_guard::TypeCheckGuard, - }, - }, -}; - -pub fn check_object_type_compact( - context: &mut TypeCheckContext, - source_object: &LuaObjectType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - match compact_type { - LuaType::Object(compact_object) => { - return check_object_type_compact_object_type( - context, - source_object, - compact_object, - check_guard.next_level()?, - ); - } - LuaType::TableConst(inst) => { - return check_object_type_compact_table_const( - context, - source_object, - inst, - check_guard.next_level()?, - ); - } - LuaType::Ref(type_id) | LuaType::Def(type_id) => { - return check_object_type_compact_type_ref( - context, - source_object, - type_id, - check_guard.next_level()?, - ); - } - LuaType::Tuple(compact_tuple) => { - return check_object_type_compact_tuple( - context, - source_object, - compact_tuple, - check_guard.next_level()?, - ); - } - LuaType::Array(array_type) => { - return check_object_type_compact_array( - context, - source_object, - array_type.get_base(), - check_guard.next_level()?, - ); - } - LuaType::Table => return Ok(()), - _ => {} - } - - Err(TypeCheckFailReason::DonotCheck) -} - -fn check_object_type_compact_object_type( - context: &mut TypeCheckContext, - source_object: &LuaObjectType, - compact_object: &LuaObjectType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let source_members = source_object.get_fields(); - let compact_members = compact_object.get_fields(); - - for (key, source_type) in source_members { - let compact_type = match compact_members.get(key) { - Some(t) => t, - None => { - if source_type.is_nullable() || source_type.is_any() { - continue; - } else { - return Err(TypeCheckFailReason::TypeNotMatch); - } - } - }; - check_general_type_compact( - context, - source_type, - compact_type, - check_guard.next_level()?, - )?; - } - - Ok(()) -} - -struct TypeMembers { - map: HashMap, - index_keys: Vec, -} - -fn collect_type_members( - context: &TypeCheckContext, - type_id: &crate::LuaTypeDeclId, - collect_index_keys: bool, -) -> Option { - let type_members = find_members(context.db, &LuaType::Ref(type_id.clone()))?; - - // Build a merged view of class members (including supertypes). When the same key appears - // multiple times (override), keep the first one (subclass wins). - let mut map: HashMap = HashMap::new(); - let mut index_keys: Vec = Vec::new(); - if collect_index_keys { - index_keys.reserve(type_members.len()); - } - - for member in type_members { - let key = member.key; - let typ = member.typ; - - if let Entry::Vacant(entry) = map.entry(key) { - if collect_index_keys { - index_keys.push(entry.key().clone()); - } - entry.insert(typ); - } - } - - Some(TypeMembers { map, index_keys }) -} - -fn check_member_value( - context: &mut TypeCheckContext, - key: &LuaMemberKey, - key_type_for_display: Option<&LuaType>, - source_type: &LuaType, - member_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - match check_general_type_compact(context, source_type, member_type, check_guard.next_level()?) { - Ok(_) => Ok(()), - Err(TypeCheckFailReason::TypeNotMatch) => { - let mut key_display = key.to_path(); - if key_display.is_empty() { - if let Some(key_type_for_display) = key_type_for_display { - key_display = - humanize_type(context.db, key_type_for_display, RenderLevel::Simple); - } - } - - Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "member %{key} not match, expect %{typ}, but got %{got}", - key = key_display, - typ = humanize_type(context.db, source_type, RenderLevel::Simple), - got = humanize_type(context.db, member_type, RenderLevel::Simple) - ) - .to_string(), - )) - } - Err(e) => Err(e), - } -} - -fn check_object_type_compact_table_const( - context: &mut TypeCheckContext, - source_object: &LuaObjectType, - table_range: &crate::InFiled, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let db = context.db; - let member_owner = LuaMemberOwner::Element(table_range.clone()); - let member_index = db.get_member_index(); - let source_fields = source_object.get_fields(); - - // 检查名称字段 - for (key, source_type) in source_fields { - let member_item = match member_index.get_member_item(&member_owner, key) { - Some(member_item) => member_item, - None => { - if source_type.is_nullable() || source_type.is_any() { - continue; - } else { - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing member %{key}", key = key.to_path().to_string()).to_string(), - )); - } - } - }; - let member_type = match member_item.resolve_type(db) { - Ok(t) => t, - _ => { - continue; - } - }; - - check_member_value(context, key, None, source_type, &member_type, check_guard)?; - } - - if source_object.get_index_access().is_empty() { - return Ok(()); - } - - // 检查索引访问字段 - let members = member_index.get_members(&member_owner).unwrap_or_default(); - for (key_type, source_type) in source_object.get_index_access() { - for member in &members { - let member = *member; - if source_fields.contains_key(member.get_key()) { - continue; - } - let Some(member_key_type) = member.get_key().to_index_type() else { - continue; - }; - - let key_match = match check_general_type_compact( - context, - key_type, - &member_key_type, - check_guard.next_level()?, - ) { - Ok(_) => true, - Err(err) => { - if err.is_type_not_match() { - false - } else { - return Err(err); - } - } - }; - - if !key_match { - continue; - } - - let member_type = match context - .db - .get_type_index() - .get_type_cache(&member.get_id().into()) - { - Some(cache) => cache.as_type(), - None => continue, - }; - - check_member_value( - context, - member.get_key(), - Some(&member_key_type), - source_type, - member_type, - check_guard, - )?; - break; - } - } - - Ok(()) -} - -fn check_object_type_compact_type_ref( - context: &mut TypeCheckContext, - source_object: &LuaObjectType, - type_id: &crate::LuaTypeDeclId, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let source_fields = source_object.get_fields(); - let has_index_access = !source_object.get_index_access().is_empty(); - - let Some(type_members) = collect_type_members(context, type_id, has_index_access) else { - return Ok(()); - }; - - for (key, source_type) in source_fields { - let Some(member_type) = type_members.map.get(key) else { - if source_type.is_nullable() || source_type.is_any() { - continue; - } - - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing member %{key}", key = key.to_path().to_string()).to_string(), - )); - }; - - check_member_value(context, key, None, source_type, member_type, check_guard)?; - } - - if !has_index_access { - return Ok(()); - } - - for (key_type, source_type) in source_object.get_index_access() { - for member_key in &type_members.index_keys { - if source_fields.contains_key(member_key) { - continue; - } - - let Some(member_key_type) = member_key.to_index_type() else { - continue; - }; - - let key_match = match check_general_type_compact( - context, - key_type, - &member_key_type, - check_guard.next_level()?, - ) { - Ok(_) => true, - Err(err) if err.is_type_not_match() => false, - Err(err) => return Err(err), - }; - - if !key_match { - continue; - } - - let Some(member_type) = type_members.map.get(member_key) else { - continue; - }; - - check_member_value( - context, - member_key, - Some(&member_key_type), - source_type, - member_type, - check_guard, - )?; - break; - } - } - - Ok(()) -} - -fn check_object_type_compact_tuple( - context: &mut TypeCheckContext, - source_object: &LuaObjectType, - tuple_type: &LuaTupleType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let source_members = source_object.get_fields(); - for (source_key, source_type) in source_members { - let idx = match source_key { - LuaMemberKey::Integer(i) => i - 1, - _ => { - if source_type.is_nullable() || source_type.is_any() { - continue; - } else { - return Err(TypeCheckFailReason::TypeNotMatch); - } - } - }; - - if idx < 0 { - continue; - } - - let idx = idx as usize; - let tuple_member_type = match tuple_type.get_type(idx) { - Some(t) => t, - None => { - if source_type.is_nullable() || source_type.is_any() { - continue; - } else { - return Err(TypeCheckFailReason::TypeNotMatch); - } - } - }; - - check_general_type_compact( - context, - source_type, - tuple_member_type, - check_guard.next_level()?, - )?; - } - - Ok(()) -} - -fn check_object_type_compact_array( - context: &mut TypeCheckContext, - source_object: &LuaObjectType, - array: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let index_access = source_object.get_index_access(); - if index_access.is_empty() { - return Err(TypeCheckFailReason::TypeNotMatch); - } - for (key, source_type) in index_access { - if !key.is_integer() { - continue; - } - match check_general_type_compact(context, source_type, array, check_guard.next_level()?) { - Ok(_) => { - return Ok(()); - } - Err(e) if e.is_type_not_match() => {} - Err(e) => { - return Err(e); - } - } - } - Err(TypeCheckFailReason::TypeNotMatch) -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/table_generic_check.rs b/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/table_generic_check.rs deleted file mode 100644 index 5dd054cb8..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/table_generic_check.rs +++ /dev/null @@ -1,158 +0,0 @@ -use crate::{ - LuaMemberKey, LuaMemberOwner, LuaType, LuaTypeCache, TypeCheckFailReason, TypeCheckResult, - semantic::type_check::{ - check_general_type_compact, type_check_context::TypeCheckContext, - type_check_guard::TypeCheckGuard, - }, -}; - -pub fn check_table_generic_type_compact( - context: &mut TypeCheckContext, - source_generic_param: &Vec, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - match compact_type { - LuaType::Table | LuaType::Global => return Ok(()), - LuaType::TableGeneric(compact_generic_param) => { - if source_generic_param.len() == 2 && compact_generic_param.len() == 2 { - let source_key = &source_generic_param[0]; - let source_value = &source_generic_param[1]; - let compact_key = &compact_generic_param[0]; - let compact_value = &compact_generic_param[1]; - - check_general_type_compact( - context, - source_key, - compact_key, - check_guard.next_level()?, - )?; - check_general_type_compact( - context, - source_value, - compact_value, - check_guard.next_level()?, - )?; - return Ok(()); - } - } - LuaType::TableConst(inst) => { - let table_member_owner = LuaMemberOwner::Element(inst.clone()); - return check_table_generic_compact_member_owner( - context, - source_generic_param, - table_member_owner, - check_guard.next_level()?, - ); - } - LuaType::Array(array_type) => { - if source_generic_param.len() == 2 { - let key = &source_generic_param[0]; - let value = &source_generic_param[1]; - if key.is_any() || key.is_integer() { - return check_general_type_compact( - context, - value, - array_type.get_base(), - check_guard.next_level()?, - ); - } - } - } - LuaType::Tuple(tuple) => { - if source_generic_param.len() == 2 { - let key = &source_generic_param[0]; - let value = &source_generic_param[1]; - if key.is_any() { - for tuple_type in tuple.get_types() { - check_general_type_compact( - context, - value, - tuple_type, - check_guard.next_level()?, - )?; - } - - return Ok(()); - } - - return Ok(()); - } - } - LuaType::Userdata => return Ok(()), - // maybe support object - // need check later - LuaType::Ref(id) | LuaType::Def(id) => { - let owner = LuaMemberOwner::Type(id.clone()); - return check_table_generic_compact_member_owner( - context, - source_generic_param, - owner, - check_guard.next_level()?, - ); - } - LuaType::Generic(_) => { - return Ok(()); - } - LuaType::Union(union) => { - for union_type in union.into_vec() { - check_table_generic_type_compact( - context, - source_generic_param, - &union_type, - check_guard, - )?; - } - - return Ok(()); - } - _ => {} - } - - Err(TypeCheckFailReason::TypeNotMatch) -} - -fn check_table_generic_compact_member_owner( - context: &mut TypeCheckContext, - source_generic_params: &[LuaType], - member_owner: LuaMemberOwner, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - if source_generic_params.len() != 2 { - return Err(TypeCheckFailReason::TypeNotMatch); - } - - let member_index = context.db.get_member_index(); - let members = match member_index.get_members(&member_owner) { - Some(members) => members, - None => return Ok(()), - }; - - let source_key = &source_generic_params[0]; - let source_value = &source_generic_params[1]; - - for member in members { - let key = member.get_key(); - let key_type = match key { - LuaMemberKey::Integer(i) => LuaType::IntegerConst(*i), - LuaMemberKey::Name(s) => LuaType::StringConst(s.clone().into()), - _ => LuaType::Any, - }; - - let member_type = context - .db - .get_type_index() - .get_type_cache(&member.get_id().into()) - .unwrap_or(&LuaTypeCache::InferType(LuaType::Unknown)) - .as_type(); - check_general_type_compact(context, source_key, &key_type, check_guard.next_level()?)?; - check_general_type_compact( - context, - source_value, - member_type, - check_guard.next_level()?, - )?; - } - - Ok(()) -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/tuple_type_check.rs b/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/tuple_type_check.rs deleted file mode 100644 index 4cc92a31f..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type/tuple_type_check.rs +++ /dev/null @@ -1,283 +0,0 @@ -use std::ops::Deref; - -use crate::{ - LuaMemberKey, LuaMemberOwner, LuaObjectType, LuaTupleType, LuaType, RenderLevel, - TypeCheckFailReason, TypeCheckResult, VariadicType, humanize_type, - semantic::type_check::{ - check_general_type_compact, type_check_context::TypeCheckContext, - type_check_guard::TypeCheckGuard, - }, -}; - -pub fn check_tuple_type_compact( - context: &mut TypeCheckContext, - tuple: &LuaTupleType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - match compact_type { - LuaType::Tuple(compact_tuple) => { - return check_tuple_type_compact_tuple( - context, - tuple, - compact_tuple, - check_guard.next_level()?, - ); - } - LuaType::Array(array_type) => { - for source_type in tuple.get_types() { - check_general_type_compact( - context, - array_type.get_base(), - source_type, - check_guard.next_level()?, - )?; - } - - return Ok(()); - } - LuaType::TableConst(inst) => { - let table_member_owner = LuaMemberOwner::Element(inst.clone()); - return check_tuple_type_compact_table( - context, - tuple, - table_member_owner, - check_guard.next_level()?, - ); - } - LuaType::Object(object) => { - return check_tuple_type_compact_object_type( - context, - tuple, - object, - check_guard.next_level()?, - ); - } - // for any untyped table - LuaType::Table => return Ok(()), - _ => {} - } - - Err(TypeCheckFailReason::DonotCheck) -} - -fn check_tuple_type_compact_tuple( - context: &mut TypeCheckContext, - source_tuple: &LuaTupleType, - compact_tuple: &LuaTupleType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let source_tuple_members = source_tuple.get_types(); - let compact_tuple_members = compact_tuple.get_types(); - - check_tuple_types_compact_tuple_types( - context, - 0, - source_tuple_members, - compact_tuple_members, - check_guard, - ) -} - -fn check_tuple_types_compact_tuple_types( - context: &mut TypeCheckContext, - source_start: usize, - sources: &[LuaType], - compacts: &[LuaType], - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let source_size = sources.len(); - let compact_size = compacts.len(); - - for i in 0..source_size { - let source_tuple_member_type = &sources[i]; - if i >= compact_size { - if source_tuple_member_type.is_optional() { - continue; - } else { - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing tuple member %{idx}", idx = i + source_start + 1).to_string(), - )); - } - } - let compact_tuple_member_type = &compacts[i]; - match (source_tuple_member_type, compact_tuple_member_type) { - (LuaType::Variadic(variadic), _) => { - if let VariadicType::Base(inner) = variadic.deref() { - let compact_rest_len = compact_size - i; - if compact_rest_len == 0 { - return Ok(()); - } - let mut new_source_types = vec![]; - for _ in 0..compact_rest_len { - new_source_types.push(inner.clone()); - } - return check_tuple_types_compact_tuple_types( - context, - i, - &new_source_types, - &compacts[i..], - check_guard.next_level()?, - ); - } - } - (_, LuaType::Variadic(variadic)) => { - if let VariadicType::Base(compact_inner) = variadic.deref() { - let source_rest_len = source_size - i; - if source_rest_len == 0 { - return Ok(()); - } - let mut new_compact_types = vec![]; - for _ in 0..source_rest_len { - new_compact_types.push(compact_inner.clone()); - } - return check_tuple_types_compact_tuple_types( - context, - i, - &sources[i..], - &new_compact_types, - check_guard.next_level()?, - ); - } - } - _ => { - match check_general_type_compact( - context, - source_tuple_member_type, - compact_tuple_member_type, - check_guard.next_level()?, - ) { - Ok(_) => {} - Err(TypeCheckFailReason::TypeNotMatch) => { - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "tuple member %{idx} not match, expect %{typ}, but got %{got}", - idx = i + source_start + 1, - typ = humanize_type( - context.db, - source_tuple_member_type, - RenderLevel::Simple - ), - got = humanize_type( - context.db, - compact_tuple_member_type, - RenderLevel::Simple - ) - ) - .to_string(), - )); - } - Err(e) => { - return Err(e); - } - } - } - } - } - - Ok(()) -} - -fn check_tuple_type_compact_table( - context: &mut TypeCheckContext, - source_tuple: &LuaTupleType, - table_owner: LuaMemberOwner, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let member_index = context.db.get_member_index(); - let tuple_members = source_tuple.get_types(); - for (i, source_tuple_member_type) in tuple_members.iter().enumerate() { - let key = LuaMemberKey::Integer((i + 1) as i64); - if let Some(member_item) = member_index.get_member_item(&table_owner, &key) { - let member_type = member_item - .resolve_type(context.db) - .map_err(|_| TypeCheckFailReason::TypeNotMatch)?; - match check_general_type_compact( - context, - source_tuple_member_type, - &member_type, - check_guard.next_level()?, - ) { - Ok(_) => {} - Err(TypeCheckFailReason::TypeNotMatch) => { - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "tuple member %{idx} not match, expect %{typ}, but got %{got}", - idx = i + 1, - typ = humanize_type( - context.db, - source_tuple_member_type, - RenderLevel::Simple - ), - got = humanize_type(context.db, &member_type, RenderLevel::Simple) - ) - .to_string(), - )); - } - Err(e) => { - return Err(e); - } - } - } else if source_tuple_member_type.is_optional() { - continue; - } else { - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing tuple member %{idx}", idx = i + 1).to_string(), - )); - } - } - - Ok(()) -} - -fn check_tuple_type_compact_object_type( - context: &mut TypeCheckContext, - source_tuple: &LuaTupleType, - object_type: &LuaObjectType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let object_members = object_type.get_fields(); - - let tuple_members = source_tuple.get_types(); - // for i in 0..size { - for (i, source_tuple_member_type) in tuple_members.iter().enumerate() { - let key = LuaMemberKey::Integer((i + 1) as i64); - if let Some(object_member_type) = object_members.get(&key) { - match check_general_type_compact( - context, - source_tuple_member_type, - object_member_type, - check_guard.next_level()?, - ) { - Ok(_) => {} - Err(TypeCheckFailReason::TypeNotMatch) => { - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "tuple member %{idx} not match, expect %{typ}, but got %{got}", - idx = i + 1, - typ = humanize_type( - context.db, - source_tuple_member_type, - RenderLevel::Simple - ), - got = - humanize_type(context.db, object_member_type, RenderLevel::Simple) - ) - .to_string(), - )); - } - Err(e) => { - return Err(e); - } - } - } else if source_tuple_member_type.is_nullable() || source_tuple_member_type.is_any() { - continue; - } else { - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing tuple member %{idx}", idx = i + 1).to_string(), - )); - } - } - - Ok(()) -} 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 deleted file mode 100644 index e115d8ea5..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/func_type.rs +++ /dev/null @@ -1,279 +0,0 @@ -use crate::{ - db_index::{LuaFunctionType, LuaOperatorMetaMethod, LuaSignatureId, LuaType, LuaTypeDeclId}, - semantic::type_check::type_check_context::TypeCheckContext, -}; - -use super::{ - TypeCheckResult, check_general_type_compact, type_check_fail_reason::TypeCheckFailReason, - type_check_guard::TypeCheckGuard, -}; - -pub fn check_doc_func_type_compact( - context: &mut TypeCheckContext, - source_func: &LuaFunctionType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - match compact_type { - LuaType::DocFunction(compact_func) => { - check_doc_func_type_compact_for_params(context, source_func, compact_func, check_guard) - } - LuaType::Signature(signature_id) => check_doc_func_type_compact_for_signature( - context, - source_func, - signature_id, - check_guard, - ), - LuaType::Ref(type_id) => { - check_doc_func_type_compact_for_custom_type(context, source_func, type_id, check_guard) - } - LuaType::Def(type_id) => { - check_doc_func_type_compact_for_custom_type(context, source_func, type_id, check_guard) - } - LuaType::Union(union) => { - for union_type in union.into_vec() { - check_doc_func_type_compact( - context, - source_func, - &union_type, - check_guard.next_level()?, - )?; - } - - Ok(()) - } - LuaType::Function => Ok(()), - _ => Err(TypeCheckFailReason::TypeNotMatch), - } -} - -fn check_doc_func_type_compact_for_params( - context: &mut TypeCheckContext, - source_func: &LuaFunctionType, - compact_func: &LuaFunctionType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let source_params = source_func.get_params(); - let mut compact_params: Vec<(String, Option)> = compact_func.get_params().to_vec(); - - if compact_func.is_colon_define() { - compact_params.insert(0, ("self".to_string(), None)); - } - - let compact_len = compact_params.len(); - - for i in 0..compact_len { - let source_param = match source_params.get(i) { - Some(p) => p, - None => { - break; - } - }; - let compact_param = &compact_params[i]; - - let source_param_type = &source_param.1; - // too many complex session to handle varargs - if source_param.0 == "..." { - check_doc_func_type_compact_for_varargs( - context, - source_param_type, - &compact_params[i..], - check_guard.next_level()?, - )?; - } - - if compact_param.0 == "..." { - break; - } - - 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, - compact_type, - source_type, - check_guard.next_level()?, - ) { - Ok(()) => {} - Err(e) if e.is_type_not_match() => { - if i == 0 && source_type.is_self_infer() && compact_param.0 == "self" { - continue; - } - // add error message - return Err(e); - } - Err(e) => { - return Err(e); - } - } - } - } - - // todo check return type - - Ok(()) -} - -fn check_doc_func_type_compact_for_varargs( - context: &mut TypeCheckContext, - varargs: &Option, - compact_params: &[(String, Option)], - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - if let Some(varargs_type) = varargs { - for compact_param in compact_params { - let compact_param_type = &compact_param.1; - if let Some(compact_param_type) = compact_param_type { - check_general_type_compact( - context, - compact_param_type, - varargs_type, - check_guard.next_level()?, - )?; - } - } - } - - Ok(()) -} - -fn check_doc_func_type_compact_for_signature( - context: &mut TypeCheckContext, - source_func: &LuaFunctionType, - signature_id: &LuaSignatureId, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let signature = context - .db - .get_signature_index() - .get(signature_id) - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - - // dotnot check generic method - if signature.is_generic() { - return Ok(()); - } - - for overload_func in &signature.overloads { - match check_doc_func_type_compact_for_params( - context, - source_func, - overload_func, - check_guard.next_level()?, - ) { - Ok(()) => { - return Ok(()); - } - Err(e) if e.is_type_not_match() => { - // continue to check next overload - continue; - } - Err(e) => { - return Err(e); - } - } - } - - let fake_doc_func = signature.to_doc_func_type(); - - check_doc_func_type_compact_for_params( - context, - source_func, - &fake_doc_func, - check_guard.next_level()?, - ) -} - -// check type is callable -fn check_doc_func_type_compact_for_custom_type( - context: &mut TypeCheckContext, - source_func: &LuaFunctionType, - custom_type_id: &LuaTypeDeclId, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let type_decl = context - .db - .get_type_index() - .get_type_decl(custom_type_id) - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - - if type_decl.is_class() { - let call_operators = context - .db - .get_operator_index() - .get_operators(&custom_type_id.clone().into(), LuaOperatorMetaMethod::Call) - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - for operator_id in call_operators { - let operator = context - .db - .get_operator_index() - .get_operator(operator_id) - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - let call_type = operator.get_operator_func(context.db); - match call_type { - LuaType::DocFunction(doc_func) => { - match check_doc_func_type_compact_for_params( - context, - source_func, - &doc_func, - check_guard.next_level()?, - ) { - Ok(()) => return Ok(()), - Err(e) if e.is_type_not_match() => continue, - Err(e) => return Err(e), - } - } - LuaType::Signature(signature_id) => { - let signature = context - .db - .get_signature_index() - .get(&signature_id) - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - let doc_f = signature.to_call_operator_func_type(); - match check_doc_func_type_compact_for_params( - context, - source_func, - &doc_f, - check_guard.next_level()?, - ) { - Ok(()) => return Ok(()), - Err(e) if e.is_type_not_match() => continue, - Err(e) => return Err(e), - } - } - _ => {} - } - } - } - - Err(TypeCheckFailReason::TypeNotMatch) -} - -pub fn check_sig_type_compact( - context: &mut TypeCheckContext, - sig_id: &LuaSignatureId, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let signature = context - .db - .get_signature_index() - .get(sig_id) - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - - // cannot check generic method - if signature.is_generic() { - return Ok(()); - } - - let fake_doc_func = signature.to_doc_func_type(); - - check_doc_func_type_compact( - context, - &fake_doc_func, - compact_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 deleted file mode 100644 index 0203bfcea..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/generic_type.rs +++ /dev/null @@ -1,307 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use crate::{ - LuaGenericType, LuaMemberOwner, LuaType, LuaTypeCache, LuaTypeDeclId, RenderLevel, - TypeSubstitutor, complete_type_generic_args_in_type, humanize_type, instantiate_type_generic, - semantic::{member::find_members, type_check::type_check_context::TypeCheckContext}, -}; - -use super::{ - TypeCheckResult, check_general_type_compact, instantiate_generic_alias_origin, - type_check_fail_reason::TypeCheckFailReason, type_check_guard::TypeCheckGuard, -}; - -pub fn check_generic_type_compact( - context: &mut TypeCheckContext, - source_generic: &LuaGenericType, - compact_type: &LuaType, - 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, - compact_type, - check_guard.next_level()?, - ); - } - - // 不检查尚未实例化的泛型类 - let is_tpl = source_generic.contain_tpl(); - - match compact_type { - LuaType::Generic(compact_generic) => { - if is_tpl { - return Ok(()); - } - let first_result = check_generic_type_compact_generic( - context, - source_generic, - compact_generic, - check_guard.next_level()?, - ); - if first_result.is_ok() { - return Ok(()); - } - - if let Some(supers) = context - .db - .get_type_index() - .get_super_types(&compact_generic.get_base_type_id()) - { - for mut super_type in supers { - if super_type.contain_tpl() { - let substitutor = - TypeSubstitutor::from_type_array(compact_generic.get_params().clone()); - super_type = - instantiate_type_generic(context.db, &super_type, &substitutor); - } - - let result = check_generic_type_compact( - context, - source_generic, - &super_type, - check_guard.next_level()?, - ); - if result.is_ok() { - return Ok(()); - } - } - } - - first_result - } - LuaType::TableConst(range) => check_generic_type_compact_table( - context, - source_generic, - LuaMemberOwner::Element(range.clone()), - check_guard.next_level()?, - ), - LuaType::Ref(ref_id) | LuaType::Def(ref_id) => { - if is_tpl { - return Ok(()); - } - check_generic_type_compact_ref_type( - context, - source_generic, - compact_type, - ref_id, - check_guard.next_level()?, - ) - } - _ => Err(TypeCheckFailReason::TypeNotMatch), - } -} - -fn check_generic_type_compact_generic( - context: &mut TypeCheckContext, - source_generic: &LuaGenericType, - compact_generic: &LuaGenericType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let source_base_id = source_generic.get_base_type_id(); - let compact_base_id = compact_generic.get_base_type_id(); - if compact_base_id != source_base_id { - return Err(TypeCheckFailReason::TypeNotMatch); - } - - let source_params = source_generic.get_params(); - let compact_params = compact_generic.get_params(); - if source_params.len() != compact_params.len() { - return Err(TypeCheckFailReason::TypeNotMatch); - } - - let next_guard = check_guard.next_level()?; - for (source_param, compact_param) in source_params.iter().zip(compact_params.iter()) { - check_general_type_compact(context, source_param, compact_param, next_guard)?; - } - - Ok(()) -} - -fn check_generic_type_compact_table( - context: &mut TypeCheckContext, - source_generic: &LuaGenericType, - table_owner: LuaMemberOwner, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let member_index = context.db.get_member_index(); - - // 构建表成员映射 - let table_member_map: HashMap<_, _> = member_index - .get_members(&table_owner) - .map(|members| { - members - .iter() - .map(|m| (m.get_key().clone(), m.get_id())) - .collect() - }) - .unwrap_or_default(); - - // 获取泛型类型的成员, 使用 find_members 来获取包括继承的所有成员 - let source_type = LuaType::Generic(Arc::new(source_generic.clone())); - let Some(source_type_members) = find_members(context.db, &source_type) else { - return Ok(()); // 空成员无需检查 - }; - - // 提前计算下一级检查守卫 - let next_guard = check_guard.next_level()?; - - for source_member in source_type_members { - let source_member_type = source_member.typ; - let key = source_member.key; - - match table_member_map.get(&key) { - Some(table_member_id) => { - let table_member = member_index - .get_member(table_member_id) - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - let table_member_type = context - .db - .get_type_index() - .get_type_cache(&table_member.get_id().into()) - .unwrap_or(&LuaTypeCache::InferType(LuaType::Any)) - .as_type(); - - if let Err(err) = check_general_type_compact( - context, - &source_member_type, - table_member_type, - next_guard, - ) && err.is_type_not_match() - { - if !context.detail { - return Err(TypeCheckFailReason::TypeNotMatch); - } - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "member %{name} type not match, expect %{expect}, got %{got}", - name = key.to_path(), - expect = - humanize_type(context.db, &source_member_type, RenderLevel::Simple), - got = humanize_type(context.db, table_member_type, RenderLevel::Simple) - ) - .to_string(), - )); - } - } - None if !source_member_type.is_optional() => { - if !context.detail { - return Err(TypeCheckFailReason::TypeNotMatch); - } - - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing member %{name}, in table", name = key.to_path()).to_string(), - )); - } - _ => {} // 可选成员未找到,继续检查 - } - } - - // 检查超类型 - let source_base_id = source_generic.get_base_type_id(); - if let Some(supers) = context.db.get_type_index().get_super_types(&source_base_id) { - let element_range = table_owner - .get_element_range() - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - let table_type = LuaType::TableConst(element_range.clone()); - - for super_type in supers { - check_general_type_compact(context, &super_type, &table_type, next_guard)?; - } - } - - Ok(()) -} - -fn check_generic_type_compact_ref_type( - context: &mut TypeCheckContext, - source_generic: &LuaGenericType, - compact_type: &LuaType, - ref_id: &LuaTypeDeclId, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let type_decl = context - .db - .get_type_index() - .get_type_decl(ref_id) - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - - if type_decl.is_alias() { - if let Some(origin_type) = type_decl.get_alias_origin(context.db, None) { - return check_general_type_compact( - context, - &LuaType::Generic(source_generic.clone().into()), - &origin_type, - check_guard.next_level()?, - ); - } - } - - // TODO: 这是对于 self 转换为 def/ref 后的处理, 转换后得到的类型并不包含泛型参数, 因此我们需要在这里再推导一次 - // 然而, 在 infer 阶段将 self 推断为包含默认值的泛型类型是否更合适呢? 值得商榷 - let completed_compact_type = complete_type_generic_args_in_type(context.db, compact_type); - if matches!(completed_compact_type, LuaType::Generic(_)) { - return check_generic_type_compact( - context, - source_generic, - &completed_compact_type, - check_guard.next_level()?, - ); - } - - for super_type in context - .db - .get_type_index() - .get_super_types(ref_id) - .unwrap_or_default() - { - if check_generic_type_compact( - context, - source_generic, - &super_type, - check_guard.next_level()?, - ) - .is_ok() - { - return Ok(()); - } - } - - // 如果泛型参数是`any`, 那么我们只需要匹配基础类型 - if source_generic.get_params().iter().any(|p| p.is_any()) { - return check_general_type_compact( - context, - &source_generic.get_base_type(), - &LuaType::Ref(ref_id.clone()), - check_guard.next_level()?, - ); - } - - Err(TypeCheckFailReason::TypeNotMatch) -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/intersection.rs b/crates/emmylua_code_analysis/src/semantic/type_check/intersection.rs new file mode 100644 index 000000000..eea06cc5f --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/intersection.rs @@ -0,0 +1,98 @@ +use crate::LuaType; + +use super::{ + mismatch::{TypeMismatch, TypePathSegment}, + relation::{IntersectionState, Relater, RelationFailure, RelationOutcome, RelationResult}, + structural::{relate_structural, relate_target_intersection_side_checks}, +}; + +pub(crate) fn relate_intersection( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + outer_intersection_state: IntersectionState, +) -> Option { + let (intersection, constituent_state) = match (source, target) { + // target intersection 必须先分派 + (_, LuaType::Intersection(target_intersection)) => { + (target_intersection, IntersectionState::TARGET) + } + (LuaType::Intersection(source_intersection), _) => { + (source_intersection, IntersectionState::SOURCE) + } + _ => return None, + }; + + if constituent_state.contains(IntersectionState::TARGET) { + for (index, member) in intersection.get_types().iter().enumerate() { + if let Err(failure) = relater.relate(source, member, constituent_state) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::IntersectionMember(index), source, target) + }))); + } + } + + if !outer_intersection_state.contains(IntersectionState::TARGET) + && let Err(failure) = + relate_target_intersection_side_checks(relater, source, target, intersection) + { + return Some(Err(failure)); + } + return Some(Ok(())); + } + + let mut best = None; + let mut indeterminate = None; + let mut related = false; + for (index, member) in intersection.get_types().iter().enumerate() { + let (outcome, progress) = relater.probe_relation(member, target, constituent_state); + match outcome { + RelationOutcome::Related => related = true, + RelationOutcome::Indeterminate(kind) => { + indeterminate.get_or_insert(kind); + } + RelationOutcome::Unrelated => { + if best + .map(|(_, current_progress)| progress > current_progress) + .unwrap_or(true) + { + best = Some((index, progress)); + } + } + } + } + + // 完整 intersection 进入具体结构 fallback + if let Some(result) = relate_structural(relater, source, target, outer_intersection_state) { + return Some(result); + } + if related { + return Some(Ok(())); + } + if let Some(kind) = indeterminate { + return Some(Err(relater.indeterminate_failure(kind, source, target))); + } + let Some((best_index, _)) = best else { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + }; + if !relater.is_explain() { + return Some(Err(RelationFailure::Unrelated(None))); + } + Some( + relater + .relate( + &intersection.get_types()[best_index], + target, + constituent_state, + ) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at( + TypePathSegment::IntersectionMember(best_index), + source, + target, + ) + }) + }), + ) +} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/intersection_utils.rs b/crates/emmylua_code_analysis/src/semantic/type_check/intersection_utils.rs deleted file mode 100644 index 26930a167..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/intersection_utils.rs +++ /dev/null @@ -1,18 +0,0 @@ -use hashbrown::HashMap; - -use crate::{DbIndex, LuaIntersectionType, LuaObjectType, LuaType, semantic::member::find_members}; - -pub(super) fn intersection_to_object( - db: &DbIndex, - intersection: &LuaIntersectionType, -) -> Option { - let intersection_type: LuaType = intersection.clone().into(); - let members = find_members(db, &intersection_type)?; - let mut fields: HashMap<_, _> = HashMap::new(); - for member in members { - fields - .entry(member.key.clone()) - .or_insert(member.typ.clone()); - } - Some(LuaObjectType::new_with_fields(fields, Vec::new())) -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/locator.rs b/crates/emmylua_code_analysis/src/semantic/type_check/locator.rs new file mode 100644 index 000000000..186a729ae --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/locator.rs @@ -0,0 +1,138 @@ +use super::mismatch::TypePathSegment; +use crate::{LuaMemberKey, LuaType}; +use emmylua_parser::{LuaAstNode, LuaExpr, LuaIndexKey, LuaTableExpr}; +use rowan::TextRange; + +pub fn locate_mismatch_range(source_expr: &LuaExpr, frames: &[TypePathSegment]) -> TextRange { + let mut current = Some(source_expr.clone()); + let mut range = source_expr.syntax().text_range(); + for frame in frames.iter().rev() { + if matches!( + frame, + TypePathSegment::SourceUnionMember(_) + | TypePathSegment::TargetUnionCandidate(_) + | TypePathSegment::IntersectionMember(_) + | TypePathSegment::GenericArgument(_) + ) { + continue; + } + let Some(expr) = current.as_ref() else { + break; + }; + let Some(table) = LuaTableExpr::cast(expr.syntax().clone()) else { + break; + }; + let key = match frame { + TypePathSegment::Member(k) => Some(k.clone()), + TypePathSegment::Index(index) => match index { + LuaType::StringConst(value) | LuaType::DocStringConst(value) => { + Some(LuaMemberKey::Name((**value).clone())) + } + LuaType::IntegerConst(value) | LuaType::DocIntegerConst(value) => { + Some(LuaMemberKey::Integer(*value)) + } + _ => break, + }, + TypePathSegment::TupleElement(i) => Some(LuaMemberKey::Integer(*i as i64 + 1)), + TypePathSegment::ArrayElement => None, + _ => break, + }; + let found = table.get_fields_with_keys().into_iter().find(|(field, k)| { + if let Some(expected) = &key { + key_matches(k, expected) + } else { + matches!(k, LuaIndexKey::Idx(_)) && field.get_value_expr().is_some() + } + }); + let Some((field, _)) = found else { + break; + }; + current = field.get_value_expr(); + range = current + .as_ref() + .map(|v| v.syntax().text_range()) + .unwrap_or_else(|| field.get_range()); + } + range +} + +fn key_matches(key: &LuaIndexKey, expected: &LuaMemberKey) -> bool { + match (key, expected) { + (LuaIndexKey::Name(a), LuaMemberKey::Name(b)) => a.get_name_text() == b.as_str(), + (LuaIndexKey::String(a), LuaMemberKey::Name(b)) => a.get_value() == b.as_str(), + (LuaIndexKey::Integer(a), LuaMemberKey::Integer(b)) => { + a.get_number_value().as_integer() == Some(*b) + } + (LuaIndexKey::Idx(a), LuaMemberKey::Integer(b)) => *a as i64 == *b, + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::VirtualWorkspace; + use emmylua_parser::{LuaAstNode, LuaLocalStat, LuaTableExpr}; + use smol_str::SmolStr; + + #[test] + fn literal_index_frames_follow_table_fields() { + let mut workspace = VirtualWorkspace::new(); + let file_id = workspace.def("local value = { foo = 'oops' }"); + let stat = workspace.get_node::(file_id); + let source_expr = stat.get_value_exprs().next().expect("initializer"); + let table = LuaTableExpr::cast(source_expr.syntax().clone()).expect("table expression"); + let (field, _) = table + .get_fields_with_keys() + .into_iter() + .next() + .expect("field"); + let expected_range = field + .get_value_expr() + .expect("field value") + .syntax() + .text_range(); + let mismatch_range = locate_mismatch_range( + &source_expr, + &[TypePathSegment::Index(LuaType::StringConst( + SmolStr::new("foo").into(), + ))], + ); + assert_eq!(mismatch_range, expected_range); + } + + #[test] + fn integer_index_frames_follow_table_fields() { + let mut workspace = VirtualWorkspace::new(); + let file_id = workspace.def("local value = { [1] = 'oops' }"); + let stat = workspace.get_node::(file_id); + let source_expr = stat.get_value_exprs().next().expect("initializer"); + let table = LuaTableExpr::cast(source_expr.syntax().clone()).expect("table expression"); + let (field, _) = table + .get_fields_with_keys() + .into_iter() + .next() + .expect("field"); + let expected_range = field + .get_value_expr() + .expect("field value") + .syntax() + .text_range(); + let mismatch_range = locate_mismatch_range( + &source_expr, + &[TypePathSegment::Index(LuaType::IntegerConst(1))], + ); + assert_eq!(mismatch_range, expected_range); + } + + #[test] + fn broad_index_frames_use_source_expr_fallback() { + let mut workspace = VirtualWorkspace::new(); + let file_id = workspace.def("local value = { foo = 'oops' }"); + let stat = workspace.get_node::(file_id); + let source_expr = stat.get_value_exprs().next().expect("initializer"); + let mismatch_range = + locate_mismatch_range(&source_expr, &[TypePathSegment::Index(LuaType::String)]); + assert_eq!(mismatch_range, source_expr.syntax().text_range()); + } +} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/mismatch.rs b/crates/emmylua_code_analysis/src/semantic/type_check/mismatch.rs new file mode 100644 index 000000000..3e82699c7 --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/mismatch.rs @@ -0,0 +1,161 @@ +use emmylua_parser::LuaExpr; +use rowan::TextRange; + +use crate::{ + DbIndex, LuaMemberKey, LuaType, RenderLevel, humanize_type, + semantic::type_check::locator::locate_mismatch_range, +}; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OverflowKind { + Recursion, + Budget, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TypePathSegment { + Member(LuaMemberKey), + Index(LuaType), + TupleElement(usize), + ArrayElement, + FunctionParameter(usize), + FunctionReturn(usize), + GenericArgument(usize), + SourceUnionMember(usize), + TargetUnionCandidate(usize), + IntersectionMember(usize), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TypeMismatchKind { + Incompatible { source: LuaType, target: LuaType }, + MissingMember { key: LuaMemberKey }, + MissingTupleElement { index: usize }, + UnresolvedType, + Overflow(OverflowKind), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TypeMismatch { + source: LuaType, + target: LuaType, + frames: Vec, + leaf: TypeMismatchKind, +} + +impl TypeMismatch { + pub(crate) fn new(source: &LuaType, target: &LuaType, leaf: TypeMismatchKind) -> Self { + Self { + source: source.clone(), + target: target.clone(), + frames: Vec::new(), + leaf, + } + } + pub(crate) fn incompatible(source: &LuaType, target: &LuaType) -> Self { + Self::new( + source, + target, + TypeMismatchKind::Incompatible { + source: source.clone(), + target: target.clone(), + }, + ) + } + + pub(crate) fn overflow(source: &LuaType, target: &LuaType, kind: OverflowKind) -> Self { + Self::new(source, target, TypeMismatchKind::Overflow(kind)) + } + + pub(crate) fn at(mut self, frame: TypePathSegment, source: &LuaType, target: &LuaType) -> Self { + self.source = source.clone(); + self.target = target.clone(); + self.frames.push(frame); + self + } + + pub fn source(&self) -> &LuaType { + &self.source + } + + pub fn target(&self) -> &LuaType { + &self.target + } + + pub fn frames(&self) -> &[TypePathSegment] { + &self.frames + } + + pub fn leaf(&self) -> &TypeMismatchKind { + &self.leaf + } + + pub fn locate_in(&self, source_expr: &LuaExpr) -> TextRange { + locate_mismatch_range(source_expr, self.frames()) + } +} + +pub fn render_type_mismatch(db: &DbIndex, mismatch: &TypeMismatch) -> String { + let mut lines = vec![format!( + "Type '{}' is not assignable to type '{}'.", + humanize_type(db, &mismatch.source, RenderLevel::Simple), + humanize_type(db, &mismatch.target, RenderLevel::Simple) + )]; + for frame in mismatch.frames.iter().rev() { + lines.push(match frame { + TypePathSegment::Member(key) => { + format!("Types of property '{}' are incompatible.", key.to_path()) + } + TypePathSegment::Index(key) => format!( + "Index type '{}' is incompatible.", + humanize_type(db, key, RenderLevel::Simple) + ), + TypePathSegment::TupleElement(i) => format!("Tuple element {} is incompatible.", i + 1), + TypePathSegment::ArrayElement => "Array element is incompatible.".into(), + TypePathSegment::FunctionParameter(i) => { + format!("Function parameter {} is incompatible.", i + 1) + } + TypePathSegment::FunctionReturn(i) => { + format!("Function return {} is incompatible.", i + 1) + } + TypePathSegment::GenericArgument(i) => { + format!("Generic argument {} is incompatible.", i + 1) + } + TypePathSegment::SourceUnionMember(i) => { + format!("Source union member {} is incompatible.", i + 1) + } + TypePathSegment::TargetUnionCandidate(i) => { + format!("Target union candidate {} is incompatible.", i + 1) + } + TypePathSegment::IntersectionMember(i) => { + format!("Intersection member {} is incompatible.", i + 1) + } + }); + } + lines.push(match &mismatch.leaf { + TypeMismatchKind::Incompatible { source, target } => format!( + "Type '{}' is not assignable to type '{}'.", + humanize_type(db, source, RenderLevel::Simple), + humanize_type(db, target, RenderLevel::Simple) + ), + TypeMismatchKind::MissingMember { key } => { + format!("Property '{}' is missing.", key.to_path()) + } + TypeMismatchKind::MissingTupleElement { index } => { + format!("Tuple element {} is missing.", index + 1) + } + TypeMismatchKind::UnresolvedType => "Type could not be resolved.".into(), + TypeMismatchKind::Overflow(kind) => format!("Type relation exceeded {:?}.", kind), + }); + lines + .into_iter() + .enumerate() + .map(|(i, line)| { + if i == 0 { + line + } else { + format!("{}{}", " ".repeat(i), line) + } + }) + .collect::>() + .join("\n") +} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/mod.rs b/crates/emmylua_code_analysis/src/semantic/type_check/mod.rs index c5fe47a3d..2110ded58 100644 --- a/crates/emmylua_code_analysis/src/semantic/type_check/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/type_check/mod.rs @@ -1,218 +1,69 @@ -mod complex_type; -mod func_type; -mod generic_type; -mod intersection_utils; -mod ref_type; -mod simple_type; +use std::sync::Arc; + +mod callable; +mod intersection; +mod locator; +mod mismatch; +mod relation; +mod simple; +mod structural; mod sub_type; mod test; -mod type_check_context; -mod type_check_fail_reason; -mod type_check_guard; +mod union; -use std::ops::Deref; - -use complex_type::check_complex_type_compact; -use func_type::{check_doc_func_type_compact, check_sig_type_compact}; -use generic_type::check_generic_type_compact; -use ref_type::check_ref_type_compact; -use simple_type::check_simple_type_compact; -pub use type_check_fail_reason::TypeCheckFailReason; -use type_check_guard::TypeCheckGuard; +pub use mismatch::{ + OverflowKind, TypeMismatch, TypeMismatchKind, TypePathSegment, render_type_mismatch, +}; +use relation::RelationSession; +pub(crate) use relation::{RelationKind, RelationOutcome}; +pub use sub_type::is_sub_type_of; use crate::{ - LuaAliasCallKind, LuaGenericType, LuaUnionType, TypeSubstitutor, - db_index::{DbIndex, LuaType}, - instantiate_type_generic, - semantic::type_check::type_check_context::TypeCheckContext, + DbIndex, LuaAliasCallKind, LuaMemberIndexItem, LuaMemberKey, LuaMemberOwner, LuaType, + LuaUnionType, TypeSubstitutor, instantiate_type_generic, }; -pub use sub_type::is_sub_type_of; -pub type TypeCheckResult = Result<(), TypeCheckFailReason>; -pub use type_check_context::TypeCheckCheckLevel; -pub fn check_type_compact( - db: &DbIndex, - source: &LuaType, - compact_type: &LuaType, -) -> TypeCheckResult { - let mut context = TypeCheckContext::new(db, false, TypeCheckCheckLevel::Normal); - check_general_type_compact(&mut context, source, compact_type, TypeCheckGuard::new()) +pub fn is_assignable(db: &DbIndex, source: &LuaType, target: &LuaType) -> bool { + !matches!( + is_assignable_ex(db, source, target, RelationKind::Assignable), + RelationOutcome::Unrelated + ) } -pub fn check_type_compact_detail( +pub fn is_assignable_ex( db: &DbIndex, source: &LuaType, - compact_type: &LuaType, -) -> TypeCheckResult { - let guard = TypeCheckGuard::new(); - let mut context = TypeCheckContext::new(db, true, TypeCheckCheckLevel::Normal); - check_general_type_compact(&mut context, source, compact_type, guard) -} + target: &LuaType, + kind: RelationKind, +) -> RelationOutcome { + if fast_eq_check(source, target) { + return RelationOutcome::Related; + } -pub fn check_type_compact_with_level( - db: &DbIndex, - source: &LuaType, - compact_type: &LuaType, - level: TypeCheckCheckLevel, -) -> TypeCheckResult { - let mut context = TypeCheckContext::new(db, false, level); - check_general_type_compact(&mut context, source, compact_type, TypeCheckGuard::new()) + RelationSession::probe(db, kind, source, target) } -fn check_general_type_compact( - context: &mut TypeCheckContext, +/// 检查 source 到 target 的赋值关系, 并在最终失败分支保留最小诊断证据. +pub fn check_assignable( + db: &DbIndex, source: &LuaType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - if is_like_any(compact_type) { - return Ok(()); - } - - if fast_eq_check(source, compact_type) { + target: &LuaType, +) -> Result<(), TypeMismatch> { + if fast_eq_check(source, target) { return Ok(()); } - if let Some(origin_type) = escape_type(context.db, compact_type) { - return check_general_type_compact( - context, - source, - &origin_type, - check_guard.next_level()?, - ); - } - - // When compact_type is an Intersection, the value satisfies all components simultaneously. - // So it can be assigned to any target that accepts at least one component. - // This must be handled before the source-based dispatch, because individual source branches - // (e.g. Array, Ref) do not know how to decompose an intersection compact_type. - if let LuaType::Intersection(compact_intersection) = compact_type { - // Skip if source is also Intersection — that case is handled symmetrically in - // check_intersection_type_compact. - if !matches!(source, LuaType::Intersection(_)) { - for component in compact_intersection.get_types() { - if check_general_type_compact(context, source, component, check_guard.next_level()?) - .is_ok() - { - return Ok(()); - } - } - return Err(TypeCheckFailReason::TypeNotMatch); - } - } - - match source { - LuaType::Unknown | LuaType::Any => Ok(()), - LuaType::TplRef(tpl) => { - if let Some(source_constraint) = tpl.get_constraint() { - return check_general_type_compact( - context, - source_constraint, - compact_type, - check_guard.next_level()?, - ); - } - - check_simple_type_compact(context, source, compact_type, check_guard) - } - // simple type - LuaType::Nil - | LuaType::Table - | LuaType::Userdata - | LuaType::Function - | LuaType::Thread - | LuaType::Boolean - | LuaType::String - | LuaType::Integer - | LuaType::Number - | LuaType::Io - | LuaType::Global - | LuaType::BooleanConst(_) - | LuaType::StringConst(_) - | LuaType::IntegerConst(_) - | LuaType::FloatConst(_) - | LuaType::TableConst(_) - | LuaType::DocStringConst(_) - | LuaType::DocIntegerConst(_) - | LuaType::DocBooleanConst(_) - | LuaType::StrTplRef(_) - | LuaType::Namespace(_) - | LuaType::Variadic(_) - | LuaType::Language(_) => { - check_simple_type_compact(context, &source, &compact_type, check_guard) - } - - // type ref - LuaType::Ref(type_decl_id) => { - check_ref_type_compact(context, type_decl_id, &compact_type, check_guard) - } - LuaType::Def(type_decl_id) => { - check_ref_type_compact(context, type_decl_id, &compact_type, check_guard) - } - // invaliad source type - // LuaType::Module(arc_intern) => todo!(), - - // function type - LuaType::DocFunction(doc_func) => { - check_doc_func_type_compact(context, doc_func, &compact_type, check_guard) - } - // signature type - LuaType::Signature(sig_id) => { - check_sig_type_compact(context, sig_id, &compact_type, check_guard) - } - - // complex type - LuaType::Array(_) - | LuaType::Tuple(_) - | LuaType::Object(_) - | LuaType::Union(_) - | LuaType::Intersection(_) - | LuaType::TableGeneric(_) - | LuaType::Call(_) - | LuaType::MultiLineUnion(_) => { - check_complex_type_compact(context, &source, &compact_type, check_guard) - } - - // generic type - LuaType::Generic(generic) => { - check_generic_type_compact(context, generic, &compact_type, check_guard) - } - // invalid source type - // LuaType::MemberPathExist(_) | - LuaType::Instance(instantiate) => check_general_type_compact( - context, - instantiate.get_base(), - &compact_type, - check_guard.next_level()?, - ), - LuaType::TypeGuard(_) => { - if compact_type.is_boolean() { - return Ok(()); - } - Err(TypeCheckFailReason::TypeNotMatch) - } - LuaType::Never => { - // never 只能赋值给 never - if compact_type.is_never() { - return Ok(()); - } - Err(TypeCheckFailReason::TypeNotMatch) - } - LuaType::ModuleRef(_) => Ok(()), - _ => Err(TypeCheckFailReason::TypeNotMatch), - } -} - -fn is_like_any(ty: &LuaType) -> bool { - match ty { - LuaType::Any | LuaType::Unknown => true, - LuaType::TplRef(tpl) => tpl.get_constraint().is_none(), - _ => false, - } + RelationSession::explain(db, RelationKind::Assignable, source, target) } -fn fast_eq_check(a: &LuaType, b: &LuaType) -> bool { - match (a, b) { +pub fn fast_eq_check(source: &LuaType, target: &LuaType) -> bool { + match (source, target) { + (LuaType::Any, _) => true, + (_, LuaType::Any | LuaType::Unknown) => true, + (LuaType::SelfInfer, _) | (_, LuaType::SelfInfer) => true, + (LuaType::TplRef(tpl), _) | (_, LuaType::TplRef(tpl)) if tpl.get_constraint().is_none() => { + true + } (LuaType::Nil, LuaType::Nil) | (LuaType::Table, LuaType::Table) | (LuaType::Userdata, LuaType::Userdata) @@ -224,85 +75,109 @@ fn fast_eq_check(a: &LuaType, b: &LuaType) -> bool { | (LuaType::Number, LuaType::Number) | (LuaType::Io, LuaType::Io) | (LuaType::Global, LuaType::Global) - | (LuaType::Unknown, LuaType::Unknown) - | (LuaType::Any, LuaType::Any) => true, - (LuaType::Ref(type_id_left), LuaType::Ref(type_id_right)) => type_id_left == type_id_right, - (LuaType::Union(u), LuaType::Ref(type_id_right)) => { - if let LuaUnionType::Nullable(LuaType::Ref(type_id_left)) = u.deref() { - return type_id_left == type_id_right; - } - false - } - (LuaType::Generic(generic_left), LuaType::Generic(generic_right)) => { - generic_left == generic_right - } + | (LuaType::Never, LuaType::Never) => true, + ( + LuaType::BooleanConst(source_value) | LuaType::DocBooleanConst(source_value), + LuaType::BooleanConst(target_value) | LuaType::DocBooleanConst(target_value), + ) => source_value == target_value, + ( + LuaType::StringConst(source_value) | LuaType::DocStringConst(source_value), + LuaType::StringConst(target_value) | LuaType::DocStringConst(target_value), + ) => source_value == target_value, + ( + LuaType::IntegerConst(source_value) | LuaType::DocIntegerConst(source_value), + LuaType::IntegerConst(target_value) | LuaType::DocIntegerConst(target_value), + ) => source_value == target_value, + (LuaType::FloatConst(source_value), LuaType::FloatConst(target_value)) => { + source_value == target_value + } + (LuaType::TableConst(source_id), LuaType::TableConst(target_id)) => source_id == target_id, + ( + LuaType::Ref(source_id) | LuaType::Def(source_id), + LuaType::Ref(target_id) | LuaType::Def(target_id), + ) => source_id == target_id, + (LuaType::Union(union), LuaType::Ref(target_id)) => matches!( + union.as_ref(), + LuaUnionType::Nullable(LuaType::Ref(source_id)) if source_id == target_id + ), + (LuaType::Array(source), LuaType::Array(target)) => Arc::ptr_eq(source, target), + (LuaType::Tuple(source), LuaType::Tuple(target)) => Arc::ptr_eq(source, target), + (LuaType::DocFunction(source), LuaType::DocFunction(target)) => Arc::ptr_eq(source, target), + (LuaType::Object(source), LuaType::Object(target)) => Arc::ptr_eq(source, target), + (LuaType::Union(source), LuaType::Union(target)) => Arc::ptr_eq(source, target), + (LuaType::Intersection(source), LuaType::Intersection(target)) => { + Arc::ptr_eq(source, target) + } + (LuaType::Generic(source), LuaType::Generic(target)) => source == target, + (LuaType::TableGeneric(source), LuaType::TableGeneric(target)) => { + Arc::ptr_eq(source, target) + } + (LuaType::TplRef(source), LuaType::TplRef(target)) => Arc::ptr_eq(source, target), + (LuaType::StrTplRef(source), LuaType::StrTplRef(target)) => Arc::ptr_eq(source, target), + (LuaType::Variadic(source), LuaType::Variadic(target)) => Arc::ptr_eq(source, target), + (LuaType::Signature(source_id), LuaType::Signature(target_id)) => source_id == target_id, + (LuaType::Instance(source), LuaType::Instance(target)) => Arc::ptr_eq(source, target), + (LuaType::Namespace(source_id), LuaType::Namespace(target_id)) => source_id == target_id, + (LuaType::Call(source), LuaType::Call(target)) => Arc::ptr_eq(source, target), + (LuaType::MultiLineUnion(source), LuaType::MultiLineUnion(target)) => { + Arc::ptr_eq(source, target) + } + (LuaType::TypeGuard(source), LuaType::TypeGuard(target)) => Arc::ptr_eq(source, target), + (LuaType::Language(source_id), LuaType::Language(target_id)) => source_id == target_id, + (LuaType::ModuleRef(source_id), LuaType::ModuleRef(target_id)) => source_id == target_id, _ => false, } } -fn instantiate_generic_alias_origin(db: &DbIndex, generic: &LuaGenericType) -> Option { - let base_id = generic.get_base_type_id(); - let decl = db.get_type_index().get_type_decl(&base_id)?; - if !decl.is_alias() { - return None; - } - - let substitutor = TypeSubstitutor::from_alias(generic.get_params().clone(), base_id); - decl.get_alias_origin(db, Some(&substitutor)) +fn visit_member_items( + db: &DbIndex, + owner: &LuaMemberOwner, + mut visitor: impl FnMut(&LuaMemberKey, &LuaMemberIndexItem) -> Result<(), E>, +) -> Result<(), E> { + let Some(mut member_items) = db.get_member_index().get_member_items(owner) else { + return Ok(()); + }; + member_items.try_for_each(|(key, item)| visitor(key, item)) } -fn escape_type(db: &DbIndex, typ: &LuaType) -> Option { +pub fn normalize_type(db: &DbIndex, typ: &LuaType) -> Option { match typ { - LuaType::TplRef(_) => { - return generic_tpl_constraint_type(typ).cloned(); - } + LuaType::TplRef(tpl) => tpl + .get_constraint() + .filter(|constraint| *constraint != typ) + .cloned(), LuaType::Generic(generic) if !generic.contain_tpl() => { - return instantiate_generic_alias_origin(db, generic); + let base_id = generic.get_base_type_id(); + let decl = db.get_type_index().get_type_decl(&base_id)?; + if !decl.is_alias() { + return None; + } + let substitutor = TypeSubstitutor::from_alias(generic.get_params().clone(), base_id); + decl.get_alias_origin(db, Some(&substitutor)) } LuaType::Ref(type_id) => { let type_decl = db.get_type_index().get_type_decl(type_id)?; - if type_decl.is_alias() - && let Some(origin_type) = type_decl.get_alias_origin(db, None) - { - return Some(origin_type.clone()); + if type_decl.is_alias() { + return type_decl.get_alias_origin(db, None); } + None } LuaType::Call(alias_call) if matches!( alias_call.get_call_kind(), - LuaAliasCallKind::Index | LuaAliasCallKind::RawGet + LuaAliasCallKind::Index | LuaAliasCallKind::RawGet | LuaAliasCallKind::KeyOf ) && !typ.contain_tpl() => { let resolved = instantiate_type_generic(db, typ, &TypeSubstitutor::new()); - if resolved != *typ { - return Some(resolved); - } - } - // todo donot escape - LuaType::Instance(inst) => { - let base = inst.get_base(); - return Some(base.clone()); - } - LuaType::MultiLineUnion(multi_union) => { - let union = multi_union.to_union(); - return Some(union); - } - LuaType::TypeGuard(_) => return Some(LuaType::Boolean), - LuaType::ModuleRef(file_id) => { - let module_info = db.get_module_index().get_module(*file_id)?; - if let Some(export_type) = &module_info.export_type { - return Some(export_type.clone()); - } - } - _ => {} - } - - None -} - -fn generic_tpl_constraint_type(typ: &LuaType) -> Option<&LuaType> { - match typ { - LuaType::TplRef(tpl) => tpl.get_constraint().filter(|constraint| *constraint != typ), + (resolved != *typ).then_some(resolved) + } + LuaType::Instance(instance) => Some(instance.get_base().clone()), + LuaType::MultiLineUnion(union) => Some(union.to_union()), + LuaType::TypeGuard(_) => Some(LuaType::Boolean), + LuaType::ModuleRef(file_id) => db + .get_module_index() + .get_module(*file_id) + .and_then(|module| module.export_type.clone()), _ => None, } } 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 deleted file mode 100644 index 4021269c7..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/ref_type.rs +++ /dev/null @@ -1,539 +0,0 @@ -use hashbrown::HashMap; - -use crate::{ - LuaMemberKey, LuaMemberOwner, LuaObjectType, LuaTupleType, LuaType, LuaTypeDecl, LuaTypeDeclId, - RenderLevel, humanize_type, - semantic::{ - member::find_members, - type_check::{ - intersection_utils::intersection_to_object, type_check_context::TypeCheckContext, - }, - }, -}; - -use super::{ - TypeCheckResult, check_general_type_compact, is_sub_type_of, sub_type::get_base_type_id, - type_check_fail_reason::TypeCheckFailReason, type_check_guard::TypeCheckGuard, -}; - -pub fn check_ref_type_compact( - context: &mut TypeCheckContext, - source_id: &LuaTypeDeclId, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let type_decl = context - .db - .get_type_index() - .get_type_decl(source_id) - // unreachable! - .ok_or(if context.detail { - TypeCheckFailReason::TypeNotMatchWithReason( - t!("type `%{name}` not found.", name = source_id.get_name()).to_string(), - ) - } else { - TypeCheckFailReason::TypeNotMatch - })?; - - 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, - compact_type, - check_guard.next_level()?, - ); - if result.is_err() && should_retry_alias_nominal_check(compact_type) { - return check_ref_class(context, source_id, compact_type, check_guard); - } - return result; - } - - return Err(TypeCheckFailReason::TypeNotMatch); - } - - if type_decl.is_enum() { - check_ref_enum(context, source_id, compact_type, check_guard, type_decl) - } else { - check_ref_class(context, source_id, compact_type, check_guard) - } -} - -fn should_retry_alias_nominal_check(compact_type: &LuaType) -> bool { - match compact_type { - LuaType::Ref(_) | LuaType::Def(_) => true, - LuaType::Generic(generic) => { - matches!(generic.get_base_type(), LuaType::Ref(_) | LuaType::Def(_)) - } - _ => false, - } -} - -fn check_ref_enum( - context: &mut TypeCheckContext, - source_id: &LuaTypeDeclId, - compact_type: &LuaType, - check_guard: TypeCheckGuard, - type_decl: &LuaTypeDecl, -) -> TypeCheckResult { - // 直接匹配相同类型 - if matches!(compact_type, LuaType::Def(id) | LuaType::Ref(id) if id == source_id) { - return Ok(()); - } - - let enum_fields = type_decl - .get_enum_field_type(context.db) - .ok_or(TypeCheckFailReason::TypeNotMatch)?; - - // 移除掉枚举类型本身 - let compact_type = match compact_type { - LuaType::Union(union_types) => { - let new_types: Vec<_> = union_types - .into_vec() - .iter() - .filter( - |typ| !matches!(typ, LuaType::Def(id) | LuaType::Ref(id) if id == source_id), - ) - .cloned() - .collect(); - LuaType::from_vec(new_types) - } - LuaType::Ref(compact_id) => { - if let Some(compact_decl) = context.db.get_type_index().get_type_decl(compact_id) - && compact_decl.is_enum() - && let Some(compact_enum_fields) = compact_decl.get_enum_field_type(context.db) - { - return check_general_type_compact( - context, - &enum_fields, - &compact_enum_fields, - check_guard.next_level()?, - ); - } - compact_type.clone() - } - _ => compact_type.clone(), - }; - - // 整数 enum 参与位运算时结果会被推断为宽泛 Integer, 但直接写入整数常量仍需匹配 enum 字段. - if let LuaType::Union(union_types) = &enum_fields - && union_types - .into_vec() - .iter() - .all(|t| matches!(t, LuaType::DocIntegerConst(_) | LuaType::IntegerConst(_))) - && matches!(compact_type, LuaType::Integer) - { - return Ok(()); - } - - check_general_type_compact( - context, - &enum_fields, - &compact_type, - check_guard.next_level()?, - ) -} - -fn check_ref_class( - context: &mut TypeCheckContext, - source_id: &LuaTypeDeclId, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - match compact_type { - LuaType::Def(id) | LuaType::Ref(id) => { - if source_id == id { - return Ok(()); - } - - // 检查子类型关系 - if is_sub_type_of(context.db, id, source_id) { - return Ok(()); - } - // 这不是正确的逻辑. 但不假设超类会自动转换为子类, 则会过于严格 - if is_sub_type_of(context.db, source_id, id) { - return Ok(()); - } - - // `compact`为枚举时的额外处理 - if let Some(compact_decl) = context.db.get_type_index().get_type_decl(id) - && compact_decl.is_enum() - && let Some(LuaType::Union(enum_fields)) = - compact_decl.get_enum_field_type(context.db) - { - let source = LuaType::Ref(source_id.clone()); - for field in enum_fields.into_vec() { - check_general_type_compact( - context, - &source, - &field, - check_guard.next_level()?, - )?; - } - return Ok(()); - } - - Err(TypeCheckFailReason::TypeNotMatch) - } - LuaType::TableConst(range) => check_ref_type_compact_table( - context, - source_id, - LuaMemberOwner::Element(range.clone()), - check_guard.next_level()?, - ), - LuaType::Object(object_type) => check_ref_type_compact_object( - context, - object_type, - source_id, - check_guard.next_level()?, - ), - LuaType::Intersection(intersection) => { - if let Some(object_type) = intersection_to_object(context.db, intersection) { - check_ref_type_compact_object( - context, - &object_type, - source_id, - check_guard.next_level()?, - ) - } else { - Err(TypeCheckFailReason::TypeNotMatch) - } - } - LuaType::Table => Ok(()), - LuaType::Union(union_type) => { - for typ in union_type.into_vec() { - check_general_type_compact( - context, - &LuaType::Ref(source_id.clone()), - &typ, - check_guard.next_level()?, - )?; - } - Ok(()) - } - LuaType::Tuple(tuple_type) => { - check_ref_type_compact_tuple(context, tuple_type, source_id, check_guard.next_level()?) - } - LuaType::Generic(generic) => { - let base_type_id = generic.get_base_type_id(); - if source_id == &base_type_id - || is_sub_type_of(context.db, &base_type_id, source_id) - || is_sub_type_of(context.db, source_id, &base_type_id) - { - Ok(()) - } else { - Err(TypeCheckFailReason::TypeNotMatch) - } - } - _ => { - if let Some(base_type_id) = get_base_type_id(compact_type) { - if source_id == &base_type_id - || is_sub_type_of(context.db, &base_type_id, source_id) - || is_sub_type_of(context.db, source_id, &base_type_id) - { - Ok(()) - } else { - Err(TypeCheckFailReason::TypeNotMatch) - } - } else { - Err(TypeCheckFailReason::TypeNotMatch) - } - } - } -} - -fn check_ref_type_compact_table( - context: &mut TypeCheckContext, - source_type_id: &LuaTypeDeclId, - table_owner: LuaMemberOwner, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let member_index = context.db.get_member_index(); - let table_members = member_index.get_members(&table_owner).unwrap_or_default(); - let table_member_map: HashMap<_, _> = table_members - .iter() - .map(|member| { - let member_type = context - .db - .get_type_index() - .get_type_cache(&member.get_id().into()) - .map(|cache| cache.as_type().clone()) - .unwrap_or(LuaType::Any); - (member.get_key().clone(), member_type) - }) - .collect(); - - let source_type_members = - member_index.get_members(&LuaMemberOwner::Type(source_type_id.clone())); - let Some(source_type_members) = source_type_members else { - return Ok(()); // empty member donot need check - }; - - for source_member in source_type_members { - let source_member_type = context - .db - .get_type_index() - .get_type_cache(&source_member.get_id().into()) - .map(|cache| cache.as_type().clone()) - .unwrap_or(LuaType::Any); - let key = source_member.get_key(); - - if context.is_key_checked(key) { - continue; - } - - if let LuaMemberKey::TypeKey(source_key_type) = key { - // 索引签名约束已有索引字段, 不要求表字面量必须包含索引字段. - for table_member in &table_members { - let Some(table_key_type) = table_member.get_key().to_index_type() else { - continue; - }; - - let key_match = match check_general_type_compact( - context, - source_key_type, - &table_key_type, - check_guard.next_level()?, - ) { - Ok(_) => true, - Err(err) if err.is_type_not_match() => false, - Err(err) => return Err(err), - }; - - if !key_match { - continue; - } - - let table_member_type = table_member_map - .get(table_member.get_key()) - .unwrap_or(&LuaType::Any); - check_ref_member_type( - context, - table_member.get_key(), - &source_member_type, - table_member_type, - check_guard, - )?; - } - - context.mark_key_checked(key.clone()); - continue; - } - - match table_member_map.get(key) { - Some(table_member_type) => { - check_ref_member_type( - context, - key, - &source_member_type, - table_member_type, - check_guard, - )?; - } - None if !source_member_type.is_optional() => { - if !context.detail { - return Err(TypeCheckFailReason::TypeNotMatch); - } - - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing member %{name}, in table", name = key.to_path()).to_string(), - )); - } - _ => {} // Optional member not found, continue - } - - context.mark_key_checked(key.clone()); - } - - // 检查超类型 - if let Some(supers) = context.db.get_type_index().get_super_types(source_type_id) { - let table_type = LuaType::TableConst( - table_owner - .get_element_range() - .ok_or(TypeCheckFailReason::TypeNotMatch)? - .clone(), - ); - for super_type in supers { - check_general_type_compact( - context, - &super_type, - &table_type, - check_guard.next_level()?, - )?; - } - } - - Ok(()) -} - -fn check_ref_member_type( - context: &mut TypeCheckContext, - key: &LuaMemberKey, - expect: &LuaType, - got: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - if let Err(err) = check_general_type_compact(context, expect, got, check_guard.next_level()?) - && err.is_type_not_match() - { - if !context.detail { - return Err(TypeCheckFailReason::TypeNotMatch); - } - - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "member %{name} type not match, expect %{expect}, got %{got}", - name = key.to_path(), - expect = humanize_type(context.db, expect, RenderLevel::Simple), - got = humanize_type(context.db, got, RenderLevel::Simple) - ) - .to_string(), - )); - } - - Ok(()) -} - -fn check_ref_type_compact_object( - context: &mut TypeCheckContext, - object_type: &LuaObjectType, - source_type_id: &LuaTypeDeclId, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - // ref 可能继承自其他类型, 所以需要使用 infer_members 来获取所有成员 - let Some(source_type_members) = find_members(context.db, &LuaType::Ref(source_type_id.clone())) - else { - return Ok(()); - }; - - for source_member in source_type_members { - let source_member_type = source_member.typ; - let key = source_member.key; - if context.is_key_checked(&key) { - continue; - } - - match get_object_field_type(object_type, &key) { - Some(field_type) => { - check_ref_member_type(context, &key, &source_member_type, field_type, check_guard)?; - } - None if !source_member_type.is_optional() => { - if !context.detail { - return Err(TypeCheckFailReason::TypeNotMatch); - } - return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing member %{name}, in table", name = key.to_path()).to_string(), - )); - } - _ => {} // Optional member not found, continue - } - - context.mark_key_checked(key); - } - - Ok(()) -} - -fn get_object_field_type<'a>( - object_type: &'a LuaObjectType, - key: &LuaMemberKey, -) -> Option<&'a LuaType> { - object_type.get_field(key).or_else(|| { - if let LuaMemberKey::TypeKey(t) = key { - object_type - .get_index_access() - .iter() - .find_map(|(index_key, value)| (index_key == t).then_some(value)) - } else { - None - } - }) -} - -fn check_ref_type_compact_tuple( - context: &mut TypeCheckContext, - tuple_type: &LuaTupleType, - source_type_id: &LuaTypeDeclId, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - let Some(source_type_members) = find_members(context.db, &LuaType::Ref(source_type_id.clone())) - else { - return Ok(()); - }; - - let tuple_types = tuple_type.get_types(); - for member in source_type_members { - let key = member.key; - if context.is_key_checked(&key) { - continue; - } - match &key { - LuaMemberKey::Integer(index) => { - // 在 lua 中数组索引从 1 开始, 当数组被解析为元组时也必然从 1 开始 - if *index <= 0 { - return Err(TypeCheckFailReason::TypeNotMatch); - } - - let Some(tuple_type) = tuple_types.get(*index as usize - 1) else { - if member.typ.is_optional() { - continue; - } - return Err(TypeCheckFailReason::TypeNotMatch); - }; - - check_general_type_compact( - context, - &member.typ, - tuple_type, - check_guard.next_level()?, - )?; - } - LuaMemberKey::TypeKey(LuaType::Integer) => { - // 遍历元组确定所有内容是否匹配 - for tuple_type in tuple_types { - check_general_type_compact( - context, - &member.typ, - tuple_type, - check_guard.next_level()?, - )?; - } - } - _ => { - if member.typ.is_optional() { - continue; - } - return Err(TypeCheckFailReason::TypeNotMatch); - } - } - - context.mark_key_checked(key); - } - - Ok(()) -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/relation.rs b/crates/emmylua_code_analysis/src/semantic/type_check/relation.rs new file mode 100644 index 000000000..9f984008c --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/relation.rs @@ -0,0 +1,1276 @@ +use std::sync::Arc; + +use hashbrown::HashSet; + +use crate::{ + DbIndex, LuaMemberOwner, LuaObjectType, LuaType, TypeOps, TypeSubstitutor, + instantiate_type_generic, semantic::type_check::normalize_type, +}; + +use super::{ + callable::relate_callable, + intersection::relate_intersection, + mismatch::{OverflowKind, TypeMismatch, TypePathSegment}, + simple::{relate_simple_target, try_simple_target}, + structural::{relate_object_members, relate_structural, relate_to_declared_target}, + union::relate_union, +}; + +pub(crate) type RelationResult = Result<(), RelationFailure>; + +#[derive(Debug, Clone)] +pub(crate) enum RelationFailure { + Unrelated(Option), + Indeterminate(OverflowKind, Option), +} + +impl RelationFailure { + pub(crate) fn map_mismatch(self, map: impl FnOnce(TypeMismatch) -> TypeMismatch) -> Self { + match self { + Self::Unrelated(Some(mismatch)) => Self::Unrelated(Some(map(mismatch))), + Self::Indeterminate(kind, Some(mismatch)) => { + Self::Indeterminate(kind, Some(map(mismatch))) + } + failure => failure, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RelationOutcome { + Related, + Unrelated, + Indeterminate(OverflowKind), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RelationKind { + Assignable, + ConditionalExtends, +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub(crate) struct IntersectionState(u32); + +impl IntersectionState { + pub(crate) const NONE: Self = Self(0); + pub(crate) const SOURCE: Self = Self(1 << 0); + pub(crate) const TARGET: Self = Self(1 << 1); + + pub(crate) fn contains(self, state: Self) -> bool { + self.0 & state.0 != 0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum EvidenceMode { + Silent, + Explain, +} + +fn relation_type_eq(source: &LuaType, target: &LuaType) -> bool { + match (source, target) { + (LuaType::FloatConst(left), LuaType::FloatConst(right)) => { + left.to_bits() == right.to_bits() + } + (LuaType::Object(left), LuaType::Object(right)) => Arc::ptr_eq(left, right), + _ => source == target, + } +} + +struct ActiveRelation<'active> { + source: &'active LuaType, + target: &'active LuaType, + intersection_state: IntersectionState, + parent: Option<&'active ActiveRelation<'active>>, +} + +pub(crate) struct RelationSession<'db> { + db: &'db DbIndex, + kind: RelationKind, + evidence: EvidenceMode, + relation_budget: u32, + recursion_depth: u16, + progress: u32, +} + +pub(crate) struct Relater<'session, 'active, 'db> { + session: &'session mut RelationSession<'db>, + active_relation: Option<&'active ActiveRelation<'active>>, +} + +impl<'db> RelationSession<'db> { + fn new(db: &'db DbIndex, kind: RelationKind, evidence: EvidenceMode) -> Self { + Self { + db, + kind, + evidence, + relation_budget: 20_000, + recursion_depth: 0, + progress: 0, + } + } + + fn relate( + &mut self, + source: &LuaType, + target: &LuaType, + intersection_state: IntersectionState, + ) -> RelationResult { + let mut relater = Relater { + session: self, + active_relation: None, + }; + relater.relate(source, target, intersection_state) + } + + pub(crate) fn probe( + db: &'db DbIndex, + kind: RelationKind, + source: &LuaType, + target: &LuaType, + ) -> RelationOutcome { + let mut session = Self::new(db, kind, EvidenceMode::Silent); + match session.relate(source, target, IntersectionState::NONE) { + Ok(()) => RelationOutcome::Related, + Err(RelationFailure::Unrelated(_)) => RelationOutcome::Unrelated, + Err(RelationFailure::Indeterminate(kind, _)) => RelationOutcome::Indeterminate(kind), + } + } + + pub(crate) fn explain( + db: &'db DbIndex, + kind: RelationKind, + source: &LuaType, + target: &LuaType, + ) -> Result<(), TypeMismatch> { + let mut session = Self::new(db, kind, EvidenceMode::Explain); + let mismatch = match session.relate(source, target, IntersectionState::NONE) { + Ok(()) => return Ok(()), + Err(RelationFailure::Unrelated(mismatch)) => { + mismatch.unwrap_or_else(|| TypeMismatch::incompatible(source, target)) + } + Err(RelationFailure::Indeterminate(kind, mismatch)) => { + mismatch.unwrap_or_else(|| TypeMismatch::overflow(source, target, kind)) + } + }; + Err(mismatch) + } +} + +impl<'session, 'active, 'db> Relater<'session, 'active, 'db> { + pub(crate) fn db(&self) -> &'db DbIndex { + self.session.db + } + + pub(crate) fn kind(&self) -> RelationKind { + self.session.kind + } + + pub(crate) fn is_explain(&self) -> bool { + matches!(self.session.evidence, EvidenceMode::Explain) + } + + pub(crate) fn note_progress(&mut self) { + self.session.progress = self.session.progress.saturating_add(1); + } + + pub(crate) fn remaining_relation_budget(&self) -> usize { + self.session.relation_budget as usize + } + + pub(crate) fn consume_relation_budget( + &mut self, + source: &LuaType, + target: &LuaType, + ) -> RelationResult { + if self.session.relation_budget == 0 { + return Err(self.budget_failure(source, target)); + } + self.session.relation_budget -= 1; + Ok(()) + } + + pub(crate) fn budget_failure(&self, source: &LuaType, target: &LuaType) -> RelationFailure { + self.indeterminate(OverflowKind::Budget, source, target) + } + + pub(crate) fn indeterminate_failure( + &self, + kind: OverflowKind, + source: &LuaType, + target: &LuaType, + ) -> RelationFailure { + self.indeterminate(kind, source, target) + } + + pub(crate) fn unrelated(&self, build: impl FnOnce() -> TypeMismatch) -> RelationResult { + let mismatch = self.is_explain().then(build); + Err(RelationFailure::Unrelated(mismatch)) + } + + fn indeterminate( + &self, + kind: OverflowKind, + source: &LuaType, + target: &LuaType, + ) -> RelationFailure { + let mismatch = self + .is_explain() + .then(|| TypeMismatch::overflow(source, target, kind)); + RelationFailure::Indeterminate(kind, mismatch) + } + + pub(crate) fn relate( + &mut self, + source: &LuaType, + target: &LuaType, + intersection_state: IntersectionState, + ) -> RelationResult { + self.relate_scoped(source, target, intersection_state, ShortcutScope::TopLevel) + } + + /// 关系入口主链. 入口成本 (闭环扫描 / 入口预算 / ActiveRelation 帧) 只支付给 + /// "可展开对" (任一侧满足 [`is_expandable`]): 只有展开步骤能在递归中再生产出 + /// 相同的类型值, 从而重现同一 (source, target) 对并需要共递归闭环. 纯结构对 + /// 是无环 Arc 树, 不可能自我重现 —— 免帧免入口预算, 工作量由 + /// `recursion_depth` 与成员义务预算约束. + fn relate_scoped( + &mut self, + source: &LuaType, + target: &LuaType, + intersection_state: IntersectionState, + scope: ShortcutScope, + ) -> RelationResult { + if relate_semantic_accept(source, target) { + self.note_progress(); + return Ok(()); + } + // Object→Object 是高频结构递归, 直接进入成员义务检查, 避免每层重复通用分类. + if let (LuaType::Object(_), LuaType::Object(target_object)) = (source, target) { + if self.session.recursion_depth >= 100 { + return Err(self.indeterminate(OverflowKind::Recursion, source, target)); + } + return self.relate_structural_no_entry(source, target, |relater| { + relate_object_members(relater, source, target, target_object, intersection_state) + }); + } + // Array→Array 只产生一个元素关系, 避免为固定形状支付完整结构分派成本. + if let (LuaType::Array(source_array), LuaType::Array(target_array)) = (source, target) { + if self.session.recursion_depth >= 100 { + return Err(self.indeterminate(OverflowKind::Recursion, source, target)); + } + let explain = self.is_explain(); + let result = if self.session.db.get_emmyrc().strict.array_index { + let target_base = + TypeOps::Union.apply(self.session.db, target_array.get_base(), &LuaType::Nil); + self.relate(source_array.get_base(), &target_base, intersection_state) + } else { + self.relate( + source_array.get_base(), + target_array.get_base(), + intersection_state, + ) + }; + return result.map_err(|failure| { + if explain { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::ArrayElement, source, target) + }) + } else { + failure + } + }); + } + // 标量/运行时叶: 判定不递归, 深度与预算守卫对其无意义, 先行终态判定. + if let Some(result) = try_simple_target(self, source, target) { + return result; + } + if self.session.recursion_depth >= 100 { + return Err(self.indeterminate(OverflowKind::Recursion, source, target)); + } + // 结构 source → Object/声明类 target 的纯结构策略, 见 `structural_shortcut`. + if let Some(result) = + self.try_structural_shortcut(source, target, intersection_state, scope) + { + return result; + } + if is_expandable(source) || is_expandable(target) { + if let Some(result) = + self.close_active_relation_if_present(source, target, intersection_state) + { + return result; + } + self.consume_relation_budget(source, target)?; + self.with_relation_scope(source, target, intersection_state, |relater| { + relater.relate_inner(source, target, intersection_state) + }) + } else { + // 非可展开对无需闭环帧; relate_inner 链上的 normalize/nominal/constraint + // 阶段对它们天然 no-op, 直达 union/intersection 拆分与结构分派. + self.relate_structural_no_entry(source, target, |relater| { + relater.relate_inner(source, target, intersection_state) + }) + } + } + + /// 结构直达路径: 命中 [`structural_shortcut`] 时跳过 nominal/callable 与全部 + /// 入口成本, 直接进行成员义务检查 (义务预算按 target 成员逐个扣减, + /// `recursion_depth` 兜底真循环). + fn try_structural_shortcut( + &mut self, + source: &LuaType, + target: &LuaType, + intersection_state: IntersectionState, + scope: ShortcutScope, + ) -> Option { + let shortcut = structural_shortcut(self.session.db, source, target, scope)?; + Some( + self.relate_structural_no_entry(source, target, |relater| match shortcut { + StructuralShortcut::ToObject(target_object) => relate_object_members( + relater, + source, + target, + target_object, + intersection_state, + ), + StructuralShortcut::ToDeclared => { + relate_to_declared_target(relater, source, target, intersection_state) + } + }), + ) + } + + /// Structural walk without entry budget or ActiveRelation frame. + fn relate_structural_no_entry( + &mut self, + source: &LuaType, + target: &LuaType, + body: impl FnOnce(&mut Relater<'_, '_, 'db>) -> RelationResult, + ) -> RelationResult { + if self.session.recursion_depth >= 100 { + return Err(self.indeterminate(OverflowKind::Recursion, source, target)); + } + self.session.recursion_depth += 1; + let result = body(self); + self.session.recursion_depth -= 1; + result + } + + /// 成员义务内的字段关系 (义务预算已由调用方逐成员扣减). 与顶层入口唯一的 + /// 差别是结构直达路径接受更宽的 source 集合, 见 [`ShortcutScope::Field`]. + pub(in crate::semantic::type_check) fn relate_field_types( + &mut self, + source_member: &LuaType, + target_member: &LuaType, + intersection_state: IntersectionState, + ) -> RelationResult { + self.relate_scoped( + source_member, + target_member, + intersection_state, + ShortcutScope::Field, + ) + } + + fn close_active_relation_if_present( + &mut self, + source: &LuaType, + target: &LuaType, + intersection_state: IntersectionState, + ) -> Option { + let mut active = self.active_relation; + while let Some(relation) = active { + if relation.intersection_state == intersection_state + && relation_type_eq(relation.source, source) + && relation_type_eq(relation.target, target) + { + self.note_progress(); + return Some(Ok(())); + } + active = relation.parent; + } + None + } + + /// 进入递归工作段: 压入 `ActiveRelation` 帧供共递归闭环. 只有可展开对 + /// (见 [`is_expandable`]) 走到这里 —— 纯结构对在 [`Relater::relate_scoped`] + /// 中已分流到免帧路径. + fn with_relation_scope( + &mut self, + source: &LuaType, + target: &LuaType, + intersection_state: IntersectionState, + body: impl FnOnce(&mut Relater<'_, '_, 'db>) -> RelationResult, + ) -> RelationResult { + self.session.recursion_depth += 1; + let active_relation = ActiveRelation { + source, + target, + intersection_state, + parent: self.active_relation, + }; + let mut relater = Relater { + session: &mut *self.session, + active_relation: Some(&active_relation), + }; + let result = body(&mut relater); + self.session.recursion_depth -= 1; + result + } + + fn relate_inner( + &mut self, + source: &LuaType, + target: &LuaType, + intersection_state: IntersectionState, + ) -> RelationResult { + if let Some(normalized) = normalize_type(self.session.db, source) + && normalized != *source + { + if matches!(source, LuaType::Ref(_)) && normalized == *target { + self.note_progress(); + return Ok(()); + } + return self.relate(&normalized, target, intersection_state); + } + if let Some(normalized) = normalize_type(self.session.db, target) + && normalized != *target + { + if matches!(target, LuaType::Ref(_)) && *source == normalized { + self.note_progress(); + return Ok(()); + } + return self.relate(source, &normalized, intersection_state); + } + + if matches!(target, LuaType::ModuleRef(_)) { + self.note_progress(); + return Ok(()); + } + + // 复合类型只在这里拆分, 具体结构处理器不会重排主关系流. + if let Some(result) = relate_union(self, source, target, intersection_state) { + return result; + } + if let Some(result) = relate_intersection(self, source, target, intersection_state) { + return result; + } + + // 自引用 constraint 无法由 normalization 展开, 仍通过关系链闭环处理. + if let LuaType::TplRef(target_tpl) = target + && let Some(constraint) = target_tpl.get_constraint() + { + return self.relate(source, constraint, intersection_state); + } + if let LuaType::TplRef(source_tpl) = source + && let Some(constraint) = source_tpl.get_constraint() + { + return self.relate(constraint, target, intersection_state); + } + if self.session.kind == RelationKind::ConditionalExtends && source.is_never() { + return Ok(()); + } + + if matches!(source, LuaType::Unknown) { + return if self.session.kind == RelationKind::ConditionalExtends { + self.unrelated(|| TypeMismatch::incompatible(source, target)) + } else { + Ok(()) + }; + } + // Never <: Never 原先依赖入口 fast_eq; 递归 relate 必须显式保留. + if matches!(source, LuaType::Never) { + if matches!(target, LuaType::Never) { + self.note_progress(); + return Ok(()); + } + return self.unrelated(|| TypeMismatch::incompatible(source, target)); + } + if matches!(target, LuaType::Never) { + return self.unrelated(|| TypeMismatch::incompatible(source, target)); + } + + let (nominal_result, nominal_indeterminate) = if matches!( + source, + LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_) + ) || matches!( + target, + LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_) + ) { + self.relate_nominal(source, target, intersection_state) + } else { + (None, None) + }; + if let Some(result) = nominal_result { + return result; + } + + let dispatched = if (matches!( + source, + LuaType::Function + | LuaType::DocFunction(_) + | LuaType::Signature(_) + | LuaType::Ref(_) + | LuaType::Def(_) + | LuaType::Generic(_) + ) || matches!( + target, + LuaType::Function + | LuaType::DocFunction(_) + | LuaType::Signature(_) + | LuaType::Ref(_) + | LuaType::Def(_) + | LuaType::Generic(_) + )) && let Some(result) = + relate_callable(self, source, target, intersection_state) + { + Some(result) + } else if let Some(result) = relate_structural(self, source, target, intersection_state) { + Some(result) + } else { + relate_simple_target(self, source, target, intersection_state) + }; + if let Some(result) = dispatched { + return match (result, nominal_indeterminate) { + (Err(RelationFailure::Unrelated(_)), Some(kind)) => { + Err(self.indeterminate(kind, source, target)) + } + (result, _) => result, + }; + } + if let Some(kind) = nominal_indeterminate { + return Err(self.indeterminate(kind, source, target)); + } + + self.unrelated(|| TypeMismatch::incompatible(source, target)) + } + + fn relate_nominal( + &mut self, + source: &LuaType, + target: &LuaType, + intersection_state: IntersectionState, + ) -> (Option, Option) { + let mut indeterminate = None; + let source_id = match source { + LuaType::Ref(type_id) | LuaType::Def(type_id) => Some(type_id.clone()), + LuaType::Generic(generic) => Some(generic.get_base_type_id()), + _ => None, + }; + let target_id = match target { + LuaType::Ref(type_id) | LuaType::Def(type_id) => Some(type_id.clone()), + LuaType::Generic(generic) => Some(generic.get_base_type_id()), + _ => None, + }; + + if let (Some(source_id), Some(target_id)) = (&source_id, &target_id) { + let same_generic_base = source_id == target_id + && matches!((source, target), (LuaType::Generic(_), LuaType::Generic(_))); + if !same_generic_base && source_id == target_id { + self.note_progress(); + return (Some(Ok(())), None); + } + } + + if let Some(target_id) = &target_id + && let Some(decl) = self.session.db.get_type_index().get_type_decl(target_id) + && decl.is_enum() + && let Some(fields) = decl.get_enum_field_type(self.session.db) + { + let source_fields = match source { + LuaType::Ref(source_id) | LuaType::Def(source_id) => self + .session + .db + .get_type_index() + .get_type_decl(source_id) + .filter(|decl| decl.is_enum()) + .and_then(|decl| decl.get_enum_field_type(self.session.db)), + _ => None, + }; + match self + .probe_relation( + source_fields.as_ref().unwrap_or(source), + &fields, + intersection_state, + ) + .0 + { + RelationOutcome::Related => { + self.note_progress(); + return (Some(Ok(())), None); + } + RelationOutcome::Indeterminate(kind) => { + indeterminate.get_or_insert(kind); + } + RelationOutcome::Unrelated => {} + } + } + + if !matches!(target, LuaType::Generic(_)) { + for super_type in declared_super_types(self.session.db, target) { + let outcome = if let Some(source_id) = &source_id { + match &super_type { + LuaType::Ref(super_id) | LuaType::Def(super_id) => { + if source_id == super_id { + RelationOutcome::Related + } else { + RelationOutcome::Unrelated + } + } + LuaType::Generic(super_generic) => { + if source_id == super_generic.get_base_type_id_ref() + && matches!(source, LuaType::Ref(_) | LuaType::Def(_)) + { + RelationOutcome::Related + } else { + RelationOutcome::Unrelated + } + } + _ => { + self.probe_relation(source, &super_type, intersection_state) + .0 + } + } + } else { + self.probe_relation(source, &super_type, intersection_state) + .0 + }; + match outcome { + RelationOutcome::Related => { + self.note_progress(); + return (Some(Ok(())), None); + } + RelationOutcome::Indeterminate(kind) => { + indeterminate.get_or_insert(kind); + } + RelationOutcome::Unrelated => {} + } + } + } + for super_type in declared_super_types(self.session.db, source) { + match self + .probe_relation(&super_type, target, intersection_state) + .0 + { + RelationOutcome::Related => { + self.note_progress(); + return (Some(Ok(())), None); + } + RelationOutcome::Indeterminate(kind) => { + indeterminate.get_or_insert(kind); + } + RelationOutcome::Unrelated => {} + } + } + + if self.session.kind == RelationKind::ConditionalExtends + && source_id.is_some() + && target_id.is_some() + { + let result = if let Some(kind) = indeterminate { + Err(self.indeterminate(kind, source, target)) + } else { + self.unrelated(|| TypeMismatch::incompatible(source, target)) + }; + return (Some(result), None); + } + // 同 base 的 Generic 参数关系交给 relate_generic, 不能因 target 无 members 直接 Unrelated. + // 否则 Holder vs Holder 会在缺少入口 fast_eq 时被误杀. + if source_id.is_some() + && target_id.is_some() + && !declared_type_has_members(self.session.db, target) + && !matches!((source, target), (LuaType::Generic(_), LuaType::Generic(_))) + { + let result = if let Some(kind) = indeterminate { + Err(self.indeterminate(kind, source, target)) + } else { + self.unrelated(|| TypeMismatch::incompatible(source, target)) + }; + return (Some(result), None); + } + + (None, indeterminate) + } + + pub(crate) fn probe_relation( + &mut self, + source: &LuaType, + target: &LuaType, + intersection_state: IntersectionState, + ) -> (RelationOutcome, u32) { + let evidence = self.session.evidence; + let progress = self.session.progress; + self.session.evidence = EvidenceMode::Silent; + self.session.progress = 0; + let relation_budget = self.session.relation_budget; + let result = self.relate(source, target, intersection_state); + self.session.relation_budget = relation_budget; + let candidate_progress = self.session.progress; + self.session.progress = progress; + self.session.evidence = evidence; + let outcome = match result { + Ok(()) => RelationOutcome::Related, + Err(RelationFailure::Unrelated(_)) => RelationOutcome::Unrelated, + Err(RelationFailure::Indeterminate(kind, _)) => RelationOutcome::Indeterminate(kind), + }; + (outcome, candidate_progress) + } +} + +pub(crate) fn declared_super_types(db: &DbIndex, typ: &LuaType) -> Vec { + let (type_id, substitutor) = match typ { + LuaType::Ref(type_id) | LuaType::Def(type_id) => (type_id.clone(), None), + LuaType::Generic(generic) => ( + generic.get_base_type_id(), + Some(TypeSubstitutor::from_type_array( + generic.get_params().clone(), + )), + ), + _ => return Vec::new(), + }; + db.get_type_index() + .get_super_types_iter(&type_id) + .map(|supers| { + supers + .map(|super_type| { + substitutor + .as_ref() + .map(|substitutor| instantiate_type_generic(db, super_type, substitutor)) + .unwrap_or_else(|| super_type.clone()) + }) + .collect() + }) + .unwrap_or_default() +} + +fn declared_type_has_members(db: &DbIndex, typ: &LuaType) -> bool { + let mut pending = vec![typ.clone()]; + let mut visited = HashSet::new(); + while let Some(current) = pending.pop() { + let type_id = match ¤t { + LuaType::Ref(type_id) | LuaType::Def(type_id) => type_id.clone(), + LuaType::Generic(generic) => generic.get_base_type_id(), + _ => continue, + }; + if !visited.insert(type_id.clone()) { + continue; + } + if db + .get_member_index() + .get_member_len(&LuaMemberOwner::Type(type_id)) + > 0 + { + return true; + } + pending.extend(declared_super_types(db, ¤t)); + } + false +} + +// 这里应该只写确定是高频或必须的类型匹配检查, 不要使用完整的 fast_eq_check +fn relate_semantic_accept(source: &LuaType, target: &LuaType) -> bool { + match (source, target) { + (LuaType::Any, _) => true, + (_, LuaType::Any | LuaType::Unknown) => true, + (LuaType::SelfInfer, _) | (_, LuaType::SelfInfer) => true, + (LuaType::TplRef(tpl), _) | (_, LuaType::TplRef(tpl)) if tpl.get_constraint().is_none() => { + true + } + _ => false, + } +} + +/// 该类型的展开是否可能在递归中"再生产出相同的类型值", 从而重现同一 +/// (source, target) 对 —— 这是需要 ActiveRelation 帧做共递归闭环的充要条件. +/// +/// = normalize 可改写 (alias Ref / Generic-alias / Call / Instance / MultiLineUnion / +/// TypeGuard / ModuleRef) ∪ 名义 super 链 (Ref/Def/Generic, 含循环继承) +/// ∪ 自引用 constraint (TplRef) ∪ 递归签名 (Signature). +/// +/// 纯结构类型 (Object/TableConst/Array/Tuple/TableGeneric/Union/Intersection/ +/// DocFunction/Variadic 及标量) 是无环 Arc 树, 结构下降严格缩小, 不可能自我重现; +/// 经由它们的循环必然穿过某个可展开对, 在那一层的帧上闭环 (可能比双方都压帧时 +/// 晚一层, 判定相同). +fn is_expandable(ty: &LuaType) -> bool { + match ty { + LuaType::Ref(_) + | LuaType::Def(_) + | LuaType::Generic(_) + | LuaType::Signature(_) + | LuaType::Instance(_) + | LuaType::Call(_) + | LuaType::Conditional(_) + | LuaType::Mapped(_) + | LuaType::MultiLineUnion(_) + | LuaType::TypeGuard(_) + | LuaType::ModuleRef(_) => true, + LuaType::TplRef(tpl) => tpl.get_constraint().is_some(), + _ => false, + } +} + +/// 结构直达路径的适用范围. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ShortcutScope { + /// 顶层入口: 仅 TableConst/Object source. Ref/Def/Generic source 必须走 + /// relate_inner 的 normalize + nominal (alias 展开的量词方向 / enum 字段 + /// 探测 / ConditionalExtends 名义拒绝). + TopLevel, + /// 成员义务内 (义务预算已由 members.rs 逐成员扣减). 宽 source 集合是既有 + /// 判定策略: Tuple/Array/TableGeneric/Intersection 与完整入口判定等价 + /// (仅省成本); Ref/Def/Generic 依赖 `find_members_with_key` 的 alias/super + /// 展开补偿, 与顶层存在三个已知行为分歧, 由 perf verdict 守卫锁定: + /// 1. alias→union 量词方向相反 (此路径任一构成含成员即命中, + /// 完整入口 normalize 后要求全部构成通过); + /// 2. alias source 的 index obligation 不展开 alias origin, 义务真空成立; + /// 3. ConditionalExtends 的名义拒绝与 enum 字段探测被跳过. + Field, +} + +enum StructuralShortcut<'target> { + ToObject(&'target LuaObjectType), + ToDeclared, +} + +/// 结构 source → Object / 非 alias 声明类 target 的"纯结构策略"分类器: 跳过 +/// nominal/callable, 直接做成员义务检查 (成员遍历自带 alias/super 展开与 +/// visited 集合). alias target 不适用: 必须先 normalize 再谈结构. +fn structural_shortcut<'target>( + db: &DbIndex, + source: &LuaType, + target: &'target LuaType, + scope: ShortcutScope, +) -> Option> { + let source_applicable = match scope { + ShortcutScope::TopLevel => { + matches!(source, LuaType::TableConst(_) | LuaType::Object(_)) + } + ShortcutScope::Field => matches!( + source, + LuaType::TableConst(_) + | LuaType::Object(_) + | LuaType::Ref(_) + | LuaType::Def(_) + | LuaType::Generic(_) + | LuaType::Tuple(_) + | LuaType::Array(_) + | LuaType::TableGeneric(_) + | LuaType::Intersection(_) + ), + }; + if !source_applicable { + return None; + } + match target { + LuaType::Object(target_object) => Some(StructuralShortcut::ToObject(target_object)), + LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_) + if !is_alias_like_target(db, target) => + { + Some(StructuralShortcut::ToDeclared) + } + _ => None, + } +} + +/// Alias / generic-alias targets need normalization; do not treat them as declared class +/// member containers on the lightweight TableConst/Object structural path. +fn is_alias_like_target(db: &DbIndex, target: &LuaType) -> bool { + let type_id = match target { + LuaType::Ref(type_id) | LuaType::Def(type_id) => type_id, + LuaType::Generic(generic) => generic.get_base_type_id_ref(), + _ => return false, + }; + db.get_type_index() + .get_type_decl(type_id) + .is_some_and(|decl| decl.is_alias()) +} + +#[cfg(test)] +mod tests { + use crate::{LuaIntersectionType, LuaTypeDeclId, LuaUnionType, VariadicType, VirtualWorkspace}; + + use super::*; + + #[test] + fn active_relation_separates_intersection_state() { + let db = DbIndex::new(); + // 闭环扫描只对可展开对生效, 用 Ref source 构造可闭环的帧. + let source = LuaType::Ref(LuaTypeDeclId::global("Active")); + let target = LuaType::Integer; + let active_relation = ActiveRelation { + source: &source, + target: &target, + intersection_state: IntersectionState::NONE, + parent: None, + }; + + // intersection_state 不同: 不闭环, 走完整入口并扣预算. + let mut session = RelationSession::new(&db, RelationKind::Assignable, EvidenceMode::Silent); + let before = session.relation_budget; + let result = { + let mut relater = Relater { + session: &mut session, + active_relation: Some(&active_relation), + }; + relater.relate(&source, &target, IntersectionState::SOURCE) + }; + assert!(matches!(result, Err(RelationFailure::Unrelated(None)))); + assert!(session.relation_budget < before); + + // intersection_state 相同: 共递归闭环, 不扣预算. + let mut session = RelationSession::new(&db, RelationKind::Assignable, EvidenceMode::Silent); + let before = session.relation_budget; + let result = { + let mut relater = Relater { + session: &mut session, + active_relation: Some(&active_relation), + }; + relater.relate(&source, &target, IntersectionState::NONE) + }; + assert!(result.is_ok()); + assert_eq!(session.relation_budget, before); + } + + #[test] + fn intersection_constituent_state_replaces_outer_state() { + let db = DbIndex::new(); + let source = LuaType::Ref(LuaTypeDeclId::global("Constituent")); + let target = LuaType::Integer; + + let target_active_relation = ActiveRelation { + source: &source, + target: &target, + intersection_state: IntersectionState::TARGET, + parent: None, + }; + let mut target_session = + RelationSession::new(&db, RelationKind::Assignable, EvidenceMode::Silent); + let target_intersection = + LuaType::Intersection(LuaIntersectionType::new(vec![target.clone()]).into()); + let target_result = { + let mut relater = Relater { + session: &mut target_session, + active_relation: Some(&target_active_relation), + }; + relater.relate(&source, &target_intersection, IntersectionState::SOURCE) + }; + assert!(target_result.is_ok()); + + let source_active_relation = ActiveRelation { + source: &source, + target: &target, + intersection_state: IntersectionState::SOURCE, + parent: None, + }; + let mut source_session = + RelationSession::new(&db, RelationKind::Assignable, EvidenceMode::Silent); + let source_intersection = + LuaType::Intersection(LuaIntersectionType::new(vec![source.clone()]).into()); + let source_result = { + let mut relater = Relater { + session: &mut source_session, + active_relation: Some(&source_active_relation), + }; + relater.relate(&source_intersection, &target, IntersectionState::TARGET) + }; + assert!(source_result.is_ok()); + } + + #[test] + fn silent_and_explain_preserve_overflow_contract() { + let db = DbIndex::new(); + // 标量叶不再扣入口预算, 用可展开 target 触发预算/深度守卫. + let source = LuaType::String; + let target = LuaType::Ref(LuaTypeDeclId::global("Overflow")); + let mut silent = RelationSession::new(&db, RelationKind::Assignable, EvidenceMode::Silent); + silent.relation_budget = 0; + assert!(matches!( + silent.relate(&source, &target, IntersectionState::NONE), + Err(RelationFailure::Indeterminate(OverflowKind::Budget, None)) + )); + + let mut explain = + RelationSession::new(&db, RelationKind::Assignable, EvidenceMode::Explain); + explain.recursion_depth = 100; + assert!(matches!( + explain.relate(&source, &target, IntersectionState::NONE), + Err(RelationFailure::Indeterminate( + OverflowKind::Recursion, + Some(_) + )) + )); + } + + #[test] + fn relation_sentinels_precede_simple_target_dispatch() { + let db = DbIndex::new(); + let target = LuaType::Variadic(VariadicType::Multi(Vec::new()).into()); + + assert_eq!( + RelationSession::probe(&db, RelationKind::Assignable, &LuaType::Unknown, &target), + RelationOutcome::Related + ); + assert_eq!( + RelationSession::probe( + &db, + RelationKind::Assignable, + &LuaType::Unknown, + &LuaType::String, + ), + RelationOutcome::Related + ); + assert_eq!( + RelationSession::probe( + &db, + RelationKind::ConditionalExtends, + &LuaType::Unknown, + &target, + ), + RelationOutcome::Unrelated + ); + assert_eq!( + RelationSession::probe( + &db, + RelationKind::ConditionalExtends, + &LuaType::Unknown, + &LuaType::String, + ), + RelationOutcome::Unrelated + ); + assert_eq!( + RelationSession::probe(&db, RelationKind::Assignable, &LuaType::Never, &target), + RelationOutcome::Unrelated + ); + assert_eq!( + RelationSession::probe( + &db, + RelationKind::Assignable, + &LuaType::Never, + &LuaType::String, + ), + RelationOutcome::Unrelated + ); + assert_eq!( + RelationSession::probe( + &db, + RelationKind::ConditionalExtends, + &LuaType::Never, + &target, + ), + RelationOutcome::Related + ); + assert_eq!( + RelationSession::probe( + &db, + RelationKind::ConditionalExtends, + &LuaType::Never, + &LuaType::String, + ), + RelationOutcome::Related + ); + assert_eq!( + RelationSession::probe( + &db, + RelationKind::Assignable, + &LuaType::String, + &LuaType::Never, + ), + RelationOutcome::Unrelated + ); + + let never_union = + LuaType::Union(LuaUnionType::from_vec(vec![LuaType::String, LuaType::Never]).into()); + assert_eq!( + RelationSession::probe(&db, RelationKind::Assignable, &LuaType::Never, &never_union), + RelationOutcome::Related + ); + + let never_intersection = + LuaType::Intersection(LuaIntersectionType::new(vec![LuaType::Never]).into()); + assert_eq!( + RelationSession::probe( + &db, + RelationKind::Assignable, + &LuaType::Never, + &never_intersection, + ), + RelationOutcome::Related + ); + } + + #[test] + fn simple_target_leaf_decides_before_guards() { + // 标量/运行时叶的判定不递归: 免入口预算、不受深度限制、不依赖闭环帧. + let db = DbIndex::new(); + let source = LuaType::IntegerConst(1); + let target = LuaType::Integer; + + let mut budgeted = + RelationSession::new(&db, RelationKind::Assignable, EvidenceMode::Silent); + budgeted.relation_budget = 0; + assert!( + budgeted + .relate(&source, &target, IntersectionState::NONE) + .is_ok() + ); + assert_eq!(budgeted.relation_budget, 0); + + let mut recursive = + RelationSession::new(&db, RelationKind::Assignable, EvidenceMode::Silent); + recursive.recursion_depth = 100; + assert!( + recursive + .relate(&source, &target, IntersectionState::NONE) + .is_ok() + ); + + // 不相关的叶在深度极限处同样给出终态判定而非 Indeterminate. + let mut deep = RelationSession::new(&db, RelationKind::Assignable, EvidenceMode::Silent); + deep.recursion_depth = 100; + assert!(matches!( + deep.relate(&LuaType::String, &LuaType::Integer, IntersectionState::NONE), + Err(RelationFailure::Unrelated(None)) + )); + } + + #[test] + fn cyclic_super_chain_closes_via_active_relation() { + let mut workspace = VirtualWorkspace::new(); + workspace.def( + r#" + ---@class CycleA: CycleB + ---@field a integer + ---@class CycleB: CycleA + ---@field b integer + "#, + ); + let cycle_a = workspace.ty("CycleA"); + let cycle_b = workspace.ty("CycleB"); + let table_source = workspace.expr_ty("{ a = 1, b = 2 }"); + let db = workspace.analysis.compilation.get_db(); + + // 循环继承的名义探测必须经 ActiveRelation 帧终止, 不允许递归耗尽. + for (source, target) in [(&cycle_a, &cycle_b), (&cycle_b, &cycle_a)] { + let outcome = RelationSession::probe(db, RelationKind::Assignable, source, target); + assert!(!matches!(outcome, RelationOutcome::Indeterminate(_))); + } + // 结构 source 对循环继承 target 走免帧结构直达, 成员遍历的 visited 集合兜底. + assert_eq!( + RelationSession::probe(db, RelationKind::Assignable, &table_source, &cycle_a), + RelationOutcome::Related + ); + } + + #[test] + fn nominal_super_probe_preserves_indeterminate() { + let mut workspace = VirtualWorkspace::new(); + workspace.def( + r#" + ---@class Base + ---@class Child: Base + "#, + ); + let target = workspace.ty("Child"); + let db = workspace.analysis.compilation.get_db(); + let mut session = RelationSession::new(db, RelationKind::Assignable, EvidenceMode::Silent); + session.recursion_depth = 99; + + assert!(matches!( + session.relate(&LuaType::String, &target, IntersectionState::NONE), + Err(RelationFailure::Indeterminate( + OverflowKind::Recursion, + None + )) + )); + } + + #[test] + fn large_table_named_member_diagnostic_does_not_scan_unrelated_fields() { + let mut workspace = VirtualWorkspace::new(); + let small = workspace.expr_ty("{ nested = { value = 'bad' }, k0 = 0 }"); + let fields = (0..10_000) + .map(|index| format!("k{index} = {index}")) + .collect::>() + .join(", "); + let large = workspace.expr_ty(&format!("{{ {fields}, nested = {{ value = 'bad' }} }}")); + let target = workspace.ty("{ nested: { value: number } }"); + let db = workspace.analysis.compilation.get_db(); + + let mut costs = Vec::new(); + for source in [&small, &large] { + let mut session = + RelationSession::new(db, RelationKind::Assignable, EvidenceMode::Silent); + let initial_budget = session.relation_budget; + assert!(matches!( + session.relate(source, &target, IntersectionState::NONE), + Err(RelationFailure::Unrelated(None)) + )); + costs.push(initial_budget - session.relation_budget); + } + + assert_eq!(costs[0], costs[1]); + assert!(costs[1] < 16); + } + + #[test] + fn source_intersection_index_checks_use_whole_intersection_members() { + let mut workspace = VirtualWorkspace::new(); + let valid = workspace.ty("{ a: integer } & { b: integer }"); + let invalid = workspace.ty("{ a: integer } & { b: string }"); + let target = workspace.ty("{ [string]: integer }"); + let db = workspace.analysis.compilation.get_db(); + + assert!( + RelationSession::probe(db, RelationKind::Assignable, &valid, &target) + == RelationOutcome::Related + ); + assert!( + RelationSession::probe(db, RelationKind::Assignable, &invalid, &target) + == RelationOutcome::Unrelated + ); + } + + #[test] + fn structural_relation_keeps_lua_wide_fields_and_optional_member_rules() { + let mut workspace = VirtualWorkspace::new(); + let extra_source = workspace.ty("{ value: integer, extra: string }"); + let value_target = workspace.ty("{ value: integer }"); + let optional_target = workspace.ty("{ missing?: string }"); + let optional_source = workspace.ty("{ value: string? }"); + let required_target = workspace.ty("{ value: string }"); + let db = workspace.analysis.compilation.get_db(); + + assert_eq!( + RelationSession::probe(db, RelationKind::Assignable, &extra_source, &value_target), + RelationOutcome::Related + ); + assert_eq!( + RelationSession::probe( + db, + RelationKind::Assignable, + &extra_source, + &optional_target + ), + RelationOutcome::Related + ); + assert_eq!( + RelationSession::probe( + db, + RelationKind::Assignable, + &optional_source, + &required_target + ), + RelationOutcome::Unrelated + ); + assert_eq!( + RelationSession::probe( + db, + RelationKind::Assignable, + &required_target, + &optional_source + ), + RelationOutcome::Related + ); + } +} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/simple.rs b/crates/emmylua_code_analysis/src/semantic/type_check/simple.rs new file mode 100644 index 000000000..df6d4ce6d --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/simple.rs @@ -0,0 +1,270 @@ +use std::ops::Deref; + +use crate::{LuaType, VariadicType}; + +use super::{ + mismatch::TypeMismatch, + relation::{IntersectionState, Relater, RelationKind, RelationResult}, +}; + +#[derive(Clone, Copy)] +enum SimpleTargetFamily<'target> { + Scalar, + Runtime, + Variadic(&'target VariadicType), +} + +// 对 simple target, 可直接确定兼容或不兼容的关系在此结束. 其余返回 None 进入完整分派. +pub(crate) fn try_simple_target( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, +) -> Option { + if matches!(source, LuaType::Unknown) { + if relater.kind() == RelationKind::ConditionalExtends { + return None; + } + return Some(Ok(())); + } + let family = classify_simple_target(target)?; + let related = match family { + SimpleTargetFamily::Scalar => scalar_target_related(relater, source, target), + SimpleTargetFamily::Runtime => runtime_target_related(source, target), + SimpleTargetFamily::Variadic(_) => return None, + }; + if related { + Some(Ok(())) + } else { + // 此时目标类型已经确定并非简单类型, 因此我们可以提前过滤一些绝不可能往下分发的终端类型. + match source { + LuaType::Nil + | LuaType::Boolean + | LuaType::BooleanConst(_) + | LuaType::DocBooleanConst(_) + | LuaType::String + | LuaType::StringConst(_) + | LuaType::DocStringConst(_) + | LuaType::StrTplRef(_) + | LuaType::Language(_) + | LuaType::Integer + | LuaType::IntegerConst(_) + | LuaType::DocIntegerConst(_) + | LuaType::Number + | LuaType::FloatConst(_) + | LuaType::Table + | LuaType::Userdata + | LuaType::Function + | LuaType::Thread + | LuaType::Io + | LuaType::Global + | LuaType::Namespace(_) => { + Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))) + } + _ => None, + } + } +} + +pub fn relate_simple_target( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + context: IntersectionState, +) -> Option { + let family = classify_simple_target(target)?; + Some(match family { + SimpleTargetFamily::Scalar => relation_result( + relater, + scalar_target_related(relater, source, target), + source, + target, + ), + SimpleTargetFamily::Runtime => relation_result( + relater, + runtime_target_related(source, target), + source, + target, + ), + SimpleTargetFamily::Variadic(target_variadic) => { + relate_variadic_target(relater, source, target, target_variadic, context) + } + }) +} + +fn classify_simple_target(target: &LuaType) -> Option> { + match target { + LuaType::Boolean + | LuaType::BooleanConst(_) + | LuaType::String + | LuaType::StringConst(_) + | LuaType::Integer + | LuaType::IntegerConst(_) + | LuaType::Number + | LuaType::FloatConst(_) + | LuaType::DocStringConst(_) + | LuaType::DocIntegerConst(_) + | LuaType::DocBooleanConst(_) + | LuaType::StrTplRef(_) + | LuaType::Language(_) => Some(SimpleTargetFamily::Scalar), + LuaType::Nil + | LuaType::Table + | LuaType::Userdata + | LuaType::Function + | LuaType::Thread + | LuaType::Io + | LuaType::Global + | LuaType::Namespace(_) => Some(SimpleTargetFamily::Runtime), + LuaType::Variadic(target_variadic) => { + Some(SimpleTargetFamily::Variadic(target_variadic.as_ref())) + } + _ => None, + } +} + +fn scalar_target_related(relater: &Relater, source: &LuaType, target: &LuaType) -> bool { + if relater.kind() == RelationKind::ConditionalExtends { + let literal_match = match target { + LuaType::StringConst(target_value) | LuaType::DocStringConst(target_value) => { + Some(matches!( + source, + LuaType::StringConst(source_value) | LuaType::DocStringConst(source_value) + if source_value == target_value + )) + } + LuaType::IntegerConst(target_value) | LuaType::DocIntegerConst(target_value) => { + Some(matches!( + source, + LuaType::IntegerConst(source_value) | LuaType::DocIntegerConst(source_value) + if source_value == target_value + )) + } + LuaType::BooleanConst(target_value) | LuaType::DocBooleanConst(target_value) => { + Some(matches!( + source, + LuaType::BooleanConst(source_value) | LuaType::DocBooleanConst(source_value) + if source_value == target_value + )) + } + LuaType::FloatConst(target_value) => Some(matches!( + source, + LuaType::FloatConst(source_value) if source_value == target_value + )), + _ => None, + }; + if let Some(related) = literal_match { + return related; + } + } + + match target { + LuaType::Boolean | LuaType::BooleanConst(_) => source.is_boolean(), + LuaType::String | LuaType::StringConst(_) => matches!( + source, + LuaType::String + | LuaType::StringConst(_) + | LuaType::DocStringConst(_) + | LuaType::StrTplRef(_) + | LuaType::Language(_) + ), + LuaType::Integer | LuaType::IntegerConst(_) => source.is_integer(), + LuaType::Number | LuaType::FloatConst(_) => source.is_number(), + LuaType::DocIntegerConst(target_value) => match source { + LuaType::IntegerConst(source_value) | LuaType::DocIntegerConst(source_value) => { + source_value == target_value + } + LuaType::Integer => { + relater + .db() + .get_emmyrc() + .strict + .doc_base_const_match_base_type + } + _ => false, + }, + LuaType::DocStringConst(target_value) => matches!( + source, + LuaType::StringConst(source_value) | LuaType::DocStringConst(source_value) + if source_value == target_value + ), + LuaType::DocBooleanConst(target_value) => matches!( + source, + LuaType::BooleanConst(source_value) | LuaType::DocBooleanConst(source_value) + if source_value == target_value + ), + LuaType::StrTplRef(_) => source.is_string() || source == target, + LuaType::Language(target_language) => match source { + LuaType::Language(source_language) => source_language == target_language, + LuaType::DocStringConst(_) | LuaType::String | LuaType::StringConst(_) => true, + _ => false, + }, + _ => false, + } +} + +fn runtime_target_related(source: &LuaType, target: &LuaType) -> bool { + match target { + LuaType::Nil => matches!(source, LuaType::Nil), + LuaType::Table => matches!( + source, + LuaType::Table + | LuaType::TableConst(_) + | LuaType::Tuple(_) + | LuaType::Array(_) + | LuaType::Object(_) + | LuaType::TableGeneric(_) + | LuaType::Global + | LuaType::Userdata + ), + LuaType::Userdata => matches!(source, LuaType::Userdata), + LuaType::Function => matches!( + source, + LuaType::Function | LuaType::DocFunction(_) | LuaType::Signature(_) + ), + LuaType::Thread => matches!(source, LuaType::Thread), + LuaType::Io => matches!(source, LuaType::Io), + LuaType::Global => matches!(source, LuaType::Global), + LuaType::Namespace(target_namespace) => { + matches!(source, LuaType::Namespace(source_namespace) if source_namespace == target_namespace) + } + _ => false, + } +} + +fn relate_variadic_target( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + target_variadic: &VariadicType, + context: IntersectionState, +) -> RelationResult { + match target_variadic { + VariadicType::Multi(_) => Ok(()), + VariadicType::Base(target_base) => match source { + LuaType::Variadic(source_variadic) => match source_variadic.deref() { + VariadicType::Base(source_base) => { + relation_result(relater, source_base == target_base, source, target) + } + VariadicType::Multi(source_types) => { + for source_type in source_types { + relater.relate(source_type, target_base, context)?; + } + Ok(()) + } + }, + _ => relater.relate(source, target_base, context), + }, + } +} + +fn relation_result( + relater: &mut Relater, + related: bool, + source: &LuaType, + target: &LuaType, +) -> RelationResult { + if related { + Ok(()) + } else { + relater.unrelated(|| TypeMismatch::incompatible(source, target)) + } +} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/simple_type.rs b/crates/emmylua_code_analysis/src/semantic/type_check/simple_type.rs deleted file mode 100644 index 32ac1818b..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/simple_type.rs +++ /dev/null @@ -1,440 +0,0 @@ -use std::ops::Deref; - -use crate::{ - DbIndex, LuaType, LuaTypeDeclId, VariadicType, - semantic::type_check::{ - is_sub_type_of, - type_check_context::{TypeCheckCheckLevel, TypeCheckContext}, - }, -}; - -use super::{ - TypeCheckResult, check_general_type_compact, sub_type::get_base_type_id, - type_check_fail_reason::TypeCheckFailReason, type_check_guard::TypeCheckGuard, -}; - -pub fn check_simple_type_compact( - context: &mut TypeCheckContext, - source: &LuaType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - match source { - LuaType::Unknown | LuaType::Any => return Ok(()), - LuaType::Nil => { - if let LuaType::Nil = compact_type { - return Ok(()); - } - } - LuaType::Table | LuaType::TableConst(_) => { - if matches!( - compact_type, - LuaType::Table - | LuaType::TableConst(_) - | LuaType::Tuple(_) - | LuaType::Array(_) - | LuaType::Object(_) - | LuaType::Ref(_) - | LuaType::Def(_) - | LuaType::TableGeneric(_) - | LuaType::Generic(_) - | LuaType::Global - | LuaType::Userdata - | LuaType::Instance(_) - | LuaType::Any - ) { - return Ok(()); - } - } - LuaType::Userdata => { - if matches!( - compact_type, - LuaType::Userdata | LuaType::Ref(_) | LuaType::Def(_) - ) { - return Ok(()); - } - } - LuaType::Function => { - if matches!( - compact_type, - LuaType::Function | LuaType::DocFunction(_) | LuaType::Signature(_) - ) { - return Ok(()); - } - } - LuaType::Thread => { - if let LuaType::Thread = compact_type { - return Ok(()); - } - } - LuaType::Boolean | LuaType::BooleanConst(_) => { - if compact_type.is_boolean() { - return Ok(()); - } - } - LuaType::String => match compact_type { - LuaType::String - | LuaType::StringConst(_) - | LuaType::DocStringConst(_) - | LuaType::StrTplRef(_) - | LuaType::Language(_) => { - return Ok(()); - } - LuaType::Ref(_) => { - match check_base_type_for_ref_compact(context, source, compact_type, check_guard) { - Ok(_) => return Ok(()), - Err(err) if err.is_type_not_match() => {} - Err(err) => return Err(err), - } - } - LuaType::Def(id) => { - if id.get_name() == "string" { - return Ok(()); - } - } - _ => {} - }, - LuaType::StringConst(s1) => match compact_type { - LuaType::String - | LuaType::StringConst(_) - | LuaType::StrTplRef(_) - | LuaType::Language(_) => { - return Ok(()); - } - LuaType::DocStringConst(s2) => { - if context.level == TypeCheckCheckLevel::GenericConditional && s1 != s2 { - return Err(TypeCheckFailReason::TypeNotMatch); - } - return Ok(()); - } - LuaType::Ref(_) => { - match check_base_type_for_ref_compact(context, source, compact_type, check_guard) { - Ok(_) => return Ok(()), - Err(err) if err.is_type_not_match() => {} - Err(err) => return Err(err), - } - } - LuaType::Def(id) => { - if id.get_name() == "string" { - return Ok(()); - } - } - _ => {} - }, - LuaType::Integer | LuaType::IntegerConst(_) => match compact_type { - LuaType::Integer | LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_) => { - return Ok(()); - } - LuaType::Ref(_) => { - match check_base_type_for_ref_compact(context, source, compact_type, check_guard) { - Ok(_) => return Ok(()), - Err(err) if err.is_type_not_match() => {} - Err(err) => return Err(err), - } - } - _ => {} - }, - LuaType::Number | LuaType::FloatConst(_) => { - if matches!( - compact_type, - LuaType::Number - | LuaType::FloatConst(_) - | LuaType::Integer - | LuaType::IntegerConst(_) - | LuaType::DocIntegerConst(_) - ) { - return Ok(()); - } - } - LuaType::Io => { - if let LuaType::Io = compact_type { - return Ok(()); - } - } - LuaType::Global => { - if let LuaType::Global = compact_type { - return Ok(()); - } - } - LuaType::DocIntegerConst(i) => match compact_type { - LuaType::IntegerConst(j) => { - if i == j { - return Ok(()); - } - - return Err(TypeCheckFailReason::TypeNotMatch); - } - LuaType::Integer => { - if context - .db - .get_emmyrc() - .strict - .doc_base_const_match_base_type - { - return Ok(()); - } - return Err(TypeCheckFailReason::TypeNotMatch); - } - LuaType::DocIntegerConst(j) => { - if i == j { - return Ok(()); - } - - return Err(TypeCheckFailReason::TypeNotMatch); - } - LuaType::Ref(_) => { - if context - .db - .get_emmyrc() - .strict - .doc_base_const_match_base_type - { - match check_base_type_for_ref_compact( - context, - source, - compact_type, - check_guard, - ) { - Ok(_) => return Ok(()), - Err(err) if err.is_type_not_match() => {} - Err(err) => return Err(err), - } - } - } - _ => {} - }, - LuaType::DocStringConst(s) => match compact_type { - LuaType::StringConst(t) => { - if s == t { - return Ok(()); - } - - return Err(TypeCheckFailReason::TypeNotMatch); - } - LuaType::String => return Err(TypeCheckFailReason::TypeNotMatch), - LuaType::DocStringConst(t) => { - if s == t { - return Ok(()); - } - - return Err(TypeCheckFailReason::TypeNotMatch); - } - LuaType::Ref(_) => { - if context - .db - .get_emmyrc() - .strict - .doc_base_const_match_base_type - { - match check_base_type_for_ref_compact( - context, - source, - compact_type, - check_guard, - ) { - Ok(_) => return Ok(()), - Err(err) if err.is_type_not_match() => {} - Err(err) => return Err(err), - } - } - } - _ => {} - }, - LuaType::DocBooleanConst(b) => match compact_type { - LuaType::BooleanConst(t) => { - if b == t { - return Ok(()); - } - - return Err(TypeCheckFailReason::TypeNotMatch); - } - LuaType::Boolean => return Err(TypeCheckFailReason::TypeNotMatch), - LuaType::DocBooleanConst(t) => { - if b == t { - return Ok(()); - } - - return Err(TypeCheckFailReason::TypeNotMatch); - } - _ => {} - }, - LuaType::StrTplRef(_) => { - if compact_type.is_string() { - return Ok(()); - } - } - LuaType::TplRef(_) => return Ok(()), - LuaType::Namespace(source_namespace) => { - if let LuaType::Namespace(compact_namespace) = compact_type - && source_namespace == compact_namespace - { - return Ok(()); - } - } - LuaType::Variadic(source_type) => { - return check_variadic_type_compact(context, source_type, compact_type, check_guard); - } - LuaType::Language(lang_str) => match compact_type { - LuaType::Language(compact_lang_str) => { - if lang_str == compact_lang_str { - return Ok(()); - } - } - LuaType::DocStringConst(_) | LuaType::String | LuaType::StringConst(_) => { - return Ok(()); - } - _ => {} - }, - _ => {} - } - - if let LuaType::Union(union) = compact_type { - for sub_compact in union.into_vec() { - match check_simple_type_compact( - context, - source, - &sub_compact, - check_guard.next_level()?, - ) { - Ok(_) => {} - Err(err) => return Err(err), - } - } - - return Ok(()); - } - - // complex infer - Err(TypeCheckFailReason::TypeNotMatch) -} - -fn get_alias_real_type<'a>( - db: &'a DbIndex, - compact_type: &'a LuaType, - check_guard: TypeCheckGuard, -) -> Result<&'a LuaType, TypeCheckFailReason> { - if let LuaType::Ref(type_decl_id) = compact_type { - let type_decl = db - .get_type_index() - .get_type_decl(type_decl_id) - .ok_or(TypeCheckFailReason::DonotCheck)?; - if type_decl.is_alias() { - return get_alias_real_type( - db, - type_decl - .get_alias_ref() - .ok_or(TypeCheckFailReason::DonotCheck)?, - check_guard.next_level()?, - ); - } - } - - Ok(compact_type) -} - -/// 检查基础类型是否匹配自定义类型 -fn check_base_type_for_ref_compact( - context: &mut TypeCheckContext, - source: &LuaType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - if let LuaType::Ref(_) = compact_type { - let real_type = get_alias_real_type(context.db, compact_type, check_guard.next_level()?)?; - match &real_type { - LuaType::MultiLineUnion(multi_line_union) => { - for (sub_type, _) in multi_line_union.get_unions() { - match check_general_type_compact( - context, - source, - sub_type, - check_guard.next_level()?, - ) { - Ok(_) => {} - Err(e) => return Err(e), - } - } - - return Ok(()); - } - LuaType::Ref(type_decl_id) => { - if let Some(source_id) = get_base_type_id(source) - && is_sub_type_of(context.db, type_decl_id, &source_id) - { - return Ok(()); - } - if let Some(decl) = context.db.get_type_index().get_type_decl(type_decl_id) - && decl.is_enum() - { - return check_enum_fields_match_source( - context, - source, - type_decl_id, - check_guard, - ); - } - } - _ => {} - } - } - Err(TypeCheckFailReason::TypeNotMatch) -} - -/// 检查`enum`的所有字段是否匹配`source` -fn check_enum_fields_match_source( - context: &mut TypeCheckContext, - source: &LuaType, - enum_type_decl_id: &LuaTypeDeclId, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - if let Some(decl) = context.db.get_type_index().get_type_decl(enum_type_decl_id) - && let Some(LuaType::Union(enum_fields)) = decl.get_enum_field_type(context.db) - { - for field in enum_fields.into_vec() { - check_general_type_compact(context, source, &field, check_guard.next_level()?)?; - } - - return Ok(()); - } - Err(TypeCheckFailReason::TypeNotMatch) -} - -fn check_variadic_type_compact( - context: &mut TypeCheckContext, - source_type: &VariadicType, - compact_type: &LuaType, - check_guard: TypeCheckGuard, -) -> TypeCheckResult { - match &source_type { - VariadicType::Base(source_base) => match compact_type { - LuaType::Variadic(compact_variadic) => match compact_variadic.deref() { - VariadicType::Base(compact_base) => { - if source_base == compact_base { - return Ok(()); - } - } - VariadicType::Multi(compact_multi) => { - for compact_type in compact_multi { - check_simple_type_compact( - context, - source_base, - compact_type, - check_guard.next_level()?, - )?; - } - } - }, - _ => { - check_simple_type_compact( - context, - source_base, - compact_type, - check_guard.next_level()?, - )?; - } - }, - VariadicType::Multi(_) => {} - } - - Ok(()) -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/structural/declared.rs b/crates/emmylua_code_analysis/src/semantic/type_check/structural/declared.rs new file mode 100644 index 000000000..4d7115428 --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/structural/declared.rs @@ -0,0 +1,89 @@ +use crate::LuaType; + +use super::super::{ + relation::{IntersectionState, Relater, RelationKind, RelationResult}, + sub_type::{get_base_type_id, is_sub_type_of}, +}; + +pub(super) fn relate_declared( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + context: IntersectionState, +) -> Option { + let source_id = match source { + LuaType::Ref(source_id) | LuaType::Def(source_id) => source_id, + LuaType::Generic(_) if matches!(target, LuaType::Table) => return Some(Ok(())), + _ => return None, + }; + let (alias, is_enum) = relater + .db() + .get_type_index() + .get_type_decl(source_id) + .map(|decl| { + let alias = decl + .is_alias() + .then(|| decl.get_alias_ref().cloned()) + .flatten(); + (alias, decl.is_enum()) + }) + .unwrap_or((None, false)); + if let Some(alias) = alias { + return Some(relater.relate(&alias, target, context)); + } + + let conditional_extends = relater.kind() == RelationKind::ConditionalExtends; + let supports_custom_primitive = matches!( + target, + LuaType::String | LuaType::StringConst(_) | LuaType::Integer | LuaType::IntegerConst(_) + ) || matches!( + target, + LuaType::DocStringConst(_) | LuaType::DocIntegerConst(_) + ) && (conditional_extends + || relater + .db() + .get_emmyrc() + .strict + .doc_base_const_match_base_type); + let target_is_literal = matches!( + target, + LuaType::BooleanConst(_) + | LuaType::StringConst(_) + | LuaType::IntegerConst(_) + | LuaType::FloatConst(_) + | LuaType::DocStringConst(_) + | LuaType::DocIntegerConst(_) + | LuaType::DocBooleanConst(_) + ); + if supports_custom_primitive + && !(conditional_extends && target_is_literal) + && let Some(target_id) = get_base_type_id(target) + && is_sub_type_of(relater.db(), source_id, &target_id) + { + return Some(Ok(())); + } + + if supports_custom_primitive && is_enum { + let enum_fields = relater + .db() + .get_type_index() + .get_type_decl(source_id) + .and_then(|decl| decl.get_enum_field_type(relater.db())); + if let Some(enum_fields) = enum_fields { + return Some(match enum_fields { + LuaType::Union(fields) => { + for field in fields.into_vec() { + match relater.relate(&field, target, context) { + Ok(()) => {} + result => return Some(result), + } + } + Ok(()) + } + field => relater.relate(&field, target, context), + }); + } + } + + matches!(target, LuaType::Table | LuaType::Userdata).then_some(Ok(())) +} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/structural/generic.rs b/crates/emmylua_code_analysis/src/semantic/type_check/structural/generic.rs new file mode 100644 index 000000000..a39c1731a --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/structural/generic.rs @@ -0,0 +1,303 @@ +use crate::{ + LuaMemberKey, LuaMemberOwner, LuaType, semantic::type_check::sub_type::is_sub_type_of, +}; + +use super::super::{ + mismatch::{TypeMismatch, TypePathSegment}, + relation::{IntersectionState, Relater, RelationResult}, + visit_member_items, +}; + +use super::members::visit_declared_members; + +pub(super) fn relate_generic( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + context: IntersectionState, +) -> Option { + match (source, target) { + (LuaType::Generic(source_generic), LuaType::Generic(target_generic)) => { + if source_generic == target_generic { + relater.note_progress(); + return Some(Ok(())); + } + if source_generic.get_base_type_id_ref() == target_generic.get_base_type_id_ref() { + if source_generic.get_params().len() != target_generic.get_params().len() { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + } + + for (index, (source_param, target_param)) in source_generic + .get_params() + .iter() + .zip(target_generic.get_params()) + .enumerate() + { + if let Err(failure) = relater.relate(source_param, target_param, context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::GenericArgument(index), source, target) + }))); + } + relater.note_progress(); + } + return Some(Ok(())); + } + + Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))) + } + (LuaType::Generic(source_generic), LuaType::Ref(target_id) | LuaType::Def(target_id)) => { + if is_sub_type_of( + relater.db(), + source_generic.get_base_type_id_ref(), + target_id, + ) { + Some(Ok(())) + } else { + None + } + } + (LuaType::Ref(source_id) | LuaType::Def(source_id), LuaType::Generic(target_generic)) => { + if is_sub_type_of( + relater.db(), + source_id, + target_generic.get_base_type_id_ref(), + ) { + if target_generic.get_params().iter().all(LuaType::is_any) { + Some(Ok(())) + } else { + Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))) + } + } else { + None + } + } + (LuaType::TableGeneric(source_params), LuaType::TableGeneric(target_params)) => { + if source_params.len() != 2 || target_params.len() != 2 { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + } + + if let Err(failure) = relater.relate(&source_params[0], &target_params[0], context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::GenericArgument(0), source, target) + }))); + } + relater.note_progress(); + if let Err(failure) = relater.relate(&source_params[1], &target_params[1], context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::GenericArgument(1), source, target) + }))); + } + relater.note_progress(); + Some(Ok(())) + } + (LuaType::Array(source_array), LuaType::TableGeneric(target_params)) => { + if target_params.len() != 2 { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + } + + if let Err(failure) = relater.relate(&LuaType::Integer, &target_params[0], context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::GenericArgument(0), source, target) + }))); + } + relater.note_progress(); + if let Err(failure) = + relater.relate(source_array.get_base(), &target_params[1], context) + { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::GenericArgument(1), source, target) + }))); + } + relater.note_progress(); + Some(Ok(())) + } + (LuaType::Tuple(source_tuple), LuaType::TableGeneric(target_params)) => { + if target_params.len() != 2 { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + } + + if let Err(failure) = relater.relate(&LuaType::Integer, &target_params[0], context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::GenericArgument(0), source, target) + }))); + } + relater.note_progress(); + for (index, source_type) in source_tuple.get_types().iter().enumerate() { + if let Err(failure) = relater.consume_relation_budget(source, target) { + return Some(Err(failure)); + } + if let Err(failure) = relater.relate(source_type, &target_params[1], context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::TupleElement(index), source, target) + }))); + } + relater.note_progress(); + } + Some(Ok(())) + } + (LuaType::Object(source_object), LuaType::TableGeneric(target_params)) => { + if target_params.len() != 2 { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + } + + for (key, source_type) in source_object.get_fields() { + if let Err(failure) = relater.consume_relation_budget(source, target) { + return Some(Err(failure)); + } + let Some(source_key_type) = key.to_index_type() else { + continue; + }; + if let Err(failure) = relater.relate(&source_key_type, &target_params[0], context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::Index(source_key_type), source, target) + }))); + } + if let Err(failure) = relater.relate(source_type, &target_params[1], context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::Member(key.clone()), source, target) + }))); + } + relater.note_progress(); + } + for (source_key_type, source_type) in source_object.get_index_access() { + if let Err(failure) = relater.consume_relation_budget(source, target) { + return Some(Err(failure)); + } + if let Err(failure) = relater.relate(source_key_type, &target_params[0], context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at( + TypePathSegment::Index(source_key_type.clone()), + source, + target, + ) + }))); + } + if let Err(failure) = relater.relate(source_type, &target_params[1], context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at( + TypePathSegment::Index(source_key_type.clone()), + source, + target, + ) + }))); + } + relater.note_progress(); + } + Some(Ok(())) + } + (LuaType::TableGeneric(source_params), LuaType::Array(target_array)) => { + if source_params.len() != 2 { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + } + if !source_params[0].is_integer() && !source_params[0].is_any() { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + } + Some( + relater + .relate(&source_params[1], target_array.get_base(), context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::ArrayElement, source, target) + }) + }), + ) + } + (LuaType::TableConst(range), LuaType::TableGeneric(target_params)) => { + Some(relate_table_const_to_table_generic( + relater, + source, + target, + range, + target_params, + context, + )) + } + ( + LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_), + LuaType::TableGeneric(target_params), + ) => { + if target_params.len() != 2 { + return Some(relater.unrelated(|| TypeMismatch::incompatible(source, target))); + } + Some(visit_declared_members( + relater, + source, + |relater, key, source_value_type| { + relate_member_to_table_generic( + relater, + source, + target, + key, + source_value_type, + target_params, + context, + ) + }, + )) + } + _ => None, + } +} + +fn relate_table_const_to_table_generic( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + range: &crate::InFiled, + target_params: &[LuaType], + context: IntersectionState, +) -> RelationResult { + if target_params.len() != 2 { + return relater.unrelated(|| TypeMismatch::incompatible(source, target)); + } + + let db = relater.db(); + let owner = LuaMemberOwner::Element(range.clone()); + visit_member_items(db, &owner, |key, item| { + let source_value_type = item.resolve_type(db).unwrap_or(LuaType::Any); + relate_member_to_table_generic( + relater, + source, + target, + key, + &source_value_type, + target_params, + context, + ) + }) +} + +fn relate_member_to_table_generic( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + key: &LuaMemberKey, + source_value_type: &LuaType, + target_params: &[LuaType], + context: IntersectionState, +) -> RelationResult { + relater.consume_relation_budget(source, target)?; + let Some(source_key_type) = key.to_index_type() else { + return Ok(()); + }; + relater + .relate(&source_key_type, &target_params[0], context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at( + TypePathSegment::Index(source_key_type.clone()), + source, + target, + ) + }) + })?; + relater + .relate(source_value_type, &target_params[1], context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::Member(key.clone()), source, target) + }) + })?; + relater.note_progress(); + Ok(()) +} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/structural/members.rs b/crates/emmylua_code_analysis/src/semantic/type_check/structural/members.rs new file mode 100644 index 000000000..92194b3fc --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/structural/members.rs @@ -0,0 +1,533 @@ +use hashbrown::HashSet; + +use crate::{ + LuaMemberKey, LuaMemberOwner, LuaObjectType, LuaType, LuaTypeDeclId, TypeSubstitutor, + instantiate_type_generic, semantic::member::find_members_with_key, +}; + +use super::super::{ + mismatch::{TypeMismatch, TypeMismatchKind, TypePathSegment}, + relation::{IntersectionState, Relater, RelationFailure, RelationOutcome, RelationResult}, + visit_member_items, +}; + +pub(super) fn relate_members( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + context: IntersectionState, +) -> Option { + if !matches!( + source, + LuaType::Object(_) + | LuaType::TableConst(_) + | LuaType::Ref(_) + | LuaType::Def(_) + | LuaType::Generic(_) + | LuaType::Intersection(_) + | LuaType::Tuple(_) + | LuaType::Array(_) + | LuaType::TableGeneric(_) + ) { + return None; + } + + match target { + LuaType::Object(target_object) => Some(relate_object_members( + relater, + source, + target, + target_object, + context, + )), + LuaType::TableConst(range) => Some(relate_to_table_target( + relater, source, target, range, context, + )), + LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_) => { + Some(relate_to_declared_target(relater, source, target, context)) + } + _ => None, + } +} + +pub(in crate::semantic::type_check) fn relate_object_members( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + target_object: &LuaObjectType, + context: IntersectionState, +) -> RelationResult { + if let LuaType::Object(source_object) = source { + for (key, target_member_type) in target_object.get_fields() { + // One obligation budget per target member (covers nested Object/simple leaves). + relater.consume_relation_budget(source, target)?; + let source_member_type = source_object.get_field(key).or_else(|| { + let LuaMemberKey::TypeKey(target_key_type) = key else { + return None; + }; + source_object.get_index_access().iter().find_map( + |(source_key_type, source_value_type)| { + (source_key_type == target_key_type).then_some(source_value_type) + }, + ) + }); + let Some(source_member_type) = source_member_type else { + if target_member_type.is_optional() { + continue; + } + return relater.unrelated(|| { + TypeMismatch::new( + source, + target, + TypeMismatchKind::MissingMember { key: key.clone() }, + ) + }); + }; + + // Nested Object / simple leaves use a lightweight field path; only Explain + // attaches member path frames (Silent keeps the cheap failure). + let field_result = + relater.relate_field_types(source_member_type, target_member_type, context); + if let Err(failure) = field_result { + if relater.is_explain() { + return Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::Member(key.clone()), source, target) + })); + } + return Err(failure); + } + relater.note_progress(); + } + } else { + for (key, target_member_type) in target_object.get_fields() { + relate_named_member_obligation( + relater, + source, + target, + key, + target_member_type, + context, + )?; + } + } + + if !context.contains(IntersectionState::TARGET) { + for (target_key_type, target_value_type) in target_object.get_index_access() { + relate_index_obligation( + relater, + source, + target, + target_key_type, + target_value_type, + context, + )?; + } + } + + Ok(()) +} + +fn relate_named_member_obligation( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + key: &LuaMemberKey, + target_member_type: &LuaType, + context: IntersectionState, +) -> RelationResult { + relater.consume_relation_budget(source, target)?; + let source_member_type = find_source_member_type(relater, source, target, key, context)?; + let Some(source_member_type) = source_member_type else { + if target_member_type.is_optional() { + return Ok(()); + } + return relater.unrelated(|| { + TypeMismatch::new( + source, + target, + TypeMismatchKind::MissingMember { key: key.clone() }, + ) + }); + }; + + // Lightweight field path for nested Object/TableConst/class/simple values. + // Silent keeps failure cheap; Explain attaches member path frames. + let field_result = relater.relate_field_types(&source_member_type, target_member_type, context); + if let Err(failure) = field_result { + if relater.is_explain() { + return Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::Member(key.clone()), source, target) + })); + } + return Err(failure); + } + relater.note_progress(); + Ok(()) +} + +fn relate_to_table_target( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + range: &crate::InFiled, + context: IntersectionState, +) -> RelationResult { + let owner = LuaMemberOwner::Element(range.clone()); + let db = relater.db(); + visit_member_items(db, &owner, |key, item| { + let target_member_type = item.resolve_type(db).unwrap_or(LuaType::Any); + if let LuaMemberKey::TypeKey(target_key_type) = key { + if context.contains(IntersectionState::TARGET) { + return Ok(()); + } + return relate_index_obligation( + relater, + source, + target, + target_key_type, + &target_member_type, + context, + ); + } + + relate_named_member_obligation(relater, source, target, key, &target_member_type, context) + }) +} + +pub(in crate::semantic::type_check) fn relate_to_declared_target( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + context: IntersectionState, +) -> RelationResult { + visit_declared_members(relater, target, |relater, key, target_member_type| { + if let LuaMemberKey::TypeKey(target_key_type) = key { + if context.contains(IntersectionState::TARGET) { + return Ok(()); + } + return relate_index_obligation( + relater, + source, + target, + target_key_type, + target_member_type, + context, + ); + } + + relate_named_member_obligation(relater, source, target, key, target_member_type, context) + }) +} + +fn find_source_member_type( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + key: &LuaMemberKey, + context: IntersectionState, +) -> Result, RelationFailure> { + let member_type = match source { + LuaType::Object(source_object) => source_object.get_field(key).cloned().or_else(|| { + let LuaMemberKey::TypeKey(target_key_type) = key else { + return None; + }; + source_object.get_index_access().iter().find_map( + |(source_key_type, source_value_type)| { + (source_key_type == target_key_type).then(|| source_value_type.clone()) + }, + ) + }), + LuaType::TableConst(range) => relater + .db() + .get_member_index() + .get_member_item(&LuaMemberOwner::Element(range.clone()), key) + .map(|item| item.resolve_type(relater.db()).unwrap_or(LuaType::Any)), + LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_) | LuaType::Intersection(_) => { + find_members_with_key(relater.db(), source, key.clone(), false) + .and_then(|members| members.into_iter().next()) + .map(|member| member.typ) + } + LuaType::Tuple(source_tuple) => match key { + LuaMemberKey::Integer(index) if *index > 0 => { + source_tuple.get_type(*index as usize - 1).cloned() + } + _ => None, + }, + LuaType::Array(source_array) => match key { + LuaMemberKey::Integer(index) if *index > 0 => Some(source_array.get_base().clone()), + LuaMemberKey::TypeKey(key_type) if key_type.is_integer() => { + Some(source_array.get_base().clone()) + } + _ => None, + }, + LuaType::TableGeneric(source_params) if source_params.len() == 2 => { + let Some(source_key_type) = key.to_index_type() else { + return Ok(None); + }; + match relater + .probe_relation(&source_key_type, &source_params[0], context) + .0 + { + RelationOutcome::Related => Some(source_params[1].clone()), + RelationOutcome::Unrelated => None, + RelationOutcome::Indeterminate(kind) => { + return Err(relater.indeterminate_failure(kind, source, target)); + } + } + } + _ => None, + }; + Ok(member_type) +} + +fn relate_index_obligation( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + target_key_type: &LuaType, + target_value_type: &LuaType, + context: IntersectionState, +) -> RelationResult { + if context.contains(IntersectionState::SOURCE) { + return Ok(()); + } + + let relate_entry = |relater: &mut Relater, + source_key_type: &LuaType, + source_value_type: &LuaType| + -> RelationResult { + relater.consume_relation_budget(source, target)?; + match relater + .probe_relation(source_key_type, target_key_type, context) + .0 + { + RelationOutcome::Related => { + relater + .relate(source_value_type, target_value_type, context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at( + TypePathSegment::Index(source_key_type.clone()), + source, + target, + ) + }) + })?; + relater.note_progress(); + Ok(()) + } + RelationOutcome::Unrelated => Ok(()), + RelationOutcome::Indeterminate(kind) => { + Err(relater.indeterminate_failure(kind, source, target)) + } + } + }; + + match source { + LuaType::Object(source_object) => { + for (key, source_value_type) in source_object.get_fields() { + let Some(source_key_type) = key.to_index_type() else { + continue; + }; + relate_entry(relater, &source_key_type, source_value_type)?; + } + for (source_key_type, source_value_type) in source_object.get_index_access() { + relate_entry(relater, source_key_type, source_value_type)?; + } + } + LuaType::TableConst(range) => { + let owner = LuaMemberOwner::Element(range.clone()); + let db = relater.db(); + visit_member_items(db, &owner, |key, item| { + let Some(source_key_type) = key.to_index_type() else { + return Ok(()); + }; + let source_value_type = item.resolve_type(db).unwrap_or(LuaType::Any); + relate_entry(relater, &source_key_type, &source_value_type) + })?; + } + LuaType::Tuple(source_tuple) => { + for (index, source_value_type) in source_tuple.get_types().iter().enumerate() { + relate_entry( + relater, + &LuaType::IntegerConst(index as i64 + 1), + source_value_type, + )?; + } + } + LuaType::Array(source_array) => { + relate_entry(relater, &LuaType::Integer, source_array.get_base())?; + } + LuaType::TableGeneric(source_params) if source_params.len() == 2 => { + relate_entry(relater, &source_params[0], &source_params[1])?; + } + LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_) => { + visit_declared_members(relater, source, |relater, key, source_value_type| { + let Some(source_key_type) = key.to_index_type() else { + return Ok(()); + }; + relate_entry(relater, &source_key_type, source_value_type) + })?; + } + LuaType::Intersection(intersection) => { + for member in intersection.get_types() { + relate_index_obligation( + relater, + member, + target, + target_key_type, + target_value_type, + context, + )?; + } + } + _ => {} + } + + Ok(()) +} + +pub(in crate::semantic::type_check) fn relate_target_intersection_side_checks( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + intersection: &crate::LuaIntersectionType, +) -> RelationResult { + for member in intersection.get_types() { + match member { + LuaType::Object(object) => { + for (key_type, value_type) in object.get_index_access() { + relate_index_obligation( + relater, + source, + target, + key_type, + value_type, + IntersectionState::NONE, + )?; + } + } + LuaType::TableConst(range) => { + let owner = LuaMemberOwner::Element(range.clone()); + let db = relater.db(); + visit_member_items(db, &owner, |key, item| { + let LuaMemberKey::TypeKey(key_type) = key else { + return Ok(()); + }; + let value_type = item.resolve_type(db).unwrap_or(LuaType::Any); + relate_index_obligation( + relater, + source, + target, + key_type, + &value_type, + IntersectionState::NONE, + ) + })?; + } + LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_) => { + visit_declared_members(relater, member, |relater, key, value_type| { + let LuaMemberKey::TypeKey(key_type) = key else { + return Ok(()); + }; + relate_index_obligation( + relater, + source, + target, + key_type, + value_type, + IntersectionState::NONE, + ) + })?; + } + LuaType::Intersection(nested) => { + relate_target_intersection_side_checks(relater, source, target, nested)?; + } + _ => {} + } + } + Ok(()) +} + +pub(super) fn visit_declared_members( + relater: &mut Relater, + declared_type: &LuaType, + mut visitor: impl FnMut(&mut Relater, &LuaMemberKey, &LuaType) -> RelationResult, +) -> RelationResult { + let mut seen_keys = HashSet::new(); + let mut visited_types = HashSet::new(); + visit_declared_type_members( + relater, + declared_type, + &mut seen_keys, + &mut visited_types, + &mut visitor, + ) +} + +fn visit_declared_type_members( + relater: &mut Relater, + declared_type: &LuaType, + seen_keys: &mut HashSet, + visited_types: &mut HashSet, + visitor: &mut impl FnMut(&mut Relater, &LuaMemberKey, &LuaType) -> RelationResult, +) -> RelationResult { + let (type_id, substitutor) = match declared_type { + LuaType::Ref(type_id) | LuaType::Def(type_id) => (type_id.clone(), None), + LuaType::Generic(generic) => ( + generic.get_base_type_id(), + Some(TypeSubstitutor::from_type_array( + generic.get_params().clone(), + )), + ), + _ => return Ok(()), + }; + if !visited_types.insert(type_id.clone()) { + return Ok(()); + } + + let db = relater.db(); + let owner = LuaMemberOwner::Type(type_id.clone()); + let has_supers = db + .get_type_index() + .get_super_types_iter(&type_id) + .is_some_and(|mut supers| supers.next().is_some()); + + if !has_supers && seen_keys.is_empty() { + visit_member_items(db, &owner, |key, item| { + let Ok(mut member_type) = item.resolve_type(db) else { + return Ok(()); + }; + if let Some(substitutor) = &substitutor { + member_type = instantiate_type_generic(db, &member_type, substitutor); + } + visitor(relater, key, &member_type) + })?; + return Ok(()); + } + + visit_member_items(db, &owner, |key, item| { + if !seen_keys.insert(key.clone()) { + return Ok(()); + } + let Ok(mut member_type) = item.resolve_type(db) else { + return Ok(()); + }; + if let Some(substitutor) = &substitutor { + member_type = instantiate_type_generic(db, &member_type, substitutor); + } + visitor(relater, key, &member_type) + })?; + + if let Some(super_types) = db.get_type_index().get_super_types_iter(&type_id) { + for super_type in super_types { + let super_type = substitutor + .as_ref() + .map(|substitutor| instantiate_type_generic(db, super_type, substitutor)) + .unwrap_or_else(|| super_type.clone()); + visit_declared_type_members(relater, &super_type, seen_keys, visited_types, visitor)?; + } + } + + Ok(()) +} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/structural/mod.rs b/crates/emmylua_code_analysis/src/semantic/type_check/structural/mod.rs new file mode 100644 index 000000000..49be5d077 --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/structural/mod.rs @@ -0,0 +1,49 @@ +mod declared; +mod generic; +mod members; +mod sequence; + +use crate::LuaType; + +use super::relation::{IntersectionState, Relater, RelationResult}; +pub(super) use members::{ + relate_object_members, relate_target_intersection_side_checks, relate_to_declared_target, +}; + +pub(super) fn relate_structural( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + context: IntersectionState, +) -> Option { + if matches!(source, LuaType::Table) + && matches!( + target, + LuaType::Object(_) + | LuaType::Tuple(_) + | LuaType::Array(_) + | LuaType::TableGeneric(_) + | LuaType::TableConst(_) + | LuaType::Ref(_) + | LuaType::Def(_) + | LuaType::Generic(_) + ) + { + relater.note_progress(); + return Some(Ok(())); + } + + if let Some(result) = declared::relate_declared(relater, source, target, context) { + return Some(result); + } + + if let Some(result) = sequence::relate_sequence(relater, source, target, context) { + return Some(result); + } + + if let Some(result) = generic::relate_generic(relater, source, target, context) { + return Some(result); + } + + members::relate_members(relater, source, target, context) +} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/structural/sequence.rs b/crates/emmylua_code_analysis/src/semantic/type_check/structural/sequence.rs new file mode 100644 index 000000000..5d458352e --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/structural/sequence.rs @@ -0,0 +1,415 @@ +use crate::{LuaMemberKey, LuaMemberOwner, LuaTupleType, LuaType, TypeOps, VariadicType}; + +use super::super::{ + mismatch::{TypeMismatch, TypeMismatchKind, TypePathSegment}, + relation::{IntersectionState, Relater, RelationResult}, +}; + +pub(super) fn relate_sequence( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + context: IntersectionState, +) -> Option { + match (source, target) { + (LuaType::Tuple(source_tuple), LuaType::Tuple(target_tuple)) => Some( + relate_tuple_to_tuple(relater, source, target, source_tuple, target_tuple, context), + ), + (LuaType::Array(source_array), LuaType::Array(target_array)) => { + let target_base = effective_array_base(relater, target_array.get_base()); + Some( + relater + .relate(source_array.get_base(), &target_base, context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::ArrayElement, source, target) + }) + }), + ) + } + (LuaType::Tuple(source_tuple), LuaType::Array(target_array)) => { + let target_base = effective_array_base(relater, target_array.get_base()); + Some(relate_tuple_to_array( + relater, + source, + target, + source_tuple, + &target_base, + context, + )) + } + (LuaType::Array(source_array), LuaType::Tuple(target_tuple)) => { + Some(relate_array_to_tuple( + relater, + source, + target, + source_array.get_base(), + target_tuple, + context, + )) + } + (LuaType::TableConst(range), LuaType::Tuple(target_tuple)) => Some(relate_table_to_tuple( + relater, + source, + target, + range, + target_tuple, + context, + )), + (LuaType::TableConst(range), LuaType::Array(target_array)) => Some(relate_table_to_array( + relater, + source, + target, + range, + &effective_array_base(relater, target_array.get_base()), + context, + )), + (LuaType::Object(source_object), LuaType::Tuple(target_tuple)) => { + for (index, target_type) in target_tuple.get_types().iter().enumerate() { + if let Err(failure) = relater.consume_relation_budget(source, target) { + return Some(Err(failure)); + } + let key = LuaMemberKey::Integer(index as i64 + 1); + let Some(source_type) = source_object.get_field(&key) else { + if target_type.is_optional() { + continue; + } + return Some(relater.unrelated(|| { + TypeMismatch::new( + source, + target, + TypeMismatchKind::MissingTupleElement { index }, + ) + })); + }; + if let Err(failure) = relater.relate(source_type, target_type, context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::TupleElement(index), source, target) + }))); + } + relater.note_progress(); + } + Some(Ok(())) + } + (LuaType::Object(source_object), LuaType::Array(target_array)) => { + let target_base = effective_array_base(relater, target_array.get_base()); + let mut checked = false; + for (key, source_type) in source_object.get_fields() { + if !matches!(key, LuaMemberKey::Integer(index) if *index > 0) { + continue; + } + if let Err(failure) = relater.consume_relation_budget(source, target) { + return Some(Err(failure)); + } + if let Err(failure) = relater.relate(source_type, &target_base, context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::Member(key.clone()), source, target) + }))); + } + checked = true; + relater.note_progress(); + } + for (source_key, source_type) in source_object.get_index_access() { + if !source_key.is_integer() { + continue; + } + if let Err(failure) = relater.consume_relation_budget(source, target) { + return Some(Err(failure)); + } + if let Err(failure) = relater.relate(source_type, &target_base, context) { + return Some(Err(failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::Index(source_key.clone()), source, target) + }))); + } + checked = true; + relater.note_progress(); + } + checked + .then_some(Ok(())) + .or_else(|| Some(relater.unrelated(|| TypeMismatch::incompatible(source, target)))) + } + ( + LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_) | LuaType::Intersection(_), + LuaType::Tuple(target_tuple), + ) => Some(relate_keyed_type_to_tuple( + relater, + source, + target, + target_tuple, + context, + )), + (LuaType::Ref(_) | LuaType::Def(_) | LuaType::Generic(_), LuaType::Array(target_array)) => { + let target_base = effective_array_base(relater, target_array.get_base()); + Some(relate_keyed_type_to_array( + relater, + source, + target, + &target_base, + context, + )) + } + _ => None, + } +} + +fn effective_array_base(relater: &Relater, base: &LuaType) -> LuaType { + if relater.db().get_emmyrc().strict.array_index { + TypeOps::Union.apply(relater.db(), base, &LuaType::Nil) + } else { + base.clone() + } +} + +fn relate_tuple_to_tuple( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + source_tuple: &LuaTupleType, + target_tuple: &LuaTupleType, + context: IntersectionState, +) -> RelationResult { + for (index, target_type) in target_tuple.get_types().iter().enumerate() { + relater.consume_relation_budget(source, target)?; + let Some(source_type) = source_tuple.get_type(index).map(|source_type| { + if let LuaType::Variadic(variadic) = source_type { + variadic.get_type(0).unwrap_or(source_type) + } else { + source_type + } + }) else { + if target_type.is_optional() { + continue; + } + return relater.unrelated(|| { + TypeMismatch::new( + source, + target, + TypeMismatchKind::MissingTupleElement { index }, + ) + }); + }; + + relater + .relate(source_type, target_type, context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::TupleElement(index), source, target) + }) + })?; + relater.note_progress(); + } + + Ok(()) +} + +fn relate_tuple_to_array( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + source_tuple: &LuaTupleType, + target_base: &LuaType, + context: IntersectionState, +) -> RelationResult { + for (index, source_type) in source_tuple.get_types().iter().enumerate() { + relater.consume_relation_budget(source, target)?; + let source_type = match source_type { + LuaType::Variadic(variadic) => match variadic.as_ref() { + VariadicType::Base(base) => base, + VariadicType::Multi(types) => { + for (offset, source_type) in types.iter().enumerate() { + relater + .relate(source_type, target_base, context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at( + TypePathSegment::TupleElement(index + offset), + source, + target, + ) + }) + })?; + relater.note_progress(); + } + continue; + } + }, + source_type => source_type, + }; + relater + .relate(source_type, target_base, context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::TupleElement(index), source, target) + }) + })?; + relater.note_progress(); + } + Ok(()) +} + +fn relate_array_to_tuple( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + source_base: &LuaType, + target_tuple: &LuaTupleType, + context: IntersectionState, +) -> RelationResult { + for (index, target_type) in target_tuple.get_types().iter().enumerate() { + relater.consume_relation_budget(source, target)?; + relater + .relate(source_base, target_type, context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::TupleElement(index), source, target) + }) + })?; + relater.note_progress(); + } + Ok(()) +} + +fn relate_table_to_tuple( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + range: &crate::InFiled, + target_tuple: &LuaTupleType, + context: IntersectionState, +) -> RelationResult { + let owner = LuaMemberOwner::Element(range.clone()); + for (index, target_type) in target_tuple.get_types().iter().enumerate() { + relater.consume_relation_budget(source, target)?; + let key = LuaMemberKey::Integer(index as i64 + 1); + let source_type = relater + .db() + .get_member_index() + .get_member_item(&owner, &key) + .map(|item| item.resolve_type(relater.db()).unwrap_or(LuaType::Any)); + let Some(source_type) = source_type else { + if target_type.is_optional() { + continue; + } + return relater.unrelated(|| { + TypeMismatch::new( + source, + target, + TypeMismatchKind::MissingTupleElement { index }, + ) + }); + }; + relater + .relate(&source_type, target_type, context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::TupleElement(index), source, target) + }) + })?; + relater.note_progress(); + } + Ok(()) +} + +fn relate_table_to_array( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + range: &crate::InFiled, + target_base: &LuaType, + context: IntersectionState, +) -> RelationResult { + let owner = LuaMemberOwner::Element(range.clone()); + let member_len = relater.db().get_member_index().get_member_len(&owner); + if member_len > relater.remaining_relation_budget() { + return Err(relater.budget_failure(source, target)); + } + + for index in 0..member_len { + relater.consume_relation_budget(source, target)?; + let key = LuaMemberKey::Integer(index as i64 + 1); + let Some(source_type) = relater + .db() + .get_member_index() + .get_member_item(&owner, &key) + .map(|item| item.resolve_type(relater.db()).unwrap_or(LuaType::Any)) + else { + return relater.unrelated(|| TypeMismatch::incompatible(source, target)); + }; + relater + .relate(&source_type, target_base, context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::TupleElement(index), source, target) + }) + })?; + relater.note_progress(); + } + Ok(()) +} + +fn relate_keyed_type_to_tuple( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + target_tuple: &LuaTupleType, + context: IntersectionState, +) -> RelationResult { + for (index, target_type) in target_tuple.get_types().iter().enumerate() { + relater.consume_relation_budget(source, target)?; + let key = LuaMemberKey::Integer(index as i64 + 1); + let source_type = + crate::semantic::member::find_members_with_key(relater.db(), source, key, false) + .and_then(|members| members.into_iter().next()) + .map(|member| member.typ); + let Some(source_type) = source_type else { + if target_type.is_optional() { + continue; + } + return relater.unrelated(|| { + TypeMismatch::new( + source, + target, + TypeMismatchKind::MissingTupleElement { index }, + ) + }); + }; + relater + .relate(&source_type, target_type, context) + .map_err(|failure| { + failure.map_mismatch(|mismatch| { + mismatch.at(TypePathSegment::TupleElement(index), source, target) + }) + })?; + relater.note_progress(); + } + Ok(()) +} + +fn relate_keyed_type_to_array( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + target_base: &LuaType, + context: IntersectionState, +) -> RelationResult { + relater.consume_relation_budget(source, target)?; + let source_type = crate::semantic::member::find_members_with_key( + relater.db(), + source, + LuaMemberKey::TypeKey(LuaType::Integer), + false, + ) + .and_then(|members| members.into_iter().next()) + .map(|member| member.typ); + let Some(source_type) = source_type else { + return relater.unrelated(|| TypeMismatch::incompatible(source, target)); + }; + relater + .relate(&source_type, target_base, context) + .map_err(|failure| { + failure + .map_mismatch(|mismatch| mismatch.at(TypePathSegment::ArrayElement, source, target)) + })?; + relater.note_progress(); + Ok(()) +} 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 fb24c3244..1a44bb4fa 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,14 @@ #[cfg(test)] mod test { - use crate::{DiagnosticCode, LuaType, VirtualWorkspace}; + use crate::{ + DiagnosticCode, FileId, LuaArrayType, LuaMember, LuaMemberFeature, LuaMemberId, + LuaMemberKey, LuaMemberOwner, LuaType, LuaTypeDeclId, VirtualWorkspace, + semantic::type_check::{ + RelationKind, RelationOutcome, TypeMismatchKind, TypePathSegment, check_assignable, + fast_eq_check, is_assignable_ex, render_type_mismatch, + }, + }; + use emmylua_parser::{LuaAstNode, LuaLocalStat, LuaSyntaxId}; #[test] fn test_string() { @@ -9,16 +17,460 @@ mod test { let string_ty = ws.ty("string"); let right_ty = ws.ty("'ssss'"); - assert!(ws.check_type(&string_ty, &right_ty)); + assert!(ws.check_type(&right_ty, &string_ty)); let right_ty = ws.ty("number"); - assert!(!ws.check_type(&string_ty, &right_ty)); + assert!(!ws.check_type(&right_ty, &string_ty)); let right_ty = ws.ty("string | number"); - assert!(!ws.check_type(&string_ty, &right_ty)); + assert!(!ws.check_type(&right_ty, &string_ty)); let right_ty = ws.ty("'a' | 'b' | 'c'"); - assert!(ws.check_type(&string_ty, &right_ty)); + assert!(ws.check_type(&right_ty, &string_ty)); + } + + #[test] + fn test_assignable_diagnostic_preserves_member_path_and_range() { + let mut ws = VirtualWorkspace::new(); + let file_id = ws.def("local value = { nested = { port = 'oops' } }"); + let source = ws.expr_ty("{ nested = { port = 'oops' } }"); + let target = ws.ty("{ nested: { port: number } }"); + let stat = ws.get_node::(file_id); + let source_expr = stat.get_value_exprs().next().expect("initializer"); + let mismatch = check_assignable(ws.analysis.compilation.get_db(), &source, &target) + .expect_err("nested literal should fail"); + assert!( + mismatch + .frames() + .iter() + .any(|f| matches!(f, TypePathSegment::Member(_))) + ); + assert!(matches!( + mismatch.leaf(), + TypeMismatchKind::Incompatible { .. } + )); + assert!(mismatch.locate_in(&source_expr).len() < source_expr.syntax().text_range().len()); + assert!( + render_type_mismatch(ws.analysis.compilation.get_db(), &mismatch) + .contains("not assignable") + ); + } + + #[test] + fn test_union_failure_keeps_single_candidate_and_source_member() { + let mut ws = VirtualWorkspace::new(); + let file_id = ws.def("local value = { value = 'oops' }"); + let source = ws.expr_ty("{ value = 'oops' }"); + let target = ws.ty("{ value: number } | { value: boolean }"); + let stat = ws.get_node::(file_id); + let source_expr = stat.get_value_exprs().next().expect("initializer"); + let mismatch = check_assignable(ws.analysis.compilation.get_db(), &source, &target) + .expect_err("all target union candidates must fail"); + assert_eq!( + mismatch + .frames() + .iter() + .filter(|f| matches!(f, TypePathSegment::TargetUnionCandidate(_))) + .count(), + 1 + ); + assert!(matches!( + mismatch.leaf(), + TypeMismatchKind::Incompatible { .. } + )); + assert!(mismatch.locate_in(&source_expr).len() < source_expr.syntax().text_range().len()); + + let source = ws.ty("string | number"); + let target = ws.ty("boolean"); + let mismatch = check_assignable(ws.analysis.compilation.get_db(), &source, &target) + .expect_err("source union must retain a failing member"); + assert!( + mismatch + .frames() + .iter() + .any(|f| matches!(f, TypePathSegment::SourceUnionMember(_))) + ); + } + + #[test] + fn test_function_parameter_and_return_frames() { + let mut ws = VirtualWorkspace::new(); + let source = ws.ty("fun(x: number): string"); + let target = ws.ty("fun(x: string): string"); + let mismatch = check_assignable(ws.analysis.compilation.get_db(), &source, &target) + .expect_err("parameter variance mismatch"); + assert!( + mismatch + .frames() + .iter() + .any(|f| matches!(f, TypePathSegment::FunctionParameter(_))) + ); + assert!(ws.check_type(&source, &source)); + + let source = ws.ty("fun(): number"); + let target = ws.ty("fun(): string"); + let mismatch = check_assignable(ws.analysis.compilation.get_db(), &source, &target) + .expect_err("return mismatch"); + assert!( + mismatch + .frames() + .iter() + .any(|f| matches!(f, TypePathSegment::FunctionReturn(_))) + ); + } + + #[test] + fn test_inline_generic_functions_still_check_concrete_parameters() { + let mut ws = VirtualWorkspace::new(); + let source = ws.ty("fun(value: T, tag: number): T"); + let target = ws.ty("fun(value: U, tag: string): U"); + + assert!(!ws.check_type(&source, &target)); + } + + #[test] + fn test_generic_class_call_operator_uses_instantiated_parameter_and_return_types() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class GenericCallable + ---@operator call(T): T + "#, + ); + let callable = ws.ty("GenericCallable"); + let compatible = ws.ty("fun(value: string): string"); + let incompatible_parameter = ws.ty("fun(value: number): string"); + let incompatible_return = ws.ty("fun(value: string): number"); + let db = ws.analysis.compilation.get_db(); + + assert!(ws.check_type(&callable, &LuaType::Function)); + assert!(ws.check_type(&callable, &compatible)); + assert!(!ws.check_type(&callable, &incompatible_parameter)); + assert!(!ws.check_type(&callable, &incompatible_return)); + assert!(ws.check_type(&compatible, &callable)); + assert!(!ws.check_type(&incompatible_parameter, &callable)); + assert!(!ws.check_type(&incompatible_return, &callable)); + + let parameter_mismatch = check_assignable(db, &callable, &incompatible_parameter) + .expect_err("instantiated parameter type must remain incompatible"); + assert!( + parameter_mismatch + .frames() + .iter() + .any(|frame| matches!(frame, TypePathSegment::FunctionParameter(0))) + ); + + let return_mismatch = check_assignable(db, &callable, &incompatible_return) + .expect_err("instantiated return type must remain incompatible"); + assert!( + return_mismatch + .frames() + .iter() + .any(|frame| matches!(frame, TypePathSegment::FunctionReturn(0))) + ); + } + + #[test] + fn test_unresolved_table_sequence_members_are_any_not_missing() { + let mut ws = VirtualWorkspace::new(); + let file_id = ws.def("local value = { [1] = 1 }"); + let stat = ws.get_node::(file_id); + let source_node = stat.get_value_exprs().next().expect("table initializer"); + let source = ws + .analysis + .compilation + .get_semantic_model(file_id) + .expect("semantic model") + .infer_expr(source_node.clone()) + .expect("table type"); + let LuaType::TableConst(range) = &source else { + panic!("table literal type expected"); + }; + let unresolved_id = LuaMemberId::new(LuaSyntaxId::from_node(source_node.syntax()), file_id); + ws.get_db_mut().get_member_index_mut().add_member( + LuaMemberOwner::Element(range.clone()), + LuaMember::new( + unresolved_id, + LuaMemberKey::Integer(2), + LuaMemberFeature::FileDefine, + None, + ), + ); + + let tuple_target = ws.ty("[integer, string]"); + let array_target = ws.ty("number[]"); + let db = ws.analysis.compilation.get_db(); + assert!(ws.check_type(&source, &tuple_target)); + assert!(check_assignable(db, &source, &tuple_target).is_ok()); + assert!(ws.check_type(&source, &array_target)); + } + + #[test] + fn test_unresolved_table_generic_member_still_checks_key() { + let mut ws = VirtualWorkspace::new(); + let file_id = ws.def("local value = { [1] = 1 }"); + let stat = ws.get_node::(file_id); + let source_node = stat.get_value_exprs().next().expect("table initializer"); + let source = ws + .analysis + .compilation + .get_semantic_model(file_id) + .expect("semantic model") + .infer_expr(source_node.clone()) + .expect("table type"); + let LuaType::TableConst(range) = &source else { + panic!("table literal type expected"); + }; + let unresolved_id = LuaMemberId::new(LuaSyntaxId::from_node(source_node.syntax()), file_id); + ws.get_db_mut().get_member_index_mut().add_member( + LuaMemberOwner::Element(range.clone()), + LuaMember::new( + unresolved_id, + LuaMemberKey::Name("bad".into()), + LuaMemberFeature::FileDefine, + None, + ), + ); + + let target = ws.ty("table"); + let db = ws.analysis.compilation.get_db(); + assert!(!ws.check_type(&source, &target)); + let mismatch = check_assignable(db, &source, &target) + .expect_err("unresolved value must not skip its key constraint"); + assert!( + mismatch + .frames() + .iter() + .any(|frame| matches!(frame, TypePathSegment::Index(LuaType::StringConst(_)))) + ); + } + + #[test] + fn test_generic_conditional_literal_relation_is_asymmetric() { + let mut ws = VirtualWorkspace::new(); + let number = ws.ty("number"); + let literal = ws.ty("1"); + assert_eq!( + is_assignable_ex( + ws.analysis.compilation.get_db(), + &literal, + &number, + RelationKind::ConditionalExtends, + ), + RelationOutcome::Related + ); + assert_eq!( + is_assignable_ex( + ws.analysis.compilation.get_db(), + &number, + &literal, + RelationKind::ConditionalExtends, + ), + RelationOutcome::Unrelated + ); + } + + #[test] + fn test_indeterminate_is_conservative_only_for_plain_assignability() { + let db = crate::DbIndex::new(); + let mut source = LuaType::String; + let mut target = LuaType::Number; + for _ in 0..101 { + source = LuaType::Array(LuaArrayType::from_base_type(source).into()); + target = LuaType::Array(LuaArrayType::from_base_type(target).into()); + } + + assert!(crate::semantic::type_check::is_assignable( + &db, &source, &target + )); + assert!(matches!( + is_assignable_ex(&db, &source, &target, RelationKind::ConditionalExtends), + RelationOutcome::Indeterminate(_) + )); + let mismatch = check_assignable(&db, &source, &target) + .expect_err("diagnostic relation must retain recursion overflow"); + assert!(matches!(mismatch.leaf(), TypeMismatchKind::Overflow(_))); + } + + #[test] + fn test_unresolved_target_module_ref_keeps_legacy_fallback() { + let db = crate::DbIndex::new(); + let unresolved_module = LuaType::ModuleRef(FileId::new(1)); + + assert!(crate::semantic::type_check::is_assignable( + &db, + &LuaType::String, + &unresolved_module + )); + assert!(crate::semantic::type_check::is_assignable( + &db, + &LuaType::Never, + &unresolved_module + )); + assert_eq!( + is_assignable_ex( + &db, + &LuaType::Unknown, + &unresolved_module, + RelationKind::ConditionalExtends, + ), + RelationOutcome::Related + ); + assert!(!crate::semantic::type_check::is_assignable( + &db, + &unresolved_module, + &LuaType::String + )); + } + + #[test] + fn test_nullable_ref_keeps_legacy_fast_path() { + let mut ws = VirtualWorkspace::new(); + ws.def("---@class LegacyFastPathRef"); + let source = ws.ty("LegacyFastPathRef?"); + let target = ws.ty("LegacyFastPathRef"); + let db = ws.analysis.compilation.get_db(); + + assert!(matches!(&source, LuaType::Union(_))); + assert!(matches!(&target, LuaType::Ref(_))); + assert!(crate::semantic::type_check::is_assignable( + db, &source, &target + )); + assert!(check_assignable(db, &source, &target).is_ok()); + assert_eq!( + is_assignable_ex(db, &source, &target, RelationKind::ConditionalExtends), + RelationOutcome::Related + ); + + let source_array = LuaType::Array(LuaArrayType::from_base_type(source).into()); + let target_array = LuaType::Array(LuaArrayType::from_base_type(target).into()); + assert!(crate::semantic::type_check::is_assignable( + db, + &source_array, + &target_array + )); + assert!(check_assignable(db, &source_array, &target_array).is_ok()); + } + + #[test] + fn test_ref_and_def_with_same_id_use_fast_path() { + let type_id = LuaTypeDeclId::global("FastNominal"); + let other_id = LuaTypeDeclId::global("OtherNominal"); + let variants = [LuaType::Ref(type_id.clone()), LuaType::Def(type_id)]; + + for source in &variants { + for target in &variants { + assert!(fast_eq_check(source, target)); + } + } + assert!(!fast_eq_check(&variants[0], &LuaType::Def(other_id))); + } + + #[test] + fn test_ref_and_def_preserve_declared_base_relations() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class TextId: string + ---@class PlainDeclared + "#, + ); + let text_id = match ws.ty("TextId") { + LuaType::Ref(type_id) => type_id, + typ => panic!("expected TextId ref, got {typ:?}"), + }; + let plain_id = match ws.ty("PlainDeclared") { + LuaType::Ref(type_id) => type_id, + typ => panic!("expected PlainDeclared ref, got {typ:?}"), + }; + let db = ws.analysis.compilation.get_db(); + + for source in [LuaType::Ref(text_id.clone()), LuaType::Def(text_id)] { + assert!(ws.check_type(&source, &LuaType::String)); + assert!(!ws.check_type(&source, &LuaType::Integer)); + assert!(check_assignable(db, &source, &LuaType::String).is_ok()); + assert!(check_assignable(db, &source, &LuaType::Integer).is_err()); + } + for source in [LuaType::Ref(plain_id.clone()), LuaType::Def(plain_id)] { + assert!(ws.check_type(&source, &LuaType::Table)); + assert!(ws.check_type(&source, &LuaType::Userdata)); + assert!(check_assignable(db, &source, &LuaType::Table).is_ok()); + assert!(check_assignable(db, &source, &LuaType::Userdata).is_ok()); + } + } + + #[test] + fn test_complex_fast_path_uses_arc_identity_only() { + let source = LuaType::Array(LuaArrayType::from_base_type(LuaType::String).into()); + let shared = source.clone(); + let structurally_equal = + LuaType::Array(LuaArrayType::from_base_type(LuaType::String).into()); + + assert!(fast_eq_check(&source, &shared)); + assert!(!fast_eq_check(&source, &structurally_equal)); + assert!(crate::semantic::type_check::is_assignable( + &crate::DbIndex::new(), + &source, + &structurally_equal + )); + } + + #[test] + fn test_locator_uses_source_expr_as_fallback() { + let mut ws = VirtualWorkspace::new(); + let source = ws.ty("string"); + let target = ws.ty("number"); + let mismatch = check_assignable(ws.analysis.compilation.get_db(), &source, &target) + .expect_err("simple target mismatch"); + + let file_id = ws.def("local value = 'oops'"); + let stat = ws.get_node::(file_id); + let source_expr = stat.get_value_exprs().next().expect("initializer"); + assert_eq!( + mismatch.locate_in(&source_expr), + source_expr.syntax().text_range() + ); + } + + #[test] + fn test_locator_follows_tuple_and_array_elements() { + let mut ws = VirtualWorkspace::new(); + let file_id = ws.def("local value = { 'oops' }"); + let source = ws.expr_ty("{ 'oops' }"); + let tuple_target = ws.ty("[number]"); + let array_source = ws.ty("string[]"); + let array_target = ws.ty("number[]"); + let stat = ws.get_node::(file_id); + let source_expr = stat.get_value_exprs().next().expect("initializer"); + + let tuple_mismatch = + check_assignable(ws.analysis.compilation.get_db(), &source, &tuple_target) + .expect_err("tuple element should fail"); + assert!( + tuple_mismatch + .frames() + .iter() + .any(|frame| matches!(frame, TypePathSegment::TupleElement(0))) + ); + assert!( + tuple_mismatch.locate_in(&source_expr).len() < source_expr.syntax().text_range().len() + ); + + let array_mismatch = check_assignable( + ws.analysis.compilation.get_db(), + &array_source, + &array_target, + ) + .expect_err("array element should fail"); + assert!( + array_mismatch + .frames() + .iter() + .any(|frame| matches!(frame, TypePathSegment::ArrayElement)) + ); + assert!( + array_mismatch.locate_in(&source_expr).len() < source_expr.syntax().text_range().len() + ); } #[test] @@ -29,16 +481,16 @@ mod test { let integer_ty = ws.ty("integer"); let number_expr1 = ws.expr_ty("1"); - assert!(ws.check_type(&number_ty, &number_expr1)); + assert!(ws.check_type(&number_expr1, &number_ty)); let number_expr2 = ws.expr_ty("1.5"); - assert!(ws.check_type(&number_ty, &number_expr2)); + assert!(ws.check_type(&number_expr2, &number_ty)); - assert!(ws.check_type(&number_ty, &integer_ty)); - assert!(!ws.check_type(&integer_ty, &number_ty)); + assert!(ws.check_type(&integer_ty, &number_ty)); + assert!(!ws.check_type(&number_ty, &integer_ty)); let number_union = ws.ty("1 | 2 | 3"); - assert!(ws.check_type(&number_ty, &number_union)); - assert!(ws.check_type(&integer_ty, &number_union)); + assert!(ws.check_type(&number_union, &number_ty)); + assert!(ws.check_type(&number_union, &integer_ty)); } #[test] @@ -50,22 +502,22 @@ mod test { let ty_string = ws.ty("string"); let ty_boolean = ws.ty("boolean"); - assert!(ws.check_type(&ty_union, &ty_number)); - assert!(ws.check_type(&ty_union, &ty_string)); - assert!(!ws.check_type(&ty_union, &ty_boolean)); + assert!(ws.check_type(&ty_number, &ty_union)); + assert!(ws.check_type(&ty_string, &ty_union)); + assert!(!ws.check_type(&ty_boolean, &ty_union)); assert!(ws.check_type(&ty_union, &ty_union)); let ty_union2 = ws.ty("number | string | boolean"); - assert!(ws.check_type(&ty_union2, &ty_number)); - assert!(ws.check_type(&ty_union2, &ty_string)); - assert!(ws.check_type(&ty_union2, &ty_union)); + assert!(ws.check_type(&ty_number, &ty_union2)); + assert!(ws.check_type(&ty_string, &ty_union2)); + assert!(ws.check_type(&ty_union, &ty_union2)); assert!(ws.check_type(&ty_union2, &ty_union2)); let ty_union3 = ws.ty("1 | 2 | 3"); let ty_union4 = ws.ty("1 | 2"); - assert!(ws.check_type(&ty_union3, &ty_union4)); - assert!(!ws.check_type(&ty_union4, &ty_union3)); + assert!(ws.check_type(&ty_union4, &ty_union3)); + assert!(!ws.check_type(&ty_union3, &ty_union4)); assert!(ws.check_type(&ty_union3, &ty_union3)); } @@ -81,9 +533,12 @@ mod test { let recursive_ty = ws.ty("Recursive"); let expanded_ty = ws.ty("string | Recursive[]"); let invalid_ty = ws.ty("boolean | Recursive[]"); + let db = ws.analysis.compilation.get_db(); - assert!(ws.check_type(&recursive_ty, &expanded_ty)); - assert!(!ws.check_type(&recursive_ty, &invalid_ty)); + assert!(ws.check_type(&expanded_ty, &recursive_ty)); + assert!(!ws.check_type(&invalid_ty, &recursive_ty)); + assert!(check_assignable(db, &expanded_ty, &recursive_ty).is_ok()); + assert!(check_assignable(db, &invalid_ty, &recursive_ty).is_err()); } #[test] @@ -98,9 +553,33 @@ mod test { let recursive_ty = ws.ty("Recursive"); let expanded_ty = ws.ty("string | Recursive[]"); let invalid_ty = ws.ty("boolean | Recursive[]"); + let db = ws.analysis.compilation.get_db(); - assert!(ws.check_type(&recursive_ty, &expanded_ty)); - assert!(!ws.check_type(&recursive_ty, &invalid_ty)); + assert!(ws.check_type(&expanded_ty, &recursive_ty)); + assert!(!ws.check_type(&invalid_ty, &recursive_ty)); + assert!(check_assignable(db, &expanded_ty, &recursive_ty).is_ok()); + assert!(check_assignable(db, &invalid_ty, &recursive_ty).is_err()); + } + + #[test] + fn test_mutually_recursive_generic_aliases_close_by_active_relation() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@alias RecursiveA { value: T, next: RecursiveA } + ---@alias RecursiveB { value: T, next: RecursiveB } + "#, + ); + + let source = ws.ty("RecursiveA"); + let compatible = ws.ty("RecursiveB"); + let incompatible = ws.ty("RecursiveB"); + let db = ws.analysis.compilation.get_db(); + + assert!(ws.check_type(&source, &compatible)); + assert!(!ws.check_type(&source, &incompatible)); + assert!(check_assignable(db, &source, &compatible).is_ok()); + assert!(check_assignable(db, &source, &incompatible).is_err()); } #[test] @@ -115,10 +594,10 @@ mod test { let matched_table_ty = ws.expr_ty("{ x = 1, y = 'test' }"); let mismatch_table_ty = ws.expr_ty("{ x = 2, y = 3 }"); - assert!(ws.check_type(&object_ty, &matched_object_ty2)); - assert!(!ws.check_type(&object_ty, &mismatch_object_ty2)); - assert!(ws.check_type(&object_ty, &matched_table_ty)); - assert!(!ws.check_type(&object_ty, &mismatch_table_ty)); + assert!(ws.check_type(&matched_object_ty2, &object_ty)); + assert!(!ws.check_type(&mismatch_object_ty2, &object_ty)); + assert!(ws.check_type(&matched_table_ty, &object_ty)); + assert!(!ws.check_type(&mismatch_table_ty, &object_ty)); } // case for tuple, object, and table @@ -127,13 +606,13 @@ mod test { let matched_tulple_ty = ws.ty("[string, number"); let matched_object_ty = ws.ty("{ [1]: 'test', [2]: 1 }"); - assert!(ws.check_type(&object_ty, &matched_tulple_ty)); - assert!(ws.check_type(&object_ty, &matched_object_ty)); + assert!(ws.check_type(&matched_tulple_ty, &object_ty)); + assert!(ws.check_type(&matched_object_ty, &object_ty)); let mismatch_tulple_ty = ws.ty("[number, string]"); - assert!(!ws.check_type(&object_ty, &mismatch_tulple_ty)); + assert!(!ws.check_type(&mismatch_tulple_ty, &object_ty)); let matched_table_ty = ws.expr_ty("{ [1] = 'test', [2] = 1 }"); - assert!(ws.check_type(&object_ty, &matched_table_ty)); + assert!(ws.check_type(&matched_table_ty, &object_ty)); } // issue #69 @@ -152,12 +631,12 @@ mod test { let matched_tuple_ty = ws.ty("[1, 2, 3]"); let mismatch_array_ty = ws.ty("['a', 'b', 'c']"); - assert!(ws.check_type(&array_ty, &matched_tuple_ty)); - assert!(!ws.check_type(&array_ty, &mismatch_array_ty)); + assert!(ws.check_type(&matched_tuple_ty, &array_ty)); + assert!(!ws.check_type(&mismatch_array_ty, &array_ty)); let array_ty2 = ws.ty("integer[]"); - assert!(ws.check_type(&array_ty, &array_ty2)); - assert!(!ws.check_type(&array_ty2, &array_ty)); + assert!(ws.check_type(&array_ty2, &array_ty)); + assert!(!ws.check_type(&array_ty, &array_ty2)); } #[test] @@ -168,12 +647,12 @@ mod test { let matched_tuple_ty = ws.ty("[1, 'test']"); let mismatch_tuple_ty = ws.ty("['a', 1]"); - assert!(ws.check_type(&tuple_ty, &matched_tuple_ty)); - assert!(!ws.check_type(&tuple_ty, &mismatch_tuple_ty)); + assert!(ws.check_type(&matched_tuple_ty, &tuple_ty)); + assert!(!ws.check_type(&mismatch_tuple_ty, &tuple_ty)); let tuple_ty2 = ws.ty("[integer, string]"); - assert!(ws.check_type(&tuple_ty, &tuple_ty2)); - assert!(!ws.check_type(&tuple_ty2, &tuple_ty)); + assert!(ws.check_type(&tuple_ty2, &tuple_ty)); + assert!(!ws.check_type(&tuple_ty, &tuple_ty2)); } #[test] @@ -182,7 +661,7 @@ mod test { let ty = ws.ty("string?"); let ty2 = ws.expr_ty("(\"hello\"):match(\".*\")"); - assert!(ws.check_type(&ty, &ty2)); + assert!(ws.check_type(&ty2, &ty)); } #[test] @@ -251,7 +730,7 @@ mod test { let intersection_ty = ws.ty("integer[] & { n: integer }"); let table_ty = ws.ty("table"); assert!( - ws.check_type(&table_ty, &intersection_ty), + ws.check_type(&intersection_ty, &table_ty), "integer[] & {{ n: integer }} should be a subtype of table" ); @@ -283,7 +762,7 @@ mod test { // Intersection type should be assignable to an array type (non-generic) let array_ty = ws.ty("integer[]"); assert!( - ws.check_type(&array_ty, &intersection_ty), + ws.check_type(&intersection_ty, &array_ty), "integer[] & {{ n: integer }} should be assignable to integer[]" ); @@ -353,4 +832,717 @@ mod test { assert_eq!(ws.expr_ty("AFTER_CAST"), LuaType::Unknown); assert_eq!(ws.expr_ty("table.__sentinel()"), ws.ty("integer")); } + + /// Nested semantic shortcuts must work on the recursive relate path. + /// Top-level fast_eq_check alone is not enough: member/param/return sub-relations + /// call Relater::relate without re-entering is_assignable's entry guard. + #[test] + fn test_nested_semantic_accept_on_recursive_relate() { + let mut ws = VirtualWorkspace::new(); + + // Nested target any: string <: any only via recursive semantic accept. + let source = ws.ty("{ value: string }"); + let target = ws.ty("{ value: any }"); + assert!(ws.check_type(&source, &target)); + + let source = ws.ty("{ nested: { value: integer } }"); + let target = ws.ty("{ nested: { value: any } }"); + assert!(ws.check_type(&source, &target)); + + let source = ws.ty("string[]"); + let target = ws.ty("any[]"); + assert!(ws.check_type(&source, &target)); + + let source = ws.ty("fun(x: string): integer"); + let target = ws.ty("fun(x: any): any"); + assert!(ws.check_type(&source, &target)); + + // Nested target unknown. + let source = ws.ty("{ value: string }"); + let target = ws.ty("{ value: unknown }"); + assert!(ws.check_type(&source, &target)); + + // Source any nested under structure still assigns to concrete target fields + // (source Any is accepted against anything). + let source = ws.ty("{ value: any }"); + let target = ws.ty("{ value: string }"); + assert!(ws.check_type(&source, &target)); + + // Diagnostic path: assigning concrete table into any-field should not error. + assert!(ws.has_no_diagnostic( + DiagnosticCode::AssignTypeMismatch, + r#" + ---@type { value: any } + local sink = { value = "ok" } + "#, + )); + assert!(ws.has_no_diagnostic( + DiagnosticCode::ParamTypeMismatch, + r#" + ---@param opts { flag: any } + local function take(opts) end + take({ flag = true }) + "#, + )); + } + + /// Fixture shared by deep_object correctness / profile / timing probes. + fn deep_object_fixture(depth: usize, success: bool) -> (VirtualWorkspace, LuaType, LuaType) { + assert!(depth >= 1, "depth must be >= 1"); + let mut source_repr = String::from("{ "); + let mut target_repr = String::from("{ "); + for level in 1..=depth { + source_repr.push_str(&format!("l{level}: {{ ")); + target_repr.push_str(&format!("l{level}: {{ ")); + } + if success { + source_repr.push_str("value: 'ok', count: 1"); + target_repr.push_str("value: string, count: integer"); + } else { + source_repr.push_str("value: 'bad', count: 1"); + target_repr.push_str("value: number, count: integer"); + } + for _ in 0..depth { + source_repr.push_str(" }"); + target_repr.push_str(" }"); + } + source_repr.push_str(" }"); + target_repr.push_str(" }"); + + let mut ws = VirtualWorkspace::new(); + let source = ws.ty(&source_repr); + let target = ws.ty(&target_repr); + (ws, source, target) + } + + fn measure_type_check_case( + name: &str, + ws: &VirtualWorkspace, + source: &LuaType, + target: &LuaType, + expected: bool, + ) { + use std::{hint::black_box, time::Instant}; + + const MINIMUM_SAMPLE_NS: u128 = 20_000_000; + const SAMPLE_COUNT: usize = 11; + + for _ in 0..200 { + assert_eq!( + black_box(ws.check_type(black_box(source), black_box(target))), + expected, + "{name} verdict changed" + ); + } + + let calibration_iterations = 200_u64; + let calibration_start = Instant::now(); + let mut calibration_successes = 0_u64; + for _ in 0..calibration_iterations { + calibration_successes += u64::from(black_box( + ws.check_type(black_box(source), black_box(target)), + )); + } + assert_eq!( + calibration_successes, + if expected { calibration_iterations } else { 0 }, + "{name} verdict changed during calibration" + ); + let calibration_ns = calibration_start.elapsed().as_nanos().max(1); + let iterations = ((u128::from(calibration_iterations) * MINIMUM_SAMPLE_NS) + .div_ceil(calibration_ns) + .clamp(200, 10_000_000)) as u64; + + let mut samples = Vec::with_capacity(SAMPLE_COUNT); + for _ in 0..SAMPLE_COUNT { + let start = Instant::now(); + let mut successes = 0_u64; + for _ in 0..iterations { + successes += u64::from(black_box( + ws.check_type(black_box(source), black_box(target)), + )); + } + assert_eq!( + successes, + if expected { iterations } else { 0 }, + "{name} verdict changed during measurement" + ); + samples.push(start.elapsed().as_nanos() as f64 / iterations as f64); + } + let mut sorted_samples = samples.clone(); + sorted_samples.sort_by(f64::total_cmp); + let median = sorted_samples[sorted_samples.len() / 2]; + let mut deviations = sorted_samples + .iter() + .map(|sample| (sample - median).abs()) + .collect::>(); + deviations.sort_by(f64::total_cmp); + let mad_percent = deviations[deviations.len() / 2] / median * 100.0; + eprintln!( + "type_check_perf case={name} iterations={iterations} median_ns={median:.3} \ + mad_percent={mad_percent:.2} samples={samples:?}" + ); + } + + #[test] + fn test_deep_object_assignability() { + let (ws, source, target) = deep_object_fixture(5, true); + assert!(ws.check_type(&source, &target)); + + let (ws, source, target) = deep_object_fixture(5, false); + assert!(!ws.check_type(&source, &target)); + + // Depth 1 and deeper nesting keep the same leaf verdict. + let (ws, source, target) = deep_object_fixture(1, true); + assert!(ws.check_type(&source, &target)); + let (ws, source, target) = deep_object_fixture(8, false); + assert!(!ws.check_type(&source, &target)); + } + + /// Game-style class graph: one root class, fields reference other classes, which + /// again reference classes (not a giant untyped TableConst config blob). + /// + /// Layout: + /// Root.a -> MidA.leaf -> Leaf + /// Root.b -> MidB.leaf -> Leaf + /// Root.c -> MidC.child -> MidA.leaf -> Leaf + fn class_field_graph_defs() -> &'static str { + r#" + ---@class ClassGraphLeaf + ---@field value string + ---@field tag string + + ---@class ClassGraphMidA + ---@field leaf ClassGraphLeaf + ---@field flag boolean + + ---@class ClassGraphMidB + ---@field leaf ClassGraphLeaf + ---@field weight number + + ---@class ClassGraphMidC + ---@field child ClassGraphMidA + ---@field label string + + ---@class ClassGraphRoot + ---@field a ClassGraphMidA + ---@field b ClassGraphMidB + ---@field c ClassGraphMidC + ---@field name string + "# + } + + /// Source shape that mirrors the class graph with concrete field types (doc Object), + /// used to exercise declared-member structural assignability into Root. + fn class_field_graph_shape_repr(success_leaf: bool) -> &'static str { + if success_leaf { + "{ \ + a: { leaf: { value: 'ok', tag: 'a' }, flag: true }, \ + b: { leaf: { value: 'ok', tag: 'b' }, weight: 2 }, \ + c: { child: { leaf: { value: 'ok', tag: 'c' }, flag: false }, label: 'mid' }, \ + name: 'root' \ + }" + } else { + // Bad leaf under c.child.leaf.value (MidC -> MidA -> Leaf chain). + "{ \ + a: { leaf: { value: 'ok', tag: 'a' }, flag: true }, \ + b: { leaf: { value: 'ok', tag: 'b' }, weight: 2 }, \ + c: { child: { leaf: { value: 1, tag: 'c' }, flag: false }, label: 'mid' }, \ + name: 'root' \ + }" + } + } + + fn class_field_graph_table_repr(success_leaf: bool) -> &'static str { + if success_leaf { + r#"{ + a = { leaf = { value = "ok", tag = "a" }, flag = true }, + b = { leaf = { value = "ok", tag = "b" }, weight = 2 }, + c = { child = { leaf = { value = "ok", tag = "c" }, flag = false }, label = "mid" }, + name = "root" + }"# + } else { + r#"{ + a = { leaf = { value = "ok", tag = "a" }, flag = true }, + b = { leaf = { value = "ok", tag = "b" }, weight = 2 }, + c = { child = { leaf = { value = 1, tag = "c" }, flag = false }, label = "mid" }, + name = "root" + }"# + } + } + + fn class_field_graph_fixture( + success: bool, + ) -> (VirtualWorkspace, LuaType, LuaType, LuaType, LuaType) { + let mut ws = VirtualWorkspace::new(); + // Target graph always expects string leaves; source may be wrong on purpose. + ws.def(class_field_graph_defs()); + let root = ws.ty("ClassGraphRoot"); + let mid_a = ws.ty("ClassGraphMidA"); + let shape = ws.ty(class_field_graph_shape_repr(success)); + let table = ws.expr_ty(class_field_graph_table_repr(success)); + (ws, root, mid_a, shape, table) + } + + #[test] + fn test_class_field_graph_assignability() { + // Nominal: same root / mid class identity (config-center style refs). + let (mut ws, root, mid_a, shape_ok, table_ok) = class_field_graph_fixture(true); + assert!(ws.check_type(&root, &root)); + assert!(ws.check_type(&mid_a, &mid_a)); + + // Structural: object shape and small table literal into the root class graph. + assert!( + ws.check_type(&shape_ok, &root), + "compatible object shape should assign to ClassGraphRoot" + ); + assert!( + ws.check_type(&table_ok, &root), + "compatible table literal should assign to ClassGraphRoot" + ); + + // Nested class field: MidA shape into MidA (one hop of ---@field Class). + let mid_shape = ws.ty("{ leaf: { value: 'ok', tag: 't' }, flag: true }"); + assert!(ws.check_type(&mid_shape, &mid_a)); + + // MidA class value used as Root.a field type (class-to-class field slot). + let root_via_classes = + ws.ty("{ a: ClassGraphMidA, b: ClassGraphMidB, c: ClassGraphMidC, name: string }"); + assert!(ws.check_type(&root_via_classes, &root)); + + // Failure deep under Root.c.child.leaf.value (MidC -> MidA -> Leaf). + let (ws, root, _, shape_bad, table_bad) = class_field_graph_fixture(false); + assert!(!ws.check_type(&shape_bad, &root)); + assert!(!ws.check_type(&table_bad, &root)); + + let db = ws.analysis.compilation.get_db(); + let mismatch = check_assignable(db, &shape_bad, &root) + .expect_err("deep class-field mismatch should explain"); + assert!( + mismatch + .frames() + .iter() + .any(|frame| matches!(frame, TypePathSegment::Member(_))), + "explain path should keep member frames through class fields, got {:?}", + mismatch.frames() + ); + } + + /// Depth scaling timing for deep_object only (skip full AB suite). + /// Run: cargo test -p emmylua_code_analysis test_deep_object_performance_probe -- --ignored --nocapture + #[test] + #[ignore = "manual deep_object performance probe"] + fn test_deep_object_performance_probe() { + for depth in [1usize, 2, 3, 5, 8, 10] { + let (ws, source, target) = deep_object_fixture(depth, true); + measure_type_check_case( + &format!("deep_object_success_d{depth}"), + &ws, + &source, + &target, + true, + ); + + let (ws, source, target) = deep_object_fixture(depth, false); + measure_type_check_case( + &format!("deep_object_failure_d{depth}"), + &ws, + &source, + &target, + false, + ); + } + } + + /// Class-field graph timing (root + nested ---@field Class refs). + /// Run: cargo test -p emmylua_code_analysis test_class_field_graph_performance_probe --release -- --ignored --nocapture + #[test] + #[ignore = "manual class field graph performance probe"] + fn test_class_field_graph_performance_probe() { + let (ws, root, _, shape_ok, table_ok) = class_field_graph_fixture(true); + measure_type_check_case("class_graph_root_to_root_success", &ws, &root, &root, true); + measure_type_check_case( + "class_graph_shape_to_root_success", + &ws, + &shape_ok, + &root, + true, + ); + measure_type_check_case( + "class_graph_tableconst_to_root_success", + &ws, + &table_ok, + &root, + true, + ); + + let (ws, root, _, shape_bad, table_bad) = class_field_graph_fixture(false); + measure_type_check_case( + "class_graph_shape_to_root_failure", + &ws, + &shape_bad, + &root, + false, + ); + measure_type_check_case( + "class_graph_tableconst_to_root_failure", + &ws, + &table_bad, + &root, + false, + ); + } + + /// Wide48 TableConst → class success (matches type-check-perf AB case). + /// Run: cargo test -p emmylua_code_analysis test_class_graph_wide48_tableconst_success_probe --release -- --ignored --nocapture + #[test] + #[ignore = "manual wide48 tableconst->class success probe"] + fn test_class_graph_wide48_tableconst_success_probe() { + use std::fmt::Write; + + const WIDE: usize = 48; + const CHAIN: usize = 12; + + let mut defs = String::with_capacity(16_384); + defs.push_str("---@class WideLeaf\n---@field value string\n---@field tag string\n\n"); + for index in 0..WIDE { + write!( + defs, + "---@class WideBranch{index}\n---@field leaf WideLeaf\n---@field id integer\n\n" + ) + .expect("write branch"); + } + for index in 0..CHAIN { + if index + 1 < CHAIN { + write!( + defs, + "---@class WideChain{index}\n---@field next WideChain{}\n\n", + index + 1 + ) + .expect("write chain"); + } else { + write!( + defs, + "---@class WideChain{index}\n---@field leaf WideLeaf\n\n" + ) + .expect("write chain leaf"); + } + } + defs.push_str("---@class WideRoot\n"); + for index in 0..WIDE { + writeln!(defs, "---@field b{index} WideBranch{index}").expect("write root field"); + } + defs.push_str("---@field chain WideChain0\n---@field name string\n"); + + let mut table = String::from("{ "); + for index in 0..WIDE { + write!( + table, + "b{index} = {{ leaf = {{ value = \"ok\", tag = \"t{index}\" }}, id = {index} }}, " + ) + .expect("write table branch"); + } + table.push_str("chain = "); + for _ in 0..(CHAIN - 1) { + table.push_str("{ next = "); + } + table.push_str(r#"{ leaf = { value = "ok", tag = "chain" } }"#); + for _ in 0..(CHAIN - 1) { + table.push_str(" }"); + } + table.push_str(", name = \"root\" }"); + + let mut ws = VirtualWorkspace::new(); + ws.def(&defs); + let root = ws.ty("WideRoot"); + let source = ws.expr_ty(&table); + assert!( + ws.check_type(&source, &root), + "wide48 tableconst should assign to WideRoot" + ); + measure_type_check_case( + "class_graph_wide48_tableconst_to_root_success", + &ws, + &source, + &root, + true, + ); + } + + #[test] + #[ignore = "manual type-check performance probe"] + fn test_type_check_performance_probe() { + use std::fmt::Write; + + let selected_case = std::env::var("TYPE_CHECK_PERF_CASE").ok(); + let should_run = |name: &str| { + selected_case + .as_deref() + .is_none_or(|selected| selected == name) + }; + + if should_run("deep_object_success") { + let (ws, source, target) = deep_object_fixture(5, true); + measure_type_check_case("deep_object_success", &ws, &source, &target, true); + } + + if should_run("deep_object_failure") { + let (ws, source, target) = deep_object_fixture(5, false); + measure_type_check_case("deep_object_failure", &ws, &source, &target, false); + } + + if should_run("wide_object_success_64") { + let mut source_repr = String::from("{"); + let mut target_repr = String::from("{"); + for index in 0..64 { + if index != 0 { + source_repr.push_str(", "); + target_repr.push_str(", "); + } + write!(source_repr, "f{index}: {index}").expect("write source field"); + write!(target_repr, "f{index}: integer").expect("write target field"); + } + source_repr.push('}'); + target_repr.push('}'); + + let mut ws = VirtualWorkspace::new(); + let source = ws.ty(&source_repr); + let target = ws.ty(&target_repr); + measure_type_check_case("wide_object_success_64", &ws, &source, &target, true); + } + + if should_run("target_union_last_match_8") || should_run("target_union_failure_8") { + let mut target_repr = String::new(); + for (index, tag) in ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] + .into_iter() + .enumerate() + { + if index != 0 { + target_repr.push_str(" | "); + } + write!( + target_repr, + "{{ kind: '{tag}', payload: {{ value: number, label: string }} }}" + ) + .expect("write union candidate"); + } + + if should_run("target_union_last_match_8") { + let mut ws = VirtualWorkspace::new(); + let source = ws.ty("{ kind: 'h', payload: { value: 1, label: 'matched' } }"); + let target = ws.ty(&target_repr); + measure_type_check_case("target_union_last_match_8", &ws, &source, &target, true); + } + + if should_run("target_union_failure_8") { + let mut ws = VirtualWorkspace::new(); + let source = ws.ty("{ kind: 'h', payload: { value: 'bad', label: 'mismatch' } }"); + let target = ws.ty(&target_repr); + measure_type_check_case("target_union_failure_8", &ws, &source, &target, false); + } + } + + if should_run("callable_failure") { + let mut ws = VirtualWorkspace::new(); + let source = + ws.ty("fun(value: string, options: { nested: { value: string } }): number"); + let target = + ws.ty("fun(value: number, options: { nested: { value: number } }): string"); + measure_type_check_case("callable_failure", &ws, &source, &target, false); + } + + if should_run("recursive_alias_success") { + let mut ws = VirtualWorkspace::new(); + ws.def("---@alias PerfRecursive string | PerfRecursive[]"); + let source = ws.ty("string | PerfRecursive[]"); + let target = ws.ty("PerfRecursive"); + measure_type_check_case("recursive_alias_success", &ws, &source, &target, true); + } + + if should_run("generic_recursive_alias_failure") { + let mut ws = VirtualWorkspace::new(); + ws.def("---@alias PerfGenericRecursive T | PerfGenericRecursive[]"); + let source = ws.ty("boolean | PerfGenericRecursive[]"); + let target = ws.ty("PerfGenericRecursive"); + measure_type_check_case( + "generic_recursive_alias_failure", + &ws, + &source, + &target, + false, + ); + } + + if should_run("tuple_tail_failure_48") { + let mut source_repr = String::from("["); + let mut target_repr = String::from("["); + for index in 0..48 { + if index != 0 { + source_repr.push_str(", "); + target_repr.push_str(", "); + } + if index == 47 { + source_repr.push_str("'bad'"); + target_repr.push_str("number"); + } else { + write!(source_repr, "{index}").expect("write source tuple element"); + target_repr.push_str("integer"); + } + } + source_repr.push(']'); + target_repr.push(']'); + + let mut ws = VirtualWorkspace::new(); + let source = ws.ty(&source_repr); + let target = ws.ty(&target_repr); + measure_type_check_case("tuple_tail_failure_48", &ws, &source, &target, false); + } + + if should_run("large_table_nested_failure_10000") { + let mut table_repr = String::with_capacity(180_064); + table_repr.push('{'); + for index in 0..10_000 { + write!(table_repr, "k{index} = {index}, ").expect("write large table field"); + } + table_repr.push_str("nested = { value = 'bad', label = 'tail' } }"); + + let mut ws = VirtualWorkspace::new(); + let source = ws.expr_ty(&table_repr); + let target = ws.ty("{ nested: { value: number, label: string } }"); + measure_type_check_case( + "large_table_nested_failure_10000", + &ws, + &source, + &target, + false, + ); + } + + // Class-field graph: root ---@field refs other classes (not giant untyped config). + if should_run("class_graph_root_to_root_success") + || should_run("class_graph_shape_to_root_success") + || should_run("class_graph_tableconst_to_root_success") + { + let (ws, root, _, shape_ok, table_ok) = class_field_graph_fixture(true); + if should_run("class_graph_root_to_root_success") { + measure_type_check_case( + "class_graph_root_to_root_success", + &ws, + &root, + &root, + true, + ); + } + if should_run("class_graph_shape_to_root_success") { + measure_type_check_case( + "class_graph_shape_to_root_success", + &ws, + &shape_ok, + &root, + true, + ); + } + if should_run("class_graph_tableconst_to_root_success") { + measure_type_check_case( + "class_graph_tableconst_to_root_success", + &ws, + &table_ok, + &root, + true, + ); + } + } + + if should_run("class_graph_shape_to_root_failure") + || should_run("class_graph_tableconst_to_root_failure") + { + let (ws, root, _, shape_bad, table_bad) = class_field_graph_fixture(false); + if should_run("class_graph_shape_to_root_failure") { + measure_type_check_case( + "class_graph_shape_to_root_failure", + &ws, + &shape_bad, + &root, + false, + ); + } + if should_run("class_graph_tableconst_to_root_failure") { + measure_type_check_case( + "class_graph_tableconst_to_root_failure", + &ws, + &table_bad, + &root, + false, + ); + } + } + + // --- Semantic shortcut coverage (missing from structural type-check-perf set) --- + // Real projects (e.g. luatest) use nested any heavily; StuckLoading-style code does not. + // Commenting out relate_semantic_accept is invisible to structural-only benches but + // breaks these cases (see test_nested_semantic_accept_on_recursive_relate). + + if should_run("nested_target_any_success") { + let mut ws = VirtualWorkspace::new(); + let source = ws.ty("{ nested: { value: string, count: 1 } }"); + let target = ws.ty("{ nested: { value: any, count: integer } }"); + measure_type_check_case("nested_target_any_success", &ws, &source, &target, true); + } + + if should_run("nested_source_any_success") { + let mut ws = VirtualWorkspace::new(); + let source = ws.ty("{ nested: { value: any } }"); + let target = ws.ty("{ nested: { value: string } }"); + measure_type_check_case("nested_source_any_success", &ws, &source, &target, true); + } + + if should_run("array_any_element_success") { + let mut ws = VirtualWorkspace::new(); + let source = ws.ty("string[]"); + let target = ws.ty("any[]"); + measure_type_check_case("array_any_element_success", &ws, &source, &target, true); + } + + if should_run("callable_any_param_return_success") { + let mut ws = VirtualWorkspace::new(); + let source = ws.ty("fun(x: string, opts: { flag: boolean }): integer"); + let target = ws.ty("fun(x: any, opts: { flag: any }): any"); + measure_type_check_case( + "callable_any_param_return_success", + &ws, + &source, + &target, + true, + ); + } + + if should_run("nested_unknown_target_success") { + let mut ws = VirtualWorkspace::new(); + let source = ws.ty("{ value: string }"); + let target = ws.ty("{ value: unknown }"); + measure_type_check_case("nested_unknown_target_success", &ws, &source, &target, true); + } + + // Shaped after luatest: any[] fields + functions taking any[]. + if should_run("luatest_shaped_any_array_success") { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@class LuatestShapedState + ---@field errors any[] + ---@field onUnhandledError? fun(error: any): boolean|nil + "#, + ); + let source = + ws.ty("{ errors: string[], onUnhandledError: fun(error: string): boolean }"); + let target = ws.ty("LuatestShapedState"); + measure_type_check_case( + "luatest_shaped_any_array_success", + &ws, + &source, + &target, + true, + ); + } + } } diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/type_check_context.rs b/crates/emmylua_code_analysis/src/semantic/type_check/type_check_context.rs deleted file mode 100644 index 260260dc8..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/type_check_context.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::collections::HashSet; - -use crate::{DbIndex, LuaMemberKey}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TypeCheckCheckLevel { - Normal, - GenericConditional, -} - -#[derive(Debug, Clone)] -pub struct TypeCheckContext<'db> { - pub detail: bool, - pub db: &'db DbIndex, - pub level: TypeCheckCheckLevel, - pub table_member_checked: Option>, -} - -impl<'db> TypeCheckContext<'db> { - pub fn new(db: &'db DbIndex, detail: bool, level: TypeCheckCheckLevel) -> Self { - Self { - detail, - db, - level, - table_member_checked: None, - } - } - - pub fn is_key_checked(&self, key: &LuaMemberKey) -> bool { - if let Some(checked) = &self.table_member_checked { - checked.contains(key) - } else { - false - } - } - - pub fn mark_key_checked(&mut self, key: LuaMemberKey) { - if self.table_member_checked.is_none() { - self.table_member_checked = Some(HashSet::new()); - } - if let Some(checked) = &mut self.table_member_checked { - checked.insert(key); - } - } -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/type_check_fail_reason.rs b/crates/emmylua_code_analysis/src/semantic/type_check/type_check_fail_reason.rs deleted file mode 100644 index fe3369804..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/type_check_fail_reason.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[derive(Debug)] -pub enum TypeCheckFailReason { - DonotCheck, - TypeNotMatch, - TypeRecursion, - TypeNotMatchWithReason(String), -} - -impl TypeCheckFailReason { - pub fn is_type_not_match(&self) -> bool { - matches!( - self, - TypeCheckFailReason::TypeNotMatch | TypeCheckFailReason::TypeNotMatchWithReason(_) - ) - } -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/type_check_guard.rs b/crates/emmylua_code_analysis/src/semantic/type_check/type_check_guard.rs deleted file mode 100644 index b778be97f..000000000 --- a/crates/emmylua_code_analysis/src/semantic/type_check/type_check_guard.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::type_check_fail_reason::TypeCheckFailReason; - -const MAX_TYPE_CHECK_LEVEL: i32 = 100; -pub type TypeCheckLevelResult = Result; - -#[derive(Debug, Clone, Copy)] -pub struct TypeCheckGuard { - stack_level: i32, -} - -impl TypeCheckGuard { - pub fn new() -> Self { - Self { stack_level: 0 } - } - - pub fn next_level(&self) -> TypeCheckLevelResult { - let next_level = self.stack_level + 1; - if next_level > MAX_TYPE_CHECK_LEVEL { - return Err(TypeCheckFailReason::TypeRecursion); - } - - Ok(Self { - stack_level: next_level, - }) - } -} diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/union.rs b/crates/emmylua_code_analysis/src/semantic/type_check/union.rs new file mode 100644 index 000000000..8456a9993 --- /dev/null +++ b/crates/emmylua_code_analysis/src/semantic/type_check/union.rs @@ -0,0 +1,141 @@ +use crate::{LuaType, LuaUnionType}; + +use super::{ + mismatch::{TypeMismatch, TypePathSegment}, + relation::{ + IntersectionState, Relater, RelationFailure, RelationKind, RelationOutcome, RelationResult, + }, +}; + +pub(crate) fn relate_union( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + intersection_state: IntersectionState, +) -> Option { + // source union 必须先分派 + match (source, target) { + (LuaType::Union(source_union), _) => Some(relate_source_union( + relater, + source, + source_union, + target, + intersection_state, + )), + (_, LuaType::Union(target_union)) => Some(relate_target_union( + relater, + source, + target, + target_union, + intersection_state, + )), + _ => None, + } +} + +fn relate_source_union( + relater: &mut Relater, + source: &LuaType, + source_union: &LuaUnionType, + target: &LuaType, + intersection_state: IntersectionState, +) -> RelationResult { + let members = source_union.into_vec(); + // ConditionalExtends 对 source union 使用 some, Assignable 使用 each. + let conditional_extends = relater.kind() == RelationKind::ConditionalExtends; + let mut first_indeterminate = None; + for (index, member) in members.iter().enumerate() { + match relater.probe_relation(member, target, intersection_state).0 { + RelationOutcome::Related if conditional_extends => return Ok(()), + RelationOutcome::Related => {} + RelationOutcome::Indeterminate(kind) => { + first_indeterminate.get_or_insert(kind); + } + RelationOutcome::Unrelated if conditional_extends => {} + RelationOutcome::Unrelated => { + if !relater.is_explain() { + return Err(RelationFailure::Unrelated(None)); + } + return explain_union_constituent( + relater, + source, + target, + member, + target, + intersection_state, + TypePathSegment::SourceUnionMember(index), + ); + } + } + } + if let Some(kind) = first_indeterminate { + return Err(relater.indeterminate_failure(kind, source, target)); + } + if conditional_extends { + relater.unrelated(|| TypeMismatch::incompatible(source, target)) + } else { + Ok(()) + } +} + +fn relate_target_union( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + target_union: &LuaUnionType, + intersection_state: IntersectionState, +) -> RelationResult { + let candidates = target_union.into_vec(); + let mut best = None; + let mut indeterminate = None; + for (index, candidate) in candidates.iter().enumerate() { + let (outcome, progress) = relater.probe_relation(source, candidate, intersection_state); + match outcome { + RelationOutcome::Related => return Ok(()), + RelationOutcome::Indeterminate(kind) => { + indeterminate.get_or_insert(kind); + } + RelationOutcome::Unrelated => { + if best + .map(|(_, current_progress)| progress > current_progress) + .unwrap_or(true) + { + best = Some((index, progress)); + } + } + } + } + if let Some(kind) = indeterminate { + return Err(relater.indeterminate_failure(kind, source, target)); + } + + let Some((best_index, _)) = best else { + return relater.unrelated(|| TypeMismatch::incompatible(source, target)); + }; + if !relater.is_explain() { + return Err(RelationFailure::Unrelated(None)); + } + explain_union_constituent( + relater, + source, + target, + source, + &candidates[best_index], + intersection_state, + TypePathSegment::TargetUnionCandidate(best_index), + ) +} + +fn explain_union_constituent( + relater: &mut Relater, + source: &LuaType, + target: &LuaType, + constituent_source: &LuaType, + constituent_target: &LuaType, + intersection_state: IntersectionState, + path: TypePathSegment, +) -> RelationResult { + relater + .relate(constituent_source, constituent_target, intersection_state) + .map_err(|failure| failure.map_mismatch(|mismatch| mismatch.at(path, source, target))) +} diff --git a/crates/emmylua_code_analysis/src/test_lib/mod.rs b/crates/emmylua_code_analysis/src/test_lib/mod.rs index 72c9c14db..4e0a23214 100644 --- a/crates/emmylua_code_analysis/src/test_lib/mod.rs +++ b/crates/emmylua_code_analysis/src/test_lib/mod.rs @@ -6,7 +6,7 @@ use tokio_util::sync::CancellationToken; use crate::{ DbIndex, DiagnosticCode, EmmyLuaAnalysis, Emmyrc, FileId, LuaType, RenderLevel, - VirtualUrlGenerator, check_type_compact, humanize_type, + VirtualUrlGenerator, humanize_type, is_assignable, }; /// A virtual workspace for testing. @@ -140,9 +140,8 @@ impl VirtualWorkspace { info.typ } - pub fn check_type(&self, source: &LuaType, compact_type: &LuaType) -> bool { - let db = &self.analysis.compilation.get_db(); - check_type_compact(db, source, compact_type).is_ok() + pub fn check_type(&self, source: &LuaType, target: &LuaType) -> bool { + is_assignable(self.analysis.compilation.get_db(), source, target) } pub fn enable_check(&mut self, diagnostic_code: DiagnosticCode) { diff --git a/crates/emmylua_ls/src/handlers/completion/providers/function_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/function_provider.rs index 8ca5254f3..219c96a34 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/function_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/function_provider.rs @@ -666,8 +666,7 @@ fn add_str_tpl_ref_completion( let current_type = LuaType::Ref(type_decl.get_id()); builder .semantic_model - .type_check(&extend_type, ¤t_type) - .is_ok() + .is_assignable(¤t_type, &extend_type) }) .map(|type_decl| { let trimmed_name = type_decl diff --git a/crates/emmylua_ls/src/handlers/completion/providers/member_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/member_provider.rs index e37349dd3..bc8b2c429 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/member_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/member_provider.rs @@ -192,9 +192,7 @@ fn filter_member_infos<'a>( let final_reference_owner = if let Some(file_decl_member_info) = file_decl_member { // 与第一个成员进行类型检查, 确保子类成员的类型与父类成员的类型一致 if let Some((first_member, first_owner)) = member_with_owners.first() { - let type_check_result = - semantic_model.type_check(&file_decl_member_info.typ, &first_member.typ); - if type_check_result.is_ok() { + if semantic_model.is_assignable(&first_member.typ, &file_decl_member_info.typ) { get_owner_type_id(semantic_model.get_db(), file_decl_member_info) } else { first_owner.clone() diff --git a/crates/emmylua_ls/src/handlers/completion/providers/table_field_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/table_field_provider.rs index 1fb976b88..701130f05 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/table_field_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/table_field_provider.rs @@ -324,7 +324,7 @@ fn in_env(builder: &mut CompletionBuilder, target_name: &str, target_type: &LuaT ) }; // 必须要名称相同 + 类型兼容 - if name == target_name && builder.semantic_model.type_check(target_type, &typ).is_ok() { + if name == target_name && builder.semantic_model.is_assignable(&typ, target_type) { return Some(()); } } diff --git a/crates/emmylua_ls/src/handlers/hover/build_hover.rs b/crates/emmylua_ls/src/handlers/hover/build_hover.rs index 438b718b3..b31fa9174 100644 --- a/crates/emmylua_ls/src/handlers/hover/build_hover.rs +++ b/crates/emmylua_ls/src/handlers/hover/build_hover.rs @@ -525,7 +525,7 @@ fn select_assignment_hover_type( return Some(target_type.clone()); } - if semantic_model.type_check(target_type, &expr_type).is_ok() { + if semantic_model.is_assignable(&expr_type, target_type) { return Some(expr_type); } diff --git a/crates/emmylua_ls/src/handlers/inlay_hint/build_inlay_hint.rs b/crates/emmylua_ls/src/handlers/inlay_hint/build_inlay_hint.rs index eeb2fc301..98c6ae0db 100644 --- a/crates/emmylua_ls/src/handlers/inlay_hint/build_inlay_hint.rs +++ b/crates/emmylua_ls/src/handlers/inlay_hint/build_inlay_hint.rs @@ -541,9 +541,7 @@ fn set_meta_call_part( let hint_new = { LuaStat::can_cast(parent.kind().into()) && !matches!(call_expr.get_prefix_expr()?, LuaExpr::CallExpr(_)) - && semantic_model - .type_check(call_func.get_ret(), &target_type) - .is_ok() + && semantic_model.is_assignable(&target_type, call_func.get_ret()) }; let (value, hint_range, padding_right) = if hint_new { diff --git a/crates/emmylua_ls/std_i18n/builtin/zh_CN.yaml b/crates/emmylua_ls/std_i18n/builtin/zh_CN.yaml index e9f361eb3..39d7c64b7 100644 --- a/crates/emmylua_ls/std_i18n/builtin/zh_CN.yaml +++ b/crates/emmylua_ls/std_i18n/builtin/zh_CN.yaml @@ -86,7 +86,6 @@ lsp_optimization: | ### 参数 - - `skip_table_fields_check`: 跳过表字段诊断。建议对所有大型配置表使用此选项。 - `delayed_definition`: 表示变量类型由第一次赋值决定,仅对没有初始值的 `local` 声明有效。 index_alias: |