diff --git a/crates/emmylua_code_analysis/locales/lint.yml b/crates/emmylua_code_analysis/locales/lint.yml index ccca63fec..cc63d42e8 100644 --- a/crates/emmylua_code_analysis/locales/lint.yml +++ b/crates/emmylua_code_analysis/locales/lint.yml @@ -68,10 +68,10 @@ Should not reassign to iter variable: zh_CN: '不应重新赋值给迭代变量' zh_HK: '不應重新指定迭代變數' -expected `%{source}` but found `%{found}`: - en: expected `%{source}` but found `%{found}` - zh_CN: '预期 `%{source}`,但得到 `%{found}`' - zh_HK: '期望 `%{source}`,但得到 `%{found}`' +expected `%{source}` but found `%{found}`. %{reason}: + en: expected `%{source}` but found `%{found}`. %{reason} + zh_CN: '预期 `%{source}`,但得到 `%{found}`。 %{reason}' + zh_HK: '期望 `%{source}`,但得到 `%{found}`。 %{reason}' function %{name} may be nil: en: function %{name} may be nil zh_CN: '函数 %{name} 可能为 nil' diff --git a/crates/emmylua_code_analysis/resources/std/debug.lua b/crates/emmylua_code_analysis/resources/std/debug.lua index 6f56c8237..871a1678c 100644 --- a/crates/emmylua_code_analysis/resources/std/debug.lua +++ b/crates/emmylua_code_analysis/resources/std/debug.lua @@ -146,9 +146,11 @@ function debug.getregistry() end --- --- Variable names starting with '(' (open parenthesis) represent variables with --- no known names (variables from chunks saved without debug information). ----@param f integer +---@param f async fun(...):any... ---@param up integer ----@return table +---@return string name +---@return any value +---@nodiscard function debug.getupvalue(f, up) end --- @@ -236,6 +238,7 @@ function debug.setlocal(thread, level, var, value) end ---@param value T ---@param meta? table ---@return T value +---@overload fun(value: table, meta: T): T function debug.setmetatable(value, meta) end --- diff --git a/crates/emmylua_code_analysis/resources/std/global.lua b/crates/emmylua_code_analysis/resources/std/global.lua index 80cb0c06b..bb52db5df 100644 --- a/crates/emmylua_code_analysis/resources/std/global.lua +++ b/crates/emmylua_code_analysis/resources/std/global.lua @@ -87,8 +87,7 @@ function dofile(filename) end --- `error` function was called. Level 2 points the error to where the function --- that called `error` was called; and so on. Passing a level 0 avoids the --- addition of error position information to the message. ----@overload fun(message:string) ----@param message string +---@param message any ---@param level? integer function error(message, level) end @@ -114,7 +113,7 @@ function getmetatable(object) end --- will iterate over the key–value pairs (1,`t[1]`), (2,`t[2]`), ..., up to --- the first absent index. ---@generic V ----@param t V[] | table +---@param t V[] | table | {[any]: V} ---@return fun(tbl: any):int, std.NotNull function ipairs(t) end @@ -232,7 +231,7 @@ function next(table, index) end --- See function `next` for the caveats of modifying the table during its --- traversal. ---@generic K, V ----@param t table +---@param t table | V[] | {[K]: V} ---@return fun(tbl: any):K, std.NotNull function pairs(t) end --- diff --git a/crates/emmylua_code_analysis/resources/std/io.lua b/crates/emmylua_code_analysis/resources/std/io.lua index 602b5f9ef..9d1c1c403 100644 --- a/crates/emmylua_code_analysis/resources/std/io.lua +++ b/crates/emmylua_code_analysis/resources/std/io.lua @@ -94,13 +94,25 @@ function io.output(file) end ---@return file function io.popen(prog, mode) end +---@alias std.readmode +---| integer +---| string +---| "n" # Reads a number, returning a float or integer based on Lua's conversion grammar. +---| "a" # Reads the entire file starting from the current position. +---| "l" # Reads a line and ignores the end-of-line marker. +---| "L" # Reads a line and preserves the end-of-line marker. +---| "*n" # Reads a number, returning a float or integer based on Lua's conversion grammar. +---| "*a" # Reads the entire file starting from the current position. +---| "*l" # Reads a line and ignores the end-of-line marker. +---| "*L" # Reads a line and preserves the end-of-line marker. + --- --- Equivalent to `io.input():read(···)`. ---- @param format '*n' | '*a' | '*l' | integer ---- @return string | integer | nil ---- @overload fun(format:'*n'): integer ---- @overload fun(format:'*a' | '*l' | integer): string | nil -function io.read(format) end +---@param ... std.readmode +---@return any +---@return any ... +---@nodiscard +function io.read(...) end --- --- In case of success, returns a handle for a temporary file. This file is @@ -195,11 +207,11 @@ function file:lines(...) end --- *number*: reads a string with up to this number of bytes, returning **nil** --- on end of file. If `number` is zero, it reads nothing and returns an --- empty string, or **nil** on end of file. ---- @param format '*n' | '*a' | '*l' | integer ---- @return string | integer | nil ---- @overload fun(format:'*n'): integer ---- @overload fun(format:'*a' | '*l' | integer): string | nil -function file:read(format) end +---@param ... std.readmode +---@return any +---@return any ... +---@nodiscard +function file:read(...) end --- --- Sets and gets the file position, measured from the beginning of the diff --git a/crates/emmylua_code_analysis/resources/std/string.lua b/crates/emmylua_code_analysis/resources/std/string.lua index b16ae06d9..b5a8dd652 100644 --- a/crates/emmylua_code_analysis/resources/std/string.lua +++ b/crates/emmylua_code_analysis/resources/std/string.lua @@ -75,12 +75,14 @@ function string.dump(func, strip) end --- --- If the pattern has captures, then in a successful match the captured values --- are also returned, after the two indices. ----@overload fun(s:string, pattern:string):integer, integer, string... ----@param s string ----@param pattern string ----@param init? integer ----@param plain? boolean ----@return integer, integer, string... +---@param s string|number +---@param pattern string|number +---@param init? integer +---@param plain? boolean +---@return integer|nil start +---@return integer|nil end +---@return string ... captured +---@nodiscard function string.find(s, pattern, init, plain) end --- @@ -276,11 +278,11 @@ function string.reverse(s) end --- corrected to 1. If `j` is greater than the string length, it is corrected to --- that length. If, after these corrections, `i` is greater than `j`, the --- function returns the empty string. ----@overload fun(s:string, i:integer):string ----@param s string ----@param i integer ----@param j integer +---@param s string|number +---@param i integer +---@param j? integer ---@return string +---@nodiscard function string.sub(s, i, j) end ---@version >5.3 diff --git a/crates/emmylua_code_analysis/resources/std/table.lua b/crates/emmylua_code_analysis/resources/std/table.lua index f3567178b..10969fa7e 100644 --- a/crates/emmylua_code_analysis/resources/std/table.lua +++ b/crates/emmylua_code_analysis/resources/std/table.lua @@ -109,9 +109,9 @@ function table.sort(list, comp) end --- return `list[i]`, `list[i+1]`, `···`, `list[j]` --- By default, i is 1 and j is #list. ---@generic T +---@param list [T...] | table ---@param i? integer ---@param j? integer ----@param list [T...] ---@return T... function table.unpack(list, i, j) end diff --git a/crates/emmylua_code_analysis/resources/std/utf8.lua b/crates/emmylua_code_analysis/resources/std/utf8.lua index d55a4f524..83f620150 100644 --- a/crates/emmylua_code_analysis/resources/std/utf8.lua +++ b/crates/emmylua_code_analysis/resources/std/utf8.lua @@ -60,12 +60,14 @@ function utf8.codepoint(s, i, j) end --- positions `i` and `j` (both inclusive). The default for `i` is 1 and for --- `j` is -1. If it finds any invalid byte sequence, returns a false value --- plus the position of the first invalid byte. ----@overload fun(s:string):number ----@param s string ----@param i? number ----@param j? number ----@return number -function utf8.len(s, i, j) end +---@param s string +---@param i? integer +---@param j? integer +---@param lax? boolean +---@return integer? +---@return integer? errpos +---@nodiscard +function utf8.len(s, i, j, lax) end --- --- Returns the position (in bytes) where the encoding of the `n`-th character diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/decl/exprs.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/decl/exprs.rs index fbe9c4133..1b32e110b 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/decl/exprs.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/decl/exprs.rs @@ -202,9 +202,9 @@ pub fn analyze_table_expr(analyzer: &mut DeclAnalyzer, expr: LuaTableExpr) -> Op ); let decl_feature = if analyzer.is_meta { - LuaMemberFeature::MetaFieldDecl + LuaMemberFeature::MetaDefine } else { - LuaMemberFeature::FileFieldDecl + LuaMemberFeature::FileDefine }; let member_id = LuaMemberId::new(field.get_syntax_id(), file_id); diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/var_analyze.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/var_analyze.rs index 8ca50194c..1c3f4f6cb 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/flow/var_analyze.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/flow/var_analyze.rs @@ -132,16 +132,35 @@ fn broadcast_up( if let Some(ne_type_assert) = type_assert.get_negation() { if let Some(else_stat) = if_stat.get_else_clause() { let block_range = else_stat.get_range(); - flow_chain.add_type_assert(path, ne_type_assert, block_range, actual_range); - } else if is_block_has_return(if_stat.get_block()?).unwrap_or(false) { + flow_chain.add_type_assert( + path, + ne_type_assert.clone(), + block_range, + actual_range, + ); + } else if is_block_has_return(if_stat.get_block()).unwrap_or(false) { let parent_block = if_stat.get_parent::()?; let parent_range = parent_block.get_range(); let if_range = if_stat.get_range(); if if_range.end() < parent_range.end() { let range = TextRange::new(if_range.end(), parent_range.end()); - flow_chain.add_type_assert(path, ne_type_assert, range, actual_range); + flow_chain.add_type_assert( + path, + ne_type_assert.clone(), + range, + actual_range, + ); } } + for else_if_clause in if_stat.get_else_if_clause_list() { + let block_range = else_if_clause.get_range(); + flow_chain.add_type_assert( + path, + ne_type_assert.clone(), + block_range, + actual_range, + ); + } } } LuaAst::LuaWhileStat(while_stat) => { @@ -445,10 +464,12 @@ fn infer_lua_type_assert( Some(()) } -fn is_block_has_return(block: LuaBlock) -> Option { - for stat in block.get_stats() { - if is_stat_change_flow(stat.clone()).unwrap_or(false) { - return Some(true); +fn is_block_has_return(block: Option) -> Option { + if let Some(block) = block { + for stat in block.get_stats() { + if is_stat_change_flow(stat.clone()).unwrap_or(false) { + return Some(true); + } } } @@ -469,9 +490,7 @@ fn is_stat_change_flow(stat: LuaStat) -> Option { Some(false) } LuaStat::ReturnStat(_) => Some(true), - LuaStat::DoStat(do_stat) => { - Some(is_block_has_return(do_stat.get_block()?).unwrap_or(false)) - } + LuaStat::DoStat(do_stat) => Some(is_block_has_return(do_stat.get_block()).unwrap_or(false)), _ => Some(false), } } diff --git a/crates/emmylua_code_analysis/src/compilation/test/flow.rs b/crates/emmylua_code_analysis/src/compilation/test/flow.rs index e2e7a1c7a..8c359d72f 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/flow.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/flow.rs @@ -278,4 +278,26 @@ print(a.field) "# )); } + + #[test] + fn test_elseif() { + let mut ws = VirtualWorkspace::new(); + + assert!(ws.check_code_for( + DiagnosticCode::NeedCheckNil, + r#" +---@class D11 +---@field public a string + +---@type D11|nil +local a + +if not a then +elseif a.a then + print(a.a) +end + + "# + )); + } } diff --git a/crates/emmylua_code_analysis/src/db_index/type/types.rs b/crates/emmylua_code_analysis/src/db_index/type/types.rs index 00d71c03a..c190d5010 100644 --- a/crates/emmylua_code_analysis/src/db_index/type/types.rs +++ b/crates/emmylua_code_analysis/src/db_index/type/types.rs @@ -262,6 +262,14 @@ impl LuaType { } } + pub fn is_optional(&self) -> bool { + match self { + LuaType::Nil | LuaType::Any | LuaType::Unknown => true, + LuaType::Union(u) => u.types.iter().any(|t| t.is_optional()), + _ => false, + } + } + pub fn is_always_truthy(&self) -> bool { match self { LuaType::Nil | LuaType::Boolean | LuaType::Any | LuaType::Unknown => false, diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/check_field.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/check_field.rs index 3ff93a133..4c1521b09 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/check_field.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/check_field.rs @@ -64,6 +64,11 @@ fn check_index_expr( let prefix_typ = semantic_model .infer_expr(index_expr.get_prefix_expr()?) .unwrap_or(LuaType::Unknown); + + if !is_valid_prefix_type(&prefix_typ) { + return Some(()); + } + let index_name = index_key.get_path_part(); match code { DiagnosticCode::InjectField => { @@ -92,3 +97,22 @@ fn check_index_expr( Some(()) } + +#[allow(dead_code)] +fn is_valid_prefix_type(typ: &LuaType) -> bool { + let mut current_typ = typ; + loop { + match current_typ { + LuaType::Any + | LuaType::Unknown + | LuaType::Table + | LuaType::TplRef(_) + | LuaType::StrTplRef(_) + | LuaType::TableConst(_) => return false, + LuaType::Instance(instance_typ) => { + current_typ = instance_typ.get_base(); + } + _ => return true, + } + } +} diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/check_param_count.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/check_param_count.rs index 734ff27b7..906fd750b 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/check_param_count.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/check_param_count.rs @@ -45,14 +45,18 @@ fn check_closure_expr( semantic_model.get_file_id(), &closure_expr, ))?; - let source_params_len = match source_typ { - LuaType::DocFunction(func_type) => func_type.get_params().len(), + let source_params_len = match &source_typ { + LuaType::DocFunction(func_type) => { + let params = func_type.get_params(); + get_params_len(params) + } LuaType::Signature(signature_id) => { let signature = context.db.get_signature_index().get(&signature_id)?; - signature.get_type_params().len() + let params = signature.get_type_params(); + get_params_len(¶ms) } _ => return Some(()), - }; + }?; // 只检查右值参数多于左值参数的情况, 右值参数少于左值参数的情况是能够接受的 if source_params_len > right_value.params.len() { @@ -166,7 +170,14 @@ fn check_call_expr( // Check for redundant parameters else if call_args_count > params.len() { // 参数定义中最后一个参数是 `...` - if params.last().map_or(false, |(name, _)| name == "...") { + if params.last().map_or(false, |(name, typ)| { + name == "..." + || if let Some(typ) = typ { + typ.is_variadic() + } else { + false + } + }) { return Some(()); } @@ -198,3 +209,18 @@ fn check_call_expr( Some(()) } + +fn get_params_len(params: &[(String, Option)]) -> Option { + if let Some((name, typ)) = params.last() { + // 如果最后一个参数是可变参数, 则直接返回, 不需要检查 + if name == "..." { + return None; + } + if let Some(typ) = typ { + if typ.is_variadic() { + return None; + } + } + } + Some(params.len()) +} diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs index 5c0bbdad0..1a3174091 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/missing_fields.rs @@ -149,7 +149,7 @@ fn record_required_fields( return; } - if decl_type.is_nullable() { + if decl_type.is_nullable() || decl_type.is_any() { optional_type.insert(name); return; } diff --git a/crates/emmylua_code_analysis/src/diagnostic/checker/param_type_check.rs b/crates/emmylua_code_analysis/src/diagnostic/checker/param_type_check.rs index a73a28333..7ff2b8a59 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/checker/param_type_check.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/checker/param_type_check.rs @@ -138,32 +138,25 @@ fn add_type_check_diagnostic( let db = semantic_model.get_db(); match result { Ok(_) => return, - Err(reason) => match reason { - TypeCheckFailReason::TypeNotMatchWithReason(reason) => { - context.add_diagnostic(DiagnosticCode::ParamTypeNotMatch, range, reason, None); - } - TypeCheckFailReason::TypeNotMatch => { - context.add_diagnostic( - DiagnosticCode::ParamTypeNotMatch, - range, - t!( - "expected `%{source}` but found `%{found}`", - source = humanize_type(db, ¶m_type, RenderLevel::Simple), - found = humanize_type(db, &expr_type, RenderLevel::Simple) - ) - .to_string(), - None, - ); - } - TypeCheckFailReason::TypeRecursion => { - context.add_diagnostic( - DiagnosticCode::ParamTypeNotMatch, - range, - "type recursion".into(), - None, - ); - } - }, + Err(reason) => { + let reason_message = match reason { + TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason, + TypeCheckFailReason::TypeNotMatch => "".to_string(), + TypeCheckFailReason::TypeRecursion => "type recursion".to_string(), + }; + context.add_diagnostic( + DiagnosticCode::ParamTypeNotMatch, + range, + t!( + "expected `%{source}` but found `%{found}`. %{reason}", + source = humanize_type(db, ¶m_type, RenderLevel::Simple), + found = humanize_type(db, &expr_type, RenderLevel::Simple), + reason = reason_message + ) + .to_string(), + None, + ); + } } } diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/assign_type_mismatch_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/assign_type_mismatch_test.rs index f5d3af5cb..fb3e06745 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/assign_type_mismatch_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/assign_type_mismatch_test.rs @@ -42,6 +42,28 @@ mod tests { )); } + #[test] + fn test_3() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.check_code_for_namespace( + DiagnosticCode::AssignTypeMismatch, + r#" + ---@param s string + ---@param i? integer + ---@param j? integer + ---@param lax? boolean + ---@return integer? + ---@return integer? errpos + ---@nodiscard + local function get_len(s, i, j, lax) end + + local len = 0 + ---@diagnostic disable-next-line: need-check-nil + len = len + get_len("", 1, 1, true) + "# + )); + } + #[test] fn test_issue_193() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/missing_fields_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/missing_fields_test.rs index 696ba319e..bc5e0b1c8 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/missing_fields_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/missing_fields_test.rs @@ -132,4 +132,35 @@ mod tests { "# )); } + + #[test] + fn test_issue_262() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.check_code_for( + DiagnosticCode::MissingFields, + r#" +--- @class D11.Opts +--- @field field? any + +--- @param opts D11.Opts +local function foo(opts) end + +foo({}) + "# + )); + } + + #[test] + fn test_1() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.check_code_for( + DiagnosticCode::MissingFields, + r#" + ---@type table + local a = {} + + print(a[1]) + "# + )); + } } diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/missing_parameter_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/missing_parameter_test.rs index 7a359949f..d2bba5834 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/missing_parameter_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/missing_parameter_test.rs @@ -2,6 +2,25 @@ mod test { use crate::{DiagnosticCode, VirtualWorkspace}; + #[test] + fn test_issue_276() { + let mut ws = VirtualWorkspace::new(); + + assert!(ws.check_code_for( + DiagnosticCode::MissingParameter, + r#" + --- @param a string + --- @param b? string + --- @param c? string + --- @return string + --- @overload fun(a: string, b: string): number + local function myfun2(a, b, c) end + + local a = myfun2('string') + "# + )); + } + #[test] fn test_issue_249() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs index 216f220ab..f9e84b1bc 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/param_type_check_test.rs @@ -329,4 +329,108 @@ mod test { "# )); } + + #[test] + fn test_function() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + + assert!(ws.check_code_for( + DiagnosticCode::ParamTypeNotMatch, + r#" + ---@param sorter function + ---@return string[] + local function getTableKeys(sorter) + local keys = {} + table.sort(keys, sorter) + return keys + end + "# + )); + } + + #[test] + fn test_table_array() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + + assert!(ws.check_code_for( + DiagnosticCode::ParamTypeNotMatch, + r#" + ---@generic K, V + ---@param t table + ---@return table + local function revertMap(t) + end + + ---@param arr any[] + local function sortCallbackOfIndex(arr) + ---@type table + local indexMap = revertMap(arr) + end + "# + )); + } + + #[test] + fn test_table_class() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + + assert!(ws.check_code_for( + DiagnosticCode::ParamTypeNotMatch, + r#" + ---@param t table + local function bar(t) + end + + ---@class D11.A + + ---@type D11.A|any + local a + + bar(a) + "# + )); + } + + #[test] + fn test_table_1() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + + assert!(ws.check_code_for( + DiagnosticCode::ParamTypeNotMatch, + r#" + ---@param t table[] + local function bar(t) + end + + ---@type table|any + local a + + bar(a) + "# + )); + } + + #[test] + fn test_pairs() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + + assert!(ws.check_code_for( + DiagnosticCode::ParamTypeNotMatch, + r#" + ---@diagnostic disable: missing-return + ---@generic K, V + ---@param t table | V[] | {[K]: V} + ---@return fun(tbl: any):K, std.NotNull + local function _pairs(t) end + + ---@class D10.A + + ---@type {[string]: D10.A, _id: D10.A} + local a + + for k, v in _pairs(a) do + end + "# + )); + } } diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/redundant_parameter_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/redundant_parameter_test.rs index 2c25872d7..7f507d422 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/redundant_parameter_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/redundant_parameter_test.rs @@ -49,6 +49,22 @@ mod test { )); } + #[test] + fn test_1() { + let mut ws = VirtualWorkspace::new(); + + assert!(ws.check_code_for( + DiagnosticCode::RedundantParameter, + r#" + ---@type fun(...)[] + local a = {} + + a[1] = function(ccc, ...) + end + "# + )); + } + #[test] fn test_dots() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/return_type_mismatch_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/return_type_mismatch_test.rs index 2b7dc708e..18bf2ed90 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/return_type_mismatch_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/return_type_mismatch_test.rs @@ -225,4 +225,38 @@ mod tests { "# )); } + + #[test] + fn test_1() { + let mut ws = VirtualWorkspace::new(); + + assert!(ws.check_code_for( + DiagnosticCode::ReturnTypeMismatch, + r#" + ---@return string? + local function a() + ---@type int? + local ccc + return ccc and a() or nil + end + "# + )); + } + + #[test] + fn test_2() { + let mut ws = VirtualWorkspace::new(); + + assert!(ws.check_code_for( + DiagnosticCode::ReturnTypeMismatch, + r#" + ---@return any[] + local function a() + ---@type table|table + local ccc + return ccc + end + "# + )); + } } diff --git a/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs b/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs index 4418f63f5..25600bb55 100644 --- a/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs +++ b/crates/emmylua_code_analysis/src/diagnostic/test/undefined_field_test.rs @@ -2,6 +2,29 @@ mod test { use crate::{DiagnosticCode, VirtualWorkspace}; + #[test] + fn test_1() { + let mut ws = VirtualWorkspace::new(); + assert!(ws.check_code_for( + DiagnosticCode::UndefinedField, + r#" + ---@alias std.NotNull T - ? + + ---@generic V + ---@param t {[any]: V} + ---@return fun(tbl: any):int, std.NotNull + function ipairs(t) end + + ---@type {[integer]: string|table} + local a = {} + + for i, extendsName in ipairs(a) do + print(extendsName.a) + end + "# + )); + } + #[test] fn test() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type_generic.rs b/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type_generic.rs index ec7f17cbc..4e50cbb78 100644 --- a/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type_generic.rs +++ b/crates/emmylua_code_analysis/src/semantic/generic/instantiate_type_generic.rs @@ -363,7 +363,7 @@ fn instantiate_alias_call( if operands.len() != 2 { return LuaType::Unknown; } - + // 如果类型为`Union`且只有一个类型, 则会解开`Union`包装 return TypeOps::Remove.apply(&operands[0], &operands[1]); } LuaAliasCallKind::Add => { diff --git a/crates/emmylua_code_analysis/src/semantic/generic/tpl_pattern.rs b/crates/emmylua_code_analysis/src/semantic/generic/tpl_pattern.rs index af8b77075..7ec87b53b 100644 --- a/crates/emmylua_code_analysis/src/semantic/generic/tpl_pattern.rs +++ b/crates/emmylua_code_analysis/src/semantic/generic/tpl_pattern.rs @@ -1,12 +1,15 @@ use std::ops::Deref; use emmylua_parser::LuaSyntaxNode; +use itertools::Itertools; use smol_str::SmolStr; use crate::{ + check_type_compact, db_index::{DbIndex, LuaGenericType, LuaType}, semantic::{member::infer_member_map, LuaInferCache}, - LuaFunctionType, LuaMemberKey, LuaMemberOwner, LuaMultiReturn, LuaTupleType, LuaUnionType, + LuaFunctionType, LuaMemberKey, LuaMemberOwner, LuaMultiReturn, LuaObjectType, LuaTupleType, + LuaUnionType, }; use super::type_substitutor::TypeSubstitutor; @@ -158,12 +161,50 @@ fn tpl_pattern_match( LuaType::Tuple(tuple) => { tuple_tpl_pattern_match(db, cache, root, tuple, target, substitutor); } + LuaType::Object(obj) => { + object_tpl_pattern_match(db, cache, root, obj, target, substitutor); + } _ => {} } Some(()) } +fn object_tpl_pattern_match( + db: &DbIndex, + cache: &mut LuaInferCache, + root: &LuaSyntaxNode, + origin_obj: &LuaObjectType, + target: &LuaType, + substitutor: &mut TypeSubstitutor, +) -> Option<()> { + match target { + LuaType::Object(target_object) => { + // 先匹配 fields + for (k, v) in origin_obj.get_fields().iter().sorted_by_key(|(k, _)| *k) { + let target_value = target_object.get_fields().get(k); + if let Some(target_value) = target_value { + tpl_pattern_match(db, cache, root, v, target_value, substitutor); + } + } + // 再匹配索引访问 + let target_index_access = target_object.get_index_access(); + for (origin_key, v) in origin_obj.get_index_access() { + // 先匹配 key 类型进行转换 + let target_access = target_index_access + .iter() + .find(|(target_key, _)| check_type_compact(db, origin_key, target_key).is_ok()); + if let Some(target_access) = target_access { + tpl_pattern_match(db, cache, root, origin_key, &target_access.0, substitutor); + tpl_pattern_match(db, cache, root, v, &target_access.1, substitutor); + } + } + } + _ => {} + } + Some(()) +} + fn array_tpl_pattern_match( db: &DbIndex, cache: &mut LuaInferCache, @@ -306,6 +347,10 @@ fn table_generic_tpl_pattern_match( }; values.push(v.clone()); } + for (k, v) in obj.get_index_access() { + keys.push(k.clone()); + values.push(v.clone()); + } let key_type = LuaType::Union(LuaUnionType::new(keys).into()); let value_type = LuaType::Union(LuaUnionType::new(values).into()); diff --git a/crates/emmylua_code_analysis/src/semantic/infer/infer_binary.rs b/crates/emmylua_code_analysis/src/semantic/infer/infer_binary.rs index e2b735235..a54ae1681 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/infer_binary.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/infer_binary.rs @@ -59,17 +59,35 @@ fn infer_binary_expr_type( } fn infer_union(db: &DbIndex, u: &LuaUnionType, right: &LuaType, op: BinaryOperator) -> InferResult { - let mut union_types = vec![]; + let mut unique_union_types = Vec::new(); + for ty in u.get_types() { - let ty = infer_binary_expr_type(db, ty.clone(), right.clone(), op)?; - union_types.push(ty); + let inferred_ty = infer_binary_expr_type(db, ty.clone(), right.clone(), op)?; + flatten_and_insert(inferred_ty, &mut unique_union_types); } - union_types.dedup(); - match union_types.len() { + match unique_union_types.len() { 0 => Ok(LuaType::Unknown), - 1 => Ok(union_types[0].clone()), - _ => Ok(LuaType::Union(LuaUnionType::new(union_types).into())), + 1 => Ok(unique_union_types.into_iter().next().unwrap()), + _ => Ok(LuaType::Union(LuaUnionType::new(unique_union_types).into())), + } +} + +fn flatten_and_insert(ty: LuaType, unique_union_types: &mut Vec) { + let mut stack = vec![ty]; + while let Some(current_ty) = stack.pop() { + match current_ty { + LuaType::Union(u) => { + for inner_ty in u.get_types() { + stack.push(inner_ty.clone()); + } + } + _ => { + if !unique_union_types.contains(¤t_ty) { + unique_union_types.push(current_ty); + } + } + } } } @@ -148,6 +166,11 @@ fn infer_binary_expr_add(db: &DbIndex, left: LuaType, right: LuaType) -> InferRe } }; } + match (left.is_nil(), right.is_nil()) { + (true, false) => return Ok(right), + (false, true) => return Ok(left), + _ => {} + } infer_binary_custom_operator(db, &left, &right, LuaOperatorMetaMethod::Add) } diff --git a/crates/emmylua_code_analysis/src/semantic/overload_resolve/mod.rs b/crates/emmylua_code_analysis/src/semantic/overload_resolve/mod.rs index 89da7399d..35a0b7660 100644 --- a/crates/emmylua_code_analysis/src/semantic/overload_resolve/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/overload_resolve/mod.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, sync::Arc}; +use std::sync::Arc; use emmylua_parser::LuaCallExpr; @@ -69,7 +69,7 @@ fn resolve_signature_by_args( ) -> InferCallFuncResult { let arg_count = arg_count.unwrap_or(0); let mut opt_funcs = Vec::with_capacity(overloads.len()); - // 函数本身签名在尾部 + for func in overloads { let params = func.get_params(); if params.len() < arg_count { @@ -81,36 +81,118 @@ fn resolve_signature_by_args( } else { 0 }; - let mut match_count = 0; + let mut total_weight = 0; // 总权重 let expr_len = expr_types.len(); - + // 检查每个参数的匹配情况 for (i, param) in params.iter().enumerate() { if i == 0 && jump_param > 0 { + // 非冒号定义但是冒号调用, 直接认为匹配 + total_weight += 100; continue; } + let param_type = param.1.as_ref().unwrap_or(&LuaType::Any); let expr_idx = i - jump_param; + if expr_idx >= expr_len { - break; + // 没有传入参数, 但参数是可空类型 + if param_type.is_nullable() { + total_weight += 1; + } + continue; } - let param_type = param.1.as_ref().unwrap_or(&LuaType::Any); let expr_type = &expr_types[expr_idx]; if *param_type == LuaType::Any || check_type_compact(db, param_type, expr_type).is_ok() { - match_count += 1; + total_weight += 100; // 类型完全匹配 } } - opt_funcs.push((func, params.len(), match_count)); + // 如果参数数量完全匹配, 则认为其权重更高 + if total_weight > 0 && params.len() == expr_types.len() { + total_weight += 50000; + } + + opt_funcs.push((func, total_weight)); } - // 优先降序匹配`match_count`使匹配度最高的函数排在前面, 其次升序匹配`params.len()`使参数个数最少的函数排在前面. - opt_funcs.sort_by(|a, b| match b.2.cmp(&a.2) { - Ordering::Equal => a.1.cmp(&b.1), - other => other, - }); + // 按权重降序排序 + opt_funcs.sort_by(|a, b| b.1.cmp(&a.1)); + + // 返回权重最高的签名,若无则取最后一个重载作为默认 opt_funcs .first() - .map(|(func, _, _)| Arc::clone(func)) + .filter(|(_, weight)| *weight > i32::MIN) // 确保不是无效签名 + .map(|(func, _)| Arc::clone(func)) .or_else(|| overloads.last().cloned()) .ok_or(InferFailReason::None) } + +// fn resolve_signature_by_args( +// db: &DbIndex, +// overloads: &[Arc], +// expr_types: &[LuaType], +// is_colon_call: bool, +// arg_count: Option, +// ) -> Option> { +// let arg_count = arg_count.unwrap_or(0); +// let mut opt_funcs = Vec::with_capacity(overloads.len()); +// // 函数本身签名在尾部 +// for func in overloads { +// let params = func.get_params(); +// if params.len() < arg_count { +// continue; +// } + +// let jump_param = if is_colon_call && !func.is_colon_define() { +// 1 +// } else { +// 0 +// }; +// let mut match_count = 0; +// let mut skip_param = 0; +// let expr_len = expr_types.len(); + +// for (i, param) in params.iter().enumerate() { +// if i == 0 && jump_param > 0 { +// continue; +// } +// let param_type = param.1.as_ref().unwrap_or(&LuaType::Any); +// let expr_idx = i - jump_param; +// if expr_idx >= expr_len { +// if param_type.is_nullable() { +// skip_param += 1; +// } +// continue; +// } + +// let expr_type = &expr_types[expr_idx]; +// if *param_type == LuaType::Any || check_type_compact(db, param_type, expr_type).is_ok() +// { +// match_count += 1; +// } +// } +// opt_funcs.push((func, params.len(), match_count, skip_param)); +// } + +// opt_funcs.sort_by(|a, b| { +// match b.2.cmp(&a.2) { +// // 比较 match_count 降序 +// Ordering::Equal => { +// // 计算有效参数个数 +// let a_effective_params = a.1 - a.3; // params.len() - skip_param +// let b_effective_params = b.1 - b.3; +// // 升序使有效参数个数最少的函数排在前面 +// match a_effective_params.cmp(&b_effective_params) { +// Ordering::Equal => a.1.cmp(&b.1), // 升序使总参数个数最少的函数排在前面 +// other => other, +// } +// } +// other => other, +// } +// }); + +// opt_funcs +// .first() +// .map(|(func, _, _, _)| Arc::clone(func)) +// .or_else(|| overloads.last().cloned()) +// } diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type.rs b/crates/emmylua_code_analysis/src/semantic/type_check/complex_type.rs index 4b2b308e7..eb578b65d 100644 --- a/crates/emmylua_code_analysis/src/semantic/type_check/complex_type.rs +++ b/crates/emmylua_code_analysis/src/semantic/type_check/complex_type.rs @@ -62,6 +62,25 @@ pub fn check_complex_type_compact( ); } LuaType::Table => return Ok(()), + LuaType::TableGeneric(compact_types) => { + if compact_types.len() == 2 { + for typ in compact_types.iter() { + if check_general_type_compact( + db, + source_base, + typ, + check_guard.next_level()?, + ) + .is_err() + { + return Err(TypeCheckFailReason::TypeNotMatch); + } + } + + return Ok(()); + } + } + LuaType::Any => return Ok(()), _ => {} }, LuaType::Tuple(tuple) => { @@ -193,7 +212,9 @@ pub fn check_complex_type_compact( if source_generic_param.len() == 2 { let key = &source_generic_param[0]; let value = &source_generic_param[1]; - if key.is_any() && check_type_compact(db, value, base).is_ok() { + if key.is_any() + || key.is_integer() && check_type_compact(db, value, base).is_ok() + { return Ok(()); } } diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/func_type.rs b/crates/emmylua_code_analysis/src/semantic/type_check/func_type.rs index 4f49c928a..a65c7cb26 100644 --- a/crates/emmylua_code_analysis/src/semantic/type_check/func_type.rs +++ b/crates/emmylua_code_analysis/src/semantic/type_check/func_type.rs @@ -42,6 +42,7 @@ pub fn check_doc_func_type_compact( Ok(()) } + LuaType::Function => Ok(()), _ => Err(TypeCheckFailReason::TypeNotMatch), } } diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/ref_type.rs b/crates/emmylua_code_analysis/src/semantic/type_check/ref_type.rs index 3350c4cee..77a057e35 100644 --- a/crates/emmylua_code_analysis/src/semantic/type_check/ref_type.rs +++ b/crates/emmylua_code_analysis/src/semantic/type_check/ref_type.rs @@ -170,7 +170,7 @@ fn check_ref_type_compact_table( .to_string(), )); } - } else if source_member_type.is_nullable() { + } else if source_member_type.is_optional() { continue; } else { return Err(TypeCheckFailReason::TypeNotMatchWithReason( diff --git a/crates/emmylua_code_analysis/src/semantic/type_check/simple_type.rs b/crates/emmylua_code_analysis/src/semantic/type_check/simple_type.rs index bd18500c1..6f3ed7c31 100644 --- a/crates/emmylua_code_analysis/src/semantic/type_check/simple_type.rs +++ b/crates/emmylua_code_analysis/src/semantic/type_check/simple_type.rs @@ -32,6 +32,7 @@ pub fn check_simple_type_compact( | LuaType::Global | LuaType::Userdata | LuaType::Instance(_) + | LuaType::Any ) { return Ok(()); } diff --git a/crates/emmylua_ls/src/context/workspace_manager.rs b/crates/emmylua_ls/src/context/workspace_manager.rs index 8317a6fa6..1887b7a4c 100644 --- a/crates/emmylua_ls/src/context/workspace_manager.rs +++ b/crates/emmylua_ls/src/context/workspace_manager.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::{path::PathBuf, sync::Arc, time::Duration}; use super::{ClientProxy, FileDiagnostic, StatusBar}; @@ -5,6 +6,7 @@ use crate::handlers::{init_analysis, ClientConfig}; use emmylua_code_analysis::update_code_style; use emmylua_code_analysis::{load_configs, EmmyLuaAnalysis, Emmyrc}; use log::{debug, info}; +use lsp_types::Uri; use tokio::sync::{Mutex, RwLock}; use tokio_util::sync::CancellationToken; @@ -17,6 +19,7 @@ pub struct WorkspaceManager { pub client_config: ClientConfig, pub workspace_folders: Vec, pub watcher: Option, + pub current_open_files: HashSet, } impl WorkspaceManager { @@ -35,6 +38,7 @@ impl WorkspaceManager { update_token: Arc::new(Mutex::new(None)), file_diagnostic, watcher: None, + current_open_files: HashSet::new(), } } diff --git a/crates/emmylua_ls/src/handlers/completion/completion_builder.rs b/crates/emmylua_ls/src/handlers/completion/completion_builder.rs index 06581ca73..29d5493ff 100644 --- a/crates/emmylua_ls/src/handlers/completion/completion_builder.rs +++ b/crates/emmylua_ls/src/handlers/completion/completion_builder.rs @@ -70,4 +70,9 @@ impl<'a> CompletionBuilder<'a> { } self.env_range = (0, 0); } + + /// 是否是触发主动补全 + pub fn is_invoked(&self) -> bool { + self.trigger_kind == CompletionTriggerKind::INVOKED + } } diff --git a/crates/emmylua_ls/src/handlers/completion/providers/keywords_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/keywords_provider.rs index 1197a9e46..58dd8ec8a 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/keywords_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/keywords_provider.rs @@ -11,6 +11,7 @@ pub fn add_completion(builder: &mut CompletionBuilder) -> Option<()> { if builder.is_cancelled() { return None; } + if is_full_match_keyword(builder).is_some() { add_stat_keyword_completions(builder, None); return Some(()); @@ -122,12 +123,17 @@ fn add_expr_keyword_completions(builder: &mut CompletionBuilder) -> Option<()> { } fn add_function_keyword_completions(builder: &mut CompletionBuilder) -> Option<()> { + // 非主动补全不添加 + if !builder.is_invoked() { + return None; + } let item = CompletionItem { label: "function".to_string(), kind: Some(lsp_types::CompletionItemKind::SNIPPET), insert_text: Some("function ${1:name}(${2:...})\n\t${0}\nend".to_string()), insert_text_format: Some(InsertTextFormat::SNIPPET), insert_text_mode: Some(InsertTextMode::ADJUST_INDENTATION), + sort_text: Some("0000".to_string()), // 优先级较高 ..CompletionItem::default() }; diff --git a/crates/emmylua_ls/src/handlers/text_document/text_document_handler.rs b/crates/emmylua_ls/src/handlers/text_document/text_document_handler.rs index e89cd5146..4aeffc773 100644 --- a/crates/emmylua_ls/src/handlers/text_document/text_document_handler.rs +++ b/crates/emmylua_ls/src/handlers/text_document/text_document_handler.rs @@ -25,6 +25,10 @@ pub async fn on_did_open_text_document( .await; } + let mut workspace = context.workspace_manager.write().await; + workspace.current_open_files.insert(uri); + drop(workspace); + Some(()) } @@ -77,8 +81,13 @@ pub async fn on_did_change_text_document( } pub async fn on_did_close_document( - _: ServerContextSnapshot, - _: DidCloseTextDocumentParams, + context: ServerContextSnapshot, + params: DidCloseTextDocumentParams, ) -> Option<()> { + let mut workspace = context.workspace_manager.write().await; + workspace + .current_open_files + .remove(¶ms.text_document.uri); + drop(workspace); Some(()) } diff --git a/crates/emmylua_ls/src/handlers/text_document/watched_file_handler.rs b/crates/emmylua_ls/src/handlers/text_document/watched_file_handler.rs index 5e812abe2..ee9393ecd 100644 --- a/crates/emmylua_ls/src/handlers/text_document/watched_file_handler.rs +++ b/crates/emmylua_ls/src/handlers/text_document/watched_file_handler.rs @@ -7,6 +7,7 @@ pub async fn on_did_change_watched_files( context: ServerContextSnapshot, params: DidChangeWatchedFilesParams, ) -> Option<()> { + let workspace = context.workspace_manager.read().await; let mut analysis = context.analysis.write().await; let emmyrc = analysis.get_emmyrc(); let encoding = &emmyrc.workspace.encoding; @@ -17,12 +18,14 @@ pub async fn on_did_change_watched_files( let file_type = get_file_type(&file_event.uri); match file_type { Some(WatchedFileType::Lua) => { - collect_lua_files( - &mut watched_lua_files, - file_event.uri, - file_event.typ, - encoding, - ); + if !workspace.current_open_files.contains(&file_event.uri) { + collect_lua_files( + &mut watched_lua_files, + file_event.uri, + file_event.typ, + encoding, + ); + } } Some(WatchedFileType::Editorconfig) => { if file_event.typ == FileChangeType::DELETED {