From 6ae3da98073e1a178d4e3e1b48581146b1602480 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 14 Oct 2026 17:04:41 +0100 Subject: [PATCH] refactor(types): share assignment, argument, and return checks Assignments, arguments, and returns use different rules to decide whether a value fits an expected type. Local exceptions duplicate logic and can accept invalid values. A union can pass even when some of its possible values do not fit. For example, these three uses must all fail because value can be an integer: ---@param value string | integer ---@param consume fun(text: string) ---@return string local function example(value, consume) ---@type string local text = value -- error: assignment consume(value) -- error: argument return value -- error: return end Use one directional compatibility check for all three contexts. Every source union member must fit at least one target alternative. Check callbacks by the arguments they must accept and the results they can produce, including optional and variadic slots. Keep strict subtype checks separate for casts and conditional types. This gives the checkers one place to define and enforce value compatibility. The shared check needs accurate input types. Preserve integer results and complete return sequences, including an expanding final call. Retain object fields from annotations and resolve inherited index types with their generic arguments and key domains. Widen inferred literals for reassignment, while keeping declared literal types exact. These changes let valid code pass without local checker exceptions. Member guards also need to describe the correct object. Track object identity through local aliases and discard a guard after a write can change the guarded value. This prevents a nil check on one object from being reused for another object or after an affected field changes. Reuse resolved syntax nodes and cache cast paths to avoid repeated tree walks during flow analysis of long expressions. Cover the shared rules across assignments, arguments, and returns, with regressions for the inference and flow information they use. --- .../src/check/checker/assign_type_mismatch.rs | 248 +----- .../src/check/checker/cast_type_mismatch.rs | 9 +- .../src/check/checker/param_count.rs | 38 +- .../src/check/checker/param_type_check.rs | 341 ++------ .../src/check/checker/return_type_mismatch.rs | 38 - .../check/test/assign_type_mismatch_test.rs | 805 +++++++++++++++++- .../src/check/test/need_check_nil_test.rs | 24 +- .../src/check/test/param_type_check_test.rs | 266 +++++- .../check/test/redundant_parameter_test.rs | 53 +- .../check/test/return_type_mismatch_test.rs | 58 +- .../semantic_db/def/lua_type/types/complex.rs | 19 + .../src/semantic_db/facade.rs | 6 + .../src/semantic_db/flow/flow_tree.rs | 27 +- .../src/semantic_db/flow/stats.rs | 44 +- .../src/semantic_db/query.rs | 235 ++++- .../src/semantic_db/tests.rs | 80 +- .../src/semantic_db/types.rs | 28 + .../src/semantic_model/access.rs | 4 +- .../src/semantic_model/cache.rs | 3 + .../src/semantic_model/flow.rs | 700 ++++++++++++--- .../src/semantic_model/infer/callable.rs | 3 - .../semantic_model/infer/function_solver.rs | 170 +--- .../infer/legacy_tests/closure_return_test.rs | 31 +- .../infer/legacy_tests/generic_test.rs | 8 +- .../infer/legacy_tests/member_infer_test.rs | 2 +- .../infer/legacy_tests/module_test.rs | 6 +- .../return_overload_generic_test.rs | 4 +- .../src/semantic_model/infer/test.rs | 50 +- .../src/semantic_model/infer/vm.rs | 145 ++-- .../src/semantic_model/member.rs | 45 +- .../src/semantic_model/signature.rs | 72 +- .../src/semantic_model/type_check/assign.rs | 48 +- .../semantic_model/type_check/comparison.rs | 17 +- .../semantic_model/type_check/complex_type.rs | 149 +++- .../src/semantic_model/type_check/context.rs | 39 +- .../semantic_model/type_check/func_type.rs | 115 ++- .../semantic_model/type_check/generic_type.rs | 14 +- .../semantic_model/type_check/legacy_tests.rs | 8 +- .../src/semantic_model/type_check/mod.rs | 256 +++--- .../src/semantic_model/type_check/ref_type.rs | 17 +- .../semantic_model/type_check/simple_type.rs | 265 ++---- .../src/semantic_model/type_check/test.rs | 74 +- .../src/semantic_model/type_eval.rs | 2 +- .../src/semantic_model/type_eval/tests.rs | 52 +- .../src/semantic_model/types.rs | 112 ++- .../emmylua_code_analysis/src/test_lib/mod.rs | 4 +- .../completion/providers/desc_provider.rs | 1 + .../completion/providers/member_provider.rs | 4 + 48 files changed, 3006 insertions(+), 1733 deletions(-) diff --git a/crates/emmylua_code_analysis/src/check/checker/assign_type_mismatch.rs b/crates/emmylua_code_analysis/src/check/checker/assign_type_mismatch.rs index e5e756e4e..957187610 100644 --- a/crates/emmylua_code_analysis/src/check/checker/assign_type_mismatch.rs +++ b/crates/emmylua_code_analysis/src/check/checker/assign_type_mismatch.rs @@ -3,22 +3,17 @@ use std::rc::Rc; use std::collections::{HashMap, HashSet}; use emmylua_parser::{ - BinaryOperator, LuaAssignStat, LuaAst, LuaAstNode, LuaCallExpr, LuaClosureExpr, LuaDocType, - LuaExpr, LuaIndexExpr, LuaIndexKey, LuaLiteralToken, LuaLocalStat, LuaTableExpr, LuaVarExpr, - NumberResult, + LuaAssignStat, LuaAst, LuaAstNode, LuaCallExpr, LuaClosureExpr, LuaExpr, LuaIndexExpr, + LuaIndexKey, LuaLiteralToken, LuaLocalStat, LuaTableExpr, LuaVarExpr, NumberResult, }; use crate::DiagnosticCode; -use crate::check::checker::param_type_check; use crate::semantic_model::SemanticModel; -use crate::semantic_model::type_check::{ - TypeCheckFailReason, check_assign_type_detail, is_compatible, -}; +use crate::semantic_model::type_check::{TypeCheckFailReason, check_type_detail, is_compatible}; use crate::{Decl, LuaMemberKey, LuaType, LuaTypeNode, SemanticId, TypeDef, TypeDefKind}; use super::{CheckContext, Checker, check_field::static_member_key}; use crate::semantic_model::render::humanize_type; -use crate::semantic_model::type_check::is_assign_compatible as type_check_is_assign_compatible; pub struct AssignTypeMismatchChecker; @@ -44,69 +39,6 @@ impl Checker for AssignTypeMismatchChecker { } } -/// Call to `---@overload fun(): integer`: when the main signature returns a union, use the overload return type. -fn call_overload_compatible( - semantic_model: &SemanticModel<'_>, - call: &LuaCallExpr, - target: &LuaType, -) -> bool { - let callee = call.get_prefix_expr(); - let Some(LuaExpr::NameExpr(name_expr)) = callee else { - return false; - }; - let Some(decl) = semantic_model.resolve_name(name_expr.get_position()) else { - return false; - }; - let SemanticId::Decl(decl_key) = decl else { - return false; - }; - let Some(facts) = semantic_model.file_facts_of(decl_key.file_id) else { - return false; - }; - let Some(decl) = facts.decl_by_id(&SemanticId::Decl(decl_key.clone())) else { - return false; - }; - let Some(closure_syntax) = decl.value_expr_syntax else { - return false; - }; - let Some(signature) = facts.signature_by_closure(closure_syntax) else { - return false; - }; - let Some(docs) = signature.docs.as_ref() else { - return false; - }; - let arg_count = call.get_args_count().unwrap_or(0); - for overload_syntax in &docs.overloads { - let Some(tree) = semantic_model.syntax_tree_of(decl_key.file_id) else { - continue; - }; - let Some(node) = overload_syntax.to_node_from_root(&tree.get_red_root()) else { - continue; - }; - let Some(LuaDocType::Func(func)) = LuaDocType::cast(node) else { - continue; - }; - if func.get_params().count() != arg_count { - continue; - } - let Some(return_list) = func.get_return_type_list() else { - continue; - }; - let Some(ret_ty) = return_list - .get_return_type_list() - .next() - .and_then(|ret| ret.get_name_and_type().1) - else { - continue; - }; - let ret = semantic_model.doc_type_lua_rich_in(decl_key.file_id, ret_ty.get_syntax_id()); - if is_assign_compatible(semantic_model, &ret, target) { - return true; - } - } - false -} - /// `---@generic T: string` plus `` `T` `` parameter: a string argument that resolves to a class name not satisfying the constraint yields AssignTypeMismatch. fn check_str_tpl_constraint_call( context: &mut CheckContext<'_>, @@ -171,7 +103,7 @@ fn check_str_tpl_constraint_call( }; for (name, constraint) in &constraints { if str_tpl_uses_name(param_ty, name) - && !is_assign_compatible(semantic_model, &resolved, constraint) + && !is_compatible(semantic_model, &resolved, constraint) { add_mismatch(context, arg.get_range(), constraint, &resolved); return true; @@ -471,7 +403,10 @@ fn check_member_assign( target_offset: rowan::TextSize, value_expr: &LuaExpr, ) { - let mut target = semantic_model.type_of_member_at(member_id, target_offset); + let mut target = semantic_model.member_assignment_type( + member_id, + semantic_model.type_of_member_at(member_id, target_offset), + ); // `t.x = value`: resolve_member first returns the synthesized member for this assignment; prefer the same-named member on the prefix's declared type. if let Some(facts) = semantic_model.file_facts() && let Some(member) = facts.member_by_id(member_id) @@ -487,7 +422,10 @@ fn check_member_assign( let declared_ty = infos .into_iter() .find(|info| info.id.as_ref() != Some(member_id)) - .map(|info| info.typ) + .map(|info| match info.id { + Some(id) => semantic_model.member_assignment_type(&id, info.typ), + None => info.typ, + }) .or_else(|| semantic_model.member_type(&prefix_ty, &key)); if let Some(declared_ty) = declared_ty { target = declared_ty; @@ -575,7 +513,7 @@ fn check_value_against( // Compatibility resolves tuple aliases and unions. Keep the intersection guard: // the general check currently accepts a table matching only one component. if !matches!(target, LuaType::Intersection(_)) - && is_assign_compatible(semantic_model, &value_ty, target) + && is_compatible(semantic_model, &value_ty, target) { return; } @@ -583,126 +521,15 @@ fn check_value_against( } return; } - // Multiple candidate signatures (`@overload fun(): integer`): pass if any candidate return type is compatible. - if let LuaExpr::CallExpr(call) = value_expr - && let Some(callee) = call.get_prefix_expr() - { - for candidate in param_type_check::callable_candidates(semantic_model, &callee) { - if is_assign_compatible(semantic_model, candidate.get_ret(), target) { - return; - } - } - if call_overload_compatible(semantic_model, call, target) { - return; - } - } let value_ty = assignment_value_type(semantic_model, value_expr); - // `b = 1 + (a and 1 or 0)`: VM maps numeric literals to Number, so allow arithmetic expressions for integer targets. - if matches!(target, LuaType::Integer) - && matches!(value_ty, LuaType::Number) - && is_numeric_binary_expr(value_expr) - { - return; - } - if let LuaType::Union(union) = &value_ty - && union - .into_vec() - .iter() - .any(|component| is_assign_compatible(semantic_model, component, target)) - && guard_narrowed_call_result(semantic_model, value_expr) - { - return; - } if matches!(value_ty, LuaType::Unknown | LuaType::Any | LuaType::Never) - || is_assign_compatible(semantic_model, &value_ty, target) + || is_compatible(semantic_model, &value_ty, target) { return; } add_mismatch(context, value_expr.get_range(), target, &value_ty); } -fn is_numeric_binary_expr(expr: &LuaExpr) -> bool { - match expr { - LuaExpr::BinaryExpr(binary) => binary.get_op_token().is_some_and(|op| { - matches!( - op.get_op(), - BinaryOperator::OpAdd - | BinaryOperator::OpSub - | BinaryOperator::OpMul - | BinaryOperator::OpIDiv - | BinaryOperator::OpDiv - | BinaryOperator::OpMod - ) - }), - LuaExpr::ParenExpr(paren) => paren - .get_expr() - .is_some_and(|inner| is_numeric_binary_expr(&inner)), - _ => false, - } -} - -/// `local ok, result = pick(...)` plus `@return_overload` guard: pass the result union when it matches the target component. -fn guard_narrowed_call_result(semantic_model: &SemanticModel<'_>, value_expr: &LuaExpr) -> bool { - let LuaExpr::NameExpr(name_expr) = value_expr else { - return false; - }; - let Some(decl) = semantic_model.resolve_name(name_expr.get_position()) else { - return false; - }; - let Some(facts) = semantic_model.file_facts() else { - return false; - }; - let Some(decl_def) = facts.decl_by_id(&decl) else { - return false; - }; - if decl_def.multi_return_index.is_none() { - return false; - } - let Some(LuaExpr::CallExpr(call)) = decl_def - .value_expr_syntax - .and_then(|syntax| { - semantic_model - .syntax_tree() - .and_then(|tree| syntax.to_node_from_root(&tree.get_red_root())) - }) - .and_then(LuaExpr::cast) - else { - return false; - }; - let Some(callee) = call.get_prefix_expr() else { - return false; - }; - let LuaExpr::NameExpr(callee_name) = callee else { - return false; - }; - let Some(callee_decl) = semantic_model.resolve_name(callee_name.get_position()) else { - return false; - }; - let Some(signature) = semantic_model.type_of_decl_signature(&callee_decl) else { - return false; - }; - let _ = signature; - // Then check the signature docs for return_overload: main signature union plus return_overload branches. - let SemanticId::Decl(decl_key) = callee_decl.clone() else { - return false; - }; - let Some(callee_facts) = semantic_model.file_facts_of(decl_key.file_id) else { - return false; - }; - let Some(callee_decl) = callee_facts.decl_by_id(&SemanticId::Decl(decl_key)) else { - return false; - }; - let Some(sig) = callee_decl - .value_expr_syntax - .and_then(|syntax| callee_facts.signature_by_closure(syntax)) - else { - return false; - }; - sig.docs - .as_ref() - .is_some_and(|docs| !docs.return_overloads.is_empty()) -} - /// Assignment RHS type: when VM yields `Function`, if the RHS is a named function, resolve its cross-file signature structure. fn assignment_value_type(semantic_model: &SemanticModel<'_>, value_expr: &LuaExpr) -> LuaType { let ty = @@ -748,19 +575,6 @@ fn resolve_unbound_generic(semantic_model: &SemanticModel<'_>, ty: LuaType) -> L ty } -/// Assignment compatibility: when target is a union, source may satisfy any component (whereas type_check's union check requires all components). -fn is_assign_compatible( - semantic_model: &SemanticModel<'_>, - source: &LuaType, - target: &LuaType, -) -> bool { - if type_check_is_assign_compatible(semantic_model, source, target) { - return true; - } - - false -} - fn check_table_fields( context: &mut CheckContext<'_>, semantic_model: &SemanticModel<'_>, @@ -815,10 +629,10 @@ fn check_table_fields( continue; } let key_ty = index_key_type(semantic_model, &key); - if let Some((_, expected)) = - object.get_index_access().iter().find(|(signature_key, _)| { - is_assign_compatible(semantic_model, &key_ty, signature_key) - }) + if let Some((_, expected)) = object + .get_index_access() + .iter() + .find(|(signature_key, _)| is_compatible(semantic_model, &key_ty, signature_key)) { check_table_value(context, semantic_model, &value_expr, expected); } @@ -926,26 +740,10 @@ fn check_table_value( } let value_ty = semantic_model.type_of_expr_at(value_expr.get_syntax_id(), value_expr.get_range().start()); - // VM projects numeric literals to Number: allow when target is integer. - if matches!(expected, LuaType::Integer) - && matches!(value_ty, LuaType::Number) - && matches!(value_expr, LuaExpr::LiteralExpr(_)) - { - return; - } - // Union element: a table-constructor value passes if any union component matches (`{ callbacks }` in guard branch). - if let LuaType::Union(union) = &value_ty - && union - .into_vec() - .iter() - .any(|component| is_assign_compatible(semantic_model, component, expected)) - { - return; - } - if matches!( - value_ty, - LuaType::Unknown | LuaType::Any | LuaType::Never | LuaType::Nil - ) || is_assign_compatible(semantic_model, &value_ty, expected) + // Nil omits an entry from a table constructor. + let expected_or_absent = LuaType::from_vec(vec![expected.clone(), LuaType::Nil]); + if matches!(value_ty, LuaType::Unknown | LuaType::Any | LuaType::Never) + || is_compatible(semantic_model, &value_ty, &expected_or_absent) { return; } @@ -1117,7 +915,7 @@ fn add_mismatch( target: &LuaType, value_ty: &LuaType, ) { - let reason = match check_assign_type_detail(context.semantic_model, value_ty, target) { + let reason = match check_type_detail(context.semantic_model, value_ty, target) { Err(TypeCheckFailReason::TypeNotMatchWithReason(reason)) => Some(reason), _ => None, }; diff --git a/crates/emmylua_code_analysis/src/check/checker/cast_type_mismatch.rs b/crates/emmylua_code_analysis/src/check/checker/cast_type_mismatch.rs index da643b738..20a9807d3 100644 --- a/crates/emmylua_code_analysis/src/check/checker/cast_type_mismatch.rs +++ b/crates/emmylua_code_analysis/src/check/checker/cast_type_mismatch.rs @@ -1,15 +1,13 @@ //! # cast_type_mismatch — cast target and expression type incompatible for `---@cast key -|+ type` //! -//! origin = key expression type; target = cast type node projection; report when `is_compatible(origin, target)` -//! fails (bidirectional super↔sub is covered by type_check ref checks). Operator casts and -//! `@cast` local/global variable declaration semantics are left for later. +//! A cast may widen or narrow a type. Use precise subtype checks in both directions +//! so broad value compatibility cannot accept an unrelated literal alternative. use emmylua_parser::{LuaAstNode, LuaDocTagCast}; use crate::DiagnosticCode; use crate::LuaType; use crate::semantic_model::SemanticModel; -use crate::semantic_model::type_check::is_compatible; use super::{CheckContext, Checker}; use crate::semantic_model::render::humanize_type; @@ -76,7 +74,8 @@ fn check_cast_tag( continue; }; let target_type = semantic_model.doc_type_lua(target_doc_type.get_syntax_id()); - if is_compatible(semantic_model, &check_origin, &target_type) + if semantic_model.type_check_subtype(&check_origin, &target_type) + || semantic_model.type_check_subtype(&target_type, &check_origin) || cast_broad_compatible(&check_origin, &target_type) { continue; diff --git a/crates/emmylua_code_analysis/src/check/checker/param_count.rs b/crates/emmylua_code_analysis/src/check/checker/param_count.rs index d67fa5768..cc2c8a38e 100644 --- a/crates/emmylua_code_analysis/src/check/checker/param_count.rs +++ b/crates/emmylua_code_analysis/src/check/checker/param_count.rs @@ -814,24 +814,7 @@ pub(crate) fn callable_functions( if let Some(facts) = semantic_model.file_facts_of(def.file_id) && let Some(op) = facts.operator_of(&def.id, "call") { - let params = op - .params - .iter() - .map(|syntax| { - ( - String::new(), - Some(semantic_model.doc_type_lua_rich_in(def.file_id, *syntax)), - ) - }) - .collect(); - out.push(LuaFunctionType::new( - AsyncState::None, - false, - false, - params, - semantic_model.doc_type_lua_rich_in(def.file_id, op.returns), - None, - )); + out.push(semantic_model.call_operator_type(def.file_id, op)); } out } @@ -871,24 +854,7 @@ pub(crate) fn callable_functions( if let Some(facts) = semantic_model.file_facts_of(def.file_id) && let Some(op) = facts.operator_of(&def.id, "call") { - let params = op - .params - .iter() - .map(|syntax| { - ( - String::new(), - Some(semantic_model.doc_type_lua_rich_in(def.file_id, *syntax)), - ) - }) - .collect(); - out.push(LuaFunctionType::new( - AsyncState::None, - false, - false, - params, - semantic_model.doc_type_lua_rich_in(def.file_id, op.returns), - None, - )); + out.push(semantic_model.call_operator_type(def.file_id, op)); } out } diff --git a/crates/emmylua_code_analysis/src/check/checker/param_type_check.rs b/crates/emmylua_code_analysis/src/check/checker/param_type_check.rs index 1012daaeb..682e47f87 100644 --- a/crates/emmylua_code_analysis/src/check/checker/param_type_check.rs +++ b/crates/emmylua_code_analysis/src/check/checker/param_type_check.rs @@ -16,10 +16,10 @@ use crate::semantic_model::infer::unify; use crate::semantic_model::infer::vm::unify_call_bindings; use crate::semantic_model::render::humanize_type; use crate::semantic_model::type_check::is_compatible; -use crate::semantic_model::{SemanticModel, type_check, type_eval}; +use crate::semantic_model::{SemanticModel, type_eval}; use crate::{ DiagnosticCode, LuaFunctionType, LuaMemberKey, LuaTupleStatus, LuaTupleType, LuaType, - LuaTypeDeclId, LuaTypeNode, TypeDef, TypeDefKind, VariadicType, + LuaTypeNode, TypeDefKind, VariadicType, }; use super::param_count::first_param_is_self; @@ -180,8 +180,21 @@ fn index_expr_callable_candidates( ), None => CallableCandidateSet::from_prefix_type(semantic_model, &prefix_ty, &key), }; + let mut candidates = candidates.into_candidates(); + // A partial class can have both `@field` overloads and runtime methods on its local owner. + for member in semantic_model + .members_of_owner_named(&resolved.owner, &resolved.name) + .iter() + { + if let Some(functions) = + crate::semantic_model::infer::vm::InferVm::new(semantic_model, &[]) + .callable_candidates_for_owner_single(&member.id) + { + candidates.extend(functions); + } + } if !candidates.is_empty() { - return candidates.into_candidates(); + return CallableCandidateSet::new(candidates).into_candidates(); } } @@ -668,10 +681,7 @@ fn check_arg_pairs( } slot_ty = unify::substitute(&slot_ty, bindings); slot_ty = type_eval::expand_alias_generic(semantic_model, &slot_ty); - if arg_ty == slot_ty - || is_compatible(semantic_model, &arg_ty, &slot_ty) - || type_check::is_assign_compatible(semantic_model, &arg_ty, &slot_ty) - { + if arg_ty == slot_ty || is_compatible(semantic_model, &arg_ty, &slot_ty) { continue; } out.push(Mismatch { @@ -702,7 +712,7 @@ fn check_arg_pairs( let param_ty = unify::substitute(¶m_ty_orig, bindings); if arg_ty == param_ty || param_generic_base_match(semantic_model, &arg_ty, ¶m_ty) - || type_check::is_assign_compatible(semantic_model, &arg_ty, ¶m_ty) + || is_compatible(semantic_model, &arg_ty, ¶m_ty) { continue; } @@ -759,12 +769,10 @@ fn check_arg_pairs( continue; } // Check the whole boolean domain before selecting a single union component. - if value_kind(semantic_model, &call_arg_ty) == ValueKind::Boolean + if is_compatible(semantic_model, &call_arg_ty, &LuaType::Boolean) && !param_ty.contains_tpl_node() { - if !is_compatible(semantic_model, &call_arg_ty, ¶m_ty) - && !literal_compatible(semantic_model, &call_arg_ty, ¶m_ty) - { + if !is_compatible(semantic_model, &call_arg_ty, ¶m_ty) { out.push(Mismatch { range: arg.get_range(), message: mismatch_message(semantic_model, &call_arg_ty, ¶m_ty, name), @@ -796,7 +804,7 @@ fn check_arg_pairs( .collect::>(); if components .iter() - .any(|component| union_component_accepts(semantic_model, &call_arg_ty, component)) + .any(|component| is_compatible(semantic_model, &call_arg_ty, component)) { continue; } @@ -810,7 +818,7 @@ fn check_arg_pairs( if union .into_vec() .iter() - .any(|component| union_component_accepts(semantic_model, &call_arg_ty, component)) + .any(|component| is_compatible(semantic_model, &call_arg_ty, component)) { continue; } @@ -826,21 +834,10 @@ fn check_arg_pairs( if call_arg_ty == param_ty { continue; } - let exact_const = match (&call_arg_ty, ¶m_ty) { - (LuaType::StringConst(a), LuaType::StringConst(b)) => a == b, - ( - LuaType::IntegerConst(a) | LuaType::DocIntegerConst(a), - LuaType::IntegerConst(b) | LuaType::DocIntegerConst(b), - ) => a == b, - _ => true, - }; - let leaf_compat = exact_const - && (!matches!(call_arg_ty, LuaType::Function) || matches!(param_ty, LuaType::Any)) + let leaf_compat = (!matches!(call_arg_ty, LuaType::Function) + || matches!(param_ty, LuaType::Any)) && param_generic_base_match(semantic_model, &call_arg_ty, ¶m_ty); - if leaf_compat - || literal_compatible(semantic_model, &call_arg_ty, ¶m_ty) - || named_kind_compatible(semantic_model, &call_arg_ty, ¶m_ty) - { + if leaf_compat || enum_name_compatible(semantic_model, &call_arg_ty, ¶m_ty) { continue; } out.push(Mismatch { @@ -1014,151 +1011,6 @@ fn generic_constraint_type(semantic_model: &SemanticModel<'_>, ty: &LuaType) -> None } -/// Basic value-domain classification: string / integer / number / boolean categories after enum/alias/class expansion. -#[derive(Clone, Copy, PartialEq, Eq)] -enum ValueKind { - Nil, - Boolean, - String, - Integer, - Number, - Other, -} - -fn value_kind(semantic_model: &SemanticModel<'_>, ty: &LuaType) -> ValueKind { - value_kind_inner(semantic_model, ty, &mut Vec::new()) -} - -fn value_kind_inner( - semantic_model: &SemanticModel<'_>, - ty: &LuaType, - visited: &mut Vec, -) -> ValueKind { - match ty { - LuaType::Nil => ValueKind::Nil, - LuaType::Boolean | LuaType::BooleanConst(_) | LuaType::DocBooleanConst(_) => { - ValueKind::Boolean - } - LuaType::String | LuaType::StringConst(_) | LuaType::DocStringConst(_) => ValueKind::String, - LuaType::Integer | LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_) => { - ValueKind::Integer - } - LuaType::Number | LuaType::FloatConst(_) => ValueKind::Number, - LuaType::Union(union) => { - let mut kind = None; - for component in union.into_vec() { - let component_kind = value_kind_inner(semantic_model, &component, visited); - if kind.is_some_and(|kind| kind != component_kind) { - return ValueKind::Other; - } - kind = Some(component_kind); - } - kind.unwrap_or(ValueKind::Other) - } - LuaType::Ref(id) | LuaType::Def(id) => { - if visited.contains(id) { - return ValueKind::Other; - } - visited.push(id.clone()); - let Some(def) = semantic_model.type_def_of(id) else { - return ValueKind::Other; - }; - match def.kind { - TypeDefKind::Alias => semantic_model - .alias_target(&def) - .map(|target| value_kind_inner(semantic_model, &target, visited)) - .unwrap_or(ValueKind::Other), - TypeDefKind::Class => { - let base_types = semantic_model.base_types(&def.type_decl_id()); - if base_types.contains(&LuaType::Integer) { - ValueKind::Integer - } else if base_types.contains(&LuaType::Number) { - ValueKind::Number - } else if base_types.contains(&LuaType::String) { - ValueKind::String - } else { - ValueKind::Other - } - } - TypeDefKind::Enum => enum_value_kind(semantic_model, &def), - } - } - _ => ValueKind::Other, - } -} - -fn enum_value_kind(semantic_model: &SemanticModel<'_>, def: &TypeDef) -> ValueKind { - let mut kind: Option = None; - let Some(facts) = semantic_model.file_facts_of(def.file_id) else { - return ValueKind::Other; - }; - let Some(decl) = facts.decl_named(def.name.as_str()) else { - return ValueKind::Other; - }; - for member_ref in semantic_model.members_of_owner(&decl.id).iter() { - let Some(member_facts) = semantic_model.file_facts_of(member_ref.file_id) else { - continue; - }; - let Some(member) = member_facts.member_by_id(&member_ref.id) else { - continue; - }; - let Some(value_syntax) = member.value_syntax else { - continue; - }; - let Some(node) = semantic_model - .syntax_tree() - .and_then(|tree| value_syntax.to_node_from_root(&tree.get_red_root())) - else { - continue; - }; - let raw = node.text().to_string(); - let item_kind = if raw.starts_with(['"', '\'']) { - ValueKind::String - } else if raw.parse::().is_ok() { - ValueKind::Integer - } else if raw.parse::().is_ok() { - ValueKind::Number - } else { - ValueKind::Other - }; - if let Some(kind) = kind - && kind != item_kind - { - return ValueKind::Other; - } - kind = Some(item_kind); - } - kind.unwrap_or(ValueKind::Other) -} - -fn named_kind_compatible( - semantic_model: &SemanticModel<'_>, - arg_ty: &LuaType, - param_ty: &LuaType, -) -> bool { - // Literal constants must match exactly (`"a"` != `"beforeAll"`). - if let (LuaType::StringConst(a), LuaType::StringConst(b)) = (arg_ty, param_ty) { - return a == b; - } - if let ( - LuaType::IntegerConst(a) | LuaType::DocIntegerConst(a), - LuaType::IntegerConst(b) | LuaType::DocIntegerConst(b), - ) = (arg_ty, param_ty) - { - return a == b; - } - let arg_kind = value_kind(semantic_model, arg_ty); - let param_kind = value_kind(semantic_model, param_ty); - if matches!(arg_kind, ValueKind::Other) || matches!(param_kind, ValueKind::Other) { - return false; - } - arg_kind == param_kind - || matches!( - (arg_kind, param_kind), - (ValueKind::Integer, ValueKind::Number) | (ValueKind::Number, ValueKind::Integer) - ) -} - /// Callback parameters vs function arguments: use the explicit work-queue function solver for structural/contravariant checks. fn is_function_like_type(ty: &LuaType) -> bool { match ty { @@ -1187,7 +1039,7 @@ fn function_arg_compatible( let expected = doc_function_of(semantic_model, param_ty); match (actual, expected) { (Some(actual), Some(expected)) => { - functions_compatible(semantic_model, &actual, &expected, bindings).compatible + functions_compatible(semantic_model, &actual, &expected, bindings) } // One side cannot be projected to DocFunction: preserve legacy M0 behavior to avoid false positives for Function/Ref forms. _ => matches!( @@ -1221,92 +1073,45 @@ fn doc_function_of(semantic_model: &SemanticModel<'_>, ty: &LuaType) -> Option, arg_ty: &LuaType, param_ty: &LuaType, ) -> bool { - let arg_text = match arg_ty { - LuaType::StringConst(s) | LuaType::DocStringConst(s) => Some(s.as_ref().to_string()), - LuaType::IntegerConst(i) | LuaType::DocIntegerConst(i) => Some(i.to_string()), - LuaType::BooleanConst(b) | LuaType::DocBooleanConst(b) => Some(b.to_string()), - _ => None, - }; - let Some(arg_text) = arg_text else { + let (LuaType::StringConst(name) | LuaType::DocStringConst(name)) = arg_ty else { return false; }; - let boolean_only = arg_ty.is_boolean(); - literal_values(semantic_model, param_ty, &mut Vec::new(), boolean_only) - .is_some_and(|values| values.iter().any(|value| value == &arg_text)) -} - -/// Literal value text for enum / alias (recursive cycle guard). -fn literal_values( - semantic_model: &SemanticModel<'_>, - ty: &LuaType, - visited: &mut Vec, - boolean_only: bool, -) -> Option> { - match ty { - LuaType::StringConst(s) | LuaType::DocStringConst(s) if !boolean_only => { - Some(vec![s.as_ref().to_string()]) - } - LuaType::IntegerConst(i) | LuaType::DocIntegerConst(i) if !boolean_only => { - Some(vec![i.to_string()]) - } - LuaType::BooleanConst(b) | LuaType::DocBooleanConst(b) => Some(vec![b.to_string()]), - LuaType::Union(union) => { - let mut values = Vec::new(); - for component in union.into_vec() { - if let Some(mut component_values) = - literal_values(semantic_model, &component, visited, boolean_only) - { - values.append(&mut component_values); + let mut pending = vec![param_ty.clone()]; + let mut visited = Vec::new(); + while let Some(ty) = pending.pop() { + match ty { + LuaType::Union(union) => pending.extend(union.into_vec()), + LuaType::Ref(id) | LuaType::Def(id) => { + if visited.contains(&id) { + continue; } - } - (!values.is_empty()).then_some(values) - } - LuaType::Ref(id) | LuaType::Def(id) => { - if visited.contains(id) { - return None; - } - visited.push(id.clone()); - let def = semantic_model.type_def_of(id)?; - if def.kind == TypeDefKind::Alias { - return semantic_model.alias_target(&def).and_then(|target| { - literal_values(semantic_model, &target, visited, boolean_only) - }); - } - // enum: runtime table member names and values. - if def.kind == TypeDefKind::Enum { - let facts = semantic_model.file_facts_of(def.file_id)?; - let decl = facts.decl_named(def.name.as_str())?; - let mut values = Vec::new(); - for member_ref in semantic_model.members_of_owner(&decl.id).iter() { - if !boolean_only { - values.push(member_ref.name.to_string()); - } - if let Some(member_facts) = semantic_model.file_facts_of(member_ref.file_id) - && let Some(member) = member_facts.member_by_id(&member_ref.id) - && let Some(value_syntax) = member.value_syntax - && let Some(node) = semantic_model - .syntax_tree() - .and_then(|tree| value_syntax.to_node_from_root(&tree.get_red_root())) - { - let raw = node.text().to_string(); - // Boolean arguments match values, not member names or quoted text. - if !boolean_only || matches!(raw.as_str(), "true" | "false") { - values.push(raw.trim_matches(['"', '\'']).to_string()); - } - } + visited.push(id.clone()); + let Some(def) = semantic_model.type_def_of(&id) else { + continue; + }; + if def.kind == TypeDefKind::Alias { + pending.extend(semantic_model.alias_target(&def)); + } else if def.kind == TypeDefKind::Enum + && let Some(facts) = semantic_model.file_facts_of(def.file_id) + && let Some(decl) = facts.decl_named(def.name.as_str()) + && semantic_model + .members_of_owner(&decl.id) + .iter() + .any(|member| member.name.as_str() == name.as_ref()) + { + return true; } - return (!values.is_empty()).then_some(values); } - None + _ => {} } - _ => None, } + false } /// Unknown/any/never arguments are not checked. @@ -1317,41 +1122,6 @@ fn skip_arg(arg_ty: &LuaType, param_ty: &LuaType) -> bool { matches!(arg_ty, LuaType::Nil) && matches!(param_ty, LuaType::Any | LuaType::Unknown) } -/// `param?` projects to T|nil: both nil and T are compatible; for union parameters choose a component based on argument shape -/// (`F|integer` + function argument -> take F). -/// Component acceptance for union parameters: constant components of the same kind with different values don't match (`keyof` member union semantics), -/// others fall back to structural compatibility (`string|true`'s boolean matches the boolean component). -fn union_component_accepts( - semantic_model: &SemanticModel<'_>, - arg: &LuaType, - component: &LuaType, -) -> bool { - if arg == component { - return true; - } - match (arg, component) { - (LuaType::StringConst(a), LuaType::StringConst(b)) - | (LuaType::DocStringConst(a), LuaType::DocStringConst(b)) - if a != b => - { - false - } - (LuaType::IntegerConst(a), LuaType::IntegerConst(b)) - | (LuaType::DocIntegerConst(a), LuaType::DocIntegerConst(b)) - if a != b => - { - false - } - (LuaType::BooleanConst(a), LuaType::BooleanConst(b)) - | (LuaType::DocBooleanConst(a), LuaType::DocBooleanConst(b)) - if a != b => - { - false - } - _ => is_compatible(semantic_model, arg, component), - } -} - fn effective_param(param_ty: &LuaType, arg_ty: &LuaType) -> LuaType { if let LuaType::Union(union) = param_ty { let components = union.into_vec(); @@ -1381,9 +1151,6 @@ fn effective_param(param_ty: &LuaType, arg_ty: &LuaType) -> LuaType { { return (*function_component).clone(); } - if let Some(non_nil) = non_nil.first() { - return (*non_nil).clone(); - } } } param_ty.clone() diff --git a/crates/emmylua_code_analysis/src/check/checker/return_type_mismatch.rs b/crates/emmylua_code_analysis/src/check/checker/return_type_mismatch.rs index 9a9af9434..75f646960 100644 --- a/crates/emmylua_code_analysis/src/check/checker/return_type_mismatch.rs +++ b/crates/emmylua_code_analysis/src/check/checker/return_type_mismatch.rs @@ -130,44 +130,6 @@ fn return_compatible( if source == target { return true; } - if let LuaType::Union(source_union) = source { - // Historical compatibility: ordinary union returns still use "any member assignable" (some flow guards do not yet fully narrow impossible components). - // But when the return value contains a function component and the target is a named class, all components must be checked; otherwise the function could be silently treated as a class instance. - let components = source_union.into_vec(); - let target_is_named_class = matches!(target, LuaType::Ref(_) | LuaType::Def(_)); - let has_function = components.iter().any(|component| { - matches!( - component, - LuaType::Function | LuaType::DocFunction(_) | LuaType::Signature(_) - ) - }); - let require_all = target_is_named_class && has_function; - return if require_all { - components - .iter() - .all(|component| return_compatible(semantic_model, component, target)) - } else { - components - .iter() - .any(|component| return_compatible(semantic_model, component, target)) - }; - } - // Integer literal -> number. - if matches!( - source, - LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_) - ) && matches!(target, LuaType::Number) - { - return true; - } - // `table` -> `V[]`. - if let (LuaType::Generic(generic), LuaType::Array(array)) = (source, target) - && generic.get_base_type_id().get_name() == "table" - && let Some(value_ty) = generic.get_params().get(1) - && return_compatible(semantic_model, value_ty, array.get_base()) - { - return true; - } if is_compatible(semantic_model, source, target) { return true; } diff --git a/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs b/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs index 26795936d..936753c40 100644 --- a/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/assign_type_mismatch_test.rs @@ -1,7 +1,731 @@ #[cfg(test)] mod tests { - use crate::check::test::check_source; - use crate::{DiagnosticCode, VirtualWorkspace}; + use crate::check::test::{check_source, check_source_with_emmyrc}; + use crate::{DiagnosticCode, Emmyrc, VirtualWorkspace}; + + #[test] + fn test_numeric_member_guards_use_equivalent_integer_keys() { + for (key, write, accepted) in [ + ("1.0", "", true), + ("1", "t[1.0] = nil", false), + ("'1'", "", false), + ] { + let source = format!( + r#" + ---@param flag boolean + local function read(flag) + local t = {{[1] = flag and 'a' or nil}} + if t[1] then + {write} + ---@type string + local value = t[{key}] + return value + end + end + return read + "# + ); + let mut ws = VirtualWorkspace::new(); + assert_eq!( + ws.has_no_diagnostic(DiagnosticCode::AssignTypeMismatch, &source), + accepted, + "{source}" + ); + } + } + + #[test] + fn test_child_alias_guards_keep_captured_object_identity() { + for (setup, guard, write, read, accepted) in [ + ("", "t.child.value", "", "b.value", true), + ("", "b.value", "", "t.child.value", true), + ("local c = (b)", "c.value", "", "t.child.value", true), + ("", "b.value", "t.child = other", "b.value", true), + ("", "b.value", "t.child = other", "t.child.value", false), + ("t.child = other", "b.value", "", "t.child.value", false), + ("", "t.child.value", "b.value = nil", "t.child.value", false), + ("", "t.child.value", "t.child.value = nil", "b.value", false), + ( + "local a = t", + "b.value", + "a.child = other", + "t.child.value", + false, + ), + ( + "local a = t; a = t", + "b.value", + "a.child = other", + "t.child.value", + false, + ), + ( + "local a = t; local d = a; a = t", + "b.value", + "d.child = other", + "t.child.value", + false, + ), + ( + "", + "b.value", + "if flag then t.child = other end", + "t.child.value", + false, + ), + ("", "b.value", "t[key] = other", "t.child.value", false), + ("", "b.value", "other.value = nil", "t.child.value", true), + ] { + let source = format!( + r#" + ---@class Child + ---@field value string? + ---@class Holder + ---@field child Child + ---@param t Holder + ---@param other Child + ---@param flag boolean + ---@param key string + local function read(t, other, flag, key) + local b = t.child + {setup} + if {guard} then + {write} + ---@type string + local value = {read} + return value + end + end + return read + "# + ); + let mut ws = VirtualWorkspace::new(); + assert_eq!( + ws.has_no_diagnostic(DiagnosticCode::AssignTypeMismatch, &source), + accepted, + "{source}" + ); + } + } + + #[test] + fn test_alias_guards_ignore_only_later_straight_line_assignments() { + for (body, accepted) in [ + ("if b.value then consume(t.value) end; b = other", true), + ("if b.value then consume(t.value) end; t = other", true), + ("b = other; if b.value then consume(t.value) end", false), + ("if b.value then b = other; consume(b.value) end", false), + ( + "while flag do if b.value then consume(t.value) end; b = other end", + false, + ), + ( + "local inner = function() if b.value then consume(t.value) end end; t = other; return inner", + false, + ), + ] { + let source = format!( + r#" + ---@class Holder + ---@field value string? + ---@param value string + local function consume(value) end + ---@param t Holder + ---@param other Holder + ---@param flag boolean + local function read(t, other, flag) + local b = t + {body} + end + return read + "# + ); + let mut ws = VirtualWorkspace::new(); + assert_eq!( + ws.has_no_diagnostic(DiagnosticCode::ParamTypeMismatch, &source), + accepted, + "{source}" + ); + } + } + + #[test] + fn test_string_enum_values_across_assignment_argument_and_return() { + for (value, accepted, argument_accepted) in [ + ("'a'", true, true), + ("'b'", true, true), + ("'first'", false, true), + ("'x'", false, false), + ] { + for (code, usage) in [ + ( + DiagnosticCode::AssignTypeMismatch, + format!("---@type Kind\nlocal value = {value}"), + ), + ( + DiagnosticCode::ParamTypeMismatch, + format!( + "---@param value Kind\nlocal function consume(value) end\nconsume({value})" + ), + ), + ( + DiagnosticCode::ReturnTypeMismatch, + format!("---@return Kind\nlocal function read() return {value} end"), + ), + ] { + let mut ws = VirtualWorkspace::new(); + let source = format!( + "---@enum Kind\nlocal Kind = {{ first = 'a', second = ('b') }}\n{usage}" + ); + // Arguments also accept enum member names. + let accepted = if code == DiagnosticCode::ParamTypeMismatch { + argument_accepted + } else { + accepted + }; + assert_eq!(ws.has_no_diagnostic(code, &source), accepted, "{source}"); + } + } + } + + #[test] + fn test_string_enum_values_from_another_file() { + let mut ws = VirtualWorkspace::new(); + ws.def("---@enum Kind\nlocal Kind = { first = 'a', second = 'b' }"); + for (value, accepted) in [("'a'", true), ("'x'", false)] { + let source = format!("---@type Kind\nlocal value = {value}"); + assert_eq!( + ws.has_no_diagnostic(DiagnosticCode::AssignTypeMismatch, &source), + accepted, + "{source}" + ); + } + } + + #[test] + fn test_value_compatibility_across_assignment_argument_and_return() { + for (source_type, target_type, accepted) in [ + ("'a'", "'b'", false), + ("'1'", "1", false), + ("true", "boolean", true), + ("true", "false", false), + ("string | nil", "string", false), + ("Required?", "Required", false), + ("fun(...: number)", "fun(...: integer)", true), + ("fun(...: integer)", "fun(...: number)", false), + ("integer", "number", true), + ("number", "integer", false), + ("string", "string | nil", true), + ("string | integer", "string | number", true), + ("string | number", "string | integer", false), + ("string | integer", "integer", false), + ("fun(): string", "fun(text: string): string", true), + ("fun(text: string): string", "fun(): string", false), + ("fun(text: string)", "fun(...: string)", false), + ("fun(text: string?)", "fun(...: string)", true), + ( + "fun(first: string, second: string)", + "fun(first: string, ...: string)", + false, + ), + ( + "fun(first: string, second: string?)", + "fun(first: string, ...: string)", + true, + ), + ("fun(...: [string, string]...)", "fun(...: string)", false), + ("fun(...: [string?, string?]...)", "fun(...: string)", true), + ( + "fun(...: [string, integer]...)", + "fun(text: string, count: integer)", + true, + ), + ("fun(...: [string, integer]...)", "fun(text: string)", false), + ("fun(...: [string, integer]...)", "fun()", false), + ("fun(...: [string, integer?]...)", "fun(text: string)", true), + ("fun(...: [string?, integer?]...)", "fun()", true), + ("fun(...: [string, integer])", "fun()", true), + ( + "fun(prefix: boolean, ...: [string, integer]...)", + "fun(prefix: boolean)", + false, + ), + ( + "fun(prefix: boolean, ...: [string, integer]...)", + "fun(prefix: boolean, text: string, count: integer)", + true, + ), + ( + "fun(value: number): integer", + "fun(value: integer): number", + true, + ), + ( + "fun(value: integer): number", + "fun(value: number): integer", + false, + ), + ("[integer]", "{[integer]: number}", true), + ("[integer]", "{[integer]: string}", false), + ("[integer, string]", "{[integer]: number}", false), + ("[integer, string]", "{[integer]: number | string}", true), + ("[integer, string]", "{[1]: number, [2]: string}", true), + ("[integer]", "{[2]: string}", false), + ("[integer]", "{[integer]: number, tag: string}", false), + ("[integer]", "{[integer]: number, tag: string?}", true), + ("integer[]", "{[integer]: number}", true), + ("integer[]", "{[integer]: string}", false), + ("integer[]", "{[integer]: number} | nil", true), + ("integer[]", "{[integer]: string} | nil", false), + ("integer[]", "{[integer]: number, [1]: string}", false), + ("integer[]", "{[number]: number}", true), + ("integer[]", "{[integer | string]: number}", true), + ("integer[]", "{[integer]: number, [number]: string}", false), + ("[integer]", "{[integer]: number, [number]: string}", false), + ("fun(): integer, string", "fun(): number, string", true), + ("fun(): integer, string", "fun(): integer, number", false), + ("fun(): integer, string", "fun(): string, string", false), + ("fun(): integer, string", "fun(): number", true), + ("fun(): integer, string", "fun(): string", false), + ("fun(): integer?, string", "fun(): number?", true), + ("fun(): integer", "fun(): number, string", false), + ("fun(): integer", "fun(): number, string?", true), + ("fun(): integer, string", "fun(): number, string...", true), + ("fun(): integer, string", "fun(): number, number...", false), + ("fun(): integer", "fun(): number, string...", true), + ( + "fun(): integer, integer...", + "fun(): number, number...", + true, + ), + ( + "fun(): integer, string...", + "fun(): number, number...", + false, + ), + ("fun(): integer...", "fun(): number, string", false), + ( + "fun(): integer, integer...", + "fun(): number, number, number", + true, + ), + ( + "fun(): integer, string, boolean", + "fun(): number, string...", + false, + ), + ( + "fun(): integer, string, boolean...", + "fun(): number, string...", + false, + ), + ("fun(): nil", "fun(): nil, nil", true), + ("fun(): any", "fun(): integer, string", true), + ("fun(): never", "fun(): integer, string", true), + ("fun(): [integer, string]...", "fun(): number, string", true), + ("fun(): [integer, integer]...", "fun(): number...", true), + ( + "fun(): [integer, string]...", + "fun(): number, number", + false, + ), + ] { + let source = format!( + r#" + ---@class Required + local _Required = {{}} + ---@param value {source_type} + ---@return {target_type} + local function convert(value) + ---@type {target_type} + local _assigned = value + ---@param argument {target_type} + local function consume(argument) return argument end + consume(value) + return value + end + return convert + "# + ); + let diagnostics = check_source(&source); + let mut actual = diagnostics + .iter() + .map(|d| (d.code, usize::from(d.range.start()))) + .collect::>(); + actual.sort_by_key(|(_, offset)| *offset); + let expected = if accepted { + vec![] + } else { + vec![ + ( + DiagnosticCode::AssignTypeMismatch, + source.find("assigned = value").unwrap() + "assigned = ".len(), + ), + ( + DiagnosticCode::ParamTypeMismatch, + source.find("consume(value)").unwrap() + "consume(".len(), + ), + ( + DiagnosticCode::ReturnTypeMismatch, + source.find("return value").unwrap() + "return ".len(), + ), + ] + }; + assert_eq!( + actual, expected, + "{source_type} -> {target_type}: {diagnostics:#?}" + ); + } + } + + #[test] + fn test_inferred_literal_unions_allow_reassignment() { + for (initial, replacement, wrong) in [ + ("flag and 'open' or 'closed'", "'other'", "false"), + ("flag and 1 or 2", "3", "'wrong'"), + ("flag and 1.5 or 2.5", "3.5", "'wrong'"), + ("flag and 'open' or nil", "'other'", "false"), + ("flag and true or nil", "false", "'wrong'"), + ] { + let source = format!( + r#" + ---@param flag boolean + local function update(flag) + local value = {initial} + value = {replacement} + value = {wrong} + local state = {{ value = {initial} }} + state.value = {replacement} + state.value = {wrong} + return value, state + end + return update + "# + ); + let mut diagnostics = check_source(&source); + diagnostics.sort_by_key(|d| d.range.start()); + let actual = diagnostics + .iter() + .map(|d| (d.code, usize::from(d.range.start()))) + .collect::>(); + assert_eq!( + actual, + ["value", "state.value"] + .map(|target| ( + DiagnosticCode::AssignTypeMismatch, + source.find(&format!("{target} = {wrong}")).unwrap() + + target.len() + + " = ".len(), + )) + .to_vec(), + "{source}\n{diagnostics:#?}" + ); + } + } + + #[test] + fn test_inferred_fields_allow_reassignment_through_aliases_and_imports() { + for imported in [false, true] { + for (annotation, replacement, accepted) in [ + ("", "'other'", true), + ("", "false", false), + ("---@type 'open' | 'closed'", "'other'", false), + ] { + let original = format!( + r#" + ---@type boolean + local flag + local original = {{ + {annotation} + state = flag and 'open' or 'closed' + }} + "# + ); + let mut ws = VirtualWorkspace::new(); + let source = if imported { + ws.def_file("config.lua", &format!("{original}\nreturn original")); + format!("local config = require('config')\nconfig.state = {replacement}") + } else { + format!("{original}\nlocal config = original\nconfig.state = {replacement}") + }; + assert_eq!( + ws.has_no_diagnostic(DiagnosticCode::AssignTypeMismatch, &source), + accepted, + "imported={imported}: {source}" + ); + } + } + } + + #[test] + fn test_integer_member_comparisons_narrow_only_the_matching_key() { + for table in [ + "local t = {[1] = _value, ['1'] = _value}", + "---@class Indexed\n---@field [1] string | integer\n---@field ['1'] string | integer\n---@type Indexed\nlocal t", + ] { + for (read, accepted) in [("t[1]", true), ("t['1']", false)] { + let source = format!( + r#" + ---@param _value string | integer + local function read(_value) + {table} + if t[1] == 'a' then + ---@type string + local _text = {read} + end + end + return read + "# + ); + let diagnostics = check_source(&source); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + if accepted { + vec![] + } else { + vec![DiagnosticCode::AssignTypeMismatch] + }, + "{source}\n{diagnostics:#?}" + ); + } + } + } + + #[test] + fn test_member_guards_follow_stable_local_aliases() { + for (setup, guarded, read, accepted) in [ + ("local b = a", "b", "a", true), + ("local b = a", "a", "b", true), + ("local b = (a); local c = b", "c", "a", true), + ("local b = _other", "b", "a", false), + ("local b = a; b = _other", "b", "a", false), + ("local b = a; a = _other", "b", "a", false), + ("a = _other; local b = a; a = _third", "b", "a", false), + ] { + let source = format!( + r#" + ---@class Node + ---@field value string? + ---@param a Node + ---@param _other Node + ---@param _third Node + local function read(a, _other, _third) + {setup} + if {guarded}.value then + ---@type string + local _text = {read}.value + end + end + return read + "# + ); + let diagnostics = check_source(&source); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + if accepted { + vec![] + } else { + vec![DiagnosticCode::AssignTypeMismatch] + }, + "{source}\n{diagnostics:#?}" + ); + } + } + + #[test] + fn test_member_guards_stop_after_alias_field_writes() { + for (write, accepted) in [ + ("a.value = nil", false), + ("b.value = nil", false), + ("if _flag then b.value = nil end", false), + ("_other.value = nil", true), + ] { + let source = format!( + r#" + ---@class Node + ---@field value string? + ---@param a Node + ---@param _other Node + ---@param _flag boolean + local function read(a, _other, _flag) + local b = a + if b.value then + {write} + ---@type string + local _text = a.value + end + end + return read + "# + ); + let diagnostics = check_source(&source); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + if accepted { + vec![] + } else { + vec![DiagnosticCode::AssignTypeMismatch] + }, + "{source}\n{diagnostics:#?}" + ); + } + } + + #[test] + fn test_member_guards_stop_after_alias_owner_writes() { + for (write, accepted) in [ + ("b.child = {value = nil}", false), + ("b.child = {value = 'safe'}", true), + ("if _flag then b.child = {value = nil} end", false), + ("b[_key] = {value = nil}", false), + ("_other.child = {value = nil}", true), + ] { + let source = format!( + r#" + ---@class Leaf + ---@field value string? + ---@class Tree + ---@field child Leaf + ---@param a Tree + ---@param _other Tree + ---@param _flag boolean + ---@param _key string + local function read(a, _other, _flag, _key) + local b = a + if b.child.value then + {write} + ---@type string + local _text = a.child.value + end + end + return read + "# + ); + let diagnostics = check_source(&source); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + if accepted { + vec![] + } else { + vec![DiagnosticCode::AssignTypeMismatch] + }, + "{source}\n{diagnostics:#?}" + ); + } + } + + #[test] + fn test_array_values_satisfy_numeric_members_of_index_unions() { + for key in ["1 | 'tag'", "Keys", "NestedKeys", "RecursiveKeys"] { + for (expected, accepted) in [("number", true), ("string", false)] { + let source = format!( + r#" + ---@alias Keys 1 | 'tag' + ---@alias NestedKeys Keys | 'meta' + ---@alias RecursiveKeys integer | 'tag' | RecursiveKeys + ---@param value integer[] + ---@return {{[{key}]: {expected}}} + local function convert(value) + ---@type {{[{key}]: {expected}}} + local _assigned = value + ---@param argument {{[{key}]: {expected}}} + local function consume(argument) return argument end + consume(value) + return value + end + return convert + "# + ); + let mut config = Emmyrc::default(); + config.strict.doc_base_const_match_base_type = false; + let mut diagnostics = check_source_with_emmyrc(&source, config); + diagnostics.sort_by_key(|d| d.range.start()); + let codes = diagnostics.iter().map(|d| d.code).collect::>(); + let expected = if accepted { + vec![] + } else { + vec![ + DiagnosticCode::AssignTypeMismatch, + DiagnosticCode::ParamTypeMismatch, + DiagnosticCode::ReturnTypeMismatch, + ] + }; + assert_eq!(codes, expected, "{source}\n{diagnostics:#?}"); + } + } + } + + #[test] + fn test_inferred_literals_can_change_but_declared_literals_cannot() { + let source = r#" + local fraction = 1.5 + fraction = 2.5 + local changed = false + changed = true + local state = { ready = true } + state.ready = false + ---@type false + local fixed = false + fixed = true + return fraction, changed, state, fixed + "#; + let diagnostics = check_source(source); + assert_eq!(diagnostics.len(), 1, "{diagnostics:#?}"); + assert_eq!(diagnostics[0].code, DiagnosticCode::AssignTypeMismatch); + assert_eq!( + usize::from(diagnostics[0].range.start()), + source.find("fixed = true").unwrap() + "fixed = ".len() + ); + } + + #[test] + fn test_parameter_reassignment_preserves_literal_annotations() { + for (parameter_type, value, accepted) in [ + ("false", "true", false), + ("false", "false", true), + ("true", "false", false), + ("boolean", "true", true), + ("'open'", "'closed'", false), + ("'open'", "'open'", true), + ("'open' | 'closed'", "'other'", false), + ("string", "'closed'", true), + ("1", "2", false), + ("1", "1", true), + ("1 | 2", "3", false), + ("integer", "2", true), + ("integer", "1 + 2", true), + ("integer", "1 / 2", false), + ("integer", "1.5 + 1", false), + ] { + let source = format!( + r#" + ---@param value {parameter_type} + local function update(value) + value = {value} + return value + end + return update + "# + ); + let diagnostics = check_source(&source); + let actual = diagnostics + .iter() + .map(|d| (d.code, usize::from(d.range.start()))) + .collect::>(); + let expected = if accepted { + vec![] + } else { + vec![( + DiagnosticCode::AssignTypeMismatch, + source.find("value = ").unwrap() + "value = ".len(), + )] + }; + assert_eq!( + actual, expected, + "{parameter_type} = {value}: {diagnostics:#?}" + ); + } + } #[test] fn test_vararg_packing_keeps_tuple_assignment_checks() { @@ -225,6 +949,10 @@ mod tests { DiagnosticCode::ParamTypeMismatch, source.rfind("values)").unwrap() ), + ( + DiagnosticCode::ParamTypeMismatch, + source.find("consume_numbers(fixed)").unwrap() + "consume_numbers(".len() + ), ], "{diagnostics:#?}" ); @@ -249,7 +977,9 @@ mod tests { ---@type {target} local reassigned reassigned = value - return initialized, reassigned + ---@type ({target})[] + local packed = {{value}} + return initialized, reassigned, packed end return use "#, @@ -263,15 +993,15 @@ mod tests { #[test] fn test_union_assignment_rejects_incompatible_source_members() { - for (source, target) in [ + for (source, target, diagnostic_count) in [ // Reject boolean, even though string and integer are accepted. - ("string|integer|boolean", "string|number"), - // Reject nil because the target does not accept it. - ("true?", "boolean|string"), + ("string|integer|boolean", "string|number", 2), + // Nil cannot supply a required value, but omits a table entry. + ("true?", "boolean|string", 1), // Reject number because it can contain non-integer values. - ("string|number", "string|integer"), + ("string|number", "string|integer", 2), // Reject integer when the target accepts only strings. - ("string|integer", "string"), + ("string|integer", "string", 2), ] { let diagnostics = check_source(&format!( r#" @@ -279,14 +1009,16 @@ mod tests { local function use(value) ---@type {target} local target = value - return target + ---@type ({target})[] + local packed = {{value}} + return target, packed end return use "#, )); assert_eq!( diagnostics.iter().map(|d| d.code).collect::>(), - vec![DiagnosticCode::AssignTypeMismatch], + vec![DiagnosticCode::AssignTypeMismatch; diagnostic_count], "{source} -> {target}: {diagnostics:?}" ); } @@ -1858,11 +2590,7 @@ return t #[test] fn test_return_overload_mixed_guards_keep_assign_narrowing() { - let mut ws = VirtualWorkspace::new(); - - assert!(ws.has_no_diagnostic( - DiagnosticCode::AssignTypeMismatch, - r#" + let source = r#" ---@generic T, E ---@param ok boolean ---@param success T @@ -1882,6 +2610,9 @@ return t local function test(cond) local ok, result = pick(cond, 1, "err") + ---@type integer + local unguarded = result + if ok == false then error(result) end @@ -1893,8 +2624,48 @@ return t ---@type integer local narrowed = result end + "#; + let diagnostics = check_source(source); + assert_eq!( + diagnostics + .iter() + .filter(|d| d.code == DiagnosticCode::AssignTypeMismatch) + .map(|d| usize::from(d.range.start())) + .collect::>(), + vec![source.find("unguarded = result").unwrap() + "unguarded = ".len()], + "{diagnostics:#?}" + ); + } + + #[test] + fn test_overloaded_call_results_use_the_matching_argument_type() { + let mut diagnostics = check_source( + r#" + ---@overload fun(value: string): integer + ---@param _value boolean + ---@return string + local function pick(_value) return 'text' end + + ---@type integer + local assigned = pick(true) + ---@param value integer + local function consume(value) return value end + consume(pick(true)) + ---@return integer + local function forward() return pick(true) end + return assigned, forward "#, - )); + ); + diagnostics.sort_by_key(|diagnostic| diagnostic.range.start()); + assert_eq!( + diagnostics.iter().map(|d| d.code).collect::>(), + vec![ + DiagnosticCode::AssignTypeMismatch, + DiagnosticCode::ParamTypeMismatch, + DiagnosticCode::ReturnTypeMismatch, + ], + "{diagnostics:#?}" + ); } #[test] diff --git a/crates/emmylua_code_analysis/src/check/test/need_check_nil_test.rs b/crates/emmylua_code_analysis/src/check/test/need_check_nil_test.rs index 2257b7002..9656297e3 100644 --- a/crates/emmylua_code_analysis/src/check/test/need_check_nil_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/need_check_nil_test.rs @@ -70,7 +70,14 @@ mod test { "if t.items[1] then t.items[_index] = nil; print(t.items[1]:sub(1)) end", "if t.items[1] then if _index == 1 then t.items = {} end; print(t.items[1]:sub(1)) end", ] { - check_class_array_guard("string", body, &[DiagnosticCode::NeedCheckNil]); + check_class_array_guard( + "string", + body, + &[ + DiagnosticCode::ParamTypeMismatch, + DiagnosticCode::NeedCheckNil, + ], + ); } } @@ -85,12 +92,23 @@ mod test { "if (t.items[1] ~= nil) ~= nil then print(t.items[1]:sub(1)) end", "if (not not t.items[1]) ~= nil then print(t.items[1]:sub(1)) end", ] { - check_class_array_guard("string", body, &[DiagnosticCode::NeedCheckNil]); + check_class_array_guard( + "string", + body, + &[ + DiagnosticCode::ParamTypeMismatch, + DiagnosticCode::NeedCheckNil, + ], + ); } check_class_array_guard( "string", "if t.items[1] then local t = _other; print(t.items[1]:sub(1)) end", - &[DiagnosticCode::RedefinedLocal, DiagnosticCode::NeedCheckNil], + &[ + DiagnosticCode::RedefinedLocal, + DiagnosticCode::ParamTypeMismatch, + DiagnosticCode::NeedCheckNil, + ], ); } diff --git a/crates/emmylua_code_analysis/src/check/test/param_type_check_test.rs b/crates/emmylua_code_analysis/src/check/test/param_type_check_test.rs index a8132e312..97375a75a 100644 --- a/crates/emmylua_code_analysis/src/check/test/param_type_check_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/param_type_check_test.rs @@ -1178,6 +1178,140 @@ mod test { )); } + #[test] + fn test_recursive_variadic_inference_preserves_other_diagnostics() { + let source = r#" + ---@param ... boolean + local function recurse(...) + if ... then return 1, 'done' end + return 1, recurse(true) + end + local result = recurse(false) + ---@type string + local wrong = 42 + return result, wrong + "#; + let diagnostics = check_source(source); + assert_eq!(diagnostics.len(), 1, "{diagnostics:#?}"); + assert_eq!(diagnostics[0].code, DiagnosticCode::AssignTypeMismatch); + assert_eq!( + usize::from(diagnostics[0].range.start()), + source.find("wrong = 42").unwrap() + "wrong = ".len() + ); + } + + #[test] + fn test_generic_callback_keeps_prefixed_variadic_results() { + let source = r#" + ---@generic First, Rest + ---@param callback fun(): First, Rest... + ---@return First, Rest... + local function wrap(callback) return callback() end + ---@param ... integer + local function values(...) return true, ... end + local packed = {wrap(function() return values(1, 2, 3) end)} + ---@type boolean + local first = packed[1] + ---@type integer + local valid = packed[3] + ---@type string? + local wrong = packed[3] + return first, valid, wrong + "#; + let diagnostics = check_source(source); + assert_eq!(diagnostics.len(), 1, "{diagnostics:#?}"); + assert_eq!(diagnostics[0].code, DiagnosticCode::AssignTypeMismatch); + assert_eq!( + usize::from(diagnostics[0].range.start()), + source.rfind("packed[3]").unwrap() + ); + } + + #[test] + fn test_long_argument_expression_keeps_type_checks() { + let expression = vec!["1"; 1500].join(" + "); + let wrong_number = format!("text_value({expression})"); + let wrong_string = "number_value(1 --[[@as string]])"; + let source = format!( + r#" + ---@param value number + local function number_value(value) return value end + ---@param value string + local function text_value(value) return value end + local unrelated = 1 --[[@as integer]] + number_value(unrelated) + number_value({expression}) + {wrong_number} + text_value(1 --[[@as string]]) + {wrong_string} + "# + ); + let diagnostics = check_source(&source); + assert_eq!(diagnostics.len(), 2, "{diagnostics:#?}"); + let actual: Vec<_> = diagnostics + .iter() + .map(|d| (d.code, usize::from(d.range.start()))) + .collect(); + assert_eq!( + actual, + vec![ + ( + DiagnosticCode::ParamTypeMismatch, + source.find(&wrong_number).unwrap() + "text_value(".len(), + ), + ( + DiagnosticCode::ParamTypeMismatch, + source.find(wrong_string).unwrap() + "number_value(".len(), + ), + ] + ); + } + + #[test] + fn test_guarded_member_tracks_each_access_and_owner_assignment() { + let source = r#" + ---@class A + ---@field type 'point' + ---@field handle string? + ---@class B + ---@field type 'unit' + ---@field handle integer? + ---@param text string + local function consume(text) return text end + ---@param target A | B + ---@param other A + local function check(target, other) + if target.handle then + if target.type == 'point' then + consume(target.handle) + consume(other.handle) + target.handle = nil + consume(target.handle) + end + target = other + if target.type == 'point' then + consume(target.handle) + end + end + end + return check + "#; + let diagnostics = check_source(source); + let mut actual = diagnostics + .iter() + .filter(|d| d.code == DiagnosticCode::ParamTypeMismatch) + .map(|d| usize::from(d.range.start())) + .collect::>(); + actual.sort_unstable(); + let expected = [ + "consume(other.handle)", + "consume(target.handle)\n end", + "consume(target.handle)\n end\n end", + ] + .map(|text| source.find(text).unwrap() + "consume(".len()); + assert_eq!(actual, expected, "{diagnostics:#?}"); + } + #[test] fn test_flow_alias() { let mut ws = VirtualWorkspace::new_with_init_std_lib(); @@ -1185,7 +1319,8 @@ mod test { emmyrc.strict.array_index = false; ws.analysis.update_config(Arc::new(emmyrc)); - assert!(ws.has_no_diagnostic( + // With no arguments, callback stays nil; event(callback) must warn. + assert!(!ws.has_no_diagnostic( DiagnosticCode::ParamTypeMismatch, r#" ---@class Trigger @@ -1227,19 +1362,130 @@ mod test { )); } + #[test] + fn test_inferred_integer_returns_keep_argument_and_return_checks() { + for (body, target, accepted) in [ + ("return 1", "integer", true), + ("local value = 1; return value", "integer", true), + ("return 7 // 3", "integer", true), + ("return 7 % 3", "integer", true), + ("return 1 + 2", "integer", true), + ( + "local value = 1; if _flag then return value end; return nil", + "integer?", + true, + ), + ( + "local value = 1; if _flag then return value end; return", + "integer?", + true, + ), + ( + "local value = 1; if _flag then return value end; return", + "integer", + false, + ), + ( + "local value = 1; if _flag then return value end; return 'text'", + "integer | string", + true, + ), + ( + "if _flag then return 1 / 2 end; return nil", + "integer?", + false, + ), + ( + "if _flag then return 1 / 2 end; return 'text'", + "integer | string", + false, + ), + ( + "---@return number\nlocal function number() return 1 end\nreturn number()", + "integer", + false, + ), + ("return 1 / 2", "integer", false), + ("return 9223372036854775807 + (1 + 0)", "integer", false), + ("return 9223372036854775807 + (-1)", "integer", true), + ] { + let source = format!( + r#" + ---@param _flag boolean + local function inferred(_flag) {body} end + ---@param value {target} + local function consume(value) return value end + consume(inferred(true)) + ---@return {target} + local function forward() return inferred(true) end + return forward + "# + ); + let diagnostics = check_source(&source); + let mut actual = diagnostics + .iter() + .map(|d| (d.code, usize::from(d.range.start()))) + .collect::>(); + actual.sort_by_key(|(_, offset)| *offset); + let expected = if accepted { + vec![] + } else { + vec![ + ( + DiagnosticCode::ParamTypeMismatch, + source.find("inferred(true)").unwrap(), + ), + ( + DiagnosticCode::ReturnTypeMismatch, + source.rfind("inferred(true)").unwrap(), + ), + ] + }; + assert_eq!(actual, expected, "{source}\n{diagnostics:#?}"); + } + } + + #[test] + fn test_division_constants_use_floating_point_precision() { + for (expression, expected, accepted) in [ + ("9007199254740993 / 1", "9007199254740992", true), + ("9007199254740993 / 1", "9007199254740993", false), + ("9007199254740993 / 3", "integer", false), + ("9223372036854775807 / 1", "9223372036854775807", false), + ("45 / 3", "15", true), + ("-6 / 2", "-3", true), + ] { + let mut ws = VirtualWorkspace::new(); + let source = format!( + r#" + ---@param value {expected} + local function consume(value) end + consume({expression}) + "# + ); + assert_eq!( + ws.has_no_diagnostic(DiagnosticCode::ParamTypeMismatch, &source), + accepted, + "{source}" + ); + } + } + #[test] fn test_int() { - let mut ws = VirtualWorkspace::new(); - assert!(ws.has_no_diagnostic( - DiagnosticCode::ParamTypeMismatch, - r#" + let source = r#" ---@param count integer - local function loop_count(count) - end - + local function loop_count(count) return count end loop_count(45 / 3) - "# - )); + loop_count(45 / 2) + "#; + let diagnostics = check_source(source); + assert_eq!(diagnostics.len(), 1, "{diagnostics:#?}"); + assert_eq!(diagnostics[0].code, DiagnosticCode::ParamTypeMismatch); + assert_eq!( + usize::from(diagnostics[0].range.start()), + source.find("45 / 2").unwrap() + ); } #[test] diff --git a/crates/emmylua_code_analysis/src/check/test/redundant_parameter_test.rs b/crates/emmylua_code_analysis/src/check/test/redundant_parameter_test.rs index 61a9aaea1..52f1d553c 100644 --- a/crates/emmylua_code_analysis/src/check/test/redundant_parameter_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/redundant_parameter_test.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod test { + use crate::check::test::check_source; use crate::{DiagnosticCode, VirtualWorkspace}; #[test] @@ -120,20 +121,44 @@ mod test { #[test] fn test_variadic_call_operator() { - let mut ws = VirtualWorkspace::new(); - let source = r#" - ---@class Callable - ---@operator call(string...): string - - ---@type Callable - local callable - - callable("a", "b") - callable("a", 1) - "#; - - assert!(ws.has_no_diagnostic(DiagnosticCode::RedundantParameter, source)); - assert!(!ws.has_no_diagnostic(DiagnosticCode::ParamTypeMismatch, source)); + for (class, instance) in [ + ("Callable", "Callable"), + ("Callable", "Callable"), + ] { + let source = format!( + r#" + ---@class {class} + ---@operator call(string...): string + + ---@type {instance} + local callable + + callable() + callable("a", "b", "c") + callable("a", 1) + "# + ); + let diagnostics = check_source(&source); + assert_eq!( + diagnostics + .iter() + .filter(|d| { + matches!( + d.code, + DiagnosticCode::ParamTypeMismatch + | DiagnosticCode::MissingParameter + | DiagnosticCode::RedundantParameter + ) + }) + .map(|d| (d.code, usize::from(d.range.start()))) + .collect::>(), + vec![( + DiagnosticCode::ParamTypeMismatch, + source.find("1)").unwrap() + )], + "{class}: {diagnostics:#?}" + ); + } } #[test] diff --git a/crates/emmylua_code_analysis/src/check/test/return_type_mismatch_test.rs b/crates/emmylua_code_analysis/src/check/test/return_type_mismatch_test.rs index 6c5b646c7..9294c5ed3 100644 --- a/crates/emmylua_code_analysis/src/check/test/return_type_mismatch_test.rs +++ b/crates/emmylua_code_analysis/src/check/test/return_type_mismatch_test.rs @@ -1,10 +1,38 @@ #[cfg(test)] mod tests { + use crate::check::test::check_source; use crate::{DiagnosticCode, VirtualWorkspace}; + #[test] + fn test_parenthesized_member_guards_narrow_return_value() { + for (guard, access) in [ + ("(box).value", "box.value"), + ("box.value", "(box).value"), + ("((box)).value", "(box).value"), + ] { + let mut ws = VirtualWorkspace::new(); + let source = format!( + r#" + ---@class Box + ---@field value string? + ---@param box Box + ---@return string + local function read(box) + if {guard} then return {access} end + return '' + end + "# + ); + assert!( + ws.has_no_diagnostic(DiagnosticCode::ReturnTypeMismatch, &source), + "{source}" + ); + } + } + #[test] fn test_issue_1226() { - let diagnostics = crate::check::test::check_source( + let diagnostics = check_source( r#" ---@return boolean local function test1() @@ -59,6 +87,34 @@ mod tests { } } + #[test] + fn test_and_or_function_guard_needs_a_truthy_result() { + for (result_type, accepted) in [("string", true), ("string?", false)] { + let source = format!( + r#" + ---@param value string | fun(): {result_type} + ---@return string + local function text(value) + return type(value) == 'function' and value() or value + end + return text + "# + ); + let diagnostics = check_source(&source); + let actual = diagnostics + .iter() + .filter(|d| d.code == DiagnosticCode::ReturnTypeMismatch) + .map(|d| usize::from(d.range.start())) + .collect::>(); + let expected = if accepted { + vec![] + } else { + vec![source.find("type(value)").unwrap()] + }; + assert_eq!(actual, expected, "{diagnostics:#?}"); + } + } + #[test] fn test_issue_242() { let mut ws = VirtualWorkspace::new_with_init_std_lib(); diff --git a/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs b/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs index 2308db9c1..f0a08d8f4 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/def/lua_type/types/complex.rs @@ -145,6 +145,25 @@ impl LuaFunctionType { self.is_variadic } + /// Parameter slots, including the receiver and any expanding or repeated tail. + pub fn get_variadic_params(&self) -> VariadicType { + let mut types: Vec<_> = self + .params + .iter() + .map(|(_, ty)| ty.clone().unwrap_or(LuaType::Unknown)) + .collect(); + if self.is_colon_define { + types.insert(0, LuaType::SelfInfer); + } + if self.is_variadic + && let Some(last) = types.last_mut() + && !matches!(last, LuaType::Variadic(_)) + { + *last = LuaType::Variadic(Rc::new(VariadicType::Repeat(last.clone()))); + } + VariadicType::Multi(types) + } + pub fn get_variadic_ret(&self) -> VariadicType { if let LuaType::Variadic(variadic) = &self.ret { return variadic.deref().clone(); diff --git a/crates/emmylua_code_analysis/src/semantic_db/facade.rs b/crates/emmylua_code_analysis/src/semantic_db/facade.rs index a551dce14..4a05422a9 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/facade.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/facade.rs @@ -703,6 +703,12 @@ impl<'db> AnalysisView<'db> { self.project_shell_in(file_id, base, generic_names), ))) } + TypeCandidate::Multi(slots) => LuaType::Variadic(Rc::new(VariadicType::Multi( + slots + .iter() + .map(|slot| self.project_shell_in(file_id, slot, generic_names)) + .collect(), + ))), TypeCandidate::Variadic(base) => LuaType::Variadic(Rc::new(VariadicType::Base( self.project_shell_in(file_id, base, generic_names), ))), diff --git a/crates/emmylua_code_analysis/src/semantic_db/flow/flow_tree.rs b/crates/emmylua_code_analysis/src/semantic_db/flow/flow_tree.rs index e4e9d33b7..932d14aab 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/flow/flow_tree.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/flow/flow_tree.rs @@ -28,6 +28,8 @@ pub struct FlowTree { decl_assignments: HashMap>, /// Sorted `(assignment statement start, flow id)` entries per assigned member. member_assignments: HashMap>, + /// Assignment statements sorted by start, including writes without member IDs. + assignments: Vec<(TextRange, FlowId)>, /// Sorted `(position, flow id)` for `DeclPosition` nodes. decl_positions: Vec<(TextSize, FlowId)>, /// Sorted `(condition position, flow id)` for true/false condition nodes. @@ -61,6 +63,7 @@ impl FlowTree { ) -> Self { let mut decl_assignments: HashMap> = HashMap::new(); let mut member_assignments: HashMap> = HashMap::new(); + let mut assignments = Vec::new(); let mut decl_positions: Vec<(TextSize, FlowId)> = Vec::new(); let mut condition_nodes: Vec<(TextSize, FlowId)> = Vec::new(); let mut cast_nodes: Vec<(TextSize, FlowId)> = Vec::new(); @@ -91,7 +94,9 @@ impl FlowTree { } match &node.kind { FlowNodeKind::Assignment(assign_ptr) => { - let assign_pos = assign_ptr.get_syntax_id().get_range().start(); + let range = assign_ptr.get_syntax_id().get_range(); + assignments.push((range, flow_id)); + let assign_pos = range.start(); for effect in node_effects .get(&flow_id) .map(|v| v.as_slice()) @@ -133,6 +138,7 @@ impl FlowTree { for entries in member_assignments.values_mut() { entries.sort_by_key(|(pos, _)| *pos); } + assignments.sort_by_key(|(range, _)| range.start()); decl_positions.sort_by_key(|(pos, _)| *pos); condition_nodes.sort_by_key(|(pos, _)| *pos); cast_nodes.sort_by_key(|(pos, _)| *pos); @@ -159,6 +165,7 @@ impl FlowTree { binding_starts, decl_assignments, member_assignments, + assignments, decl_positions, condition_nodes, cast_nodes, @@ -285,6 +292,24 @@ impl FlowTree { idx.checked_sub(1).map(|idx| entries[idx]) } + /// Assignment statements fully contained in the inclusive position interval. + pub fn assignments_between( + &self, + start: TextSize, + end: TextSize, + ) -> impl Iterator + '_ { + let first = self + .assignments + .partition_point(|(range, _)| range.start() < start); + let last = self + .assignments + .partition_point(|(range, _)| range.start() <= end); + self.assignments[first.min(last)..last] + .iter() + .copied() + .filter(move |(range, _)| range.end() <= end) + } + /// Latest `(assignment position, flow id)` for `member` at or before `offset`. pub fn latest_member_assignment( &self, diff --git a/crates/emmylua_code_analysis/src/semantic_db/flow/stats.rs b/crates/emmylua_code_analysis/src/semantic_db/flow/stats.rs index c9bcf1436..efa9d7bc0 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/flow/stats.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/flow/stats.rs @@ -154,6 +154,34 @@ pub(super) fn finish_entered_loop_post_flow( } } +/// Read a numeric literal through parentheses and unary signs, without queries. +pub(super) fn static_number_value(expr: &LuaExpr) -> Option { + let mut expr = expr.clone(); + let mut negate = false; + loop { + expr = match expr { + LuaExpr::ParenExpr(paren) => paren.get_expr()?, + LuaExpr::UnaryExpr(unary) if unary.get_op_token()?.get_op() == UnaryOperator::OpUnm => { + negate = !negate; + unary.get_expr()? + } + LuaExpr::LiteralExpr(literal) => { + let LuaLiteralToken::Number(number) = literal.get_literal()? else { + return None; + }; + let value = match number.get_number_value() { + NumberResult::Int(value) => value, + NumberResult::Uint(value) => value as i64, + NumberResult::Float(value) => return Some(if negate { -value } else { value }), + NumberResult::Number => return None, + }; + return Some(if negate { value.checked_neg()? } else { value } as f64); + } + _ => return None, + }; + } +} + /// Static reachability judgement for loops; only accepts straightforward literal truthiness. /// /// This is not full constant evaluation or path inference. Dynamic expressions and complex @@ -177,19 +205,3 @@ pub(super) fn static_literal_truthiness(expr: &LuaExpr) -> Option { _ => None, } } - -pub(super) fn static_number_value(expr: &LuaExpr) -> Option { - match expr { - LuaExpr::LiteralExpr(literal_expr) => match literal_expr.get_literal()? { - LuaLiteralToken::Number(number_token) => match number_token.get_number_value() { - NumberResult::Int(value) => Some(value as f64), - NumberResult::Uint(value) => Some(value as f64), - NumberResult::Float(value) => Some(value), - NumberResult::Number => None, - }, - _ => None, - }, - LuaExpr::ParenExpr(paren_expr) => static_number_value(&paren_expr.get_expr()?), - _ => None, - } -} diff --git a/crates/emmylua_code_analysis/src/semantic_db/query.rs b/crates/emmylua_code_analysis/src/semantic_db/query.rs index 4e99494d7..6cfa5445c 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/query.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/query.rs @@ -19,7 +19,9 @@ use super::exports::{ }; use super::facts::{FactsBuilder, FileFacts}; use super::inputs::WorkspaceRoot; -use super::types::{LiteralShell, PrimitiveType, TableId, TypeCandidate, TypeShell}; +use super::types::{ + LiteralShell, PrimitiveType, TableId, TypeCandidate, TypeShell, checked_integer_arithmetic, +}; use crate::Emmyrc; use crate::FileId; use emmylua_parser::{ @@ -2563,8 +2565,8 @@ pub(crate) fn decl_references( // L3 semantics: signature returns (with cycle convergence) // ────────────────────────────────────────────── -/// Per-slot function return types. Doc annotations take priority (one slot per `---@return`); -/// otherwise scan the function body's `return` statements and merge by slot. Mutual recursion converges via semantic fixed point. +/// Doc annotations use one shell per return slot. Inferred returns use one shell +/// containing complete return-row alternatives, so shorter rows retain missing values. pub(crate) fn signature_returns( db: &SemanticDatabase, file: FileId, @@ -2640,28 +2642,15 @@ pub(crate) fn signature_returns( .is_some_and(|owner| owner.get_syntax_id() == closure_syntax) }) .collect(); - let multi_path_returns = returns.len() > 1; - let mut slots: Vec = Vec::new(); + let mut rows = TypeShell::unknown(); for ret in returns { let exprs: Vec<_> = ret.get_expr_list().collect(); + let mut slots = Vec::with_capacity(exprs.len()); for (index, expr) in exprs.iter().enumerate() { let shell = match &expr { LuaExpr::NameExpr(name) if name.get_name_text().as_deref() == Some("self") => { method_self_return_shell(&facts, closure_syntax) } - // Preserve integer literal precision when merging multi-path returns (a `return 1` branch should not be widened to number). - LuaExpr::LiteralExpr(literal) - if multi_path_returns - && let Some(LuaLiteralToken::Number(number)) = literal.get_literal() => - { - match number.get_number_value() { - emmylua_parser::NumberResult::Int(_) - | emmylua_parser::NumberResult::Uint(_) => { - Some(TypeShell::from_primitive(PrimitiveType::Integer)) - } - _ => None, - } - } _ => None, } .unwrap_or_else(|| expr_type_of(db, file, config, expr.get_syntax_id())); @@ -2670,20 +2659,24 @@ pub(crate) fn signature_returns( } else { shell }; - if let Some(slot) = slots.get_mut(index) { - slot.merge(&shell); - } else { - slots.push(shell); - } + slots.push(shell); } + let row = if slots.len() == 1 { + slots.pop().expect("len checked") + } else { + TypeShell { + candidates: vec![TypeCandidate::Multi(slots)], + } + }; + rows.merge(&row); } // When there is no explicit return annotation and the body cannot infer a type, keep member returns from class-field declarations. - if slots.is_empty() || slots.iter().all(TypeShell::is_unknown) { + if rows.is_unknown() { if let Some(expected) = member_expected_returns(db, &facts, file, config, closure_syntax) { return expected; } } - slots + vec![rows] } /// When a member implementation function (`function Test.e()`) has no `---@return`, use the return type @@ -3123,11 +3116,11 @@ fn expr_type_node( BinaryOperator::OpAdd | BinaryOperator::OpSub | BinaryOperator::OpMul - | BinaryOperator::OpDiv | BinaryOperator::OpIDiv | BinaryOperator::OpMod - | BinaryOperator::OpPow, - ) => TypeShell::from_primitive(PrimitiveType::Number), + | BinaryOperator::OpDiv, + ) => expr_type_arithmetic(db, facts, file, config, LuaExpr::BinaryExpr(binary)), + Some(BinaryOperator::OpPow) => TypeShell::from_primitive(PrimitiveType::Number), _ => TypeShell::unknown(), } } @@ -3135,6 +3128,8 @@ fn expr_type_node( let op = unary.get_op_token().map(|token| token.get_op()); if op == Some(UnaryOperator::OpNot) { TypeShell::from_primitive(PrimitiveType::Boolean) + } else if op == Some(UnaryOperator::OpUnm) { + expr_type_arithmetic(db, facts, file, config, LuaExpr::UnaryExpr(unary)) } else { unary .get_expr() @@ -3150,6 +3145,157 @@ fn expr_type_node( } } +/// Evaluate numeric categories and checked constants in one source-bounded walk. +/// Keeping both on the same task stack avoids native recursion and repeated subtree scans. +fn expr_type_arithmetic( + db: &SemanticDatabase, + facts: &FileFacts, + file: FileId, + config: &Emmyrc, + expr: LuaExpr, +) -> TypeShell { + enum Task { + Expr(LuaExpr), + Binary(BinaryOperator), + Negate, + } + let mut tasks = vec![Task::Expr(expr)]; + let mut values: Vec<(TypeShell, Option)> = Vec::new(); + while let Some(task) = tasks.pop() { + match task { + Task::Expr(LuaExpr::ParenExpr(paren)) => { + let Some(expr) = paren.get_expr() else { + return TypeShell::unknown(); + }; + tasks.push(Task::Expr(expr)); + } + Task::Expr(LuaExpr::UnaryExpr(unary)) + if unary + .get_op_token() + .is_some_and(|op| op.get_op() == UnaryOperator::OpUnm) => + { + let Some(expr) = unary.get_expr() else { + return TypeShell::unknown(); + }; + tasks.push(Task::Negate); + tasks.push(Task::Expr(expr)); + } + Task::Expr(LuaExpr::BinaryExpr(binary)) + if binary.get_op_token().is_some_and(|op| { + matches!( + op.get_op(), + BinaryOperator::OpAdd + | BinaryOperator::OpSub + | BinaryOperator::OpMul + | BinaryOperator::OpDiv + | BinaryOperator::OpIDiv + | BinaryOperator::OpMod + ) + }) => + { + let Some((left, right)) = binary.get_exprs() else { + return TypeShell::unknown(); + }; + tasks.push(Task::Binary( + binary.get_op_token().expect("guard checked").get_op(), + )); + tasks.push(Task::Expr(right)); + tasks.push(Task::Expr(left)); + } + Task::Expr(expr) => { + let constant = match &expr { + LuaExpr::LiteralExpr(literal) => match literal.get_literal() { + Some(LuaLiteralToken::Number(number)) => match number.get_number_value() { + emmylua_parser::NumberResult::Int(value) => Some(value), + emmylua_parser::NumberResult::Uint(value) => Some(value as i64), + _ => None, + }, + _ => None, + }, + _ => None, + }; + values.push(( + expr_type(db, facts, file, config, expr).first_result(), + constant, + )); + } + Task::Binary(op) => { + let (right, right_constant) = values.pop().expect("right operand evaluated"); + let (left, left_constant) = values.pop().expect("left operand evaluated"); + let constants = left_constant.zip(right_constant); + let constant = + constants.and_then(|(left, right)| checked_integer_arithmetic(op, left, right)); + let integer = |candidate: &TypeCandidate| { + matches!( + candidate, + TypeCandidate::Primitive(PrimitiveType::Integer) + | TypeCandidate::Literal(LiteralShell::Integer(_)) + ) + }; + let mut shell = TypeShell::unknown(); + if op == BinaryOperator::OpDiv { + shell = TypeShell::from_primitive(if constant.is_some() { + PrimitiveType::Integer + } else { + PrimitiveType::Number + }); + } else if matches!( + op, + BinaryOperator::OpAdd | BinaryOperator::OpSub | BinaryOperator::OpMul + ) && constants.is_some() + && constant.is_none() + { + shell = TypeShell::from_primitive(PrimitiveType::Number); + } else if !left.is_unknown() && !right.is_unknown() { + // Map operand pairs so growing candidate sets remain monotone. + if left.candidates.iter().any(integer) && right.candidates.iter().any(integer) { + shell.merge(&TypeShell::from_primitive(PrimitiveType::Integer)); + } + if left.candidates.iter().any(|ty| !integer(ty)) + || right.candidates.iter().any(|ty| !integer(ty)) + { + shell.merge(&TypeShell::from_primitive(PrimitiveType::Number)); + } + } + values.push((shell, constant)); + } + Task::Negate => { + let (shell, constant) = values.pop().expect("operand evaluated"); + let negated = constant.and_then(i64::checked_neg); + if constant.is_some() && negated.is_none() { + values.push((TypeShell::from_primitive(PrimitiveType::Number), None)); + continue; + } + let mut result = TypeShell::unknown(); + for candidate in shell.candidates { + let candidate = match candidate { + TypeCandidate::Literal(LiteralShell::Float(bits)) => { + TypeCandidate::Literal(LiteralShell::Float( + (-f64::from_bits(bits)).to_bits(), + )) + } + TypeCandidate::Literal(LiteralShell::Integer(value)) => { + match value.checked_neg() { + Some(value) => TypeCandidate::Literal(LiteralShell::Integer(value)), + None => TypeCandidate::Primitive(PrimitiveType::Number), + } + } + candidate => candidate, + }; + result.merge(&TypeShell { + candidates: vec![candidate], + }); + } + values.push((result, negated)); + } + } + } + values + .pop() + .map(|(shell, _)| shell) + .unwrap_or_else(TypeShell::unknown) +} + fn expr_type_index( db: &SemanticDatabase, facts: &FileFacts, @@ -3258,12 +3404,29 @@ fn expr_type_call( } } match callee_closure_syntax(facts, prefix) { - Some(closure_syntax) => signature_return(db, file, config, closure_syntax), + Some(closure_syntax) => { + let mut slots = signature_returns(db, file, config, closure_syntax); + if slots.is_empty() { + TypeShell::unknown() + } else if slots.len() == 1 { + slots.pop().expect("len checked") + } else { + TypeShell { + candidates: vec![TypeCandidate::Multi(slots)], + } + } + } None => { // Function-value call: if callee type is fun(...), take its return type (including generic substitution). for candidate in &prefix_shell.candidates { if let TypeCandidate::Function(fun) = candidate { - return fun.returns.clone(); + return if fun.returns_multi.len() > 1 { + TypeShell { + candidates: vec![TypeCandidate::Multi(fun.returns_multi.clone())], + } + } else { + fun.returns.clone() + }; } } TypeShell::unknown() @@ -3365,6 +3528,16 @@ fn substitute_generics( base, params, args, ))); } + TypeCandidate::Multi(slots) => { + out.merge(&TypeShell { + candidates: vec![TypeCandidate::Multi( + slots + .iter() + .map(|slot| substitute_generics(slot, params, args)) + .collect(), + )], + }); + } TypeCandidate::Tuple(types) => { out.merge(&TypeShell::from_tuple( types @@ -3409,7 +3582,7 @@ fn literal_type(literal: &LuaLiteralExpr) -> TypeShell { Some(LuaLiteralToken::String(_)) => TypeShell::from_primitive(PrimitiveType::String), Some(LuaLiteralToken::Number(number)) => match number.get_number_value() { emmylua_parser::NumberResult::Int(_) | emmylua_parser::NumberResult::Uint(_) => { - TypeShell::from_primitive(PrimitiveType::Number) + TypeShell::from_primitive(PrimitiveType::Integer) } emmylua_parser::NumberResult::Float(f) => { TypeShell::from_literal(LiteralShell::Float(f.to_bits())) diff --git a/crates/emmylua_code_analysis/src/semantic_db/tests.rs b/crates/emmylua_code_analysis/src/semantic_db/tests.rs index 5b1f4dda6..f7d5857bc 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/tests.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/tests.rs @@ -147,7 +147,7 @@ fn test_decl_type_from_literal_initializer() { let a = decl_local(&facts, "a"); assert_primitive( &db.analysis().decl_type(fid, a).expect("type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -160,7 +160,7 @@ fn test_decl_type_name_resolution_chain() { let y = decl_local(&facts, "y"); assert_primitive( &db.analysis().decl_type(fid, y).expect("type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -174,7 +174,7 @@ fn test_decl_type_self_reference_cycle_fixpoint() { let a = decl_local(&facts, "a"); assert_primitive( &db.analysis().decl_type(fid, a).expect("type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -188,11 +188,11 @@ fn test_decl_type_forward_reference_chain() { let b = decl_local(&facts, "b"); assert_primitive( &db.analysis().decl_type(fid, a).expect("a type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); assert_primitive( &db.analysis().decl_type(fid, b).expect("b type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -534,7 +534,7 @@ fn test_member_type() { assert_primitive( &db.analysis().member_type(fid, find("foo")).expect("foo"), - PrimitiveType::Number, + PrimitiveType::Integer, ); assert_primitive( &db.analysis().member_type(fid, find("bar")).expect("bar"), @@ -561,7 +561,7 @@ fn test_member_type_via_name_reference() { .expect("member a"); assert_primitive( &db.analysis().member_type(fid, a_local).expect("a type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -579,7 +579,7 @@ fn test_member_global_root() { #[test] fn test_member_chain_resolution() { let mut db = setup(); - // T.a = T.b; T.b = 1 → a resolves to Number through the IndexExpr member chain. + // T.a = T.b; T.b = 1 → a resolves to Integer through the IndexExpr member chain. let fid = set_test_file(&mut db, 1, "a.lua", "local T = {}\nT.a = T.b\nT.b = 1"); let facts = db.analysis().file_facts(fid).expect("facts"); @@ -591,7 +591,7 @@ fn test_member_chain_resolution() { .expect("member a"); assert_primitive( &db.analysis().member_type(fid, a_local).expect("a type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -631,7 +631,7 @@ fn test_member_phase2_cross_file_resolution() { &db.analysis() .member_type(x_member.file_id, x_member.id.clone()) .expect("x type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -741,7 +741,7 @@ fn test_phase2_cross_file_member_in_expr_type() { let y = decl_local(&facts, "y"); assert_primitive( &db.analysis().decl_type(fid, y).expect("y type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -921,23 +921,23 @@ fn test_require_module_resolution() { let v = decl_local(&facts, "v"); assert_primitive( &db.analysis().decl_type(fid, v).expect("v type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } #[test] fn test_require_module_subdir_suffix() { let mut db = setup(); - // B: subdirectory module returns number. + // B: subdirectory module returns an integer. set_test_file(&mut db, 2, "sub/mod.lua", "return 42"); - // A: require('sub.mod') suffix match → Number. + // A: require('sub.mod') suffix match → Integer. let fid = set_test_file(&mut db, 1, "a.lua", "local n = require('sub.mod')"); let facts = db.analysis().file_facts(fid).expect("facts"); let n = decl_local(&facts, "n"); assert_primitive( &db.analysis().decl_type(fid, n).expect("n type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -952,7 +952,7 @@ fn test_invalidation_granularity_cross_file_decl_reexecutes() { let y = decl_local(&facts, "y"); assert_primitive( &db.analysis().decl_type(fid, y.clone()).expect("y"), - PrimitiveType::Number, + PrimitiveType::Integer, ); // 所有相关查询已经是普通函数,这里只验证跨文件编辑后结果确实刷新为 String。 @@ -1059,7 +1059,7 @@ fn test_require_module_init_lua() { let n = decl_local(&facts, "n"); assert_primitive( &db.analysis().decl_type(fid, n).expect("n type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -1081,7 +1081,7 @@ fn test_require_module_map_rewrite() { let n = decl_local(&facts, "n"); assert_primitive( &db.analysis().decl_type(fid, n).expect("n type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -1102,13 +1102,13 @@ fn test_require_fuzzy_suffix_prefers_exact() { let n = decl_local(&facts, "n"); assert_primitive( &db.analysis().decl_type(fid, n).expect("n"), - PrimitiveType::Number, + PrimitiveType::Integer, ); // require("sub.b") → exact match. let m = decl_local(&facts, "m"); assert_primitive( &db.analysis().decl_type(fid, m).expect("m type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -1149,13 +1149,13 @@ fn test_dual_identity_type_and_runtime_members() { type_keys ); - // Member type: `M.x` in A reads B's runtime x = 1 → Number. + // Member type: `M.x` in A reads B's runtime x = 1 → Integer. let fid = set_test_file(&mut db, 1, "a.lua", "local y = M.x"); let facts = db.analysis().file_facts(fid).expect("facts"); let y = decl_local(&facts, "y"); assert_primitive( &db.analysis().decl_type(fid, y).expect("y type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -1178,7 +1178,7 @@ fn test_anonymous_table_module_member() { let v = decl_local(&facts, "v"); assert_primitive( &db.analysis().decl_type(fid, v).expect("v type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -1478,7 +1478,7 @@ fn test_lua_syntax_id_disambiguates_nested_same_start() { let a = decl_local(&facts, "a"); assert_primitive( &db.analysis().decl_type(fid, a).expect("type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } @@ -1615,7 +1615,23 @@ fn test_call_returns_function_return_type() { let x = decl_local(&facts, "x"); assert_primitive( &db.analysis().decl_type(fid, x).expect("x type"), - PrimitiveType::Number, + PrimitiveType::Integer, + ); +} + +#[test] +fn test_long_integer_arithmetic_returns_stay_integer() { + let mut db = setup(); + let source = format!( + "---@param value integer\nlocal function f(value) return value{} end\nlocal result = f(0)", + " + 1".repeat(4096), + ); + let fid = set_test_file(&mut db, 1, "a.lua", &source); + let facts = db.analysis().file_facts(fid).expect("facts"); + let result = decl_local(&facts, "result"); + assert_primitive( + &db.analysis().decl_type(fid, result).expect("result type"), + PrimitiveType::Integer, ); } @@ -1636,12 +1652,12 @@ fn test_signature_doc_return() { #[test] fn test_signature_mutual_recursion_converges() { let mut db = setup(); - // foo → bar → foo: semantic cycle_fn converges, no panic. + // Recursive calls, including a prefixed return, stop without a panic. let fid = set_test_file( &mut db, 1, "a.lua", - "function foo() return bar() end\nfunction bar() return foo() end", + "function foo() return bar() end\nfunction bar() return foo() end\nfunction prefixed() return 1, prefixed() end", ); let facts = db.analysis().file_facts(fid).expect("facts"); @@ -1655,8 +1671,16 @@ fn test_signature_mutual_recursion_converges() { .iter() .find(|s| s.name.as_deref() == Some("bar")) .expect("bar sig"); + let prefixed_sig = facts + .signatures + .iter() + .find(|s| s.name.as_deref() == Some("prefixed")) + .expect("prefixed sig"); let _ = db.analysis().signature_return(fid, foo_sig.closure_syntax); let _ = db.analysis().signature_return(fid, bar_sig.closure_syntax); + let _ = db + .analysis() + .signature_return(fid, prefixed_sig.closure_syntax); } #[test] @@ -1981,7 +2005,7 @@ fn test_global_assignment_extracts_decl() { let foo = decl_local(&facts, "foo"); assert_primitive( &db.analysis().decl_type(fid, foo).expect("type"), - PrimitiveType::Number, + PrimitiveType::Integer, ); } diff --git a/crates/emmylua_code_analysis/src/semantic_db/types.rs b/crates/emmylua_code_analysis/src/semantic_db/types.rs index 29daba2de..9d6e5205b 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/types.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/types.rs @@ -6,10 +6,27 @@ use std::sync::Arc; +use emmylua_parser::BinaryOperator; use smol_str::SmolStr; use crate::FileId; +/// Convert an exact, in-range floating-point integer without saturating the cast. +pub(crate) fn exact_integer(value: f64) -> Option { + (value.fract() == 0.0 && (i64::MIN as f64..i64::MAX as f64).contains(&value)) + .then_some(value as i64) +} + +pub(crate) fn checked_integer_arithmetic(op: BinaryOperator, left: i64, right: i64) -> Option { + match op { + BinaryOperator::OpAdd => left.checked_add(right), + BinaryOperator::OpSub => left.checked_sub(right), + BinaryOperator::OpMul => left.checked_mul(right), + BinaryOperator::OpDiv => exact_integer(left as f64 / right as f64), + _ => None, + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum PrimitiveType { Nil, @@ -100,6 +117,10 @@ pub enum TypeCandidate { Array(Arc), /// Variadic: `T...` → base type. Variadic(Arc), + /// Ordered result slots, with an optional expanding final slot. + /// `return true, ...` with integer varargs keeps + /// `Multi([Boolean, Repeat(Integer)])`, rather than `Boolean | Integer`. + Multi(Vec), /// Repeated runtime values; tuple values do not expand into their fields. Repeat(Arc), /// Tuple: `[number, string]` → per-slot types. @@ -238,6 +259,13 @@ impl TypeShell { } } TypeCandidate::Repeat(base) => out.merge(base), + TypeCandidate::Multi(slots) => { + if let Some(first) = slots.first() { + out.merge(&first.first_result()); + } else { + out.merge(&Self::from_primitive(PrimitiveType::Nil)); + } + } _ => out.merge(&Self { candidates: vec![candidate.clone()], }), diff --git a/crates/emmylua_code_analysis/src/semantic_model/access.rs b/crates/emmylua_code_analysis/src/semantic_model/access.rs index a9f87df27..3c4683eb4 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/access.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/access.rs @@ -681,13 +681,13 @@ impl<'db> SemanticModel<'db> { .unwrap_or_default(); let arg_types: Vec = args .iter() - .map(|arg| self.type_of_expr(arg.get_syntax_id())) + .map(|arg| self.type_of_expr_at(arg.get_syntax_id(), arg.get_position())) .collect(); let colon_call = call_expr.is_colon_call(); let receiver_ty = if colon_call { LuaIndexExpr::cast(callee.syntax().clone()) .and_then(|index| index.get_prefix_expr()) - .map(|prefix| self.type_of_expr(prefix.get_syntax_id())) + .map(|prefix| self.type_of_expr_at(prefix.get_syntax_id(), prefix.get_position())) .unwrap_or(LuaType::Unknown) } else { LuaType::Unknown diff --git a/crates/emmylua_code_analysis/src/semantic_model/cache.rs b/crates/emmylua_code_analysis/src/semantic_model/cache.rs index a143d39cd..abd117409 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/cache.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/cache.rs @@ -5,6 +5,7 @@ use hashbrown::{HashMap, HashSet}; use std::cell::Cell; +use std::rc::Rc; use emmylua_parser::{LuaExpr, LuaSyntaxId}; use rowan::TextSize; @@ -203,6 +204,8 @@ pub(crate) struct SemanticLocalCache { pub(crate) member_type: HashMap<(FileId, SemanticId), CacheEntry>>, pub(crate) resolve_member: HashMap<(FileId, LuaSyntaxId), Option>, pub(crate) expr_type_at: HashMap<(FileId, LuaSyntaxId, TextSize), CacheEntry>, + /// Cast-bearing flow bindings on the node and its ancestors, nearest first. + pub(crate) expr_cast_flows: HashMap<(FileId, LuaSyntaxId), Rc>>, pub(crate) resolve_name: HashMap<(FileId, TextSize), Option>, pub(crate) resolve_owner_set: HashMap>, pub(crate) member_type_at: HashMap<(FileId, SemanticId, TextSize), LuaType>, diff --git a/crates/emmylua_code_analysis/src/semantic_model/flow.rs b/crates/emmylua_code_analysis/src/semantic_model/flow.rs index c8f651c83..b83959e59 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/flow.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/flow.rs @@ -160,7 +160,8 @@ pub fn type_of_member_at_flow_id( }; let mut visited = HashSet::new(); let mut path = PathState::default(); - trace_member(model, member, &tree, start, &mut visited, &mut path).unwrap_or_else(fallback) + trace_member(model, member, None, &tree, start, &mut visited, &mut path) + .unwrap_or_else(fallback) } /// Target type for assignment checks: apply `---@cast x +T` and exclude this assignment, but **do not apply branch narrowing** — @@ -312,9 +313,15 @@ pub fn type_of_member_at(model: &SemanticModel, member: &SemanticId, offset: Tex { let mut visited = HashSet::new(); let mut path = PathState::default(); - if let Some(ty) = - trace_member(model, member, &tree, assign_flow, &mut visited, &mut path) - { + if let Some(ty) = trace_member( + model, + member, + None, + &tree, + assign_flow, + &mut visited, + &mut path, + ) { #[cfg(test)] flow_metrics::record_fast_member_hit(); return ty; @@ -325,7 +332,8 @@ pub fn type_of_member_at(model: &SemanticModel, member: &SemanticId, offset: Tex } let mut visited = HashSet::new(); let mut path = PathState::default(); - trace_member(model, member, &tree, start, &mut visited, &mut path).unwrap_or_else(fallback) + trace_member(model, member, None, &tree, start, &mut visited, &mut path) + .unwrap_or_else(fallback) } /// Skip this assignment only when `offset` is inside the `t.x = value` assignment and that node assigns this member. @@ -417,6 +425,7 @@ pub fn type_of_member_before_at( walk_member( model, member, + None, &tree, node.antecedent.as_ref(), &mut visited, @@ -431,26 +440,48 @@ pub(crate) fn type_of_expr_at_node( expr: LuaExpr, offset: TextSize, ) -> LuaType { - let expr_syntax = expr.get_syntax_id(); + let apply_casts = |base, mode| apply_expr_casts_to_node(model, expr.syntax(), base, mode); match &expr { LuaExpr::NameExpr(name_expr) => { if let Some(decl) = model.resolve_name(name_expr.get_position()) { - return apply_expr_casts( - model, - expr_syntax, - model.type_of_decl_at(&decl, offset), - AsOnly, - ); + return apply_casts(model.type_of_decl_at(&decl, offset), AsOnly); } - type_of_expr_with_cast(model, expr_syntax, AsOnly) + apply_casts(model.type_of_expr_node(&expr), AsOnly) } LuaExpr::IndexExpr(index_expr) => { if let Some(resolved) = model.resolve_member(index_expr) - && let Some(member_id) = resolved.member_id + && let Some(member_id) = resolved.member_id.clone().or_else(|| { + let prefix = index_expr.get_prefix_expr()?; + let prefix_ty = model.type_of_expr_at(prefix.get_syntax_id(), offset); + if matches!(prefix_ty, LuaType::Union(_)) { + return None; + } + let key = member_key_from_index_expr(model, index_expr)?; + model + .member_infos_with_key(&prefix_ty, &key) + .into_iter() + .find_map(|info| info.id) + }) { - let mut member_ty = type_of_member_at(model, &member_id, offset) - .get_result_slot_type(0) - .unwrap_or(LuaType::Nil); + let mut member_ty = if let Some(flow_tree) = model.flow_tree() + && (flow_tree.has_condition() || flow_tree.has_complex_control()) + && let Some(flow_id) = flow_tree.get_flow_id_at(offset) + { + trace_member( + model, + &member_id, + Some(index_expr), + &flow_tree, + skip_own_member_assign(&member_id, &flow_tree, flow_id, offset), + &mut HashSet::new(), + &mut PathState::default(), + ) + .unwrap_or_else(|| type_of_member_at(model, &member_id, offset)) + } else { + type_of_member_at(model, &member_id, offset) + } + .get_result_slot_type(0) + .unwrap_or(LuaType::Nil); // Old semantics use `LuaType::Signature` to represent the closure member's definition site; // flow-sensitive member values also keep that identity, matching the VM's member access path. if matches!(member_ty, LuaType::Function) @@ -495,7 +526,15 @@ pub(crate) fn type_of_expr_at_node( ) && let Some(prefix_member_ty) = model.member_type(&prefix_ty, &key) && !matches!(prefix_member_ty, LuaType::Unknown) { - return apply_expr_casts(model, expr_syntax, prefix_member_ty, All); + let ty = if prefix_member_ty.is_nullable() + && !member_ty.is_nullable() + && member_type_more_specific(model, &member_ty, &prefix_member_ty) + { + remove_type(prefix_member_ty, &LuaType::Nil) + } else { + prefix_member_ty + }; + return apply_casts(ty, All); } } // Computed-key fields in table literals (`[key] = 1`) keep literal values in type queries: @@ -518,16 +557,16 @@ pub(crate) fn type_of_expr_at_node( // Dynamic access to a computed key `[key] = 1` can return `integer`; // named fields still keep `number` through the existing projection. LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_) => { - return apply_expr_casts(model, expr_syntax, LuaType::Integer, All); + return apply_casts(LuaType::Integer, All); } LuaType::StringConst(_) | LuaType::DocStringConst(_) => { - return apply_expr_casts(model, expr_syntax, expr_ty, All); + return apply_casts(expr_ty, All); } _ => {} } } if let Some(ty) = model.member_type(&prefix_ty, &member_def.key) { - return apply_expr_casts(model, expr_syntax, ty, All); + return apply_casts(ty, All); } } if let SemanticId::Decl(owner_decl) = &resolved.owner @@ -543,7 +582,7 @@ pub(crate) fn type_of_expr_at_node( { let runtime_ty = type_of_member_at(model, &runtime_member.id, offset); if !matches!(runtime_ty, LuaType::Unknown) { - return apply_expr_casts(model, expr_syntax, runtime_ty, All); + return apply_casts(runtime_ty, All); } } } @@ -558,38 +597,38 @@ pub(crate) fn type_of_expr_at_node( && (member_type_more_specific(model, &prefix_member_ty, &member_ty) || matches!(&prefix_ty, LuaType::Generic(_))) { - return apply_expr_casts(model, expr_syntax, prefix_member_ty, All); + return apply_casts(prefix_member_ty, All); } } let member_ty = guarded_index_type(model, index_expr, offset, &member_ty).unwrap_or(member_ty); - return apply_expr_casts(model, expr_syntax, member_ty, AsOnly); + return apply_casts(member_ty, AsOnly); } // Inside a `#d == n` branch, `d[1]` no longer carries nil when the array length is known. if let Some(known_ty) = array_index_with_known_len(model, &index_expr, offset) { - return apply_expr_casts(model, expr_syntax, known_ty, All); + return apply_casts(known_ty, All); } - let ty = model.type_of_expr(expr_syntax); + let ty = model.type_of_expr_node(&expr); let ty = guarded_index_type(model, index_expr, offset, &ty).unwrap_or(ty); - apply_expr_casts(model, expr_syntax, ty, All) + apply_casts(ty, All) } LuaExpr::CallExpr(_) | LuaExpr::TableExpr(_) | LuaExpr::LiteralExpr(_) => { - apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax), All) + apply_casts(model.type_of_expr_node(&expr), All) } LuaExpr::ClosureExpr(closure_expr) => { // Anonymous closure keeps its actual signature on the flow path, avoiding the assigned function value degrading to bare `Function`. let base = model .type_of_signature(closure_expr.get_syntax_id()) .map(|fun| LuaType::DocFunction(Rc::new(fun))) - .unwrap_or_else(|| model.type_of_expr(expr_syntax)); - apply_expr_casts(model, expr_syntax, base, All) + .unwrap_or_else(|| model.type_of_expr_node(&expr)); + apply_casts(base, All) } LuaExpr::BinaryExpr(binary_expr) => { let Some(op_token) = binary_expr.get_op_token() else { - return apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax), All); + return apply_casts(model.type_of_expr_node(&expr), All); }; let Some((left_expr, right_expr)) = binary_expr.get_exprs() else { - return apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax), All); + return apply_casts(model.type_of_expr_node(&expr), All); }; // Preserve the legacy semantics that "array table literals in short-circuit expressions are tuple": // only when the right side is a sequential array table and the left side is a logical short-circuit, replace the right table type with tuple. @@ -598,14 +637,17 @@ pub(crate) fn type_of_expr_at_node( BinaryOperator::OpAnd | BinaryOperator::OpOr ) && let Some(tuple_ty) = array_table_literal_tuple_type(model, &right_expr) { - let left_ty = model.type_of_expr_at(left_expr.get_syntax_id(), offset); + let left_ty = + model.type_of_expr_at(left_expr.get_syntax_id(), left_expr.get_position()); let result = logical_binary_type(model, op_token.get_op(), left_ty, tuple_ty); - return apply_expr_casts(model, expr_syntax, result, All); + return apply_casts(result, All); } // General binary expressions: subexpressions first use flow-sensitive types (member/variable branch narrowing), // then merge with the same VM operator rules, ensuring `right.value + 1` uses the narrow type of `right.value` in the branch. - let left_ty = model.type_of_expr_at(left_expr.get_syntax_id(), offset); - let right_ty = model.type_of_expr_at(right_expr.get_syntax_id(), offset); + let left_ty = + model.type_of_expr_at(left_expr.get_syntax_id(), left_expr.get_position()); + let right_ty = + model.type_of_expr_at(right_expr.get_syntax_id(), right_expr.get_position()); // If either operand in an arithmetic expression fails to infer (unresolved global/field), treat it as an "inference error": // don't return the VM's fallback number; let the assignment layer keep the variable's previously known type. if matches!( @@ -620,12 +662,12 @@ pub(crate) fn type_of_expr_at_node( ) && (matches!(&left_ty, LuaType::Unknown | LuaType::Any) || matches!(&right_ty, LuaType::Unknown | LuaType::Any)) { - return apply_expr_casts(model, expr_syntax, LuaType::Unknown, All); + return apply_casts(LuaType::Unknown, All); } let result = binary_type(model, op_token.get_op(), &left_ty, &right_ty); - apply_expr_casts(model, expr_syntax, result, All) + apply_casts(result, All) } - _ => type_of_expr_with_cast(model, expr_syntax, AsOnly), + _ => apply_casts(model.type_of_expr_node(&expr), AsOnly), } } @@ -648,7 +690,12 @@ fn index_access_path( keys.push(match index.get_index_key()? { emmylua_parser::LuaIndexKey::Integer(num) => match num.get_number_value() { emmylua_parser::NumberResult::Int(i) => LuaMemberKey::Integer(i), - // Other numeric spellings can address the same slot (e.g. 1.0 and 1). + emmylua_parser::NumberResult::Uint(i) => LuaMemberKey::Integer(i as i64), + emmylua_parser::NumberResult::Float(f) => { + crate::semantic_db::types::exact_integer(f) + .map(LuaMemberKey::Integer) + .unwrap_or(LuaMemberKey::None) + } _ => LuaMemberKey::None, }, emmylua_parser::LuaIndexKey::Idx(i) => LuaMemberKey::Integer(i as i64), @@ -683,7 +730,9 @@ fn guarded_index_type( } else { return None; }; - let target = index_access_path(model, &LuaExpr::IndexExpr(index.clone()))?; + let access = LuaExpr::IndexExpr(index.clone()); + let target = + stable_access_path(model, &access).or_else(|| index_access_path(model, &access))?; let closure = LuaClosureId::from_node(index.syntax()); if target.1.iter().any(LuaMemberKey::is_none) { return None; @@ -704,15 +753,9 @@ fn guarded_index_type( FlowNodeKind::Assignment(ptr) if !ptr.get_syntax_id().get_range().contains(offset) => { let assign = flow_assign_stat(model, &tree, current)?; for var in assign.get_var_and_expr_list().0 { - if let Some(written) = LuaExpr::cast(var.syntax().clone()) - .and_then(|expr| index_access_path(model, &expr)) - && written.0 == target.0 - && written.1.len() <= target.1.len() - && written - .1 - .iter() - .zip(&target.1) - .all(|(a, b)| a.is_none() || a == b) + if let Some(written) = LuaExpr::cast(var.syntax().clone()).and_then(|expr| { + stable_access_path(model, &expr).or_else(|| index_access_path(model, &expr)) + }) && path_write_affects(&written, &target.0, &target.1) { return None; } @@ -727,6 +770,7 @@ fn guarded_index_type( if let Some(guard) = narrow_index_condition( model, &target, + &access, cond, matches!(node.kind, FlowNodeKind::TrueCondition(_)), ) { @@ -754,6 +798,7 @@ fn guarded_index_type( fn narrow_index_condition( model: &SemanticModel, target: &(SemanticId, Vec), + access: &LuaExpr, mut cond: LuaExpr, mut branch_is_true: bool, ) -> Option { @@ -780,12 +825,18 @@ fn narrow_index_condition( } _ => return None, }; - return (index_access_path(model, &value).as_ref() == Some(target)) - .then_some(Narrowing::NotNil); + return (stable_access_path_at(model, &value, access) + .or_else(|| index_access_path(model, &value)) + .as_ref() + == Some(target)) + .then_some(Narrowing::NotNil); } _ => { return (branch_is_true - && index_access_path(model, &cond).as_ref() == Some(target)) + && stable_access_path_at(model, &cond, access) + .or_else(|| index_access_path(model, &cond)) + .as_ref() + == Some(target)) .then_some(Narrowing::Truthy); } } @@ -1201,26 +1252,65 @@ pub(super) fn apply_expr_casts( let Some(tree) = model.flow_tree() else { return base; }; - // Inline casts may be bound to the expression itself or an ancestor node (e.g. an assignment statement), - // so check both the expression and all ancestors' flow bindings. - let mut flow_ids = Vec::new(); - if let Some(flow_id) = tree.get_flow_id(expr_syntax) { - flow_ids.push(flow_id); + if !tree.has_tag_cast() { + return base; } - if let Some(syntax_tree) = model.syntax_tree() - && let Some(node) = expr_syntax.to_node_from_root(&syntax_tree.get_red_root()) - { - for ancestor in node.ancestors() { - let ancestor_syntax = emmylua_parser::LuaSyntaxId::from_node(&ancestor); - if let Some(flow_id) = tree.get_flow_id(ancestor_syntax) - && !flow_ids.contains(&flow_id) - { - flow_ids.push(flow_id); - } + let Some(node) = model + .syntax_tree() + .and_then(|syntax_tree| expr_syntax.to_node_from_root(&syntax_tree.get_red_root())) + else { + return base; + }; + apply_expr_casts_to_node(model, &node, base, mode) +} + +fn apply_expr_casts_to_node( + model: &SemanticModel, + node: &emmylua_parser::LuaSyntaxNode, + base: LuaType, + mode: ExprCastMode, +) -> LuaType { + let Some(tree) = model.flow_tree().filter(|tree| tree.has_tag_cast()) else { + return base; + }; + let expr_syntax = emmylua_parser::LuaSyntaxId::from_node(node); + // Cache each ancestor path once. A cast elsewhere in the file must not make + // every operand of `1 + 1 + ...` walk back to the syntax root. + let mut path = Vec::new(); + let mut flow_ids = Rc::new(Vec::new()); + for ancestor in node.ancestors() { + let syntax = emmylua_parser::LuaSyntaxId::from_node(&ancestor); + if let Some(cached) = model + .cache + .borrow() + .expr_cast_flows + .get(&(model.file_id(), syntax)) + { + flow_ids = cached.clone(); + break; + } + path.push(syntax); + } + for syntax in path.into_iter().rev() { + if let Some(flow_id) = tree.get_flow_id(syntax) + && tree + .get_flow_effects(flow_id) + .iter() + .any(|effect| matches!(effect, FlowEffect::TagCast(_) | FlowEffect::AsCast(_))) + && flow_ids.first() != Some(&flow_id) + { + let mut ids = vec![flow_id]; + ids.extend(flow_ids.iter().copied().filter(|id| *id != flow_id)); + flow_ids = Rc::new(ids); } + model + .cache + .borrow_mut() + .expr_cast_flows + .insert((model.file_id(), syntax), flow_ids.clone()); } let mut ty = base; - for flow_id in flow_ids { + for &flow_id in flow_ids.iter() { for effect in tree.get_flow_effects(flow_id) { match effect { FlowEffect::TagCast(cast_ptr) if matches!(mode, All) => { @@ -1306,43 +1396,288 @@ fn ambiguous_cast_target_matches( let Some(LuaExpr::IndexExpr(index)) = LuaExpr::cast(node) else { return false; }; - let mut left = target.clone(); - let mut right = LuaExpr::IndexExpr(index.clone()); - loop { - match (left, right) { - (LuaExpr::IndexExpr(left_index), LuaExpr::IndexExpr(right_index)) => { - let static_key = - |index: &emmylua_parser::LuaIndexExpr| match index.get_index_key()? { - emmylua_parser::LuaIndexKey::Name(name) => { - Some(name.get_name_text().to_owned()) - } - emmylua_parser::LuaIndexKey::String(string) => Some(string.get_value()), - _ => None, - }; - let Some(key) = static_key(&left_index) else { - return false; - }; - if static_key(&right_index).as_ref() != Some(&key) { - return false; - } - let (Some(left_prefix), Some(right_prefix)) = - (left_index.get_prefix_expr(), right_index.get_prefix_expr()) - else { - return false; - }; - left = left_prefix; - right = right_prefix; + !model + .resolve_member(&index) + .is_some_and(|member| member.member_id.is_some()) + && same_member_access(model, target, &LuaExpr::IndexExpr(index)) +} + +fn member_access_matches( + model: &SemanticModel, + member: &SemanticId, + access: Option<&emmylua_parser::LuaIndexExpr>, + index: &emmylua_parser::LuaIndexExpr, +) -> bool { + if let Some(access) = access { + same_member_access( + model, + &LuaExpr::IndexExpr(access.clone()), + &LuaExpr::IndexExpr(index.clone()), + ) + } else { + model + .resolve_member(index) + .and_then(|resolved| resolved.member_id) + .as_ref() + == Some(member) + } +} + +fn same_member_access(model: &SemanticModel, left: &LuaExpr, right: &LuaExpr) -> bool { + let (Some(left_path), Some(right_path)) = ( + index_access_path(model, left), + index_access_path(model, right), + ) else { + return false; + }; + if left_path == right_path { + return !left_path.1.iter().any(LuaMemberKey::is_none); + } + let query = if left.get_position() >= right.get_position() { + left + } else { + right + }; + stable_access_path_at(model, left, query) + .zip(stable_access_path_at(model, right, query)) + .is_some_and(|(left, right)| left == right && !left.1.iter().any(LuaMemberKey::is_none)) +} + +#[derive(PartialEq)] +enum MemberWrite { + Exact, + Invalidates, + Unrelated, +} + +fn member_write_match( + model: &SemanticModel, + tree: &FlowTree, + flow_id: FlowId, + member: &SemanticId, + access: Option<&emmylua_parser::LuaIndexExpr>, + assigned: Option<&SemanticId>, +) -> MemberWrite { + if assigned == Some(member) { + return MemberWrite::Exact; + } + let Some(access) = access else { + return MemberWrite::Unrelated; + }; + let key_range = assigned.and_then(SemanticId::member_key_range); + let Some(assign) = flow_assign_stat(model, tree, flow_id) else { + return MemberWrite::Unrelated; + }; + let target = LuaExpr::IndexExpr(access.clone()); + let Some(target_path) = index_access_path(model, &target) else { + return MemberWrite::Unrelated; + }; + for var in assign.get_var_and_expr_list().0 { + let Some(written) = LuaExpr::cast(var.syntax().clone()) else { + continue; + }; + if key_range.is_some_and(|range| !written.get_range().contains_range(range)) { + continue; + } + let Some(written_path) = index_access_path(model, &written) else { + continue; + }; + let paths = if path_write_affects(&written_path, &target_path.0, &target_path.1) { + Some((written_path, target_path.clone())) + } else { + stable_access_path(model, &written) + .zip(stable_access_path(model, &target)) + .filter(|(written, target)| path_write_affects(written, &target.0, &target.1)) + }; + if let Some(((_, written_keys), (_, keys))) = paths { + return if assigned.is_some() + && written_keys == keys + && !written_keys.iter().any(LuaMemberKey::is_none) + { + MemberWrite::Exact + } else { + MemberWrite::Invalidates + }; + } + } + MemberWrite::Unrelated +} + +fn path_write_affects( + written: &(SemanticId, Vec), + owner: &SemanticId, + keys: &[LuaMemberKey], +) -> bool { + &written.0 == owner + && written.1.len() <= keys.len() + && written + .1 + .iter() + .zip(keys) + .all(|(written, key)| written.is_none() || written == key) +} + +/// Expand initializer aliases without inferring types. Each captured member path +/// must still identify the same object at this occurrence. +fn stable_access_path( + model: &SemanticModel, + expr: &LuaExpr, +) -> Option<(SemanticId, Vec)> { + stable_access_path_at(model, expr, expr) +} + +fn stable_access_path_at( + model: &SemanticModel, + expr: &LuaExpr, + query: &LuaExpr, +) -> Option<(SemanticId, Vec)> { + let context = AliasContext::new(model, query)?; + let (path, captures) = local_alias_path(model, expr, context)?; + if captures.is_empty() { + return Some(path); + } + let tree = model.flow_tree()?; + let all_writes = context.all_writes + || captures + .iter() + .any(|(_, _, closure)| *closure != context.closure); + let (start, end) = if all_writes { + (TextSize::default(), TextSize::from(u32::MAX)) + } else { + (captures.iter().map(|(_, at, _)| *at).min()?, context.at) + }; + for (range, flow_id) in tree.assignments_between(start, end) { + let affects_capture = |(_, captured_at, captured_closure): &AliasCapture| { + context.all_writes + || *captured_closure != context.closure + || (*captured_at <= range.start() && range.end() <= context.at) + }; + if !captures.iter().any(affects_capture) { + continue; + } + let assign = flow_assign_stat(model, tree, flow_id)?; + for var in assign.get_var_and_expr_list().0 { + let written = LuaExpr::cast(var.syntax().clone())?; + let raw = index_access_path(model, &written)?; + // Keep possible writes through initializer aliases even when an + // earlier member write may have detached their captured paths. + let written = local_alias_path(model, &written, AliasContext::new(model, &written)?) + .map(|(path, _)| path); + // An unresolved receiver, including a reassigned alias in its + // initializer chain, can still refer to a captured owner. + if written.is_none() && !raw.1.is_empty() { + return None; } - (LuaExpr::NameExpr(left), LuaExpr::NameExpr(right)) => { - return model.resolve_name(left.get_position()).is_some_and(|decl| { - model.resolve_name(right.get_position()).as_ref() == Some(&decl) - }) && !model - .resolve_member(&index) - .is_some_and(|member| member.member_id.is_some()); + let written = written.unwrap_or(raw); + if captures + .iter() + .filter(|capture| affects_capture(capture)) + .any(|(tail_len, _, _)| { + path_write_affects(&written, &path.0, &path.1[..path.1.len() - tail_len]) + }) + { + return None; } - _ => return false, } } + Some(path) +} + +type AliasCapture = (usize, TextSize, LuaClosureId); + +#[derive(Clone, Copy)] +struct AliasContext { + at: TextSize, + closure: LuaClosureId, + all_writes: bool, +} + +impl AliasContext { + fn new(model: &SemanticModel, expr: &LuaExpr) -> Option { + use emmylua_parser::LuaSyntaxKind; + + let tree = model.flow_tree()?; + let at = expr.get_position(); + // Some loop bodies are not linked to a CFG loop label. A lexical loop + // still permits a later assignment to run before the next read. + let in_loop = expr + .syntax() + .ancestors() + .map(|node| LuaSyntaxKind::from(node.kind())) + .take_while(|kind| *kind != LuaSyntaxKind::ClosureExpr) + .any(|kind| { + matches!( + kind, + LuaSyntaxKind::WhileStat + | LuaSyntaxKind::RepeatStat + | LuaSyntaxKind::ForStat + | LuaSyntaxKind::ForRangeStat + ) + }); + Some(Self { + at, + closure: LuaClosureId::from_node(expr.syntax()), + all_writes: in_loop || tree.is_loop_reachable(tree.get_flow_id_at(at)?), + }) + } +} + +fn local_alias_path( + model: &SemanticModel, + expr: &LuaExpr, + context: AliasContext, +) -> Option<((SemanticId, Vec), Vec)> { + let (mut decl, mut keys) = index_access_path(model, expr)?; + let facts = model.file_facts()?; + let flow = model.flow_tree()?; + let root = model.syntax_tree()?.get_red_root(); + let mut all_writes = context.all_writes; + let mut visited = HashSet::new(); + let mut captures = Vec::new(); + while visited.insert(decl.clone()) { + let info = facts.decl_by_id(&decl)?; + if let Some(owner) = info + .owner_syntax + .and_then(|syntax| syntax.to_node_from_root(&root)) + { + all_writes |= LuaClosureId::from_node(&owner) != context.closure; + } + if !matches!( + info.kind, + crate::DeclKind::Local { is_iter: false, .. } | crate::DeclKind::Param + ) || flow.latest_decl_assignment(&decl, context.at).is_some() + || (all_writes && flow.has_decl_assignment(&decl)) + || info.multi_return_index.is_some_and(|index| index > 0) + { + return None; + } + let Some(syntax) = info.value_expr_syntax else { + break; + }; + let initializer = LuaExpr::cast(syntax.to_node_from_root(&root)?)?; + let initializer_closure = LuaClosureId::from_node(initializer.syntax()); + all_writes |= initializer_closure != context.closure; + if all_writes && flow.has_decl_assignment(&decl) { + return None; + } + let Some((owner, mut prefix)) = index_access_path(model, &initializer) else { + break; + }; + if prefix.iter().any(LuaMemberKey::is_none) { + return None; + } + if !prefix.is_empty() { + captures.push(( + keys.len(), + initializer.get_range().end(), + initializer_closure, + )); + } + prefix.extend(keys); + keys = prefix; + decl = owner; + } + Some(((decl, keys), captures)) } /// `---@cast` path operation: widen / remove / replace. @@ -1362,8 +1697,6 @@ struct PathState { casts: Vec, } -impl PathState {} - /// Checks from an antecedent toward the declaration whether a narrowing node already exists. /// Used for `self` return_cast: only allow it if the receiver was first narrowed by another condition/`@cast`. /// Uses an explicit work stack; Multiple requires all branches to satisfy the condition. @@ -1818,6 +2151,7 @@ fn member_annotation_type(model: &SemanticModel, member: &SemanticId) -> Option< fn trace_member( model: &SemanticModel, member: &SemanticId, + access: Option<&emmylua_parser::LuaIndexExpr>, tree: &FlowTree, flow_id: FlowId, visited: &mut HashSet, @@ -1839,8 +2173,24 @@ fn trace_member( value_syntax, .. } = effect - && assigned_member == member { + match member_write_match( + model, + tree, + current, + member, + access, + Some(assigned_member), + ) { + MemberWrite::Unrelated => continue, + MemberWrite::Invalidates => { + // Replacing an owner or writing through a dynamic key removes + // earlier guards, but preserves guards after this assignment. + return flow_member_value_type(model, member) + .map(|ty| finalize(model, ty, path)); + } + MemberWrite::Exact => {} + } // Member assignment overrides earlier guard narrowing; cast widening is retained. let assign_path = PathState { narrowings: Vec::new(), @@ -1848,7 +2198,7 @@ fn trace_member( }; let mut value_ty = type_of_expr_with_cast(model, *value_syntax, AsOnly); if let Some(annotation) = member_annotation_type(model, member) { - if model.type_check(&annotation, &value_ty) { + if model.type_check(&value_ty, &annotation) { value_ty = widen_const_type(value_ty); } else { value_ty = annotation; @@ -1857,17 +2207,29 @@ fn trace_member( return Some(finalize(model, value_ty, &assign_path)); } } + // A dynamic-key write can lack a member ID and still replace + // the guarded value or one of its owners. + if member_write_match(model, tree, current, member, access, None) + == MemberWrite::Invalidates + { + return flow_member_value_type(model, member) + .map(|ty| finalize(model, ty, path)); + } } FlowNodeKind::TagCast(cast_ptr) => { collect_member_cast(model, member, cast_ptr, path); } FlowNodeKind::TrueCondition(cond_ptr) => { - if let Some(narrowing) = narrow_member_condition(model, member, cond_ptr, true) { + if let Some(narrowing) = + narrow_member_condition(model, member, access, cond_ptr, true) + { path.narrowings.push(narrowing); } } FlowNodeKind::FalseCondition(cond_ptr) => { - if let Some(narrowing) = narrow_member_condition(model, member, cond_ptr, false) { + if let Some(narrowing) = + narrow_member_condition(model, member, access, cond_ptr, false) + { path.narrowings.push(narrowing); } } @@ -1890,14 +2252,22 @@ fn trace_member( // jump directly to the common predecessor and continue backtracking, avoiding equal-depth recursion from consecutive ifs. if let Some(branches) = tree.get_multi_antecedents(*multi_id) && let Some(common) = local_merge_common(tree, branches) - && branches - .iter() - .all(|branch| member_branch_is_pure(tree, member, *branch, common)) + && branches.iter().all(|branch| { + member_branch_is_pure(model, tree, member, access, *branch, common) + }) { current = common; continue; } - return walk_member(model, member, tree, node.antecedent.as_ref(), visited, path); + return walk_member( + model, + member, + access, + tree, + node.antecedent.as_ref(), + visited, + path, + ); } } } @@ -2574,7 +2944,26 @@ fn call_signature_return( model: &SemanticModel, call: &emmylua_parser::LuaCallExpr, ) -> Option { - let (file_id, closure_syntax, signature) = call_signature(model, call)?; + let Some((file_id, closure_syntax, signature)) = call_signature(model, call) else { + let LuaExpr::NameExpr(callee) = call.get_prefix_expr()? else { + return None; + }; + let decl = model.resolve_name(callee.get_position())?; + let ty = model.type_of_decl(&decl)?; + let candidates = super::infer::vm::expand_callable_types_in_model(model, &ty); + return (!candidates.is_empty()).then(|| { + LuaType::from_vec( + candidates + .iter() + .map(|fun| { + fun.get_ret() + .get_result_slot_type(0) + .unwrap_or(LuaType::Nil) + }) + .collect(), + ) + }); + }; let shell = model.analysis().signature_return(file_id, closure_syntax)?; let generic_names: Vec = signature .docs @@ -3387,6 +3776,7 @@ fn narrow_condition( fn narrow_member_condition( model: &SemanticModel, member: &SemanticId, + access: Option<&emmylua_parser::LuaIndexExpr>, cond_ptr: &emmylua_parser::LuaAstPtr, branch_is_true: bool, ) -> Option { @@ -3395,11 +3785,7 @@ fn narrow_member_condition( if let LuaExpr::CallExpr(call) = &cond { if let Some((arg, guard_ty)) = type_guard_call_target(model, call) && let LuaExpr::IndexExpr(index_expr) = &arg - && model - .resolve_member(index_expr) - .and_then(|resolved| resolved.member_id) - .as_ref() - == Some(member) + && member_access_matches(model, member, access, index_expr) { return Some(Narrowing::Replace(guard_ty)); } @@ -3411,19 +3797,15 @@ fn narrow_member_condition( } if branch_is_true { if let LuaExpr::IndexExpr(index_expr) = &cond - && model - .resolve_member(index_expr) - .and_then(|resolved| resolved.member_id) - .as_ref() - == Some(member) + && member_access_matches(model, member, access, index_expr) { return Some(Narrowing::Truthy); } - if let Some(narrowing) = alias_member_condition_narrowing(model, member, &cond) { + if let Some(narrowing) = alias_member_condition_narrowing(model, member, access, &cond) { return Some(narrowing); } } - direct_narrow_member_condition(model, member, &cond, branch_is_true) + direct_narrow_member_condition(model, member, access, &cond, branch_is_true) } /// One-level inheritance for boolean alias guards: after `local ok = t.x ~= nil`, `if ok` can narrow `t.x`. @@ -3431,6 +3813,7 @@ fn narrow_member_condition( fn alias_member_condition_narrowing( model: &SemanticModel, member: &SemanticId, + access: Option<&emmylua_parser::LuaIndexExpr>, cond: &LuaExpr, ) -> Option { let LuaExpr::NameExpr(name_expr) = cond else { @@ -3440,7 +3823,7 @@ fn alias_member_condition_narrowing( if matches!(expr, LuaExpr::NameExpr(_)) { return None; } - direct_narrow_member_condition(model, member, &expr, true) + direct_narrow_member_condition(model, member, access, &expr, true) } /// Whether a call is a rawget-like member truthiness guard: arguments are instance name + string key, and the key matches the current member. @@ -3492,6 +3875,7 @@ fn rawget_member_narrowing( fn direct_narrow_member_condition( model: &SemanticModel, member: &SemanticId, + access: Option<&emmylua_parser::LuaIndexExpr>, cond: &LuaExpr, branch_is_true: bool, ) -> Option { @@ -3508,8 +3892,7 @@ fn direct_narrow_member_condition( (LuaExpr::LiteralExpr(lit), LuaExpr::IndexExpr(index)) => (index, lit), _ => return None, }; - let resolved = model.resolve_member(&index_expr)?; - if resolved.member_id.as_ref() != Some(member) { + if !member_access_matches(model, member, access, &index_expr) { return None; } let lit_ty = literal_type(&lit_expr)?; @@ -3993,6 +4376,10 @@ fn direct_narrow_condition( LuaExpr::BinaryExpr(binary) => { let op = binary.get_op_token()?.get_op(); let (left, right) = binary.get_exprs()?; + // If the right result is always falsy, `a or b` can only be true when a is true. + if op == BinaryOperator::OpOr && condition_branch_unreachable(model, &right, true) { + return direct_narrow_condition(model, decl, &left, has_prior_narrowing); + } // `x.kind == 'A'`: member discriminant narrowing. if matches!(op, BinaryOperator::OpEq | BinaryOperator::OpNe) && let Some((disc_decl, key, literal)) = @@ -4239,6 +4626,10 @@ fn direct_narrow_condition_false( LuaExpr::BinaryExpr(binary) => { let op = binary.get_op_token()?.get_op(); let (left, right) = binary.get_exprs()?; + // If the right result is always truthy, `a and b` can only be false when a is false. + if op == BinaryOperator::OpAnd && condition_branch_unreachable(model, &right, false) { + return direct_narrow_condition_false(model, decl, &left, has_prior_narrowing); + } // `x.kind == 'A'` false: keep components different from the literal. if matches!(op, BinaryOperator::OpEq | BinaryOperator::OpNe) && let Some((disc_decl, key, literal)) = @@ -4824,6 +5215,23 @@ fn component_matches_primitive( { return false; } + if *primitive == LuaType::Table + && let LuaType::Ref(id) | LuaType::Def(id) = component + && let Some(def) = model.type_def_of(id) + && def.kind == TypeDefKind::Class + && [ + LuaType::Userdata, + LuaType::Number, + LuaType::String, + LuaType::Boolean, + LuaType::Function, + LuaType::Thread, + ] + .iter() + .any(|base| model.type_check(component, base)) + { + return false; + } model.type_check(component, primitive) } @@ -5008,8 +5416,10 @@ fn walk_decl( /// Whether the linear branch from `start` to `stop` (excluding `stop`) has no effect on member `member`. fn member_branch_is_pure( + model: &SemanticModel, tree: &FlowTree, member: &SemanticId, + access: Option<&emmylua_parser::LuaIndexExpr>, start: FlowId, stop: FlowId, ) -> bool { @@ -5035,7 +5445,27 @@ fn member_branch_is_pure( .. } if assigned_member == member ) - }) { + }) || member_write_match(model, tree, current, member, access, None) + != MemberWrite::Unrelated + { + return false; + } + } + FlowNodeKind::TrueCondition(cond) | FlowNodeKind::FalseCondition(cond) => { + if let Some(access) = access + && let Some(cond) = flow_condition_expr(model, cond) + && cond + .syntax() + .descendants() + .filter_map(emmylua_parser::LuaIndexExpr::cast) + .any(|index| { + same_member_access( + model, + &LuaExpr::IndexExpr(access.clone()), + &LuaExpr::IndexExpr(index), + ) + }) + { return false; } } @@ -5052,6 +5482,7 @@ fn member_branch_is_pure( fn walk_member( model: &SemanticModel, member: &SemanticId, + access: Option<&emmylua_parser::LuaIndexExpr>, tree: &FlowTree, antecedent: Option<&FlowAntecedent>, visited: &mut HashSet, @@ -5061,16 +5492,16 @@ fn walk_member( // Backtracked to the start: use the member's declaration type as the base and apply cast widening collected on the path. None => flow_member_value_type(model, member).map(|ty| finalize(model, ty, path)), Some(FlowAntecedent::Single(next)) => { - trace_member(model, member, tree, *next, visited, path) + trace_member(model, member, access, tree, *next, visited, path) } Some(FlowAntecedent::Multiple(multi_id)) => { let branches = tree.get_multi_antecedents(*multi_id)?; if let Some(common) = local_merge_common(tree, branches) - && branches - .iter() - .all(|branch| member_branch_is_pure(tree, member, *branch, common)) + && branches.iter().all(|branch| { + member_branch_is_pure(model, tree, member, access, *branch, common) + }) { - return trace_member(model, member, tree, common, visited, path); + return trace_member(model, member, access, tree, common, visited, path); } let mut merged = LuaType::Unknown; for branch in branches { @@ -5079,6 +5510,7 @@ fn walk_member( if let Some(branch_ty) = trace_member( model, member, + access, tree, *branch, &mut branch_visited, diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/callable.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/callable.rs index 211a26e28..272ad025e 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/callable.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/callable.rs @@ -31,9 +31,6 @@ impl CallableCandidateSet { self.candidates } - pub(crate) fn is_empty(&self) -> bool { - self.candidates.is_empty() - } /// All callable candidates for `prefix_type.key` (all same-key/inherited members). pub(crate) fn from_prefix_type( model: &SemanticModel<'_>, diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/function_solver.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/function_solver.rs index 7e6e96280..8ee7b81dd 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/function_solver.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/function_solver.rs @@ -20,7 +20,7 @@ use std::collections::VecDeque; use crate::LuaFunctionType; use crate::LuaType; use crate::semantic_model::SemanticModel; -use crate::semantic_model::type_check::is_compatible; +use crate::semantic_model::type_check::{function_param_pairs, is_compatible}; use super::unify::{self, TplBindings}; @@ -33,65 +33,22 @@ enum Constraint { Assign { source: LuaType, target: LuaType }, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] struct ActualExpected { actual: LuaFunctionType, expected: LuaFunctionType, } -/// Solver result. -#[derive(Debug)] -pub struct FunctionCheckResult { - pub compatible: bool, -} - -/// Contravariant check for function parameter positions: when `source` is a union, -/// every component must be assignable to `target`. -fn parameter_assignable(model: &SemanticModel, source: &LuaType, target: &LuaType) -> bool { - if let LuaType::Union(union) = source { - return union - .into_vec() - .iter() - .all(|component| parameter_assignable(model, component, target)); - } - types_assignable(model, source, target) -} - -/// Leaf assignability check: `type_check` requires all union-target components to be -/// compatible; here we also allow "any component compatible" (needed for function -/// parameter positions). -fn types_assignable(model: &SemanticModel, source: &LuaType, target: &LuaType) -> bool { - if source == target { - return true; - } - if is_compatible(model, source, target) { - return true; - } - if let LuaType::Union(union) = target { - return union - .into_vec() - .iter() - .any(|component| types_assignable(model, source, component)); - } - if let LuaType::Union(union) = source { - return union - .into_vec() - .iter() - .any(|component| types_assignable(model, component, target)); - } - false -} - /// Iteratively check whether `actual` can be used where `expected` is expected. /// /// `bindings` are generic bindings already inferred at the call site; this function -/// extends and returns the final bindings. +/// extends a local copy while checking constraints. pub fn functions_compatible( model: &SemanticModel, actual: &LuaFunctionType, expected: &LuaFunctionType, bindings: &TplBindings, -) -> FunctionCheckResult { +) -> bool { let mut bindings = bindings.clone(); let mut queue = VecDeque::new(); queue.push_back(Constraint::FnPair(ActualExpected { @@ -100,112 +57,58 @@ pub fn functions_compatible( })); // Cycle guard: expand each function pair at most once. + // Type keys are immutable; their OnceCell only caches a stable hash. + #[allow(clippy::mutable_key_type)] let mut seen_pairs = std::collections::HashSet::new(); - let mut failed = false; - while let Some(constraint) = queue.pop_front() { match constraint { Constraint::FnPair(pair) => { - let key = fn_pair_key(&pair.actual, &pair.expected); - if !seen_pairs.insert(key) { + if !seen_pairs.insert(pair.clone()) { continue; } - let failed_before = failed; - expand_function_pair(model, &pair, &mut bindings, &mut queue, &mut |_, _| { - failed = true; - }); - // Stop immediately if expansion hits a hard failure. - if !failed_before && failed { - break; - } + expand_function_pair(&pair, &mut bindings, &mut queue); } Constraint::Assign { source, target } => { - if !types_assignable(model, &source, &target) { - failed = true; - break; + if !is_compatible(model, &source, &target) { + return false; } } } } - FunctionCheckResult { - compatible: !failed, - } + true } -/// Expand one function pair: push contravariant parameter constraints and covariant -/// return constraints onto the queue. On leaves call `fail_if_incompatible` (records -/// only the first failure). +/// Push contravariant parameter constraints and covariant result constraints. +/// Nested functions stay on the queue instead of calling the checker recursively. fn expand_function_pair( - model: &SemanticModel, pair: &ActualExpected, bindings: &mut TplBindings, queue: &mut VecDeque, - fail_if_incompatible: &mut dyn FnMut(LuaType, LuaType), ) { - let actual_params = pair.actual.get_params(); - let expected_params = pair.expected.get_params(); - - for (index, (_, expected_param)) in expected_params.iter().enumerate() { - let Some(expected_param) = expected_param else { - continue; - }; - let actual_param = actual_params - .get(index) - .and_then(|(_, ty)| ty.clone()) - .unwrap_or(LuaType::Unknown); - - let expected_param = unify::substitute(expected_param, bindings); + for (expected_param, actual_param) in function_param_pairs(&pair.actual, &pair.expected) { + let expected_param = unify::substitute(&expected_param, bindings); let actual_param = unify::substitute(&actual_param, bindings); - - // Generic binding: if `actual` is still a TplRef, let `expected` (the value the - // caller actually passes) flow in. unify_bindings_or_queue(&actual_param, &expected_param, bindings, queue); - - // `self` parameter is covariant: if `expected` is `self`, any receiver works - // (e.g. `fun(self: A) -> fun(self: self)`). - if expected_param.is_self_infer() || actual_param.is_self_infer() { - continue; - } - - // Nested function -> new work item (contravariant parameter). - match (&actual_param, &expected_param) { - (LuaType::DocFunction(actual_fun), LuaType::DocFunction(expected_fun)) => { - queue.push_back(Constraint::FnPair(ActualExpected { - actual: actual_fun.as_ref().clone(), - expected: expected_fun.as_ref().clone(), - })); - } - // `expected` is a function but `actual` did not resolve to one -> leaf failure. - (_, LuaType::DocFunction(_)) => { - fail_if_incompatible(expected_param.clone(), actual_param.clone()); - } - _ => { - // Contravariant: `expected` (what the caller passes) must be assignable - // to `actual` (what the callee accepts). For a source union in parameter - // position, every component must work because the caller may pass any member. - if !parameter_assignable(model, &expected_param, &actual_param) { - fail_if_incompatible(expected_param.clone(), actual_param.clone()); - } - } - } + // Contravariance reverses the function pair as well as leaf types. + queue_type_pair(expected_param, actual_param, queue); } // Return covariance: actual return -> expected return. let actual_ret = unify::substitute(pair.actual.get_ret(), bindings); let expected_ret = unify::substitute(pair.expected.get_ret(), bindings); - match (&actual_ret, &expected_ret) { - (LuaType::DocFunction(actual_fun), LuaType::DocFunction(expected_fun)) => { + queue_type_pair(actual_ret, expected_ret, queue); +} + +fn queue_type_pair(source: LuaType, target: LuaType, queue: &mut VecDeque) { + match (&source, &target) { + (LuaType::DocFunction(actual), LuaType::DocFunction(expected)) => { queue.push_back(Constraint::FnPair(ActualExpected { - actual: actual_fun.as_ref().clone(), - expected: expected_fun.as_ref().clone(), + actual: actual.as_ref().clone(), + expected: expected.as_ref().clone(), })); } - _ => { - if !types_assignable(model, &actual_ret, &expected_ret) { - fail_if_incompatible(actual_ret.clone(), expected_ret.clone()); - } - } + _ => queue.push_back(Constraint::Assign { source, target }), } } @@ -233,24 +136,3 @@ fn unify_bindings_or_queue( } } } - -/// Function pair key: a lightweight fingerprint beyond `Debug` output, to prevent -/// alias-cycle expansion from exploding. -fn fn_pair_key(actual: &LuaFunctionType, expected: &LuaFunctionType) -> (String, String) { - (function_fingerprint(actual), function_fingerprint(expected)) -} - -fn function_fingerprint(fun: &LuaFunctionType) -> String { - let mut out = String::new(); - for (name, ty) in fun.get_params() { - out.push_str(name); - out.push(':'); - if let Some(ty) = ty { - out.push_str(&format!("{:?}", ty)); - } - out.push(';'); - } - out.push_str("->"); - out.push_str(&format!("{:?}", fun.get_ret())); - out -} diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/closure_return_test.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/closure_return_test.rs index fc790bfea..2e1c6cbe9 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/closure_return_test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/closure_return_test.rs @@ -9,9 +9,7 @@ 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_eq!(ty, expected); } #[test] @@ -363,9 +361,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_eq!(ty, expected); } #[test] @@ -392,8 +388,25 @@ 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_eq!(ty, expected); + } + + #[test] + fn test_inferred_return_rows_keep_missing_slots_and_final_calls() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + local function tail() return 'text' end + ---@param flag boolean + local function choose(flag) + if flag then return 1 end + return 1, tail() + end + first, second = choose(true) + "#, + ); + + assert_eq!(ws.expr_ty("first"), ws.ty("integer")); + assert_eq!(ws.expr_ty("second"), ws.ty("string|nil")); } } diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/generic_test.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/generic_test.rs index aae28fad3..23e6d8349 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/generic_test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/generic_test.rs @@ -1995,10 +1995,12 @@ mod test { local value A = wrapped(value) + B = wrapped(nil) "#, ); - assert_eq!(ws.expr_ty("A"), ws.ty("string|nil")); + assert_eq!(ws.expr_ty("A"), crate::LuaType::Never); + assert_eq!(ws.expr_ty("B"), crate::LuaType::Nil); } #[test] @@ -2017,10 +2019,12 @@ mod test { local value A = wrappedTuple(value) + B = wrappedTuple(nil) "#, ); - assert_eq!(ws.expr_ty("A"), ws.ty("string|nil")); + assert_eq!(ws.expr_ty("A"), crate::LuaType::Never); + assert_eq!(ws.expr_ty("B"), crate::LuaType::Nil); } #[test] diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/member_infer_test.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/member_infer_test.rs index 00307054f..f3837c720 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/member_infer_test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/member_infer_test.rs @@ -69,7 +69,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/semantic_model/infer/legacy_tests/module_test.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/module_test.rs index 320071e0d..746a95bd7 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/module_test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/module_test.rs @@ -133,9 +133,7 @@ mod test { // `def()` creates `virtual_0.lua`, so the block is requireable as `virtual_0`. 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_eq!(ty, integer); } // Migrated to semantic check/ (new pipeline); module export metadata block semantics pending semantic support. @@ -177,7 +175,7 @@ mod test { ("a/helper.lua", "return 11"), ("b/helper.lua", "return 'from-b'"), ]); - for (path, expected) in [("a/control.lua", "number"), ("b/control.lua", "string")] { + for (path, expected) in [("a/control.lua", "integer"), ("b/control.lua", "string")] { let file = ws.def_file(path, "local value = require('helper')"); let local = ws.get_node::(file); let model = ws.analysis.semantic_model(file); diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/return_overload_generic_test.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/return_overload_generic_test.rs index 2b508128f..ee4f478cb 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/return_overload_generic_test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/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,7 +169,7 @@ 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(&integer_or_string_ty, &value_ty)); assert!(ws.check_type(&value_ty, &integer_ty)); assert!(ws.check_type(&value_ty, &string_ty)); } diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs index f53c601f1..739c0711f 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/test.rs @@ -138,7 +138,7 @@ fn test_infer_expr_function_structure() { #[test] fn test_infer_expr_member_cross_file() { - // B defines M.x = 1; A reads M.x -> Number. + // B defines M.x = 1; A reads M.x -> Integer. let emmyrc = Arc::new(Emmyrc::default()); let mut db = SemanticDatabase::new(); db.update_config(emmyrc); @@ -152,7 +152,7 @@ fn test_infer_expr_member_cross_file() { let model = env.model(); let y = decl_of(&model, "y"); - assert_eq!(model.type_of_decl(&y), Some(LuaType::Number)); + assert_eq!(model.type_of_decl(&y), Some(LuaType::Integer)); } #[test] @@ -275,7 +275,7 @@ fn test_vm_name_binary() { #[test] fn test_vm_member_access() { - // VM:LoadName(t) + IndexMember(a) → Number。 + // VM:LoadName(t) + IndexMember(a) → Integer。 let env = model_of("local t = {}\nt.a = 5\nlocal y = t.a"); let model = env.model(); let y = model @@ -286,12 +286,12 @@ fn test_vm_member_access() { .expect("y"); let init = y.value_expr_syntax.expect("init"); let ty = infer_expr_vm(&model, init); - assert_eq!(ty, LuaType::Number); + assert_eq!(ty, LuaType::Integer); } #[test] fn test_vm_call() { - // VM: LoadName(f) + Call(0) -> returns Number. + // A call keeps the inferred integer return type. let env = model_of("local function f() return 1 end\nlocal y = f()"); let model = env.model(); let y = model @@ -302,7 +302,7 @@ fn test_vm_call() { .expect("y"); let init = y.value_expr_syntax.expect("init"); let ty = infer_expr_vm(&model, init); - assert_eq!(ty, LuaType::Number); + assert_eq!(ty, LuaType::Integer); } #[test] @@ -1082,7 +1082,7 @@ fn test_semantic_info_member_use() { let info = model .semantic_info(rowan::NodeOrToken::Token(token)) .expect("semantic info"); - assert_eq!(info.typ, LuaType::Number); + assert_eq!(info.typ, LuaType::Integer); assert_eq!(info.decl, Some(member)); } @@ -1838,3 +1838,39 @@ fn test_operator_len() { let n = decl_of(&model, "n"); assert_eq!(model.type_of_decl(&n), Some(LuaType::Number)); } + +#[test] +fn expression_casts_use_the_current_file() { + let mut ws = crate::VirtualWorkspace::new(); + let plain = ws.def_file( + "plain.lua", + "local first = 1\nlocal other = 2 --[[@as boolean]]", + ); + let cast = ws.def_file( + "cast.lua", + "local first = 1 --[[@as string]]\nlocal other = 2", + ); + let first_value = |file| ws.get_node::(file); + let plain_value = first_value(plain); + let cast_value = first_value(cast); + + // Both views share one request. Read them in both orders. + for files in [[plain, cast], [cast, plain]] { + let model = ws.analysis.semantic_model(files[0]); + for file in files { + let view = model.model_for(file); + let value = if file == plain { + &plain_value + } else { + &cast_value + }; + let actual = view.type_of_expr_at(value.get_syntax_id(), value.get_position()); + let expected = if file == plain { + LuaType::IntegerConst(1) + } else { + LuaType::String + }; + assert_eq!(actual, expected); + } + } +} diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs index 559a04e21..2506404e1 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs @@ -2098,33 +2098,33 @@ impl<'a> InferVm<'a> { let Some(closure) = LuaClosureExpr::cast(node) else { return LuaType::Unknown; }; - let return_exprs: Vec = closure - .descendants::() - .filter(|ret| { - ret.ancestors::() - .next() - .is_some_and(|owner| owner.get_syntax_id() == closure_syntax) - }) - .flat_map(|ret| ret.get_expr_list()) - .collect(); - if return_exprs.is_empty() { - return LuaType::Unknown; - } let mut types = Vec::new(); - for expr in return_exprs { - let expr_syntax = expr.get_syntax_id(); - let mut code = Vec::new(); - compile(expr, self.model.file_id(), &mut code); - code.push(Instr::Result); - let mut vm = InferVm::new(self.model, &code); - vm.closure_params = self.closure_params.clone(); - let mut ty = vm.run(); - // `---@as` / flow casts are attached to return expression nodes; flow queries can - // give a more precise return type than bare VM (e.g. `{} --[[@as Promise]]`). - let flow_ty = type_of_expr_with_cast(self.model, expr_syntax, ExprCastMode::All); - if !matches!(flow_ty, LuaType::Unknown | LuaType::Any) && flow_ty != ty { - ty = flow_ty; + for ret in closure.descendants::().filter(|ret| { + ret.ancestors::() + .next() + .is_some_and(|owner| owner.get_syntax_id() == closure_syntax) + }) { + let exprs: Vec<_> = ret.get_expr_list().collect(); + let mut slots = Vec::new(); + for (index, expr) in exprs.iter().enumerate() { + let expr_syntax = expr.get_syntax_id(); + let mut ty = self.type_of_expr_inherit(expr_syntax); + let flow_ty = type_of_expr_with_cast(self.model, expr_syntax, ExprCastMode::All); + if !matches!(flow_ty, LuaType::Unknown | LuaType::Any) && flow_ty != ty { + ty = flow_ty; + } + if index + 1 < exprs.len() { + ty = ty.get_result_slot_type(0).unwrap_or(LuaType::Nil); + } + slots.push(ty); } + // Values in one return form a sequence; different return statements + // form alternatives. `return true, ...` keeps its boolean prefix. + let ty = if slots.len() == 1 { + slots.pop().expect("len checked") + } else { + LuaType::Variadic(Rc::new(VariadicType::Multi(slots))) + }; if !matches!(ty, LuaType::Unknown | LuaType::Any) && !types.contains(&ty) { types.push(ty); } @@ -3129,24 +3129,7 @@ impl<'a> InferVm<'a> { if let Some(facts) = self.model.file_facts_of(def.file_id) && let Some(op) = facts.operator_of(&def.id, "call") { - let params = op - .params - .iter() - .map(|syntax| { - ( - String::new(), - Some(self.model.doc_type_lua_rich_in(def.file_id, *syntax)), - ) - }) - .collect(); - out.push(LuaFunctionType::new( - AsyncState::None, - false, - false, - params, - self.model.doc_type_lua_rich_in(def.file_id, op.returns), - None, - )); + out.push(self.model.call_operator_type(def.file_id, op)); } } visited.pop(); @@ -3181,24 +3164,7 @@ impl<'a> InferVm<'a> { if let Some(facts) = self.model.file_facts_of(def.file_id) && let Some(op) = facts.operator_of(&def.id, "call") { - let params = op - .params - .iter() - .map(|syntax| { - ( - String::new(), - Some(self.model.doc_type_lua_rich_in(def.file_id, *syntax)), - ) - }) - .collect(); - out.push(LuaFunctionType::new( - AsyncState::None, - false, - false, - params, - self.model.doc_type_lua_rich_in(def.file_id, op.returns), - None, - )); + out.push(self.model.call_operator_type(def.file_id, op)); } } visited.pop(); @@ -3301,6 +3267,7 @@ fn constructor_candidate( id: Some(member.id.clone()), file_id: Some(member.file_id), is_method: true, + index_key_type: None, }) })?; let member_file = member_info.file_id?; @@ -4215,6 +4182,11 @@ pub fn infer_expr_vm(model: &SemanticModel, expr_syntax: LuaSyntaxId) -> LuaType let Some(expr) = LuaExpr::cast(node) else { return LuaType::Unknown; }; + infer_expr_node(model, expr) +} + +/// Infer an already resolved expression without another walk from the syntax root. +pub(crate) fn infer_expr_node(model: &SemanticModel, expr: LuaExpr) -> LuaType { let mut code = Vec::new(); compile(expr, model.file_id(), &mut code); code.push(Instr::Result); @@ -4567,25 +4539,13 @@ pub(crate) fn binary_type( if let (Some(left_i), Some(right_i)) = (integer_const_value(left), integer_const_value(&right)) { - let value = match op { - OpAdd => left_i.checked_add(right_i), - OpSub => left_i.checked_sub(right_i), - OpMul => left_i.checked_mul(right_i), - _ => None, - }; + let value = + crate::semantic_db::types::checked_integer_arithmetic(op, left_i, right_i); return value.map(LuaType::IntegerConst).unwrap_or(LuaType::Number); } // Addition like `integer + 1` (one side is the integer base type, the other is an // integer literal) still stays integer. - let left_is_integer = matches!( - left, - LuaType::Integer | LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_) - ); - let right_is_integer = matches!( - right, - LuaType::Integer | LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_) - ); - if left_is_integer && right_is_integer { + if integer_operand(left) && integer_operand(&right) { LuaType::Integer } else { LuaType::Number @@ -4594,6 +4554,20 @@ pub(crate) fn binary_type( OpDiv | OpIDiv | OpMod | OpPow => { if left.is_any() || right.is_any() { LuaType::Any + } else if matches!(op, OpDiv) + && let (Some(left), Some(right)) = + (integer_const_value(left), integer_const_value(&right)) + && right != 0 + { + // Lua `/` converts both operands to floating-point before division. + crate::semantic_db::types::checked_integer_arithmetic(op, left, right) + .map(LuaType::IntegerConst) + .unwrap_or(LuaType::Number) + } else if matches!(op, OpIDiv | OpMod) + && integer_operand(left) + && integer_operand(&right) + { + LuaType::Integer } else { LuaType::Number } @@ -4882,6 +4856,13 @@ fn integer_const_value(ty: &LuaType) -> Option { } } +fn integer_operand(ty: &LuaType) -> bool { + match ty { + LuaType::Union(union) => union.iter().all(LuaType::is_integer), + _ => ty.is_integer(), + } +} + fn is_enum_like_dynamic_key(model: &SemanticModel, ty: &LuaType) -> bool { let (LuaType::Ref(id) | LuaType::Def(id)) = ty else { return false; @@ -4919,6 +4900,18 @@ fn unary_type( { return ty; } + if op == OpUnm { + match operand { + LuaType::IntegerConst(value) | LuaType::DocIntegerConst(value) => { + return value + .checked_neg() + .map(LuaType::IntegerConst) + .unwrap_or(LuaType::Number); + } + LuaType::FloatConst(value) => return LuaType::FloatConst(-value), + _ => {} + } + } operand.clone() } diff --git a/crates/emmylua_code_analysis/src/semantic_model/member.rs b/crates/emmylua_code_analysis/src/semantic_model/member.rs index 057e55a88..4b13f12f3 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/member.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/member.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use emmylua_parser::{LuaAstNode, LuaTableExpr, LuaTableField}; +use emmylua_parser::{LuaAstNode, LuaDocFieldKey, LuaDocTagField, LuaTableExpr, LuaTableField}; use hashbrown::HashMap; use crate::semantic_db::def::{DeclKind, MemberRef, SemanticId, TypeDef, TypeDefKind}; @@ -27,6 +27,8 @@ pub struct MemberInfo { pub key: LuaMemberKey, /// Member type (after projection / generic substitution). pub typ: LuaType, + /// Declared index domain after generic substitution; absent for ordinary or synthesized members. + pub index_key_type: Option, /// Declaration identity (`SemanticId::Member`; `None` for synthesized members). pub id: Option, /// Declaring file. @@ -37,10 +39,15 @@ pub struct MemberInfo { /// All members of a prefix type (completion candidates; deduplicated by key, with this class's `@field` before parent/runtime values). pub fn member_infos(model: &SemanticModel, prefix_type: &LuaType) -> Vec { + dedup_by_key(member_infos_all(model, prefix_type)) +} + +/// All inherited and substituted members, before choosing one declaration per key. +pub(super) fn member_infos_all(model: &SemanticModel, prefix_type: &LuaType) -> Vec { let mut out = Vec::new(); let mut visited = Vec::new(); collect_members(model, prefix_type, None, None, &mut visited, &mut out, None); - dedup_by_key(out) + out } /// Member with the given key of a prefix type (first match). @@ -279,6 +286,7 @@ fn direct_member_info( LuaType::Object(object) => object.get_fields().get(key).cloned().map(|typ| MemberInfo { key: key.clone(), typ, + index_key_type: None, id: None, file_id: None, is_method: false, @@ -326,6 +334,7 @@ pub(crate) fn member_info_impl( return Some(MemberInfo { key: key.clone(), typ, + index_key_type: None, id: first.as_ref().and_then(|info| info.id.clone()), file_id: first.as_ref().and_then(|info| info.file_id), is_method: first.as_ref().is_some_and(|info| info.is_method), @@ -351,6 +360,7 @@ pub(crate) fn member_info_impl( return Some(MemberInfo { key: key.clone(), typ: ret, + index_key_type: None, id: None, file_id: None, is_method: false, @@ -732,6 +742,7 @@ fn collect_members( MemberInfo { key: key.clone(), typ: ty.clone(), + index_key_type: None, id: None, file_id: None, is_method: false, @@ -745,6 +756,7 @@ fn collect_members( MemberInfo { key: LuaMemberKey::Name("[index]".into()), typ: ty.clone(), + index_key_type: None, id: None, file_id: None, is_method: false, @@ -863,6 +875,7 @@ fn expand_table_multi_return_member( .map(|(index, ty)| MemberInfo { key: LuaMemberKey::Integer(start + index as i64), typ: ty, + index_key_type: None, id: None, file_id: None, is_method: false, @@ -1012,6 +1025,27 @@ fn member_info_of( return None; } let mut typ = model.type_of_member(&member_ref.id)?; + let mut index_key_type = member.is_index_signature.then(|| { + (|| { + let tree = model.syntax_tree_of(member_ref.file_id)?; + let node = member + .value_syntax? + .to_node_from_root(&tree.get_red_root())?; + let field = node.ancestors().find_map(LuaDocTagField::cast)?; + let LuaDocFieldKey::Type(key) = field.get_field_key()? else { + return None; + }; + let generics = facts + .type_def_by_id(&member.owner) + .map(|def| def.generic_params.as_slice()) + .unwrap_or_default(); + Some(model.doc_type_lua_in(member_ref.file_id, key.get_syntax_id(), generics)) + })() + .unwrap_or_else(|| { + model.cache.borrow_mut().mark_incomplete(); + LuaType::Unknown + }) + }); // Cross-file projection of runtime function members: `type_of_member` often returns a wide Function, // so look up the real signature in the member's declaring file to let `require(...).module.fn()` keep inferring. // Standard library members keep the wide Function behavior to avoid disturbing parameter checks / math library overloads. @@ -1063,14 +1097,17 @@ fn member_info_of( }); if let Some(bindings) = bindings { typ = unify::substitute(&typ, bindings); + index_key_type = index_key_type.map(|key| unify::substitute(&key, bindings)); } if let Some(name_bindings) = name_bindings { typ = substitute_named_refs(&typ, name_bindings); + index_key_type = index_key_type.map(|key| substitute_named_refs(&key, name_bindings)); } Some(MemberInfo { key: member_key_to_lua(&member.key), typ, + index_key_type, id: Some(member.id.clone()), file_id: Some(member_ref.file_id), is_method: member.is_method, @@ -1078,7 +1115,7 @@ fn member_info_of( } /// Deduplicate by key (keep first: collection order = this class's @field → parent → runtime values). -fn dedup_by_key(infos: Vec) -> Vec { +pub(super) fn dedup_by_key(infos: Vec) -> Vec { let mut seen: Vec = Vec::new(); let mut out = Vec::new(); for info in infos { @@ -1221,7 +1258,7 @@ mod tests { let model = env.model(); let infos = model.member_infos(&LuaType::Ref(LuaTypeDeclId::global("M"))); let map = member_map(&infos); - assert_eq!(map.get("y"), Some(&LuaType::Number)); + assert_eq!(map.get("y"), Some(&LuaType::Integer)); } /// Generic instance: `value` member of `Box` substitutes to `number`. diff --git a/crates/emmylua_code_analysis/src/semantic_model/signature.rs b/crates/emmylua_code_analysis/src/semantic_model/signature.rs index 26ef66977..5c7d6b2f7 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/signature.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/signature.rs @@ -1,7 +1,18 @@ -use crate::MemberList; +use crate::{MemberList, OperatorDef}; use super::prelude::*; +// Object annotations lose their fields when lowered to `table` or `table | nil`. +fn needs_rich_doc_type(ty: &LuaType) -> bool { + match ty { + LuaType::Unknown | LuaType::Table => true, + LuaType::Union(union) => union + .iter() + .all(|part| matches!(part, LuaType::Table | LuaType::Nil)), + _ => false, + } +} + impl<'db> SemanticModel<'db> { /// Members whose owner is a `SemanticId` (cross-file). pub fn members_of_owner(&self, owner: &SemanticId) -> MemberList { @@ -47,6 +58,21 @@ impl<'db> SemanticModel<'db> { let mut ty = self .analysis() .type_shell_lua_in(self.view.file_id(), &shell, &generic_names); + // Object annotations lower to broad `table` in TypeShell. Keep their + // fields and index signatures when checking a function's return value. + if needs_rich_doc_type(&ty) + && let Some(docs) = self + .file_facts()? + .signature_by_closure(closure_syntax)? + .docs + .as_ref() + && docs.returns.len() == 1 + { + let rich = self.doc_type_lua_rich(docs.returns[0]); + if !matches!(rich, LuaType::Unknown) { + ty = rich; + } + } // `self` in method annotations is the receiver instance; concretize it by the owner type before return checks. if let Some(owner_ty) = self.method_owner_type(closure_syntax) { ty = infer::vm::replace_self_type(&ty, &owner_ty); @@ -143,6 +169,19 @@ impl<'db> SemanticModel<'db> { self.type_of_expr_impl(expr_syntax) } pub(crate) fn type_of_expr_impl(&self, expr_syntax: LuaSyntaxId) -> LuaType { + self.type_of_expr_cached(expr_syntax, || infer::infer_expr(self, expr_syntax)) + } + /// Reuse a resolved node while keeping the ordinary query's cycle guard. + pub(crate) fn type_of_expr_node(&self, expr: &LuaExpr) -> LuaType { + self.type_of_expr_cached(expr.get_syntax_id(), || { + infer::vm::infer_expr_node(self, expr.clone()) + }) + } + fn type_of_expr_cached( + &self, + expr_syntax: LuaSyntaxId, + infer: impl FnOnce() -> LuaType, + ) -> LuaType { let key = (self.view.file_id(), expr_syntax); { let cache = self.cache.borrow(); @@ -159,7 +198,7 @@ impl<'db> SemanticModel<'db> { .expr_type .insert(key, cache::CacheEntry::InProgress); let revision = self.cache.borrow().revision(); - let ty = infer::infer_expr(self, expr_syntax); + let ty = infer(); let mut cache = self.cache.borrow_mut(); let entry = cache::CacheEntry::finish(ty.clone(), cache.is_complete(revision)); cache.expr_type.insert(key, entry); @@ -173,7 +212,7 @@ impl<'db> SemanticModel<'db> { pub fn eval_conditionals_for_hover(&self, ty: &LuaType) -> LuaType { type_eval::eval_conditionals(self, ty) } - /// Type compatibility check (boolean version, mirrors the old `SemanticModel::type_check`). + /// Whether a value of `source` can be used where `target` is expected. pub fn type_check(&self, source: &LuaType, target: &LuaType) -> bool { self.type_check_cached(source, target, false) .unwrap_or(false) @@ -289,6 +328,31 @@ impl<'db> SemanticModel<'db> { } ret } + /// Use the same call-operator signature for inference and diagnostics. + pub(crate) fn call_operator_type(&self, file_id: FileId, op: &OperatorDef) -> LuaFunctionType { + let params = op + .params + .iter() + .map(|syntax| { + ( + String::new(), + Some(self.doc_type_lua_rich_in(file_id, *syntax)), + ) + }) + .collect::>(); + let is_variadic = params + .last() + .is_some_and(|(_, ty)| matches!(ty, Some(LuaType::Variadic(_)))); + LuaFunctionType::new( + AsyncState::None, + false, + is_variadic, + params, + self.doc_type_lua_rich_in(file_id, op.returns), + None, + ) + } + /// Function signature (by closure syntax location) -> structured function type (`DocFunction`). pub fn type_of_signature(&self, closure_syntax: LuaSyntaxId) -> Option { let signature = self.file_facts()?.signature_by_closure(closure_syntax)?; @@ -308,7 +372,7 @@ impl<'db> SemanticModel<'db> { let mut ty = self .param_type(closure_syntax, index) .unwrap_or(LuaType::Unknown); - if matches!(ty, LuaType::Unknown | LuaType::Table) + if needs_rich_doc_type(&ty) && let Some(docs) = signature.docs.as_ref() && let Some((_, syntax)) = docs .param_types diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs index 4161a98ef..01410a031 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/assign.rs @@ -1,12 +1,10 @@ -//! Assignment-specific compatibility checks: assignment semantics not yet covered by the general type_check path. -//! -//! These checks use semantic member/type-definition facilities; the goal is to gradually fold them into the general checks. +//! Additional value compatibility checks for named structural types. use crate::{LuaMemberKey, LuaType, SemanticId, TypeDef}; use crate::semantic_model::SemanticModel; -use super::context::{TypeCheckContext, TypeCheckMode}; +use super::context::TypeCheckContext; use super::{RequiredChecks, TypeCheckFailReason, check_general_type_compact}; pub(crate) fn check_special( @@ -20,9 +18,6 @@ pub(crate) fn check_special( if check_named_intersection(context, source, target)?.is_some() { return Ok(Some(())); } - if context.mode == TypeCheckMode::Assignment { - return check_function_assign(context, source, target); - } Ok(None) } @@ -132,45 +127,6 @@ fn check_named_intersection( Ok(Some(())) } -/// Function assignment: parameter contravariance and return covariance. -fn check_function_assign( - context: &mut TypeCheckContext, - source: &LuaType, - target: &LuaType, -) -> Result, TypeCheckFailReason> { - let (LuaType::DocFunction(source_fun), LuaType::DocFunction(target_fun)) = (source, target) - else { - return Ok(None); - }; - let source_params = source_fun.get_params(); - let target_params = target_fun.get_params(); - if source_params.len() != target_params.len() { - return Err(context.mismatch(source, target)); - } - let mut checks = RequiredChecks::default(); - for (source_param, target_param) in source_params.iter().zip(target_params.iter()) { - if let (Some(source_ty), Some(target_ty)) = (&source_param.1, &target_param.1) { - // Parameter position is contravariant: the target parameter type must be acceptable by the source parameter type (including union component rules). - checks - .require(check_general_type_compact(context, target_ty, source_ty)) - .map_err(|err| { - if err.is_type_not_match() { - context.mismatch(source, target) - } else { - err - } - })?; - } - } - checks.require(check_general_type_compact( - context, - source_fun.get_ret(), - target_fun.get_ret(), - ))?; - checks.finish()?; - Ok(Some(())) -} - fn named_def(model: &SemanticModel<'_>, ty: &LuaType) -> Option { let id = match ty { LuaType::Ref(id) | LuaType::Def(id) => id, diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/comparison.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/comparison.rs index 8a28e23a6..8a60ea0b8 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/comparison.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/comparison.rs @@ -7,17 +7,14 @@ use hashbrown::hash_map::EntryRef; use hashbrown::{HashMap, HashSet}; use std::collections::VecDeque; -use super::{TypeCheckContext, TypeCheckFailReason, TypeCheckMode, TypeCheckResult}; +use super::{TypeCheckContext, TypeCheckFailReason, TypeCheckResult}; use crate::LuaType; #[derive(Clone, PartialEq, Eq, Hash)] struct Comparison { source: LuaType, target: LuaType, - mode: TypeCheckMode, - strict_union: bool, - strict_object: bool, - strict_generic: bool, + strict_subtype: bool, } struct Node { @@ -173,10 +170,7 @@ pub(super) fn check( let key = Comparison { source: source.clone(), target: target.clone(), - mode: context.mode, - strict_union: context.strict_union, - strict_object: context.strict_object, - strict_generic: context.strict_generic, + strict_subtype: context.strict_subtype, }; let comparisons = context.comparisons.clone(); if comparisons.borrow().current.is_some() { @@ -219,10 +213,7 @@ fn evaluate(context: &TypeCheckContext, id: usize, key: &Comparison) -> TypeChec graph.current = Some(id); } let mut current = TypeCheckContext { - mode: key.mode, - strict_union: key.strict_union, - strict_object: key.strict_object, - strict_generic: key.strict_generic, + strict_subtype: key.strict_subtype, ..context.clone() }; let result = super::check_comparison(&mut current, &key.source, &key.target); diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs index a2fe5ab35..e10bae24a 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/complex_type.rs @@ -1,10 +1,12 @@ -//! Complex type checks: Union/Intersection/Array/TableGeneric/Tuple/Object/Call/MultiLineUnion. +//! Complex type checks: Array/TableGeneric/Tuple/Object/Call/MultiLineUnion. -use crate::{LuaMemberKey, LuaType}; +use crate::semantic_model::type_eval::expand_alias_branches; +use crate::{LuaMemberKey, LuaObjectType, LuaType}; use super::context::TypeCheckContext; use super::{ - RequiredChecks, TypeCheckResult, check_all, check_alternatives, check_general_type_compact, + RequiredChecks, TypeCheckFailReason, TypeCheckResult, check_all, check_alternatives, + check_general_type_compact, }; pub fn check_complex_type_compact( @@ -19,24 +21,7 @@ pub fn check_complex_type_compact( LuaType::Array(compact_array) => { check_general_type_compact(context, &base, compact_array.get_base()) } - LuaType::Object(object) if context.mode.uses_assignment_rules() => { - let array_base = source_array.get_base(); - check_alternatives( - context.mismatch(source, compact_type), - object - .get_index_access() - .iter() - .filter(|(key_ty, _)| { - matches!( - key_ty, - LuaType::Integer | LuaType::Number | LuaType::IntegerConst(_) - ) - }) - .map(|(_, expected)| { - check_general_type_compact(context, array_base, expected) - }), - ) - } + LuaType::Object(object) => check_sequence_object(context, source, object), LuaType::Tuple(tuple) => check_all( tuple .get_types() @@ -87,20 +72,22 @@ pub fn check_complex_type_compact( .map(|(s, t)| check_general_type_compact(context, s, t)), ) } - LuaType::Table - | LuaType::Array(_) - | LuaType::TableGeneric(_) - | LuaType::Ref(_) - | LuaType::Def(_) => Ok(()), + LuaType::Array(array) => check_all( + source_tuple + .get_types() + .iter() + .map(|actual| check_general_type_compact(context, actual, array.get_base())), + ), + LuaType::Object(object) => check_sequence_object(context, source, object), + LuaType::Table | LuaType::TableGeneric(_) | LuaType::Ref(_) | LuaType::Def(_) => Ok(()), _ => Err(context.mismatch(source, compact_type)), }, - LuaType::Object(source_object) if context.strict_object => match compact_type { + LuaType::Object(source_object) if context.strict_subtype => match compact_type { LuaType::Object(compact_object) => { let mut checks = RequiredChecks::default(); - for (key, expected) in source_object.get_fields() { - if let Some(actual) = compact_object.get_field(key) { - checks.require(check_general_type_compact(context, expected, actual))?; - } + for (key, expected) in compact_object.get_fields() { + let actual = source_object.get_field(key).unwrap_or(&LuaType::Nil); + checks.require(check_general_type_compact(context, actual, expected))?; } checks.finish() } @@ -141,14 +128,6 @@ pub fn check_complex_type_compact( | LuaType::Generic(_) => Ok(()), _ => Err(context.mismatch(source, compact_type)), }, - LuaType::Union(_) => { - // Source union: every member must be assignable to the target. - Err(context.mismatch(source, compact_type)) - } - LuaType::Intersection(_) => { - // Source intersection: any member matching is enough (handled at the dispatch layer). - Err(context.mismatch(source, compact_type)) - } LuaType::Call(source_call) => { // M0: call types such as keyof are not expanded (semantic has no member-key union). if let LuaType::Call(compact_call) = compact_type @@ -165,3 +144,95 @@ pub fn check_complex_type_compact( _ => Err(context.mismatch(source, compact_type)), } } + +/// Arrays and tuples must satisfy both the fields and the index signatures of +/// an object. Extra keys are allowed; missing required fields are checked as nil. +fn check_sequence_object( + context: &mut TypeCheckContext, + source: &LuaType, + object: &LuaObjectType, +) -> TypeCheckResult { + let mut checks = RequiredChecks::default(); + for (key, expected) in object.get_fields() { + let actual = match (source, key) { + (LuaType::Array(array), LuaMemberKey::Integer(index)) if *index > 0 => { + Some(array.get_base()) + } + (LuaType::Tuple(tuple), LuaMemberKey::Integer(index)) if *index > 0 => { + tuple.get_types().get(*index as usize - 1) + } + _ => None, + } + .unwrap_or(&LuaType::Nil); + checks.require(check_general_type_compact(context, actual, expected))?; + } + + for (key_type, expected) in object.get_index_access() { + match source { + LuaType::Array(array) => match integer_index_applies(context, key_type) { + Ok(true) => checks.require(check_general_type_compact( + context, + array.get_base(), + expected, + ))?, + Ok(false) => {} + Err(err) => checks.require(Err(err))?, + }, + LuaType::Tuple(tuple) => { + for (index, actual) in tuple.get_types().iter().enumerate() { + let key = LuaType::IntegerConst(index as i64 + 1); + match check_general_type_compact(context, &key, key_type) { + Ok(()) => { + checks.require(check_general_type_compact(context, actual, expected))? + } + Err(err) if err.is_type_not_match() => {} + Err(err) => checks.require(Err(err))?, + } + } + } + _ => unreachable!("only arrays and tuples use sequence object checks"), + } + } + checks.finish() +} + +pub(crate) fn integer_index_applies( + context: &mut TypeCheckContext, + key_type: &LuaType, +) -> Result { + // A whole-type match can prove overlap without expanding recursive branches. + if check_alternatives( + context.mismatch(&LuaType::Integer, key_type), + [ + check_general_type_compact(context, &LuaType::Integer, key_type), + check_general_type_compact(context, key_type, &LuaType::Integer), + ], + ) + .is_ok() + { + return Ok(true); + } + // One numeric alternative is enough to constrain the array's values. + // Use the shared alias expansion so named unions follow the same rule. + let key_type = context + .require_complete(|| expand_alias_branches(context.model, key_type))? + .ok_or(TypeCheckFailReason::Unresolved)?; + let failure = context.mismatch(&LuaType::Integer, &key_type); + let keys = match key_type { + LuaType::Union(union) => union.into_vec(), + key => vec![key], + }; + match check_alternatives( + failure, + keys.iter().flat_map(|key| { + [ + check_general_type_compact(context, &LuaType::Integer, key), + check_general_type_compact(context, key, &LuaType::Integer), + ] + }), + ) { + Ok(()) => Ok(true), + Err(err) if err.is_type_not_match() => Ok(false), + Err(err) => Err(err), + } +} diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs index 197f86545..3549312a3 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/context.rs @@ -12,42 +12,12 @@ use super::fail_reason::TypeCheckFailReason; use crate::semantic_model::SemanticModel; use crate::semantic_model::render::humanize_type; -/// Rules for comparing a value's type with the expected type. -/// Call argument counts are checked separately. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum TypeCheckMode { - /// General compatibility checks, such as checking function arguments. - /// If `Dog` extends `Animal`, a `Dog` can supply an `Animal` parameter. - Compatibility, - - /// Check assignments to declared variables and table entries. - /// For example, a `string` can be assigned to `string | nil`. - /// Function types use the assignment-specific parameter rules. - Assignment, - - /// Check result sequences during a compatibility check. - /// For example, `integer...` can supply `number...`, but not the reverse. - /// Use assignment rules for result values and `Compatibility` rules - /// for function parameters. - Result, -} - -impl TypeCheckMode { - pub(crate) fn uses_assignment_rules(self) -> bool { - self != Self::Compatibility - } -} - #[derive(Clone)] pub struct TypeCheckContext<'db> { pub detail: bool, pub model: &'db SemanticModel<'db>, - pub mode: TypeCheckMode, - /// Strict subtype mode (legacy `type_check_subtype`: all components of a union target, object field level). - pub strict_union: bool, - pub strict_object: bool, - /// Strict generic inheritance mode: retain parent class generic arguments for precise inheritance checks. - pub strict_generic: bool, + /// Subtype proofs also check object fields and generic inheritance arguments. + pub strict_subtype: bool, pub(super) comparisons: std::rc::Rc>, } @@ -56,10 +26,7 @@ impl<'db> TypeCheckContext<'db> { Self { detail, model, - mode: TypeCheckMode::Compatibility, - strict_union: false, - strict_object: false, - strict_generic: false, + strict_subtype: false, comparisons: Default::default(), } } diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/func_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/func_type.rs index 2e22c3630..6b735ed40 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/func_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/func_type.rs @@ -7,7 +7,7 @@ use crate::LuaSignatureId; use crate::LuaType; use super::context::TypeCheckContext; -use super::{RequiredChecks, TypeCheckResult, check_all, check_general_type_compact}; +use super::{RequiredChecks, TypeCheckResult, check_general_type_compact}; pub fn check_doc_func_type_compact( context: &mut TypeCheckContext, @@ -36,13 +36,6 @@ pub fn check_doc_func_type_compact( )) } } - LuaType::Union(union) => check_all(union.iter().map(|union_type| { - check_general_type_compact( - context, - &LuaType::DocFunction(Rc::new(source_func.clone())), - union_type, - ) - })), LuaType::Function => Ok(()), _ => Err(context.mismatch( &LuaType::DocFunction(Rc::new(source_func.clone())), @@ -56,50 +49,9 @@ fn check_doc_func_type_compact_for_params( source_func: &LuaFunctionType, compact_func: &LuaFunctionType, ) -> 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 source_is_variadic = source_func.is_variadic(); - let compact_is_variadic = compact_func.is_variadic(); - let source_len = source_params.len(); - let compact_len = compact_params.len(); let mut checks = RequiredChecks::default(); - for i in 0..compact_len { - let Some(source_param) = source_params.get(i) else { - break; - }; - let compact_param = &compact_params[i]; - let source_param_type = &source_param.1; - - if source_is_variadic && i + 1 == source_len { - checks.require(check_doc_func_type_compact_for_varargs( - context, - source_param_type, - &compact_params[i..], - ))?; - } - - if compact_is_variadic && i + 1 == compact_len { - break; - } - - let compact_param_type = &compact_param.1; - if let (Some(source_type), Some(compact_type)) = (source_param_type, compact_param_type) { - // Parameters: when injecting values, the compact parameter type must be acceptable to the source. - let result = check_general_type_compact(context, compact_type, source_type); - if result.as_ref().is_err_and(|err| err.is_type_not_match()) - && i == 0 - && source_type.is_self_infer() - && compact_param.0 == "self" - { - continue; - } - checks.require(result)?; - } + for (source, target) in function_param_pairs(source_func, compact_func) { + checks.require(check_general_type_compact(context, &source, &target))?; } // Return covariance (not checked in the old version; added here): the source return must be assignable to the compact return. @@ -111,22 +63,51 @@ fn check_doc_func_type_compact_for_params( checks.finish() } -fn check_doc_func_type_compact_for_varargs( - context: &mut TypeCheckContext, - varargs: &Option, - compact_params: &[(String, Option)], -) -> TypeCheckResult { - let Some(varargs_type) = varargs else { - return Ok(()); - }; - check_all( - compact_params - .iter() - .filter_map(|(_, ty)| ty.as_ref()) - .map(|compact_param_type| { - check_general_type_compact(context, compact_param_type, varargs_type) - }), - ) +/// Parameter constraints in value-flow order: the caller's argument type must +/// fit the implementation's parameter type. Both checkers use the same slot rules. +pub(crate) fn function_param_pairs( + source_func: &LuaFunctionType, + compact_func: &LuaFunctionType, +) -> Vec<(LuaType, LuaType)> { + let source_params = source_func.get_variadic_params(); + let target_params = compact_func.get_variadic_params(); + // After both fixed prefixes, one comparison covers repeated parameter tails. + let count = source_params + .get_max_len() + .unwrap_or_else(|| source_params.get_min_len().unwrap_or(0) + 1) + .max( + target_params + .get_max_len() + .unwrap_or_else(|| target_params.get_min_len().unwrap_or(0) + 1), + ); + // Expanding tuple tails contain fixed slots; repeated tails contain none. + let fixed_source_count = source_params.get_min_len().unwrap_or(0); + let fixed_target_count = target_params.get_min_len().unwrap_or(0); + let mut pairs = Vec::new(); + for index in 0..count { + let Some(source_param) = source_params.get_type(index) else { + // The function ignores arguments beyond its own parameters. + break; + }; + let target_param = match target_params.get_type(index) { + Some(ty) => ty, + None if index < fixed_source_count => &LuaType::Nil, + None => break, + }; + if source_param.is_self_infer() || target_param.is_self_infer() { + continue; + } + // A repeated caller tail can be empty. A fixed implementation slot + // beyond the caller's prefix must also accept a missing argument. + let target_param = if index < fixed_source_count && index >= fixed_target_count { + LuaType::from_vec(vec![target_param.clone(), LuaType::Nil]) + } else { + target_param.clone() + }; + pairs.push((target_param, source_param.clone())); + } + + pairs } fn check_doc_func_type_compact_for_signature( diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/generic_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/generic_type.rs index 4265d8031..9197cbba2 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/generic_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/generic_type.rs @@ -62,17 +62,17 @@ pub fn check_generic_type_compact( } check_generic_type_compact_ref_type(context, source_generic, ref_id) } + LuaType::Array(array) if source_generic.get_base_type_id().get_name() == "table" => { + let value = source_generic + .get_params() + .get(1) + .unwrap_or(&LuaType::Unknown); + check_general_type_compact(context, value, array.get_base()) + } LuaType::Table | LuaType::TableConst(_) | LuaType::Object(_) | LuaType::Tuple(_) => { // Table structures: M0 checks by base type name (`table`). Ok(()) } - LuaType::Union(union) => check_all(union.iter().map(|sub| { - check_general_type_compact( - context, - &LuaType::Generic(Rc::new(source_generic.clone())), - sub, - ) - })), _ => Err(context.mismatch( &LuaType::Generic(Rc::new(source_generic.clone())), compact_type, diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/legacy_tests.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/legacy_tests.rs index fb24c3244..f0b432abb 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/legacy_tests.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/legacy_tests.rs @@ -124,13 +124,13 @@ mod test { // case for tuple, object, and table { let object_ty = ws.ty("{ [1]: string, [2]: number }"); - let matched_tulple_ty = ws.ty("[string, number"); + let matched_tuple_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_tuple_ty)); assert!(ws.check_type(&object_ty, &matched_object_ty)); - let mismatch_tulple_ty = ws.ty("[number, string]"); - assert!(!ws.check_type(&object_ty, &mismatch_tulple_ty)); + let mismatch_tuple_ty = ws.ty("[number, string]"); + assert!(!ws.check_type(&object_ty, &mismatch_tuple_ty)); let matched_table_ty = ws.expr_ty("{ [1] = 'test', [2] = 1 }"); assert!(ws.check_type(&object_ty, &matched_table_ty)); diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs index c8f913f02..a3865c6d5 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/mod.rs @@ -22,14 +22,14 @@ mod sub_type; #[cfg(test)] mod test; -use std::ops::Deref; - -pub use context::{TypeCheckContext, TypeCheckMode}; +pub(crate) use complex_type::integer_index_applies; +pub use context::TypeCheckContext; pub use fail_reason::TypeCheckFailReason; +pub(crate) use func_type::function_param_pairs; use crate::semantic_model::SemanticModel; use crate::semantic_model::type_eval::expand_alias_head; -use crate::{InFiled, LuaType, LuaTypeDeclId, LuaUnionType, SemanticId, TypeDef, TypeDefKind}; +use crate::{InFiled, LuaType, SemanticId, TypeDef, TypeDefKind}; pub type TypeCheckResult = Result<(), TypeCheckFailReason>; @@ -110,11 +110,6 @@ pub fn check_type_detail( check_general_type_compact(&mut context, source, target) } -/// Assignment-compatibility boolean interface: uses assignment semantics. -pub fn is_assign_compatible(model: &SemanticModel, source: &LuaType, target: &LuaType) -> bool { - check_assign_type_detail(model, source, target).is_ok() -} - /// Strict subtype check: for cases requiring precise union/object subtype relationships. pub fn check_type_subtype( model: &SemanticModel, @@ -122,20 +117,7 @@ pub fn check_type_subtype( target: &LuaType, ) -> TypeCheckResult { let mut context = TypeCheckContext::new(model, false); - context.strict_union = true; - context.strict_object = true; - context.strict_generic = true; - check_general_type_compact(&mut context, source, target) -} - -/// Assignment-compatibility detail interface: includes a detailed reason on failure. -pub fn check_assign_type_detail( - model: &SemanticModel, - source: &LuaType, - target: &LuaType, -) -> TypeCheckResult { - let mut context = TypeCheckContext::new(model, true); - context.mode = TypeCheckMode::Assignment; + context.strict_subtype = true; check_general_type_compact(&mut context, source, target) } @@ -201,73 +183,89 @@ fn check_general_type_compact_inner( source: &LuaType, compact_type: &LuaType, ) -> TypeCheckResult { - if fast_eq_check(source, compact_type) { - return Ok(()); + for (is_source, ty) in [(true, source), (false, compact_type)] { + if let LuaType::Ref(id) | LuaType::Def(id) = ty + && let Some(def) = context.type_def_of(id) + && def.kind == TypeDefKind::Enum + && !matches!( + if is_source { compact_type } else { source }, + LuaType::Table | LuaType::TableConst(_) + ) + && let Some(values) = + context.require_complete(|| enum_value_type(context.model, &def))? + { + return if is_source { + check_general_type_compact(context, &values, compact_type) + } else { + check_general_type_compact(context, source, &values) + }; + } } - // Check the whole boolean domain before assignment mode selects a union member. + // Check both boolean values before selecting a target union member. if matches!(source, LuaType::Boolean) { return simple_type::check_simple_type_compact(context, source, compact_type); } - // Assignment mode: integer is assignable to number; number is not assignable to integer. - if context.mode.uses_assignment_rules() { - match (source, compact_type) { - ( - LuaType::Integer | LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_), - LuaType::Number, - ) => return Ok(()), - ( - LuaType::Number | LuaType::FloatConst(_), - LuaType::Integer | LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_), - ) => return Err(context.mismatch(source, compact_type)), - _ => {} - } - // Assignment mode: assigning a table literal to a named class requires nominal relationship. - if let (LuaType::TableConst(table), LuaType::Ref(target_id) | LuaType::Def(target_id)) = - (source, compact_type) - { - let source_def = table_associated_type_def(context.model, table); - let target_def = context.model.resolve_type_def(target_id.get_name()); - if let (Some(source_def), Some(target_def)) = (source_def, target_def) - && !defs_related_ctx(context.model, &source_def, &target_def, &mut Vec::new()) - { - return Err(context.mismatch(source, compact_type)); - } - } - // Assignment mode: an integer enum target accepts broad integers; literals must be member values. - if let LuaType::Ref(target_id) | LuaType::Def(target_id) = compact_type - && let Some(def) = context.model.resolve_type_def(target_id.get_name()) - && def.kind == TypeDefKind::Enum - { - match source { - LuaType::Integer | LuaType::DocIntegerConst(_) => return Ok(()), - LuaType::IntegerConst(value) => { - if enum_integer_values_ctx(context.model, &def).contains(value) { - return Ok(()); - } - return Err(context.mismatch(source, compact_type)); - } - _ => {} - } - } - // Source union members can match different target members; check them below. - if let LuaType::Union(union) = compact_type - && !matches!(source, LuaType::Union(_)) - { - return check_alternatives( - context.mismatch(source, compact_type), - union - .iter() - .map(|component| check_general_type_compact(context, source, component)), - ); - } + if let LuaType::Union(union) = source { + return check_all( + union + .iter() + .map(|member| check_general_type_compact(context, member, compact_type)), + ); + } + // Reduce a result sequence before selecting a scalar target union member. + // Its first value can itself be a union (for example, `integer?`). + if matches!(source, LuaType::Variadic(_)) && !compact_type.contain_multi_return() { + return simple_type::check_variadic_type_compact(context, source, compact_type); + } + if let LuaType::Union(union) = compact_type { + return check_alternatives( + context.mismatch(source, compact_type), + union + .iter() + .map(|member| check_general_type_compact(context, source, member)), + ); + } + + if matches!(compact_type, LuaType::Variadic(_)) + && !matches!( + source, + LuaType::Any + | LuaType::Unknown + | LuaType::Never + | LuaType::TplRef(_) + | LuaType::ModuleRef(_) + ) + { + return simple_type::check_variadic_type_compact(context, source, compact_type); } - // Assignment-specific checks: semantics for object/intersection/function assignment not yet in the general path. - if context.mode.uses_assignment_rules() - && assign::check_special(context, source, compact_type)?.is_some() + // Values use the same rules in assignments, arguments, and results. + match (source, compact_type) { + ( + LuaType::Integer | LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_), + LuaType::Number, + ) => return Ok(()), + ( + LuaType::Number | LuaType::FloatConst(_), + LuaType::Integer | LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_), + ) => return Err(context.mismatch(source, compact_type)), + _ => {} + } + // Assigning a table literal to a named class requires nominal relationship. + if let (LuaType::TableConst(table), LuaType::Ref(target_id) | LuaType::Def(target_id)) = + (source, compact_type) { + let source_def = table_associated_type_def(context.model, table); + let target_def = context.model.resolve_type_def(target_id.get_name()); + if let (Some(source_def), Some(target_def)) = (source_def, target_def) + && !defs_related_ctx(context.model, &source_def, &target_def, &mut Vec::new()) + { + return Err(context.mismatch(source, compact_type)); + } + } + if assign::check_special(context, source, compact_type)?.is_some() { return Ok(()); } @@ -371,31 +369,6 @@ fn check_union_intersection_source( compact_type: &LuaType, ) -> TypeCheckResult { match source { - LuaType::Union(union) => { - // Assignment mode: every member of the source union must be assignable to the target. - if context.mode.uses_assignment_rules() { - return check_all( - union - .iter() - .map(|member| check_general_type_compact(context, member, compact_type)), - ); - } - // Strict subtype mode: when the target is also a union, every target component must have a matching member in the source union. - if context.strict_union - && let LuaType::Union(target_union) = compact_type - { - return check_all(target_union.iter().map(|target_component| { - check_general_type_compact(context, source, target_component) - })); - } - // Source union: pass if any member is assignable (old semantics). - check_alternatives( - context.mismatch(source, compact_type), - union - .iter() - .map(|member| check_general_type_compact(context, member, compact_type)), - ) - } LuaType::Intersection(intersection) => { // Source intersection: any member matching is enough. check_alternatives( @@ -450,29 +423,25 @@ fn def_extends_ctx( false } -fn enum_integer_values_ctx(model: &SemanticModel, def: &TypeDef) -> std::collections::HashSet { - let mut out = std::collections::HashSet::new(); - for member_ref in model.members_of_owner(&def.id).iter() { - let Some(facts) = model.file_facts_of(member_ref.file_id) else { - continue; - }; - let Some(member) = facts.member_by_id(&member_ref.id) else { - continue; - }; - let Some(value_syntax) = member.value_syntax else { - continue; - }; - let Some(tree) = model.syntax_tree_of(member_ref.file_id) else { - continue; - }; - let Some(node) = value_syntax.to_node_from_root(&tree.get_red_root()) else { - continue; +fn enum_value_type(model: &SemanticModel, def: &TypeDef) -> Option { + let facts = model.file_facts_of(def.file_id)?; + let decl = facts.decl_named(def.name.as_str())?; + let mut values = Vec::new(); + for member_ref in model.members_of_owner(&decl.id).iter() { + let facts = model.file_facts_of(member_ref.file_id)?; + let member = facts.member_by_id(&member_ref.id)?; + let syntax = member.value_syntax?; + let tree = model.syntax_tree_of(member_ref.file_id)?; + let node = syntax.to_node_from_root(&tree.get_red_root())?; + // Preserve literal values while sharing recursion state across file views. + let value = if let Some(value) = eval_enum_integer_expr(node.text().to_string()) { + LuaType::IntegerConst(value) + } else { + model.model_for(member_ref.file_id).type_of_expr(syntax) }; - if let Some(value) = eval_enum_integer_expr(node.text().to_string()) { - out.insert(value); - } + values.push(value); } - out + (!values.is_empty()).then(|| LuaType::from_vec(values)) } fn eval_enum_integer_expr(text: String) -> Option { @@ -495,36 +464,3 @@ fn is_like_any(ty: &LuaType) -> bool { _ => false, } } - -fn fast_eq_check(a: &LuaType, b: &LuaType) -> bool { - match (a, b) { - (LuaType::Nil, LuaType::Nil) - | (LuaType::Table, LuaType::Table) - | (LuaType::Userdata, LuaType::Userdata) - | (LuaType::Function, LuaType::Function) - | (LuaType::Thread, LuaType::Thread) - | (LuaType::Boolean, LuaType::Boolean) - | (LuaType::String, LuaType::String) - | (LuaType::Integer, LuaType::Integer) - | (LuaType::Number, LuaType::Number) - | (LuaType::Io, LuaType::Io) - | (LuaType::Global, LuaType::Global) - | (LuaType::Unknown, LuaType::Unknown) - | (LuaType::Any, LuaType::Any) => true, - (LuaType::Ref(left), LuaType::Ref(right)) => left == right, - (LuaType::Union(u), LuaType::Ref(id)) => { - if let LuaUnionType::Nullable(LuaType::Ref(left)) = u.deref() { - return left == id; - } - false - } - (LuaType::Generic(left), LuaType::Generic(right)) => left == right, - _ => false, - } -} - -// Ensure LuaTypeDeclId can be constructed globally (used for tests/base type names). -#[allow(unused)] -fn _assert_decl_id(id: LuaTypeDeclId) { - let _ = id; -} diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/ref_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/ref_type.rs index fbb6b5a5a..55c4941ae 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/ref_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/ref_type.rs @@ -6,7 +6,7 @@ use crate::{LuaGenericType, LuaType, LuaTypeDeclId, TypeDefKind}; use super::context::TypeCheckContext; use super::sub_type::{get_base_type_id, is_sub_type_of}; -use super::{TypeCheckResult, check_all, check_general_type_compact}; +use super::{TypeCheckResult, check_general_type_compact}; /// Generic inheritance matching: whether sub's inheritance chain contains a generic parent matching the target generic instance arguments. fn is_sub_type_of_generic( @@ -117,18 +117,19 @@ fn check_ref_class( } Err(context.mismatch(&LuaType::Ref(source_id.clone()), compact_type)) } - LuaType::Table => Ok(()), - LuaType::Union(union_type) => { - check_all(union_type.iter().map(|typ| { - check_general_type_compact(context, &LuaType::Ref(source_id.clone()), typ) - })) + LuaType::Array(array) => { + let value = context + .require_complete(|| context.model.infer_iter_from_type_def(source_id, 1))? + .ok_or_else(|| context.mismatch(&LuaType::Ref(source_id.clone()), compact_type))?; + check_general_type_compact(context, &value, array.get_base()) } + LuaType::Table => Ok(()), LuaType::Generic(generic) => { let base_type_id = generic.get_base_type_id(); - if (context.strict_generic + if (context.strict_subtype && context .require_complete(|| is_sub_type_of_generic(context, source_id, generic))?) - || (!context.strict_generic + || (!context.strict_subtype && (source_id == &base_type_id || is_sub_type_of(context, &base_type_id, source_id) || is_sub_type_of(context, source_id, &base_type_id))) diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs index 577016f23..661c5a2bc 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/simple_type.rs @@ -1,14 +1,11 @@ //! Compatibility of base types and constants (fully ported from the old //! `semantic::type_check::simple_type`, with db access going through the semantic context). -use std::ops::Deref; -use std::rc::Rc; +use crate::{LuaMemberKey, LuaType, LuaTypeDeclId}; -use crate::{LuaMemberKey, LuaType, LuaTypeDeclId, VariadicType}; - -use super::context::{TypeCheckContext, TypeCheckMode}; +use super::context::TypeCheckContext; use super::sub_type::{base_type_name, get_base_type_id, is_sub_type_of}; -use super::{TypeCheckResult, check_all, check_alternatives}; +use super::{TypeCheckResult, check_all}; pub fn check_simple_type_compact( context: &mut TypeCheckContext, @@ -17,21 +14,8 @@ pub fn check_simple_type_compact( ) -> TypeCheckResult { match source { LuaType::Unknown | LuaType::Any => return Ok(()), - LuaType::Nil => { - if let LuaType::Nil = compact_type { - return Ok(()); - } - } LuaType::Table | LuaType::TableConst(_) => { if let (LuaType::TableConst(_), LuaType::Tuple(tuple)) = (source, compact_type) { - // Table entries use assignment rules, including optional and union elements. - let mut element_context = TypeCheckContext { - mode: match context.mode { - TypeCheckMode::Compatibility => TypeCheckMode::Assignment, - mode => mode, - }, - ..context.clone() - }; return check_all( tuple .get_types() @@ -42,14 +26,20 @@ pub fn check_simple_type_compact( let actual = context .require_complete(|| context.model.member_type(source, &key))? .unwrap_or(LuaType::Nil); - super::check_general_type_compact( - &mut element_context, - &actual, - expected, - ) + super::check_general_type_compact(context, &actual, expected) }), ); } + if let (LuaType::TableConst(_), LuaType::Object(object)) = (source, compact_type) + && context.strict_subtype + { + return check_all(object.get_fields().iter().map(|(key, expected)| { + let actual = context + .require_complete(|| context.model.member_type(source, key))? + .unwrap_or(LuaType::Nil); + super::check_general_type_compact(context, &actual, expected) + })); + } if matches!( compact_type, LuaType::Table @@ -85,11 +75,6 @@ pub fn check_simple_type_compact( return Ok(()); } } - LuaType::Thread => { - if let LuaType::Thread = compact_type { - return Ok(()); - } - } LuaType::Boolean => { // Both values must fit, possibly through different target union members. return check_all([true, false].into_iter().map(|value| { @@ -105,22 +90,18 @@ pub fn check_simple_type_compact( LuaType::BooleanConst(other) | LuaType::DocBooleanConst(other) if value == other => { return Ok(()); } - LuaType::Union(union) => { - return check_alternatives( - context.mismatch(source, compact_type), - union.iter().map(|component| { - super::check_general_type_compact(context, source, component) - }), - ); - } _ => {} }, LuaType::String => match compact_type { - LuaType::String - | LuaType::StringConst(_) - | LuaType::DocStringConst(_) - | LuaType::StrTplRef(_) - | LuaType::Language(_) => { + LuaType::String | LuaType::StrTplRef(_) | LuaType::Language(_) => return Ok(()), + LuaType::StringConst(_) | LuaType::DocStringConst(_) + if !context.strict_subtype + && context + .model + .db() + .config_input() + .is_none_or(|config| config.strict.doc_base_const_match_base_type) => + { return Ok(()); } LuaType::Ref(_) => { @@ -137,14 +118,9 @@ pub fn check_simple_type_compact( } _ => {} }, - LuaType::StringConst(_) => match compact_type { - LuaType::String - | LuaType::StringConst(_) - | LuaType::StrTplRef(_) - | LuaType::Language(_) => { - return Ok(()); - } - LuaType::DocStringConst(_) => { + LuaType::StringConst(value) | LuaType::DocStringConst(value) => match compact_type { + LuaType::String | LuaType::StrTplRef(_) | LuaType::Language(_) => return Ok(()), + LuaType::StringConst(other) | LuaType::DocStringConst(other) if value == other => { return Ok(()); } LuaType::Ref(_) => { @@ -162,7 +138,15 @@ pub fn check_simple_type_compact( _ => {} }, LuaType::Integer => match compact_type { - LuaType::Integer | LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_) => { + LuaType::Integer => return Ok(()), + LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_) + if !context.strict_subtype + && context + .model + .db() + .config_input() + .is_none_or(|config| config.strict.doc_base_const_match_base_type) => + { return Ok(()); } LuaType::Ref(_) => { @@ -174,20 +158,9 @@ pub fn check_simple_type_compact( } _ => {} }, - LuaType::IntegerConst(value) => match compact_type { - LuaType::IntegerConst(other) => { - if context.mode.uses_assignment_rules() || value == other { - return Ok(()); - } - return Err(context.mismatch(source, compact_type)); - } - LuaType::DocIntegerConst(other) => { - if context.mode.uses_assignment_rules() || value == other { - return Ok(()); - } - return Err(context.mismatch(source, compact_type)); - } - LuaType::Integer | LuaType::Number => { + LuaType::IntegerConst(value) | LuaType::DocIntegerConst(value) => match compact_type { + LuaType::Integer | LuaType::Number => return Ok(()), + LuaType::IntegerConst(other) | LuaType::DocIntegerConst(other) if value == other => { return Ok(()); } LuaType::Ref(_) => { @@ -199,64 +172,9 @@ pub fn check_simple_type_compact( } _ => {} }, - 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(context.mismatch(source, compact_type)); - } - LuaType::Integer | LuaType::Number => { - return Ok(()); - } - LuaType::DocIntegerConst(j) => { - if i == j { - return Ok(()); - } - return Err(context.mismatch(source, compact_type)); - } - LuaType::Ref(_) => { - // M0: doc constants are checked against custom integer types by nominal base type (integer). - return check_base_type_for_ref_compact(context, source, compact_type); - } - _ => {} - }, - LuaType::DocStringConst(s) => match compact_type { - LuaType::StringConst(t) => { - if s == t { - return Ok(()); - } - return Err(context.mismatch(source, compact_type)); - } - LuaType::String => return Err(context.mismatch(source, compact_type)), - LuaType::DocStringConst(t) => { - if s == t { - return Ok(()); - } - return Err(context.mismatch(source, compact_type)); - } + LuaType::FloatConst(value) => match compact_type { + LuaType::Number => return Ok(()), + LuaType::FloatConst(other) if value == other => return Ok(()), _ => {} }, LuaType::StrTplRef(_) => { @@ -265,15 +183,8 @@ pub fn check_simple_type_compact( } } 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); + LuaType::Variadic(_) => { + return check_variadic_type_compact(context, source, compact_type); } LuaType::Language(lang_str) => match compact_type { LuaType::Language(compact_lang_str) => { @@ -289,14 +200,6 @@ pub fn check_simple_type_compact( _ => {} } - if let LuaType::Union(union) = compact_type { - return check_all( - union - .iter() - .map(|sub_compact| super::check_general_type_compact(context, source, sub_compact)), - ); - } - Err(context.mismatch(source, compact_type)) } @@ -336,57 +239,43 @@ fn base_type_name_of_ref(id: &LuaTypeDeclId) -> Option<&'static str> { } } -fn check_variadic_type_compact( +pub(super) fn check_variadic_type_compact( context: &mut TypeCheckContext, - source_type: &VariadicType, + source: &LuaType, compact_type: &LuaType, ) -> TypeCheckResult { - let mut element_context = TypeCheckContext { - mode: match context.mode { - TypeCheckMode::Compatibility => TypeCheckMode::Result, - mode => mode, - }, - ..context.clone() + let LuaType::Variadic(target_results) = compact_type else { + // A single-value context uses only the first result. + let first = source.get_result_slot_type(0).unwrap_or(LuaType::Nil); + return super::check_general_type_compact(context, &first, compact_type); }; - match &source_type { - VariadicType::Base(source_base) | VariadicType::Repeat(source_base) => match compact_type { - LuaType::Variadic(compact_variadic) => match compact_variadic.deref() { - VariadicType::Base(_) | VariadicType::Repeat(_) => { - let count = compact_variadic - .get_max_len() - .or_else(|| source_type.get_max_len()) - .unwrap_or(1); - return check_all((0..count).map(|index| { - let source = source_type.get_type(index).cloned().unwrap_or(LuaType::Nil); - let compact = compact_variadic - .get_type(index) - .cloned() - .unwrap_or(LuaType::Nil); - super::check_general_type_compact(&mut element_context, &source, &compact) - })); - } - VariadicType::Multi(compact_multi) => { - return check_all(compact_multi.iter().enumerate().map( - |(index, compact_type)| { - let source = if compact_type.contain_multi_return() { - LuaType::Variadic(Rc::new(source_type.get_new_variadic_from(index))) - } else { - source_type.get_type(index).cloned().unwrap_or(LuaType::Nil) - }; - super::check_general_type_compact( - &mut element_context, - &source, - compact_type, - ) - }, - )); - } - }, - _ => { - super::check_general_type_compact(context, source_base, compact_type)?; - } - }, - VariadicType::Multi(_) => {} + let (source_max, source_count) = match source { + LuaType::Variadic(results) => ( + results.get_max_len(), + results + .get_max_len() + .unwrap_or_else(|| results.get_min_len().unwrap_or(0) + 1), + ), + _ => (Some(1), 1), + }; + let target_min = target_results.get_min_len().unwrap_or(0); + // One position after both fixed prefixes covers repeated tails. Finite + // targets ignore extra results; finite sources may omit a target's tail. + let count = target_results + .get_max_len() + .unwrap_or_else(|| source_count.max(target_min + 1)); + let mut checks = super::RequiredChecks::default(); + for index in 0..count { + if index >= target_min && source_max.is_some_and(|len| index >= len) { + break; + } + let actual = source.get_result_slot_type(index).unwrap_or(LuaType::Nil); + let expected = compact_type + .get_result_slot_type(index) + .unwrap_or(LuaType::Nil); + checks.require(super::check_general_type_compact( + context, &actual, &expected, + ))?; } - Ok(()) + checks.finish() } diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_check/test.rs b/crates/emmylua_code_analysis/src/semantic_model/type_check/test.rs index 13a07fa35..8be4eaa2a 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_check/test.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_check/test.rs @@ -42,14 +42,11 @@ fn union(types: Vec) -> LuaType { #[test] fn test_primitives_and_consts() { use LuaType as LT; - assert!( - ok(<::Number, <::Integer), - "旧语义:Number 可赋给 Integer" - ); - assert!(!ok(<::Integer, <::Number)); + assert!(!ok(<::Number, <::Integer)); + assert!(ok(<::Integer, <::Number)); assert!(!ok(<::String, <::Number)); assert!(ok(<::IntegerConst(1), <::Integer)); - assert!(ok(<::Number, <::IntegerConst(1))); + assert!(!ok(<::Number, <::IntegerConst(1))); assert!(ok(<::StringConst(SmolStr::new("x").into()), <::String)); assert!(ok(<::Nil, <::Nil)); assert!(!ok(<::Nil, <::Number)); @@ -94,15 +91,15 @@ fn test_union_intersection() { use LuaType as LT; // Target union: source matches any one component. let t_union = union(vec![LT::String, LT::Number]); - assert!(!ok(<::String, &t_union), "target 并集要求全部接受"); + assert!(ok(<::String, &t_union)); assert!(!ok(<::Boolean, &t_union)); - // Source union: any member matching is enough. + // Every possible source value must fit the target. let s_union = union(vec![LT::String, LT::Number]); - assert!(ok(&s_union, <::Number)); + assert!(!ok(&s_union, <::Number)); assert!(!ok(&s_union, <::Boolean)); - // Nullable: `integer?` is assignable to `integer`. + // A nullable value cannot supply a required integer. let nullable = LT::Union(Rc::new(LuaUnionType::Nullable(LT::Integer))); - assert!(ok(&nullable, <::Integer)); + assert!(!ok(&nullable, <::Integer)); } #[test] @@ -110,8 +107,8 @@ fn test_function_structure() { use LuaType as LT; // Return covariance. assert!( - ok(&doc_fun(vec![], LT::Number), &doc_fun(vec![], LT::Integer)), - "返回协变(旧数值宽松语义)" + ok(&doc_fun(vec![], LT::Integer), &doc_fun(vec![], LT::Number)), + "Integer results can supply number results" ); assert!( !ok(&doc_fun(vec![], LT::String), &doc_fun(vec![], LT::Integer)), @@ -157,6 +154,55 @@ fn test_ref_inheritance() { assert!(is_compatible(&model, &foo, &foo)); } +#[test] +fn test_named_arrays_keep_inherited_index_types() { + let mut ws = crate::VirtualWorkspace::new(); + let file = ws.def( + r#" + ---@class ArrayBase: table + ---@field [integer] string + ---@field label boolean + ---@class IntegerFieldArray: ArrayBase + ---@field integer boolean + + ---@class GenericMetadata: table + ---@field [K] boolean + ---@class ArrayWithGenericMetadata: GenericMetadata + ---@field [integer] string + + ---@class GenericArrayBase: table + ---@field [integer] T + ---@class StringArray: GenericArrayBase + ---@class OverriddenArray: GenericArrayBase + ---@field [integer] string + + ---@class RecursiveArray: RecursiveArrayBase + ---@class RecursiveArrayBase: RecursiveArray + ---@field [integer] string + "#, + ); + let strings = ws.ty("string[]"); + let numbers = ws.ty("number[]"); + for name in [ + "IntegerFieldArray", + "ArrayWithGenericMetadata", + "StringArray", + "OverriddenArray", + "RecursiveArray", + ] { + let source = ws.ty(name); + let model = ws.analysis.semantic_model(file); + assert!( + model.type_check(&source, &strings), + "{name} is a string array" + ); + assert!( + !model.type_check(&source, &numbers), + "{name} is not a number array" + ); + } +} + #[test] fn test_alias_nominal() { use lsp_types::Uri; @@ -202,7 +248,7 @@ fn test_instance_base() { LT::Number, InFiled::new(FileId::new(0), Default::default()), ))); - assert!(ok(&inst, <::Integer)); + assert!(!ok(&inst, <::Integer)); assert!(!ok(&inst, <::String)); } diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs b/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs index 68338e954..a27ced9c9 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_eval.rs @@ -797,7 +797,7 @@ fn check_conditional_extends( ConditionalCheck::False }; } - match model.type_check_cached(source, target, false) { + match model.type_check_cached(source, target, true) { Some(true) => ConditionalCheck::True, Some(false) => ConditionalCheck::False, None => ConditionalCheck::Unresolved, diff --git a/crates/emmylua_code_analysis/src/semantic_model/type_eval/tests.rs b/crates/emmylua_code_analysis/src/semantic_model/type_eval/tests.rs index 1cf1b4a3d..7d632d14f 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/type_eval/tests.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/type_eval/tests.rs @@ -70,10 +70,13 @@ fn unresolved_conditional_comparisons_stay_deferred() { ws.ty("Cycle extends string and boolean or integer"), ws.ty("string extends (Cycle | integer) and boolean or integer"), ws.ty("Cycle extends {value: infer T} and T or integer"), - ws.ty("Branch extends Source and boolean or integer"), ]; let accepted = ws.ty("string extends string and boolean or integer"); - let rejected = ws.ty("integer extends string and boolean or integer"); + let rejected = [ + ws.ty("integer extends string and boolean or integer"), + // One rejected source alternative is enough even if another is unfinished. + ws.ty("Branch extends Source and boolean or integer"), + ]; let known_union_branch = ws.ty("string extends (Cycle | string) and boolean or integer"); let deferred_field = ws.ty("{value: Cycle extends string and boolean or integer}"); let target = ws.ty("{value: string}"); @@ -94,7 +97,9 @@ fn unresolved_conditional_comparisons_stay_deferred() { Err(TypeCheckFailReason::Unresolved) ); assert_eq!(eval_conditionals(&model, &accepted), LuaType::Boolean); - assert_eq!(eval_conditionals(&model, &rejected), LuaType::Integer); + for rejected in rejected { + assert_eq!(eval_conditionals(&model, &rejected), LuaType::Integer); + } assert_eq!( eval_conditionals(&model, &known_union_branch), LuaType::Boolean @@ -202,7 +207,14 @@ fn recursive_alternatives_keep_only_proven_answers() { let model = ws.analysis.semantic_model(file); assert!(!model.type_check(&ty, &LuaType::Integer)); assert_eq!(eval_conditionals(&model, &conditional), conditional); - assert_eq!(eval_conditionals(&model, &productive), LuaType::String); + // A source union requires every alternative. An intersection can use + // its known integer component without resolving the cycle. + let expected = if operator == "|" { + productive.clone() + } else { + LuaType::String + }; + assert_eq!(eval_conditionals(&model, &productive), expected); } } @@ -225,7 +237,6 @@ fn growing_computations_report_excessive_evaluation() { "#, ); for name in [ - "Grow", "Left", "Right", "InferGrow", @@ -246,7 +257,16 @@ fn growing_computations_report_excessive_evaluation() { ); assert_eq!(eval_conditionals(&model, &conditional), conditional); } - let known = ws.ty("Productive extends integer and boolean or string"); + // The string alternative proves a mismatch before the growing branch is needed. + let rejected = ws.ty("Grow"); + let conditional = ws.ty("Grow extends integer and boolean or string"); + let model = ws.analysis.semantic_model(file); + assert!( + check_type(&model, &rejected, &LuaType::Integer).is_err_and(|err| err.is_type_not_match()) + ); + assert_eq!(eval_conditionals(&model, &conditional), LuaType::String); + + let known = ws.ty("integer extends (Productive | integer) and boolean or string"); let model = ws.analysis.semantic_model(file); assert_eq!(eval_conditionals(&model, &known), LuaType::Boolean); } @@ -274,7 +294,8 @@ fn recursive_alias_arguments_can_change_within_a_finite_set() { model.member_type(&candidate, &LuaMemberKey::Name("value".into())), Some(LuaType::Integer) ); - assert!(model.type_check(&changed, &LuaType::Integer)); + // The string alternative cannot supply an integer. + assert!(!model.type_check(&changed, &LuaType::Integer)); assert!(model.type_check_subtype(&switch, &accepted)); assert!(!model.type_check_subtype(&switch, &rejected)); } @@ -340,7 +361,8 @@ fn inferred_components_can_consume_recursive_alias_arguments() { } let wrapped = ws.ty("Wrapped"); let model = ws.analysis.semantic_model(file); - assert!(model.type_check(&wrapped, &LuaType::Integer)); + // Resolving a later integer alternative must not discard the string alternative. + assert!(!model.type_check(&wrapped, &LuaType::Integer)); } #[test] @@ -364,11 +386,8 @@ fn recursive_json_aliases_keep_finite_nested_values() { ] { let source_type = ws.ty(source); let model = ws.analysis.semantic_model(file); - let result = crate::semantic_model::type_check::check_assign_type_detail( - &model, - &source_type, - &target, - ); + let result = + crate::semantic_model::type_check::check_type_detail(&model, &source_type, &target); if matches!(source, "fun()" | "thread") { assert!( result.is_err_and(|err| err.is_type_not_match()), @@ -400,7 +419,7 @@ fn recursive_matches_ignore_unused_union_alternatives() { let target = ws.ty(target); let model = ws.analysis.semantic_model(file); assert_eq!( - crate::semantic_model::type_check::check_assign_type_detail(&model, &source, &target), + crate::semantic_model::type_check::check_type_detail(&model, &source, &target), Ok(()), "{target:?}" ); @@ -415,8 +434,7 @@ fn finite_comparisons_have_no_depth_cutoff() { for (leaf, accepted) in [("string", true), ("(fun())", false)] { let source = ws.ty(&format!("{leaf}{}", "[]".repeat(256))); let model = ws.analysis.semantic_model(file); - let result = - crate::semantic_model::type_check::check_assign_type_detail(&model, &source, &target); + let result = crate::semantic_model::type_check::check_type_detail(&model, &source, &target); assert_eq!(result.is_ok(), accepted, "{leaf}"); if !accepted { assert!(result.is_err_and(|err| err.is_type_not_match())); @@ -451,7 +469,7 @@ fn function_parameters_keep_independent_alias_paths() { expected ); assert_eq!( - crate::semantic_model::type_check::check_assign_type_detail( + crate::semantic_model::type_check::check_type_detail( &model, &source_type, &target_type diff --git a/crates/emmylua_code_analysis/src/semantic_model/types.rs b/crates/emmylua_code_analysis/src/semantic_model/types.rs index c6f3a3b01..9db132287 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/types.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/types.rs @@ -752,25 +752,39 @@ impl<'db> SemanticModel<'db> { let substituted = infer::unify::substitute(slot, &bindings); (!matches!(substituted, LuaType::Unknown)).then_some(substituted) } - /// Infers `ipairs` slots directly from integer-index members of named types (does not depend on global pairs/ipairs signatures). + /// Infers `ipairs` slots from integer-index members, including inherited generic members. pub(crate) fn infer_iter_from_type_def( &self, id: &LuaTypeDeclId, index: usize, ) -> Option { - let def = self.type_def_of(id)?; - let mut values: Vec = Vec::new(); - for member_ref in self.members_of_owner(&def.id).iter() { - let facts = self.file_facts_of(member_ref.file_id)?; - let member = facts.member_by_id(&member_ref.id)?; - let is_integer_key = matches!(member.key, LuaMemberKey::Integer(_)); - let is_index_sig = member.is_index_signature; - if !is_integer_key && !is_index_sig { - continue; + let mut members = member::member_infos_all(self, &LuaType::Ref(id.clone())); + // Filter before selecting overrides: an ordinary `integer` field and + // an `[integer]` index signature have the same text key. + let mut context = type_check::TypeCheckContext::new(self, false); + let mut incomplete = false; + members.retain(|info| { + if matches!(info.key, LuaMemberKey::Integer(_)) { + return true; } - let value = self - .type_of_member(&member_ref.id) - .unwrap_or(LuaType::Unknown); + let Some(key) = &info.index_key_type else { + return false; + }; + match type_check::integer_index_applies(&mut context, key) { + Ok(applies) => applies, + Err(_) => { + incomplete = true; + false + } + } + }); + if incomplete { + self.cache.borrow_mut().mark_incomplete(); + return None; + } + let mut values: Vec = Vec::new(); + for info in member::dedup_by_key(members) { + let value = info.typ; if !values.contains(&value) { values.push(value); } @@ -1299,7 +1313,8 @@ impl<'db> SemanticModel<'db> { ) -> LuaType { flow::type_of_decl_before_at(self, decl, offset) } - /// Target type for assignment checks: applies `---@cast +T`, excludes this assignment, but does not apply conditional narrowing. + /// Assignment contract: widens inferred literals, preserves parameter and + /// `---@type` contracts, and applies casts without conditional narrowing. pub fn type_of_decl_assign_target_at(&self, decl: &SemanticId, offset: TextSize) -> LuaType { self.type_of_decl_assign_target_at_impl(decl, offset) } @@ -1308,7 +1323,34 @@ impl<'db> SemanticModel<'db> { decl: &SemanticId, offset: TextSize, ) -> LuaType { - flow::type_of_decl_assign_target_at(self, decl, offset) + let target = flow::type_of_decl_assign_target_at(self, decl, offset); + let inferred = self + .file_facts() + .and_then(|facts| facts.decl_by_id(decl)) + .is_some_and(|decl| { + decl.doc_type_syntax.is_none() && !matches!(decl.kind, DeclKind::Param) + }); + if inferred { + widen_assignment_type(target) + } else { + target + } + } + /// Member assignment contract, keeping explicit field annotations exact. + pub(crate) fn member_assignment_type(&self, member: &SemanticId, target: LuaType) -> LuaType { + let inferred = match member { + SemanticId::Member(key) => self.file_facts_of(key.file_id), + _ => None, + } + .and_then(|facts| facts.member_by_id(member)) + .is_some_and(|member| { + member.doc_type_syntax.is_none() && !matches!(member.owner, SemanticId::TypeDef(_)) + }); + if inferred { + widen_assignment_type(target) + } else { + target + } } /// Member type before the flow node at offset (for assignment checks: this member assignment does not participate in the target type). pub fn type_of_member_before_at(&self, member: &SemanticId, offset: TextSize) -> LuaType { @@ -1333,8 +1375,8 @@ impl<'db> SemanticModel<'db> { // Flow reads must not turn a long binary/member chain into native // recursion. Evaluate each prefix/operand before its parent, as the VM does. enum Task { - Enter(LuaExpr), - Finish(LuaExpr, u64), + Enter(LuaExpr, TextSize), + Finish(LuaExpr, TextSize, u64), } let file_id = self.view.file_id(); let root_key = (file_id, expr_syntax, offset); @@ -1352,11 +1394,11 @@ impl<'db> SemanticModel<'db> { return self.type_of_expr(expr_syntax); }; let _query = cache::QueryCacheScope::new(&self.cache); - let mut pending = vec![Task::Enter(expr)]; + let mut pending = vec![Task::Enter(expr, offset)]; while let Some(task) = pending.pop() { match task { - Task::Enter(expr) => { - let key = (file_id, expr.get_syntax_id(), offset); + Task::Enter(expr, at) => { + let key = (file_id, expr.get_syntax_id(), at); if self.cache.borrow().expr_type_at.contains_key(&key) { continue; } @@ -1364,26 +1406,32 @@ impl<'db> SemanticModel<'db> { .borrow_mut() .expr_type_at .insert(key, cache::CacheEntry::InProgress); - pending.push(Task::Finish(expr.clone(), self.cache.borrow().revision())); + pending.push(Task::Finish( + expr.clone(), + at, + self.cache.borrow().revision(), + )); match expr { LuaExpr::BinaryExpr(binary) => { if let Some((left, right)) = binary.get_exprs() { - pending.push(Task::Enter(right)); - pending.push(Task::Enter(left)); + let right_pos = right.get_position(); + let left_pos = left.get_position(); + pending.push(Task::Enter(right, right_pos)); + pending.push(Task::Enter(left, left_pos)); } } LuaExpr::IndexExpr(index) => { if let Some(prefix) = index.get_prefix_expr() { - pending.push(Task::Enter(prefix)); + pending.push(Task::Enter(prefix, at)); } } _ => {} } } - Task::Finish(expr, revision) => { + Task::Finish(expr, at, revision) => { let syntax = expr.get_syntax_id(); let is_index = matches!(expr, LuaExpr::IndexExpr(_)); - let ty = flow::type_of_expr_at_node(self, expr, offset); + let ty = flow::type_of_expr_at_node(self, expr, at); let ty = if is_index { let ty = type_eval::expand_alias_generic(self, &ty); type_eval::eval_conditionals(self, &ty) @@ -1392,7 +1440,7 @@ impl<'db> SemanticModel<'db> { }; let mut cache = self.cache.borrow_mut(); let entry = cache::CacheEntry::finish(ty, cache.is_complete(revision)); - cache.expr_type_at.insert((file_id, syntax, offset), entry); + cache.expr_type_at.insert((file_id, syntax, at), entry); } } } @@ -1548,6 +1596,16 @@ fn project_base_type(model: &SemanticModel<'_>, def: &TypeDef, doc_type: LuaDocT model.doc_type_lua_in(def.file_id, doc_type.get_syntax_id(), &def.generic_params) } +fn widen_assignment_type(ty: LuaType) -> LuaType { + match ty { + LuaType::Union(union) => { + LuaType::from_vec(union.iter().cloned().map(widen_assignment_type).collect()) + } + LuaType::FloatConst(_) => LuaType::Number, + _ => infer::vm::widen_variadic_const(&ty), + } +} + /// Removes nil from a type (used for generic for loop keys: the loop stops when the first value is nil). fn remove_nil_from_type(ty: LuaType) -> LuaType { match ty { diff --git a/crates/emmylua_code_analysis/src/test_lib/mod.rs b/crates/emmylua_code_analysis/src/test_lib/mod.rs index 72bdf89f6..62020ba2a 100644 --- a/crates/emmylua_code_analysis/src/test_lib/mod.rs +++ b/crates/emmylua_code_analysis/src/test_lib/mod.rs @@ -150,12 +150,12 @@ impl VirtualWorkspace { render::humanize_type_detailed(&model, &ty) } - pub fn check_type(&self, source: &LuaType, target: &LuaType) -> bool { + pub fn check_type(&self, expected: &LuaType, actual: &LuaType) -> bool { let Some(file_id) = self.last_file_id else { return false; }; let model = self.analysis.semantic_model(file_id); - model.type_check_subtype(source, target) + model.type_check_subtype(actual, expected) } pub fn enable_check(&mut self, diagnostic_code: DiagnosticCode) { diff --git a/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs index f0e4d77de..06f0cdc4e 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs @@ -541,6 +541,7 @@ fn add_comment_scope_members(builder: &mut CompletionBuilder, seen: &mut HashSet let info = emmylua_code_analysis::MemberInfo { key: LuaMemberKey::Name(label.clone().into()), typ: ty.clone(), + index_key_type: None, id: None, file_id: None, is_method: false, 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 1d5832773..1d419fe30 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/member_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/member_provider.rs @@ -266,6 +266,7 @@ fn doc_generic_keyof_member_infos( LuaMemberKey::Name(name) => Some(MemberInfo { key: LuaMemberKey::Name(name), typ: LuaType::Unknown, + index_key_type: None, id: None, file_id: None, is_method: false, @@ -299,6 +300,7 @@ fn generic_keyof_member_infos( LuaMemberKey::Name(name) => Some(MemberInfo { key: LuaMemberKey::Name(name), typ: LuaType::Unknown, + index_key_type: None, id: None, file_id: None, is_method: false, @@ -327,6 +329,7 @@ fn type_def_member_infos_from_facts( MemberInfo { key, typ: model.type_of_member(&member.id).unwrap_or(LuaType::Unknown), + index_key_type: None, id: Some(member.id.clone()), file_id: Some(def.file_id), is_method: member.is_method, @@ -395,6 +398,7 @@ fn associated_type_member_infos( out.push(MemberInfo { key, typ: model.type_of_member(&member.id).unwrap_or(LuaType::Unknown), + index_key_type: None, id: Some(member.id.clone()), file_id: Some(member_ref.file_id), is_method: member.is_method,