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,