diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2e0a168df..3a947ac98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,9 +13,9 @@ repos: rev: v6.0.0 hooks: - id: trailing-whitespace - exclude: '(^crates/glua_ls/std_i18n/.*\.yaml$|^\.github/skills/.*)' + exclude: '^\.github/skills/.*' - id: end-of-file-fixer - exclude: '(^crates/glua_ls/std_i18n/.*\.yaml$|^\.github/skills/.*)' + exclude: '^\.github/skills/.*' - id: fix-byte-order-marker - id: mixed-line-ending args: [--fix=lf] diff --git a/Cargo.lock b/Cargo.lock index c0c88b2b6..6ad2e47d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,12 +104,6 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - [[package]] name = "atomic-waker" version = "1.1.2" @@ -144,15 +138,6 @@ dependencies = [ "fs_extra", ] -[[package]] -name = "base62" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10e52a7bcb1d6beebee21fb5053af9e3cbb7a7ed1a4909e534040e676437ab1f" -dependencies = [ - "rustversion", -] - [[package]] name = "base64" version = "0.22.1" @@ -796,17 +781,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "globwalk" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" -dependencies = [ - "bitflags 1.3.2", - "ignore", - "walkdir", -] - [[package]] name = "globwalk" version = "0.9.1" @@ -842,6 +816,7 @@ dependencies = [ name = "glua_code_analysis" version = "0.1.5" dependencies = [ + "aho-corasick", "dirs", "emmy_lsp_types", "emmylua_codestyle", @@ -860,7 +835,6 @@ dependencies = [ "regex", "reqwest", "rowan", - "rust-i18n", "rustc-hash 2.1.1", "schema_to_glua", "schemars 1.0.4", @@ -930,7 +904,6 @@ dependencies = [ "glua_parser", "glua_parser_desc", "googletest", - "include_dir", "internment", "itertools 0.14.0", "log", @@ -938,10 +911,8 @@ dependencies = [ "mimalloc", "notify", "rowan", - "rust-i18n", "serde", "serde_json", - "serde_yml", "smol_str", "tokio", "tokio-util", @@ -954,7 +925,6 @@ name = "glua_parser" version = "0.1.5" dependencies = [ "rowan", - "rust-i18n", "serde", ] @@ -1660,15 +1630,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "normpath" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "notify" version = "8.2.0" @@ -2172,60 +2133,6 @@ dependencies = [ "text-size", ] -[[package]] -name = "rust-i18n" -version = "3.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332" -dependencies = [ - "globwalk 0.8.1", - "once_cell", - "regex", - "rust-i18n-macro", - "rust-i18n-support", - "smallvec", -] - -[[package]] -name = "rust-i18n-macro" -version = "3.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965" -dependencies = [ - "glob", - "once_cell", - "proc-macro2", - "quote", - "rust-i18n-support", - "serde", - "serde_json", - "serde_yaml", - "syn", -] - -[[package]] -name = "rust-i18n-support" -version = "3.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19" -dependencies = [ - "arc-swap", - "base62", - "globwalk 0.8.1", - "itertools 0.11.0", - "lazy_static", - "normpath", - "once_cell", - "proc-macro2", - "regex", - "serde", - "serde_json", - "serde_yaml", - "siphasher", - "toml", - "triomphe", -] - [[package]] name = "rustc-hash" version = "1.1.0" @@ -2505,15 +2412,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_with" version = "3.14.0" @@ -2546,19 +2444,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.10.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "serde_yml" version = "0.0.12" @@ -2654,16 +2539,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "std_i18n" -version = "0.1.0" -dependencies = [ - "glua_parser", - "serde", - "serde_yml", - "walkdir", -] - [[package]] name = "strsim" version = "0.11.1" @@ -2736,7 +2611,7 @@ checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" dependencies = [ "chrono", "chrono-tz", - "globwalk 0.9.1", + "globwalk", "humansize", "lazy_static", "percent-encoding", @@ -2913,26 +2788,11 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] [[package]] name = "toml_edit" @@ -2941,8 +2801,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.10.0", - "serde", - "serde_spanned", "toml_datetime", "toml_write", "winnow", @@ -3018,17 +2876,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "triomphe" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" -dependencies = [ - "arc-swap", - "serde", - "stable_deref_trait", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -3071,12 +2918,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 33ecd1650..f4342c31c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ glua_diagnostic_macro = { path = "crates/glua_diagnostic_macro", version = "0.1. schema_to_glua = { path = "crates/schema_to_glua", version = "0.1.5" } # external +aho-corasick = "1.1.3" lsp-server = "0.7.9" tokio = { version = "1.48", features = ["full"] } serde = { version = "1.0.228", features = ["derive"] } @@ -30,7 +31,6 @@ lsp_types = { version = "0.1.0", package = "emmy_lsp_types" } schemars = "1.0.4" regex = "1" internment = { version = "0.8.6", features = ["arc"] } -rust-i18n = "3" log = "0.4.28" fern = "0.7.1" chrono = "0.4.42" @@ -140,4 +140,4 @@ debug = false [profile.debugging] inherits = "dev" -debug = true \ No newline at end of file +debug = true diff --git a/crates/glua_check/src/init.rs b/crates/glua_check/src/init.rs index 11381e962..fe35e4f02 100644 --- a/crates/glua_check/src/init.rs +++ b/crates/glua_check/src/init.rs @@ -96,7 +96,7 @@ pub async fn load_workspace( .collect::>(); let mut analysis = EmmyLuaAnalysis::new(); analysis.update_config(emmyrc.clone().into()); - analysis.init_std_lib(None); + analysis.init_std_lib(); // Add GMod annotations as library workspace if provided if let Some(annotations_path) = gmod_annotations { @@ -115,8 +115,41 @@ pub async fn load_workspace( } } + // Canonicalize main workspace root for self-overlap detection. + let main_root_canon = main_path.canonicalize().ok(); + for lib in &emmyrc.workspace.library { - let path = PathBuf::from(lib.get_path().clone()); + let configured = lib.get_path(); + let path = PathBuf::from(configured); + + // Filter invalid library paths: empty, nonexistent, not a directory, + // or resolves to the same path as the main workspace root (self-overlap + // would cause a redundant full re-scan of the workspace). + if configured.trim().is_empty() { + log::warn!( + "Skipping empty library path from config entry: {:?}", + configured + ); + continue; + } + if !path.exists() { + log::warn!("Skipping library path that does not exist: {:?}", path); + continue; + } + if !path.is_dir() { + log::warn!("Skipping library path that is not a directory: {:?}", path); + continue; + } + if let (Ok(lib_canon), Some(ws_canon)) = (path.canonicalize(), &main_root_canon) + && lib_canon == *ws_canon + { + log::warn!( + "Skipping library path that resolves to the workspace root (self-overlap): {:?}", + path + ); + continue; + } + analysis.add_library_workspace(path.clone()); workspace_folders.push(WorkspaceFolder::new(path.clone(), true)); } diff --git a/crates/glua_code_analysis/Cargo.toml b/crates/glua_code_analysis/Cargo.toml index ee8108b45..43b1053e5 100644 --- a/crates/glua_code_analysis/Cargo.toml +++ b/crates/glua_code_analysis/Cargo.toml @@ -31,6 +31,7 @@ glua_parser.workspace = true glua_diagnostic_macro.workspace = true # external +aho-corasick.workspace = true serde.workspace = true serde_json.workspace = true lsp_types.workspace = true @@ -40,7 +41,6 @@ regex.workspace = true internment.workspace = true log.workspace = true tokio-util.workspace = true -rust-i18n.workspace = true walkdir.workspace = true ignore.workspace = true dirs.workspace = true @@ -58,7 +58,3 @@ luars.workspace = true reqwest.workspace = true schema_to_glua.workspace = true rustc-hash.workspace = true - -[package.metadata.i18n] -available-locales = ["en", "zh_CN", "zh_HK"] -default-locale = "en" diff --git a/crates/glua_code_analysis/locales/codestyle.yml b/crates/glua_code_analysis/locales/codestyle.yml deleted file mode 100644 index f88f81258..000000000 --- a/crates/glua_code_analysis/locales/codestyle.yml +++ /dev/null @@ -1,45 +0,0 @@ -_version: 2 -codestyle.NonLiteralExpressionsInAssert: - en: | - Using an assert call with an expensive (non-literal) message expression may cause serious performance regressions. - The assert macro is only allowed if the error message is a fixed string literal. - Please refactor your code to separate the condition check and error handling. - - Instead of: - local a = assert(foo(), expensive_msg_expression) - - Use one of the following forms: - local a = foo() - if not a then - error(expensive_msg_expression) - end - - - zh_CN: | - 使用 assert 调用时,如果错误信息参数是个昂贵的计算表达式(非字面量),可能会引起严重的性能回归。 - assert 宏仅允许错误信息为硬编码的字符串字面量。 - 请重构代码,将条件判断与错误处理分离。 - - 例如,将: - local a = assert(foo(), expensive_msg_expression) - - 修改为: - local a = foo() - if not a then - error(expensive_msg_expression) - end - - zh_HK: | - 當使用 assert 調用時,如果錯誤信息參數是一個耗資昂貴的計算表達式(非字面量),可能會引起嚴重的性能回歸。 - assert 語句僅允許錯誤信息為硬編碼的字串字面量。 - 請重構代碼,將條件檢查與錯誤處理分離。 - - 例如,將: - local a = assert(foo(), expensive_msg_expression) - - 修改爲: - - local a = foo() - if not a then - error(expensive_msg_expression) - end diff --git a/crates/glua_code_analysis/locales/lint.yml b/crates/glua_code_analysis/locales/lint.yml deleted file mode 100644 index cb23e1c39..000000000 --- a/crates/glua_code_analysis/locales/lint.yml +++ /dev/null @@ -1,275 +0,0 @@ -_version: 2 -Type '%{name}' already defined: - en: Type '%{name}' already defined - zh_CN: 类型 '%{name}' 已经定义 - zh_HK: 類型 '%{name}' 已經定義 -Type '%{name}' not found: - en: Type '%{name}' not found - zh_CN: 类型 '%{name}' 未找到 - zh_HK: 類型 '%{name}' 未找到 -'%{name} is never used, if this is intentional, prefix it with an underscore: _%{name}': - en: '%{name} is never used, if this is intentional, prefix it with an underscore: _%{name}' - zh_CN: '%{name} 从未被使用,如果这是有意的,请在前面加下划线: _%{name}' - zh_HK: '%{name} 從未被使用,如果這是有意的,請在前面加下劃線: _%{name}' -The current Lua version %{version} is not accessible; expected %{conds}.: - en: 'The current Lua version %{version} is not accessible; expected %{conds}.' - zh_CN: '当前的 Lua 版本 %{version} 无法访问;预期为 %{conds}。' - zh_HK: '當前的 Lua 版本 %{version} 無法訪問;預期為 %{conds}。' -The property is package-private and cannot be accessed outside the package.: - en: 'The property is package-private and cannot be accessed outside the package.' - zh_CN: '该属性为包私有,无法在包外访问。' - zh_HK: '該屬性為包私有,無法在包外訪問。' -The property is private and cannot be accessed outside the class.: - en: 'The property is private and cannot be accessed outside the class.' - zh_CN: '该属性为私有,无法在类外访问。' - zh_HK: '該屬性為私有,無法在類外訪問。' -The property is protected and cannot be accessed outside its subclasses.: - en: 'The property is protected and cannot be accessed outside its subclasses.' - zh_CN: '该属性受保护,无法在其子类之外访问。' - zh_HK: '該屬性受保護,無法在其子類之外訪問。' -expected %{num} parameters but found %{found_num}: - en: 'expected %{num} parameters but found %{found_num}' - zh_CN: '期望 %{num} 个参数,但找到 %{found_num} 个' - zh_HK: '期望 %{num} 個參數,但找到 %{found_num} 個' -expected %{num} parameters but found %{found_num}. %{infos}: - en: 'expected %{num} parameters but found %{found_num}. %{infos}' - zh_CN: '期望 %{num} 个参数,但找到 %{found_num} 个。%{infos}' - zh_HK: '期望 %{num} 個參數,但找到 %{found_num} 個。%{infos}' -'missing parameter: %{name}': - en: 'missing parameter: %{name}' - zh_CN: '缺少参数: %{name}' - zh_HK: '缺少參數: %{name}' -'undefined global variable: %{name}': - en: 'undefined global variable: %{name}' - zh_CN: '未定义的全局变量: %{name}' - zh_HK: '未定義的全局變量: %{name}' -'%{name} may be nil': - en: '%{name} may be nil' - zh_CN: '%{name} 可能为 nil' - zh_HK: '%{name} 可能為 nil' -'%{name} value may be nil': - en: '%{name} value may be nil' - zh_CN: '%{name} 的值可能是 nil' - zh_HK: '%{name} 的值可能為 nil' -Cannot reassign to a constant variable: - en: Cannot reassign to a constant variable - zh_CN: '无法重新赋值给常量变量' - zh_HK: '不可重新指定常量變數' -Invalid hex escape sequence '\x%{hex}': - en: Invalid hex escape sequence '\x%{hex}' - zh_CN: '无效的十六进制转义序列 "\x%{hex}"' - zh_HK: '無效的十六進制轉義序列 "\x%{hex}"' -Invalid unicode escape sequence '\u{{%{unicode_hex}}}': - en: Invalid unicode escape sequence '\u{{%{unicode_hex}}}' - zh_CN: '无效的 Unicode 转义序列 "\u{{%{unicode_hex}}}"' - zh_HK: '無效的 Unicode 轉義序列 "\u{{%{unicode_hex}}}"' -Should not reassign to iter variable: - en: Should not reassign to iter variable - zh_CN: '不应重新赋值给迭代变量' - zh_HK: '不應重新指定迭代變數' - -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' - zh_HK: '函式 %{name} 可能為 nil' -member %{key} not match, expect %{typ}, but got %{got}: - en: member %{key} not match, expect %{typ}, but got %{got} - zh_CN: '成员 %{key} 不匹配,期望 %{typ},但得到 %{got}' - zh_HK: '成員 %{key} 不匹配,期望 %{typ},但得到 %{got}' -member %{name} type not match, expect %{expect}, got %{got}: - en: member %{name} type not match, expect %{expect}, got %{got} - zh_CN: '成员 %{name} 的类型不匹配,期望 %{expect},但得到 %{got}' - zh_HK: '成員 %{name} 的類型不匹配,期望 %{expect},但得到 %{got}' -missing member %{key}: - en: missing member %{key} - zh_CN: '缺少成员 %{key}' - zh_HK: '缺少成員 %{key}' -missing member %{name}, in table: - en: missing member %{name}, in table - zh_CN: '在表中缺少成员 %{name}' - zh_HK: '在表中缺少成員 %{name}' -missing tuple member %{idx}: - en: missing tuple member %{idx} - zh_CN: '缺少元组成员 %{idx}' - zh_HK: '缺少元組成員 %{idx}' -tuple member %{idx} not match, expect %{typ}, but got %{got}: - en: tuple member %{idx} not match, expect %{typ}, but got %{got} - zh_CN: '元组成员 %{idx} 不匹配,期望 %{typ},但得到 %{got}' - zh_HK: '元組成員 %{idx} 不匹配,期望 %{typ},但得到 %{got}' - -Annotations specify that return value %{index} has a type of `%{source}`, returning value of type `%{found}` here instead. %{reason}: - en: Annotations specify that return value %{index} has a type of `%{source}`, returning value of type `%{found}` here instead. %{reason} - zh_CN: '第 %{index} 个返回值的类型为 `%{source}`,但实际返回类型为 `%{found}`。 %{reason}' - zh_HK: '第 %{index} 個回傳值的類型為 `%{source}` ,但實際回傳的是 `%{found}`。 %{reason}' -Annotations specify that at most %{max} return value(s) are required, found %{rmax} returned here instead.: - en: 'Annotations specify that at most %{max} return value(s) are required, found %{rmax} returned here instead.' - zh_CN: '最多只有 %{max} 个返回值,但此处返回了 %{rmax} 个' - zh_HK: '最多只有 %{max} 個回傳值,但此處返回 %{rmax} 個' -Annotations specify that at least %{min} return value(s) are required, found %{rmin} returned here instead.: - en: 'Annotations specify that at least %{min} return value(s) are required, found %{rmin} returned here instead.' - zh_CN: '至少需要 %{min} 个返回值,但此处只返回了 %{rmin} 个' - zh_HK: '至少需要 %{min} 個回傳值,但此處只返回 %{rmin} 個' -Cannot use `...` outside a vararg function.: - en: Cannot use `...` outside a vararg function. - zh_CN: '不能在非可变参数函数中使用 `...`' - zh_HK: '不能在非可變參數函數中使用 `...`' -'Undefined doc param: `%{name}`': - en: 'Undefined doc param: `%{name}`' - zh_CN: '指向了未定义的参数 `%{name}`' - zh_HK: '指向了未定義的參數 `%{name}`' -'Undefined field: `%{name}`': - en: 'Undefined field: `%{name}`' - zh_CN: '未定义字段 `%{name}`' - zh_HK: '未定義字段 `%{name}`' -'Redefined local variable `%{name}`': - en: 'Redefined local variable `%{name}`' - zh_CN: '重定义局部变量 `%{name}`' - zh_HK: '重定義局部變量 `%{name}`' -'Missing required fields in type `%{typ}`: %{fields}': - en: 'Missing required fields in type `%{typ}`: %{fields}' - zh_CN: '缺少类型 `%{typ}` 的必要字段:%{fields}' - zh_HK: '缺少類型 `%{typ}` 的必要字段:%{fields}' -'Fields cannot be injected into the reference of `%{class}` for `%{field}`. ': - en: 'Fields cannot be injected into the reference of `%{class}` for `%{field}`. ' - zh_CN: '不能在 `%{class}` 的引用中注入字段 `%{field}` 。' - zh_HK: '不能在 `%{class}` 的引用中注入字段 `%{field}` 。' -'Undefined field `%{field}`. ': - en: 'Undefined field `%{field}`. ' - zh_CN: '未定义的属性/字段 `%{field}`。' - zh_HK: '未定義的屬性/字段 `%{field}`。' -'Circularly inherited classes.': - en: 'Circularly inherited classes.' - zh_CN: '循环继承的类' - zh_HK: '循環繼承的類' -'Missing comment for global function `%{name}`.': - en: 'Missing comment for global function `%{name}`.' - zh_CN: '全局函数 `%{name}` 缺少注释' - zh_HK: '全局函數 `%{name}` 缺少註釋' -'Missing @param annotation for parameter `%{name}` in global function `%{function_name}`.': - en: 'Missing @param annotation for parameter `%{name}` in global function `%{function_name}`.' - zh_CN: '全局函数 `%{function_name}` 的参数 `%{name}` 缺少 @param 注解。' - zh_HK: '全局函數 `%{function_name}` 的參數 `%{name}` 缺少 @param 註釋。' -'Missing @return annotation at index `%{index}` in global function `%{function_name}`.': - en: 'Missing @return annotation at index `%{index}` in global function `%{function_name}`.' - zh_CN: '全局函数 `%{function_name}` 的第 `%{index}` 个返回值缺少 @return 注解。' - zh_HK: '全局函數 `%{function_name}` 的第 `%{index}` 個回傳值缺少 @return 註釋。' -'Incomplete signature. Missing @param annotation for parameter `%{name}`.': - en: 'Incomplete signature. Missing @param annotation for parameter `%{name}`.' - zh_CN: '不完整的签名。参数 `%{name}` 缺少 @param 注解。' - zh_HK: '不完整的簽名。參數 `%{name}` 缺少 @param 註釋。' -'Incomplete signature. Missing @return annotation at index `%{index}`.': - en: 'Incomplete signature. Missing @return annotation at index `%{index}`.' - zh_CN: '不完整的签名。第 `%{index}` 个返回值缺少 @return 注解。' - zh_HK: '不完整的簽名。第 `%{index}` 個回傳值缺少 @return 註釋。' -'Cannot assign `%{value}` to `%{source}`. %{reason}': - en: 'Cannot assign `%{value}` to `%{source}`. %{reason}' - zh_CN: '不能将 `%{value}` 赋值给 `%{source}`。%{reason}' - zh_HK: '不能將 `%{value}` 賦值給 `%{source}`。%{reason}' -'The same file is required multiple times.': - en: 'The same file is required multiple times.' - zh_CN: '同一个文件被重复 require。' - zh_HK: '同一個文件被重複 require。' -'Annotations specify that a return value is required here.': - en: 'Annotations specify that a return value is required here.' - zh_CN: '此处需要返回值。' - zh_HK: '此處需要回傳值。' -"Duplicate class '%{name}', if this is intentional, please add the 'partial' attribute for every class define": - en: "Duplicate class '%{name}', if this is intentional, please add the 'partial' attribute for every class define" - zh_CN: '重复的类 "%{name}",如果这是有意的,请为每个类定义添加 (partial) 属性' - zh_HK: '重複的類 "%{name}",如果這是有意的,請為每個類定義添加 (partial) 屬性' -"Duplicate class '%{name}'. The class %{name} is defined as both partial and non-partial.": - en: "Duplicate class '%{name}'. The class %{name} is defined as both partial and non-partial." - zh_CN: '重复的类 "%{name}"。类 "%{name}" 被定义为部分类和非部分类。' - zh_HK: '重複的類 "%{name}"。類 "%{name}" 被定義為部分類和非部分類。' -"Duplicate enum '%{name}', if this is intentional, please add the 'partial' attribute for every enum define": - en: "Duplicate enum '%{name}', if this is intentional, please add the 'partial' attribute for every enum define" - zh_CN: '重复的枚举 "%{name}",如果这是有意的,请为每个枚举定义添加 (partial) 属性' - zh_HK: '重複的枚舉 "%{name}",如果這是有意的,請為每個枚舉定義添加 (partial) 屬性' -"Duplicate enum '%{name}'. The enum %{name} is defined as both partial and non-partial.": - en: "Duplicate enum '%{name}'. The enum %{name} is defined as both partial and non-partial." - zh_CN: '重复的枚举 "%{name}"。枚举 "%{name}" 被定义为部分枚举和非部分枚举。' - zh_HK: '重複的枚舉 "%{name}"。枚舉 "%{name}" 被定義為部分枚舉和非部分枚舉。' -"Duplicate alias '{name}'. Alias definitions cannot be partial.": - en: "Duplicate alias '{name}'. Alias definitions cannot be partial." - zh_CN: '重复的别名 "{name}"。别名定义不能是部分定义。' - zh_HK: '重複的別名 "{name}"。別名定義不能是部分定義。' -"The value is assigned as `nil` because the number of values is not enough.": - en: "The value is assigned as `nil` because the number of values is not enough." - zh_CN: "由于值的数量不够而被赋值为了 `nil` 。" - zh_HK: "由于值的数量不够而被赋值为了 `nil` 。" -"Async function can only be called in async function.": - en: "Async function can only be called in async function." - zh_CN: "只能在标记为异步的函数中调用异步函数。" - zh_HK: "只能在標記為非同步的函式中呼叫非同步函式。" -'Unnecessary assert: this expression is always truthy': - en: 'Unnecessary assert: this expression is always truthy' - zh_CN: '不必要的断言: 这个表达式始终为真' - zh_HK: '不必要的斷言: 這個表達式始終為真' -'Impossible assert: this expression is always falsy; prefer `error()`': - en: 'Impossible assert: this expression is always falsy; prefer `error()`' - zh_CN: '不可能的断言: 该表达式始终为假;建议使用 `error()`' - zh_HK: '不可能的斷言: 該表達式始終為假;建議使用 `error()`' -'Unnecessary `if` statement: this condition is always truthy': - en: 'Unnecessary `if` statement: this condition is always truthy' - zh_CN: '不必要的 `if` 语句:此条件始终为真' - zh_HK: '不必要的 `if` 陳述式:此條件始終為真' -'Impossible `if` statement: this condition is always falsy': - en: 'Impossible `if` statement: this condition is always falsy' - zh_CN: '不可能的 `if` 语句:此条件始终为假' - zh_HK: '不可能的 `if` 陳述式:此條件始終為假' -"`...` should be the last arg.": - en: "`...` should be the last arg." - zh_CN: "`...`必须是最后一个参数。" - zh_HK: "`...`必須是最後一個引數。" -"type `%{name}` not found.": - en: "type `%{name}` not found." - zh_CN: "类型 `%{name}` 未找到。" - zh_HK: "類型 `%{name}` 未找到。" -"Duplicate class constructor '%{name}'. constructor must have only one.": - en: "Duplicate class constructor '%{name}'. constructor must have only one." - zh_CN: "类有重复的 (constructor) 定义 '%{name}'。(constructor) 必须只有一个。" - zh_HK: "類有重複的 (constructor) 定義 '%{name}'。(constructor) 必須只有一個。" -"Duplicate field `%{name}`.": - en: "Duplicate field `%{name}`." - zh_CN: "重复定义的字段 `%{name}`." - zh_HK: "重複定義的字段 `%{name}`." -"Duplicate index `%{name}`.": - en: "Duplicate index `%{name}`." - zh_CN: "重复定义的索引 `%{name}`." - zh_HK: "重複定義的索引 `%{name}`." -"type `%{found}` does not satisfy the constraint `%{source}`. %{reason}": - en: "type `%{found}` does not satisfy the constraint `%{source}`. %{reason}" - zh_CN: "泛型约束要求为 `%{source}` 的子类, 但找到了 `%{found}`. %{reason}" - zh_HK: "泛型约束要求为 `%{source}` 的子类, 但找到了 `%{found}`. %{reason}" -"the string template type does not match any type declaration": - en: "the string template type does not match any type declaration" - zh_CN: "字符串模板类型与任何类型声明不匹配" - zh_HK: "字串模板類型與任何類型聲明不匹配" -"the string template type must be a string constant": - en: "the string template type must be a string constant" - zh_CN: "字符串模板类型必须是字符串常量" - zh_HK: "字串模板類型必須是字串常量" -"Cannot cast `%{original}` to `%{target}`. %{reason}": - en: "Cannot cast `%{original}` to `%{target}`. %{reason}" - zh_CN: "不能将 `%{original}` 转换为 `%{target}`。%{reason}" - zh_HK: "不能將 `%{original}` 轉換為 `%{target}`。%{reason}" -"type recursion": - en: "type recursion" - zh_CN: "类型递归" - zh_HK: "類型遞歸" -"Module '%{module}' is not visible. It has @export restrictions.": - en: "Module '%{module}' is not visible. It has @export restrictions." - zh_CN: "模块 '%{module}' 不可见。它有 @export 限制。" - zh_HK: "模組 '%{module}' 不可見。它有 @export 限制。" -"Unknown doc tag: `%{name}`": - en: "Unknown doc tag: `%{name}`" - # TODO: translate - -"Value '%{value}' does not match any enum value. Expected one of: %{enum_values}": - en: "Value '%{value}' does not match any enum value. Expected one of: %{enum_values}" - zh_CN: "值 '%{value}' 与任何枚举值都不匹配。应为以下之一: %{enum_values}" - zh_HK: "值 '%{value}' 與任何枚舉值都不匹配。應為以下之一: %{enum_values}" diff --git a/crates/glua_code_analysis/resources/std/builtin.lua b/crates/glua_code_analysis/resources/std/builtin.lua index 1d46d2290..ae16d2a70 100644 --- a/crates/glua_code_analysis/resources/std/builtin.lua +++ b/crates/glua_code_analysis/resources/std/builtin.lua @@ -197,6 +197,27 @@ --- - `return_self`: Whether the constructor is forced to return `self`, defaults to `true` ---@attribute constructor(name: string, root_class: string?, strip_self: boolean?, return_self: boolean?) +--- +--- Marks a function parameter as carrying call-site metadata. Language server features can use +--- the `domain` and `role` pair to classify string literals and other arguments without matching +--- the callable by name. +--- +--- Parameters: +--- - `domain`: Stable namespace for the metadata, such as `gmod.net_message`. +--- - `role`: Meaning of this parameter in that domain, such as `define` or `reference`. +--- - `priority`: Optional tie-breaker when a union exposes multiple roles for the same argument. +---@attribute call_arg(domain: string, role: string, priority: integer?) + +--- Marks a parameter inside the following `@overload` function type as carrying call-site +--- metadata. +--- +--- Parameters: +--- - `param`: Zero-based overload parameter index. +--- - `domain`: Stable namespace for the metadata, such as `gmod.network_var`. +--- - `role`: Meaning of this parameter in that domain, such as `define`. +--- - `priority`: Optional tie-breaker when a union exposes multiple roles for the same argument. +---@attribute overload_call_arg(param: integer, domain: string, role: string, priority: integer?) + --- --- Associates `getter` and `setter` methods with a field. Currently provides only definition navigation functionality, --- and the target methods must reside within the same class. diff --git a/crates/glua_code_analysis/resources/std/global.lua b/crates/glua_code_analysis/resources/std/global.lua index 3f4216a28..6164bb826 100644 --- a/crates/glua_code_analysis/resources/std/global.lua +++ b/crates/glua_code_analysis/resources/std/global.lua @@ -98,6 +98,7 @@ function dofile(filename) end --- addition of error position information to the message. ---@param message any ---@param level? integer +---@return never function error(message, level) end --- diff --git a/crates/glua_code_analysis/src/compilation/analyzer/doc/attribute_tags.rs b/crates/glua_code_analysis/src/compilation/analyzer/doc/attribute_tags.rs index cef3cb0f3..808eb6294 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/doc/attribute_tags.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/doc/attribute_tags.rs @@ -148,6 +148,7 @@ fn attribute_find_doc(comment: &LuaSyntaxNode) -> Option { match sibling.kind() { LuaKind::Syntax( LuaSyntaxKind::DocTagField + | LuaSyntaxKind::DocTagOverload | LuaSyntaxKind::DocTagParam | LuaSyntaxKind::DocTagReturn, ) => { @@ -207,10 +208,17 @@ fn find_up_attribute( } pub fn find_attach_attribute(ast: LuaAst) -> Option> { - if let LuaAst::LuaDocTagParam(param) = ast { - let mut result = Vec::new(); - find_up_attribute(param.syntax(), &mut result, true); - return Some(result); + match ast { + LuaAst::LuaDocTagParam(param) => { + let mut result = Vec::new(); + find_up_attribute(param.syntax(), &mut result, true); + Some(result) + } + LuaAst::LuaDocTagOverload(overload) => { + let mut result = Vec::new(); + find_up_attribute(overload.syntax(), &mut result, true); + Some(result) + } + _ => None, } - None } diff --git a/crates/glua_code_analysis/src/compilation/analyzer/doc/field_or_operator_def_tags.rs b/crates/glua_code_analysis/src/compilation/analyzer/doc/field_or_operator_def_tags.rs index d056eef20..5136d041b 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/doc/field_or_operator_def_tags.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/doc/field_or_operator_def_tags.rs @@ -27,7 +27,7 @@ pub fn analyze_field(analyzer: &mut DocAnalyzer, tag: LuaDocTagField) -> Option< analyzer.file_id, AnalyzeError { kind: DiagnosticCode::AnnotationUsageError, - message: t!("`@field` must be used under a `@class`").to_string(), + message: "`@field` must be used under a `@class`".to_string(), range: tag.get_range(), }, ); diff --git a/crates/glua_code_analysis/src/compilation/analyzer/doc/infer_type.rs b/crates/glua_code_analysis/src/compilation/analyzer/doc/infer_type.rs index a8ac02d17..a8cea678a 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/doc/infer_type.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/doc/infer_type.rs @@ -191,7 +191,7 @@ fn infer_buildin_or_ref_type( analyzer.file_id, AnalyzeError::new( DiagnosticCode::TypeNotFound, - &t!("Type '%{name}' not found", name = name), + &format!("Type '{name}' not found", name = name), range, ), ); @@ -242,7 +242,7 @@ fn infer_generic_type(analyzer: &mut DocAnalyzer, generic_type: &LuaDocGenericTy analyzer.file_id, AnalyzeError::new( DiagnosticCode::TypeNotFound, - &t!("Type '%{name}' not found", name = name), + &format!("Type '{name}' not found", name = name), generic_type.get_range(), ), ); @@ -502,6 +502,7 @@ fn infer_func_type(analyzer: &mut DocAnalyzer, func: &LuaDocFuncType) -> LuaType } let mut params_result = Vec::new(); + let mut optional_params = Vec::new(); let mut is_variadic = false; for param in func.get_params() { let name = if let Some(param) = param.get_name_token() { @@ -525,6 +526,7 @@ fn infer_func_type(analyzer: &mut DocAnalyzer, func: &LuaDocFuncType) -> LuaType None }; + optional_params.push(nullable && name != "..."); params_result.push((name, type_ref)); } @@ -581,6 +583,7 @@ fn infer_func_type(analyzer: &mut DocAnalyzer, func: &LuaDocFuncType) -> LuaType params_result, return_type, ) + .with_optional_params(optional_params) .into(), ) } diff --git a/crates/glua_code_analysis/src/compilation/analyzer/doc/tags.rs b/crates/glua_code_analysis/src/compilation/analyzer/doc/tags.rs index ed54787c0..f704994ef 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/doc/tags.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/doc/tags.rs @@ -252,7 +252,7 @@ pub fn report_orphan_tag(analyzer: &mut DocAnalyzer, tag: &impl LuaAstNode) { analyzer.file_id, AnalyzeError { kind: DiagnosticCode::AnnotationUsageError, - message: t!("`@%{name}` can't be used here", name = tag.get_text()).to_string(), + message: format!("`@{name}` can't be used here", name = tag.get_text()).to_string(), range: tag.get_range(), }, ); diff --git a/crates/glua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs b/crates/glua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs index 34a954d3a..c5714a42d 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs @@ -4,6 +4,7 @@ use glua_parser::{ LuaDocTagReturn, LuaDocTagReturnCast, LuaDocTagSchema, LuaDocTagSee, LuaDocTagType, LuaDocTypeFlag, LuaExpr, LuaIndexKey, LuaLocalName, LuaTokenKind, LuaVarExpr, }; +use std::sync::Arc; use super::{ DocAnalyzer, apply_nullable_doc_default, convert_doc_default_value, @@ -19,8 +20,8 @@ use crate::{ }, }; use crate::{ - InFiled, JsonSchemaFile, LuaOperatorMetaMethod, LuaTypeCache, LuaTypeOwner, OperatorFunction, - ReturnTypeKind, SignatureReturnStatus, + InFiled, JsonSchemaFile, LuaCallArgRole, LuaOperatorMetaMethod, LuaTypeCache, LuaTypeOwner, + OVERLOAD_CALL_ARG_ATTRIBUTE, OperatorFunction, ReturnTypeKind, SignatureReturnStatus, compilation::analyzer::common::bind_type, db_index::{ AccessorFuncAnnotation, LuaDeclId, LuaDocParamInfo, LuaDocReturnInfo, LuaInstanceType, @@ -452,6 +453,13 @@ pub fn analyze_overload(analyzer: &mut DocAnalyzer, tag: LuaDocTagOverload) -> O } else if let Some(closure) = find_owner_closure_or_report(analyzer, &tag) { let type_ref = infer_type(analyzer, tag.get_type()?); if let LuaType::DocFunction(func) = type_ref { + let func = if let Some(call_arg_roles) = overload_call_arg_roles(analyzer, &tag) + && !call_arg_roles.is_empty() + { + Arc::new(func.as_ref().clone().with_call_arg_roles(call_arg_roles)) + } else { + func + }; let id = LuaSignatureId::from_closure(analyzer.file_id, &closure); let signature = analyzer.db.get_signature_index_mut().get_or_create(id); signature.overloads.push(func); @@ -460,6 +468,59 @@ pub fn analyze_overload(analyzer: &mut DocAnalyzer, tag: LuaDocTagOverload) -> O Some(()) } +fn overload_call_arg_roles( + analyzer: &mut DocAnalyzer, + tag: &LuaDocTagOverload, +) -> Option> { + let mut roles = Vec::new(); + let attributes = find_attach_attribute(LuaAst::LuaDocTagOverload(tag.clone()))?; + for tag_use in attributes { + for attribute_use in infer_attribute_uses(analyzer, tag_use)? { + if attribute_use.id.get_name() != OVERLOAD_CALL_ARG_ATTRIBUTE { + continue; + } + + let Some(LuaType::DocIntegerConst(param_idx) | LuaType::IntegerConst(param_idx)) = + attribute_use.get_param_by_name("param") + else { + continue; + }; + let Some(domain) = attribute_string_param(&attribute_use, "domain") else { + continue; + }; + let Some(role) = attribute_string_param(&attribute_use, "role") else { + continue; + }; + let priority = match attribute_use.get_param_by_name("priority") { + Some(LuaType::DocIntegerConst(value) | LuaType::IntegerConst(value)) => { + Some(*value) + } + _ => None, + }; + + let Ok(param_idx) = usize::try_from(*param_idx) else { + continue; + }; + + roles.push(LuaCallArgRole { + param_idx, + domain, + role, + priority, + }); + } + } + + Some(roles) +} + +fn attribute_string_param(attribute_use: &LuaAttributeUse, name: &str) -> Option { + match attribute_use.get_param_by_name(name)? { + LuaType::DocStringConst(value) | LuaType::StringConst(value) => Some(value.to_string()), + _ => None, + } +} + pub fn analyze_module(analyzer: &mut DocAnalyzer, tag: LuaDocTagModule) -> Option<()> { let module_path = tag.get_module_path()?; let module_info = analyzer diff --git a/crates/glua_code_analysis/src/compilation/analyzer/flow/bind_analyze/check_goto.rs b/crates/glua_code_analysis/src/compilation/analyzer/flow/bind_analyze/check_goto.rs index 8b697221b..5c490db26 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/flow/bind_analyze/check_goto.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/flow/bind_analyze/check_goto.rs @@ -13,8 +13,8 @@ pub fn check_goto_label(binder: &mut FlowBinder) { { binder.report_error(AnalyzeError::new( DiagnosticCode::SyntaxError, - &t!( - "goto label '%{label_name}' not found", + &format!( + "goto label '{label_name}' not found", label_name = label_name ), label_token.get_range(), diff --git a/crates/glua_code_analysis/src/compilation/analyzer/flow/bind_analyze/stats.rs b/crates/glua_code_analysis/src/compilation/analyzer/flow/bind_analyze/stats.rs index d43f60008..43a073824 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/flow/bind_analyze/stats.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/flow/bind_analyze/stats.rs @@ -278,7 +278,7 @@ pub fn bind_break_stat( // report a error if we are trying to break outside a loop binder.report_error(AnalyzeError::new( DiagnosticCode::SyntaxError, - &t!("Break outside loop"), + "Break outside loop", break_stat.get_range(), )); return current; diff --git a/crates/glua_code_analysis/src/compilation/analyzer/gmod/mod.rs b/crates/glua_code_analysis/src/compilation/analyzer/gmod/mod.rs index 60d4910cf..ec5ea7d4e 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/gmod/mod.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/gmod/mod.rs @@ -3,21 +3,23 @@ use std::{ sync::Arc, }; +use aho_corasick::AhoCorasick; use glua_parser::{ LuaAssignStat, LuaAst, LuaAstNode, LuaAstToken, LuaBlock, LuaCallExpr, LuaChunk, LuaClosureExpr, LuaComment, LuaCommentOwner, LuaDocDescriptionOwner, LuaDocTag, LuaDocTagFileparam, LuaDocTagRealm, LuaElseClauseStat, LuaElseIfClauseStat, LuaExpr, LuaForRangeStat, LuaForStat, LuaFuncStat, LuaIfStat, LuaIndexKey, LuaLiteralToken, - LuaLocalFuncStat, LuaLocalName, LuaLocalStat, LuaRepeatStat, LuaStat, LuaSyntaxNode, - LuaTableExpr, LuaVarExpr, LuaWhileStat, NumberResult, PathTrait, + LuaLocalFuncStat, LuaLocalName, LuaLocalStat, LuaNameExpr, LuaRepeatStat, LuaStat, + LuaSyntaxNode, LuaTableExpr, LuaVarExpr, LuaWhileStat, NumberResult, PathTrait, }; use crate::{ - EmmyrcGmodRealm, FileId, GmodClassCallLiteral, GmodScriptedClassCallKind, - GmodScriptedClassCallMetadata, GmodScriptedClassFileMetadata, InFiled, LuaDecl, LuaDeclExtra, - LuaDeclId, LuaDeclLocation, LuaDeclTypeKind, LuaFunctionType, LuaMember, LuaMemberFeature, - LuaMemberId, LuaMemberKey, LuaType, LuaTypeCache, LuaTypeDecl, LuaTypeDeclId, LuaTypeFlag, - LuaTypeOwner, + EmmyrcGmodRealm, FileId, GmodClassCallLiteral, GmodDermaSkinCallRoles, + GmodNamedStringCallRoles, GmodNetworkVarCallRoles, GmodScriptedClassCallKind, + GmodScriptedClassCallMetadata, GmodScriptedClassFileMetadata, GmodVguiPanelCallRoles, InFiled, + LuaCallArgRole, LuaDecl, LuaDeclExtra, LuaDeclId, LuaDeclLocation, LuaDeclTypeKind, + LuaFunctionType, LuaMember, LuaMemberFeature, LuaMemberId, LuaMemberKey, LuaSignature, + LuaSignatureId, LuaType, LuaTypeCache, LuaTypeDecl, LuaTypeDeclId, LuaTypeFlag, LuaTypeOwner, compilation::analyzer::{AnalysisPipeline, AnalyzeContext, common::add_member}, db_index::{ AsyncState, DbIndex, GmodCallbackSiteMetadata, GmodConVarKind, GmodConVarSiteMetadata, @@ -41,6 +43,8 @@ struct GmodKeywords { has_net: bool, /// timer/concommand/ConVar/AddNetworkString — system call metadata has_system_call: bool, + /// Annotated scripted-class wrappers (VGUI, Derma, NetworkVar, inheritance) + has_scripted_class_call: bool, /// "GM:" or "GAMEMODE:" — GM/GAMEMODE method sites has_gm_func: bool, /// "CLIENT" or "SERVER" — branch realm ranges (if CLIENT/if SERVER) @@ -49,6 +53,14 @@ struct GmodKeywords { has_realm_anno: bool, } +#[derive(Default)] +struct AnnotatedGmodCandidatePresence { + has_system: bool, + has_net: bool, + has_hook: bool, + has_scripted_class: bool, +} + impl GmodKeywords { /// Whether the LuaCallExpr walk in collect_hook_metadata is needed fn needs_call_walk(&self) -> bool { @@ -66,17 +78,37 @@ impl GmodKeywords { } } -fn scan_gmod_keywords(content: &str, formatted_hook_prefixes: &[String]) -> GmodKeywords { +fn scan_gmod_keywords( + content: &str, + formatted_hook_prefixes: &[String], + annotated_global_call_roles: &AnnotatedGmodGlobalCallRoleMap, +) -> GmodKeywords { let has_gm_func = content.contains("GM:") || content.contains("GAMEMODE:") || formatted_hook_prefixes.iter().any(|p| content.contains(p)); + let has_hook_annotation = content.contains("gmod.hook"); + let has_net_annotation = content.contains("gmod.net_message"); + let has_system_annotation = has_net_annotation + || content.contains("gmod.concommand") + || content.contains("gmod.convar") + || content.contains("gmod.timer"); + let has_scripted_class_annotation = content.contains("gmod.vgui_panel") + || content.contains("gmod.derma_skin") + || content.contains("gmod.network_var") + || content.contains("gmod.class_base") + || content.contains("gmod.gamemode"); + let annotated_candidates = annotated_global_call_roles.candidate_call_paths_in_content(content); GmodKeywords { - has_hook: content.contains("hook"), - has_net: content.contains("net."), + has_hook: content.contains("hook") || has_hook_annotation || annotated_candidates.has_hook, + has_net: content.contains("net.") || has_net_annotation || annotated_candidates.has_net, has_system_call: content.contains("timer.") || content.contains("concommand") || content.contains("ConVar") - || content.contains("AddNetworkString"), + || content.contains("AddNetworkString") + || has_system_annotation + || annotated_candidates.has_system, + has_scripted_class_call: has_scripted_class_annotation + || annotated_candidates.has_scripted_class, has_gm_func, has_realm_branch: content.contains("CLIENT") || content.contains("SERVER"), has_realm_anno: content.contains("@realm"), @@ -99,7 +131,6 @@ impl AnalysisPipeline for GmodPreAnalysisPipeline { let _p = Profile::cond_new("gmod pre-analyze", context.tree_list.len() > 1); let tree_list = context.tree_list.clone(); - let file_ids: Vec = tree_list.iter().map(|x| x.file_id).collect(); let do_profile = tree_list.len() > 100 && log::log_enabled!(log::Level::Info); // Pre-compute scripted class scope for all files (compile globs once) @@ -134,6 +165,31 @@ impl AnalysisPipeline for GmodPreAnalysisPipeline { .iter() .map(|p| format!("{p}:")) .collect(); + let annotated_global_call_roles = AnnotatedGmodGlobalCallRoleMap::build(db); + + let t_class = do_profile.then(std::time::Instant::now); + collect_annotated_scripted_class_calls_with( + db, + context, + &formatted_hook_prefixes, + &annotated_global_call_roles, + ); + if let Some(t_class) = t_class { + log::info!( + "gmod pre: annotated_scripted_class_calls cost {:?}", + t_class.elapsed() + ); + } + + let t_vgui = do_profile.then(std::time::Instant::now); + let file_ids: Vec = tree_list.iter().map(|tree| tree.file_id).collect(); + synthesize_vgui_registrations(db, &file_ids); + if let Some(t_vgui) = t_vgui { + log::info!( + "gmod pre: vgui_registration_bindings cost {:?}", + t_vgui.elapsed() + ); + } for in_filed_tree in &tree_list { let is_in_scope = scripted_scope_files.contains(&in_filed_tree.file_id); @@ -142,7 +198,9 @@ impl AnalysisPipeline for GmodPreAnalysisPipeline { let keywords = db .get_vfs() .get_file_content(&in_filed_tree.file_id) - .map(|c| scan_gmod_keywords(c, &formatted_hook_prefixes)) + .map(|c| { + scan_gmod_keywords(c, &formatted_hook_prefixes, &annotated_global_call_roles) + }) .unwrap_or_default(); if let Some(profile) = profile.as_mut() { profile.files_scanned += 1; @@ -157,6 +215,7 @@ impl AnalysisPipeline for GmodPreAnalysisPipeline { in_filed_tree.file_id, in_filed_tree.value.clone(), &helper_registry, + &annotated_global_call_roles, &mut local_fns, ) } else { @@ -403,10 +462,13 @@ impl AnalysisPipeline for GmodPostAnalysisPipeline { ); } - let t0 = do_profile.then(std::time::Instant::now); - synthesize_scripted_class_members(db, &scripted_scope_files, &file_ids); - if let Some(t0) = t0 { - log::info!("gmod post: scripted_class_members cost {:?}", t0.elapsed()); + let t_class = do_profile.then(std::time::Instant::now); + collect_annotated_scripted_class_calls(db, context); + if let Some(t_class) = t_class { + log::info!( + "gmod post: annotated_scripted_class_calls cost {:?}", + t_class.elapsed() + ); } let t1 = do_profile.then(std::time::Instant::now); @@ -414,6 +476,74 @@ impl AnalysisPipeline for GmodPostAnalysisPipeline { if let Some(t1) = t1 { log::info!("gmod post: vgui_registrations cost {:?}", t1.elapsed()); } + + let t0 = do_profile.then(std::time::Instant::now); + synthesize_scripted_class_members(db, &scripted_scope_files, &file_ids); + if let Some(t0) = t0 { + log::info!("gmod post: scripted_class_members cost {:?}", t0.elapsed()); + } + } +} + +fn collect_annotated_scripted_class_calls(db: &mut DbIndex, context: &AnalyzeContext) { + let formatted_hook_prefixes: Vec = db + .get_emmyrc() + .gmod + .hook_mappings + .method_prefixes + .iter() + .map(|p| format!("{p}:")) + .collect(); + let annotated_global_call_roles = AnnotatedGmodGlobalCallRoleMap::build(db); + collect_annotated_scripted_class_calls_with( + db, + context, + &formatted_hook_prefixes, + &annotated_global_call_roles, + ); +} + +fn collect_annotated_scripted_class_calls_with( + db: &mut DbIndex, + context: &AnalyzeContext, + formatted_hook_prefixes: &[String], + annotated_global_call_roles: &AnnotatedGmodGlobalCallRoleMap, +) { + for in_filed_tree in &context.tree_list { + let keywords = db + .get_vfs() + .get_file_content(&in_filed_tree.file_id) + .map(|content| { + scan_gmod_keywords( + content, + formatted_hook_prefixes, + annotated_global_call_roles, + ) + }) + .unwrap_or_default(); + if !keywords.has_scripted_class_call { + continue; + } + + let annotated_call_roles = AnnotatedGmodCallRoleMap::build( + db, + in_filed_tree.file_id, + &in_filed_tree.value, + annotated_global_call_roles, + ); + for call_expr in in_filed_tree + .value + .syntax() + .descendants() + .filter_map(LuaCallExpr::cast) + { + collect_annotated_scripted_class_call_metadata( + db, + in_filed_tree.file_id, + &annotated_call_roles, + call_expr, + ); + } } } @@ -701,16 +831,21 @@ fn collect_hook_metadata( file_id: FileId, root: LuaChunk, helper_registry: &HelperRegistry, + annotated_global_call_roles: &AnnotatedGmodGlobalCallRoleMap, local_fns: &mut LocalFnCache, ) -> (Vec<(String, GmodRealm)>, Vec) { let mut gm_method_realms = Vec::new(); let mut receive_flows = Vec::new(); + let annotated_call_roles = + AnnotatedGmodCallRoleMap::build(db, file_id, &root, annotated_global_call_roles); // Single descendants walk dispatching by node kind. Avoids two separate // O(N) walks for the LuaCallExpr and LuaFuncStat passes. for node in root.syntax().descendants() { if let Some(call_expr) = LuaCallExpr::cast(node.clone()) { - if let Some(site) = collect_hook_call_site(db, call_expr.clone()) { + if let Some(site) = + collect_hook_call_site(db, file_id, &annotated_call_roles, call_expr.clone()) + { db.get_gmod_infer_index_mut().add_hook_site(file_id, site); } @@ -720,7 +855,7 @@ fn collect_hook_metadata( receive_flows.push(receive_flow); } - collect_system_call_metadata(db, file_id, call_expr); + collect_system_call_metadata(db, file_id, &annotated_call_roles, call_expr); continue; } @@ -2211,10 +2346,13 @@ fn synthesize_vgui_registrations(db: &mut DbIndex, file_ids: &[FileId]) { for call in &metadata.vgui_register_calls { let register_position = call.syntax_id.get_range().start(); - if let Some(Some(GmodClassCallLiteral::String(panel_name))) = call.literal_args.first() + let panel_arg_idx = call.vgui_panel_define_arg_idx(); + let table_arg_idx = call.vgui_panel_table_arg_idx(1); + if let Some(Some(GmodClassCallLiteral::String(panel_name))) = + call.literal_args.get(panel_arg_idx) { if let Some(Some(GmodClassCallLiteral::NameRef(table_var))) = - call.literal_args.get(1) + call.literal_args.get(table_arg_idx) && let Some((decl_id, region_start)) = resolve_local_registration_region(db, file_id, table_var, register_position) { @@ -2232,10 +2370,13 @@ fn synthesize_vgui_registrations(db: &mut DbIndex, file_ids: &[FileId]) { for call in &metadata.derma_define_control_calls { let register_position = call.syntax_id.get_range().start(); - if let Some(Some(GmodClassCallLiteral::String(panel_name))) = call.literal_args.first() + let panel_arg_idx = call.vgui_panel_define_arg_idx(); + let table_arg_idx = call.vgui_panel_table_arg_idx(2); + if let Some(Some(GmodClassCallLiteral::String(panel_name))) = + call.literal_args.get(panel_arg_idx) { if let Some(Some(GmodClassCallLiteral::NameRef(table_var))) = - call.literal_args.get(2) + call.literal_args.get(table_arg_idx) && let Some((decl_id, region_start)) = resolve_local_registration_region(db, file_id, table_var, register_position) { @@ -2883,7 +3024,7 @@ fn resolve_effective_inheritance_call( fn valid_inheritance_literal(call: &GmodScriptedClassCallMetadata) -> bool { matches!( - call.literal_args.first(), + call.literal_args.get(call.inheritance_name_arg_idx()), Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() ) } @@ -2893,7 +3034,7 @@ fn resolve_effective_inheritance_base( class_name_prefix: Option<&str>, ) -> Option<(String, bool)> { let call = resolve_effective_inheritance_call(metadata)?; - let base_name = match call.literal_args.first() { + let base_name = match call.literal_args.get(call.inheritance_name_arg_idx()) { Some(Some(GmodClassCallLiteral::String(name))) => name.as_str(), _ => return None, }; @@ -2946,7 +3087,7 @@ fn synthesize_define_baseclass_parent_alias( _ => return, }; - let base_name = match call.literal_args.first() { + let base_name = match call.literal_args.get(call.inheritance_name_arg_idx()) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => name.as_str(), _ => return, }; @@ -3114,22 +3255,33 @@ fn synthesize_network_var( // args[1] = slot (integer) OR name (string, if 2-arg form) // args[2] = name (string, if 3-arg form) - let type_name = match call.literal_args.first() { + let type_arg_idx = call.network_var_type_arg_idx().unwrap_or(0); + let type_name = match call.literal_args.get(type_arg_idx) { Some(Some(GmodClassCallLiteral::String(name))) => name.clone(), _ => return, }; - // Try index 2 first (3-arg form), then index 1 (2-arg form) - let (prop_name, prop_name_arg_idx) = match call.literal_args.get(2) { - Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => { - (name.clone(), 2usize) - } - _ => match call.literal_args.get(1) { + let (prop_name, prop_name_arg_idx) = if let Some(name_arg_idx) = call.network_var_name_arg_idx() + { + match call.literal_args.get(name_arg_idx) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => { - (name.clone(), 1usize) + (name.clone(), name_arg_idx) } _ => return, - }, + } + } else { + // Try index 2 first (3-arg form), then index 1 (2-arg form) + match call.literal_args.get(2) { + Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => { + (name.clone(), 2usize) + } + _ => match call.literal_args.get(1) { + Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => { + (name.clone(), 1usize) + } + _ => return, + }, + } }; let value_type = resolve_networkvar_type(&type_name); @@ -3193,27 +3345,42 @@ fn synthesize_network_var_element( // args[2] = element or name // args[3] = name (if 4-arg form) - // Type name must be present (first arg is always the type) - if call.literal_args.first().and_then(|a| a.as_ref()).is_none() { + let type_arg_idx = call.network_var_type_arg_idx().unwrap_or(0); + if call + .literal_args + .get(type_arg_idx) + .and_then(|a| a.as_ref()) + .is_none() + { return; } - // Find the property name: try index 3, then 2, then 1 - let (prop_name, prop_name_arg_idx) = match call.literal_args.get(3) { - Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => { - (name.clone(), 3usize) + let (prop_name, prop_name_arg_idx) = if let Some(name_arg_idx) = call.network_var_name_arg_idx() + { + match call.literal_args.get(name_arg_idx) { + Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => { + (name.clone(), name_arg_idx) + } + _ => return, } - _ => match call.literal_args.get(2) { + } else { + // Find the property name: try index 3, then 2, then 1 + match call.literal_args.get(3) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => { - (name.clone(), 2usize) + (name.clone(), 3usize) } - _ => match call.literal_args.get(1) { + _ => match call.literal_args.get(2) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => { - (name.clone(), 1usize) + (name.clone(), 2usize) } - _ => return, + _ => match call.literal_args.get(1) { + Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => { + (name.clone(), 1usize) + } + _ => return, + }, }, - }, + } }; // NetworkVarElement always produces number accessors @@ -3272,17 +3439,21 @@ fn synthesize_vgui_register( // args[0] = panel name (string) // args[1] = table variable (name ref) // args[2] = base panel name (string) - let panel_name = match call.literal_args.first() { + let panel_arg_idx = call.vgui_panel_define_arg_idx(); + let table_arg_idx = call.vgui_panel_table_arg_idx(1); + let base_arg_idx = call.vgui_panel_base_arg_idx(Some(2)).unwrap_or(2); + + let panel_name = match call.literal_args.get(panel_arg_idx) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => name.clone(), _ => return, }; - let table_var_name = match call.literal_args.get(1) { + let table_var_name = match call.literal_args.get(table_arg_idx) { Some(Some(GmodClassCallLiteral::NameRef(name))) => Some(name.clone()), _ => None, }; - let base_panel = match call.literal_args.get(2) { + let base_panel = match call.literal_args.get(base_arg_idx) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => Some(name.clone()), _ => None, }; @@ -3308,17 +3479,21 @@ fn synthesize_derma_define_control( // args[1] = description (string, ignored) // args[2] = table variable (name ref) // args[3] = base panel name (string) - let control_name = match call.literal_args.first() { + let panel_arg_idx = call.vgui_panel_define_arg_idx(); + let table_arg_idx = call.vgui_panel_table_arg_idx(2); + let base_arg_idx = call.vgui_panel_base_arg_idx(Some(3)).unwrap_or(3); + + let control_name = match call.literal_args.get(panel_arg_idx) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => name.clone(), _ => return, }; - let table_var_name = match call.literal_args.get(2) { + let table_var_name = match call.literal_args.get(table_arg_idx) { Some(Some(GmodClassCallLiteral::NameRef(name))) => Some(name.clone()), _ => None, }; - let base_panel = match call.literal_args.get(3) { + let base_panel = match call.literal_args.get(base_arg_idx) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => Some(name.clone()), _ => None, }; @@ -3561,12 +3736,14 @@ fn synthesize_panel_class( LuaTypeCache::InferType(class_type.clone()), ); } - } else if !decl_has_reassignment(db, file_id, decl_id) { - // Compatibility fallback for single-panel files (one `local PANEL`, - // no plain reassignments) where the RHS is not a recoverable table - // literal. Binding the decl slot is safe here because the local is - // never reused, so there is no region to collapse. Reassigned - // locals are deliberately left untouched to avoid collapse. + } + + if !decl_has_reassignment(db, file_id, decl_id) { + // For single-panel files the `PANEL` local has one stable identity. + // Bind the decl slot too so method-self collection during the Lua + // pass sees the synthesized class before it caches member values. + // Reassigned locals remain table-literal-only to avoid collapsing + // distinct registration regions onto one class. db.get_type_index_mut() .force_bind_type(decl_id.into(), LuaTypeCache::InferType(class_type.clone())); } @@ -3804,8 +3981,12 @@ fn synthesize_panel_baseclass_member( } let base_arg_index = match call_kind { - GmodScriptedClassCallKind::VguiRegister => 2, - GmodScriptedClassCallKind::DermaDefineControl => 3, + GmodScriptedClassCallKind::VguiRegister => { + call.vgui_panel_base_arg_idx(Some(2)).unwrap_or(2) + } + GmodScriptedClassCallKind::DermaDefineControl => { + call.vgui_panel_base_arg_idx(Some(3)).unwrap_or(3) + } _ => return, }; @@ -3931,223 +4112,1544 @@ enum GmodSystemCallKind { TimerSimple, } -fn collect_system_call_metadata( - db: &mut DbIndex, - file_id: FileId, - call_expr: LuaCallExpr, -) -> Option<()> { - let call_path = call_expr.get_access_path()?; - let kind = classify_system_call_path(&call_path)?; +#[derive(Debug, Clone, Copy)] +struct GmodSystemCallSite { + kind: GmodSystemCallKind, + name_arg_idx: Option, + callback_arg_idx: Option, +} - match kind { - GmodSystemCallKind::AddNetworkString => { - let (name, name_range) = extract_static_string_arg(call_expr.clone(), 0); - db.get_gmod_infer_index_mut().add_net_message_registration( - file_id, - GmodNamedSiteMetadata { - syntax_id: call_expr.get_syntax_id(), - name, - name_range, - }, - ); +#[derive(Default)] +struct AnnotatedGmodGlobalCallRoleMap { + roles_by_path: HashMap, + candidate_call_path_matcher: Option, + candidate_call_path_kinds: Vec, +} + +struct AnnotatedGmodCallRoleMap<'a> { + global_roles: &'a AnnotatedGmodGlobalCallRoleMap, + local_roles_by_decl: HashMap, + local_roles_by_path: HashMap<(LuaDeclId, String), AnnotatedGmodCallRoles>, + local_candidate_names: HashSet, +} + +#[derive(Clone, Default)] +struct AnnotatedGmodCallRoles { + is_colon_define: bool, + params: Vec>, + optional_params: Vec, + is_variadic: bool, + overloads: Vec, + system_roles: Vec<(GmodSystemCallKind, usize, i64)>, + system_callback_roles: Vec<(GmodSystemCallKind, usize, i64)>, + hook_roles: Vec<(GmodHookKind, usize, i64)>, + hook_callback_roles: Vec<(usize, i64)>, + inheritance_roles: Vec<(GmodScriptedClassCallKind, usize, i64)>, + network_var_kind: Option, + network_var_type_roles: Vec<(usize, i64)>, + network_var_define_roles: Vec<(usize, i64)>, + vgui_panel_kind: Option, + vgui_panel_define_roles: Vec<(usize, i64)>, + vgui_panel_table_roles: Vec<(usize, i64)>, + vgui_panel_base_roles: Vec<(usize, i64)>, + derma_skin_define_roles: Vec<(usize, i64)>, +} + +impl AnnotatedGmodCallRoles { + fn from_signature_shape(signature: &LuaSignature) -> Self { + Self { + is_colon_define: signature.is_colon_define, + params: signature + .params + .iter() + .enumerate() + .map(|(idx, _)| { + signature + .param_docs + .get(&idx) + .map(|param| param.type_ref.clone()) + }) + .collect(), + optional_params: signature.get_param_optional_flags(), + is_variadic: signature.is_vararg, + ..Self::default() } - GmodSystemCallKind::NetStart => { - let (name, name_range) = extract_static_string_arg(call_expr.clone(), 0); - db.get_gmod_infer_index_mut().add_net_start_site( - file_id, - GmodNamedSiteMetadata { - syntax_id: call_expr.get_syntax_id(), - name, - name_range, - }, - ); + } + + fn from_function_shape(func: &LuaFunctionType) -> Self { + Self { + is_colon_define: func.is_colon_define(), + params: func + .get_params() + .iter() + .map(|(_, typ)| typ.clone()) + .collect(), + optional_params: func.get_optional_params().to_vec(), + is_variadic: func.is_variadic(), + ..Self::default() } - GmodSystemCallKind::NetReceive => { - let (message_name, name_range) = extract_static_string_arg(call_expr.clone(), 0); - let callback = extract_callback_arg(call_expr.clone(), 1); - db.get_gmod_infer_index_mut().add_net_receive_site( - file_id, - GmodNetReceiveSiteMetadata { - syntax_id: call_expr.get_syntax_id(), - message_name, - name_range, - callback, - }, - ); + } + + fn add_call_arg_role(&mut self, role: &LuaCallArgRole) { + let priority = role.priority.unwrap_or(0); + match (role.domain.as_str(), role.role.as_str()) { + ("gmod.net_message", "define") => self.system_roles.push(( + GmodSystemCallKind::AddNetworkString, + role.param_idx, + priority, + )), + ("gmod.net_message", "start") => { + self.system_roles + .push((GmodSystemCallKind::NetStart, role.param_idx, priority)); + } + ("gmod.net_message", "receive") => { + self.system_roles + .push((GmodSystemCallKind::NetReceive, role.param_idx, priority)); + } + ("gmod.net_message", "callback") => self.system_callback_roles.push(( + GmodSystemCallKind::NetReceive, + role.param_idx, + priority, + )), + ("gmod.concommand", "define") => self.system_roles.push(( + GmodSystemCallKind::ConcommandAdd, + role.param_idx, + priority, + )), + ("gmod.concommand", "callback") => self.system_callback_roles.push(( + GmodSystemCallKind::ConcommandAdd, + role.param_idx, + priority, + )), + ("gmod.convar", "define") | ("gmod.convar", "define_server") => self + .system_roles + .push((GmodSystemCallKind::CreateConVar, role.param_idx, priority)), + ("gmod.convar", "define_client") => self.system_roles.push(( + GmodSystemCallKind::CreateClientConVar, + role.param_idx, + priority, + )), + ("gmod.timer", "define") => { + self.system_roles + .push((GmodSystemCallKind::TimerCreate, role.param_idx, priority)) + } + ("gmod.timer", "callback") => self.system_callback_roles.push(( + GmodSystemCallKind::TimerCreate, + role.param_idx, + priority, + )), + ("gmod.timer", "simple") => self.system_callback_roles.push(( + GmodSystemCallKind::TimerSimple, + role.param_idx, + priority, + )), + ("gmod.hook", "add") => { + self.hook_roles + .push((GmodHookKind::Add, role.param_idx, priority)) + } + ("gmod.hook", "emit") => { + self.hook_roles + .push((GmodHookKind::Emit, role.param_idx, priority)) + } + ("gmod.hook", "callback") => { + self.hook_callback_roles.push((role.param_idx, priority)); + } + ("gmod.class_base", "reference") => self.inheritance_roles.push(( + GmodScriptedClassCallKind::DefineBaseClass, + role.param_idx, + priority, + )), + ("gmod.gamemode", "reference") => self.inheritance_roles.push(( + GmodScriptedClassCallKind::DeriveGamemode, + role.param_idx, + priority, + )), + ("gmod.network_var", "type") => { + self.network_var_type_roles.push((role.param_idx, priority)); + } + ("gmod.network_var", "define") => { + self.network_var_kind = self + .network_var_kind + .or(Some(GmodScriptedClassCallKind::NetworkVar)); + self.network_var_define_roles + .push((role.param_idx, priority)); + } + ("gmod.network_var", "define_element") => { + self.network_var_kind = Some(GmodScriptedClassCallKind::NetworkVarElement); + self.network_var_define_roles + .push((role.param_idx, priority)); + } + ("gmod.vgui_panel", "define") => { + self.vgui_panel_kind = self + .vgui_panel_kind + .or(Some(GmodScriptedClassCallKind::VguiRegister)); + self.vgui_panel_define_roles + .push((role.param_idx, priority)); + } + ("gmod.vgui_panel", "define_control") => { + self.vgui_panel_kind = Some(GmodScriptedClassCallKind::DermaDefineControl); + self.vgui_panel_define_roles + .push((role.param_idx, priority)); + } + ("gmod.vgui_panel", "table") => { + self.vgui_panel_table_roles.push((role.param_idx, priority)); + } + ("gmod.vgui_panel", "base") => { + self.vgui_panel_base_roles.push((role.param_idx, priority)); + } + ("gmod.derma_skin", "define") => { + self.derma_skin_define_roles + .push((role.param_idx, priority)); + } + _ => {} } - GmodSystemCallKind::ConcommandAdd => { - let (command_name, name_range) = extract_static_string_arg(call_expr.clone(), 0); - let callback = extract_callback_arg(call_expr.clone(), 1); - db.get_gmod_infer_index_mut().add_concommand_site( - file_id, - GmodConcommandSiteMetadata { - syntax_id: call_expr.get_syntax_id(), - command_name, - name_range, - callback, - }, - ); + } + + fn sort_roles(&mut self) { + self.system_roles + .sort_by_key(|(_, param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + self.system_callback_roles + .sort_by_key(|(_, param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + self.hook_roles + .sort_by_key(|(_, param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + self.hook_callback_roles + .sort_by_key(|(param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + self.inheritance_roles + .sort_by_key(|(_, param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + self.network_var_type_roles + .sort_by_key(|(param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + self.network_var_define_roles + .sort_by_key(|(param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + self.vgui_panel_define_roles + .sort_by_key(|(param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + self.vgui_panel_table_roles + .sort_by_key(|(param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + self.vgui_panel_base_roles + .sort_by_key(|(param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + self.derma_skin_define_roles + .sort_by_key(|(param_idx, priority)| (std::cmp::Reverse(*priority), *param_idx)); + } + + fn has_any_roles(&self) -> bool { + !self.system_roles.is_empty() + || !self.system_callback_roles.is_empty() + || !self.hook_roles.is_empty() + || !self.hook_callback_roles.is_empty() + || !self.inheritance_roles.is_empty() + || !self.network_var_define_roles.is_empty() + || !self.vgui_panel_define_roles.is_empty() + || !self.derma_skin_define_roles.is_empty() + } + + fn select_for_call(&self, call_expr: &LuaCallExpr) -> Option { + let mut best_roles = None; + let mut best_score = i32::MIN; + + for roles in std::iter::once(self).chain(self.overloads.iter()) { + let Some(score) = roles.match_score(call_expr) else { + continue; + }; + if score > best_score { + best_score = score; + best_roles = Some(roles.clone_without_overloads()); + } } - GmodSystemCallKind::CreateConVar | GmodSystemCallKind::CreateClientConVar => { - let (convar_name, name_range) = extract_static_string_arg(call_expr.clone(), 0); - db.get_gmod_infer_index_mut().add_convar_site( - file_id, - GmodConVarSiteMetadata { - syntax_id: call_expr.get_syntax_id(), - kind: if kind == GmodSystemCallKind::CreateClientConVar { - GmodConVarKind::Client - } else { - GmodConVarKind::Server - }, - convar_name, - name_range, - }, - ); + + best_roles.or_else(|| Some(self.clone_without_overloads())) + } + + fn clone_without_overloads(&self) -> AnnotatedGmodCallRoles { + let mut roles = self.clone(); + roles.overloads.clear(); + roles + } + + fn match_score(&self, call_expr: &LuaCallExpr) -> Option { + if self.params.is_empty() && self.optional_params.is_empty() && !self.is_variadic { + return Some(0); } - GmodSystemCallKind::TimerCreate => { - let (timer_name, name_range) = extract_static_string_arg(call_expr.clone(), 0); - let callback = extract_callback_arg(call_expr.clone(), 3); - db.get_gmod_infer_index_mut().add_timer_site( - file_id, - GmodTimerSiteMetadata { - syntax_id: call_expr.get_syntax_id(), - kind: GmodTimerKind::Create, - timer_name, - name_range, - callback, - }, - ); + + let args = call_expr.get_args_list()?.get_args().collect::>(); + let effective_arg_count = + args.len() + usize::from(call_expr.is_colon_call() && !self.is_colon_define); + let required_count = self + .params + .iter() + .enumerate() + .filter(|(idx, _)| !self.optional_params.get(*idx).copied().unwrap_or(false)) + .count(); + + if effective_arg_count < required_count { + return None; } - GmodSystemCallKind::TimerSimple => { - let callback = extract_callback_arg(call_expr.clone(), 1); - db.get_gmod_infer_index_mut().add_timer_site( - file_id, - GmodTimerSiteMetadata { - syntax_id: call_expr.get_syntax_id(), - kind: GmodTimerKind::Simple, - timer_name: None, - name_range: None, - callback, - }, - ); + if !self.is_variadic && effective_arg_count > self.params.len() { + return None; } - } - Some(()) -} + let first_param_offset = usize::from(call_expr.is_colon_call() && !self.is_colon_define); + let mut score = 0; + for (arg_idx, arg) in args.iter().enumerate() { + let param_idx = arg_idx + first_param_offset; + let Some(Some(param_type)) = self.params.get(param_idx) else { + continue; + }; + match static_arg_matches_type(arg, param_type) { + StaticArgTypeMatch::Match => score += 2, + StaticArgTypeMatch::Unknown => {} + StaticArgTypeMatch::Mismatch => return None, + } + } -fn classify_system_call_path(path: &str) -> Option { - if matches_call_path(path, "util.AddNetworkString") { - return Some(GmodSystemCallKind::AddNetworkString); - } - if matches_call_path(path, "net.Start") { - return Some(GmodSystemCallKind::NetStart); + Some(score) } - if matches_call_path(path, "net.Receive") { - return Some(GmodSystemCallKind::NetReceive); - } - if matches_call_path(path, "concommand.Add") { - return Some(GmodSystemCallKind::ConcommandAdd); + + fn system_call_site(&self) -> Option { + if let Some((kind, name_arg_idx, _)) = self.system_roles.first() { + return Some(GmodSystemCallSite { + kind: *kind, + name_arg_idx: Some(*name_arg_idx), + callback_arg_idx: self.callback_arg_idx_for_kind(*kind), + }); + } + + let (kind, callback_arg_idx, _) = self + .system_callback_roles + .iter() + .find(|(kind, _, _)| *kind == GmodSystemCallKind::TimerSimple)?; + + Some(GmodSystemCallSite { + kind: *kind, + name_arg_idx: None, + callback_arg_idx: Some(*callback_arg_idx), + }) } - if matches_call_path(path, "CreateClientConVar") { - return Some(GmodSystemCallKind::CreateClientConVar); + + fn callback_arg_idx_for_kind(&self, call_kind: GmodSystemCallKind) -> Option { + self.system_callback_roles + .iter() + .find(|(kind, _, _)| *kind == call_kind) + .map(|(_, param_idx, _)| *param_idx) + } + + fn candidate_presence(&self) -> AnnotatedGmodCandidatePresence { + AnnotatedGmodCandidatePresence { + has_system: !self.system_roles.is_empty() || !self.system_callback_roles.is_empty(), + has_net: self.system_roles.iter().any(|(kind, _, _)| { + matches!( + kind, + GmodSystemCallKind::AddNetworkString + | GmodSystemCallKind::NetStart + | GmodSystemCallKind::NetReceive + ) + }), + has_hook: !self.hook_roles.is_empty() || !self.hook_callback_roles.is_empty(), + has_scripted_class: !self.inheritance_roles.is_empty() + || !self.network_var_define_roles.is_empty() + || !self.vgui_panel_define_roles.is_empty() + || !self.derma_skin_define_roles.is_empty(), + } } - if matches_call_path(path, "CreateConVar") { - return Some(GmodSystemCallKind::CreateConVar); + + fn inheritance_call( + &self, + is_colon_call: bool, + ) -> Option<(GmodScriptedClassCallKind, GmodNamedStringCallRoles)> { + let (kind, param_idx, _) = *self.inheritance_roles.first()?; + Some(( + kind, + GmodNamedStringCallRoles { + name_arg_idx: param_idx_to_call_arg_idx( + param_idx, + is_colon_call, + self.is_colon_define, + )?, + }, + )) + } + + fn network_var_call( + &self, + is_colon_call: bool, + ) -> Option<(GmodScriptedClassCallKind, GmodNetworkVarCallRoles)> { + let (define_param_idx, _) = *self.network_var_define_roles.first()?; + let kind = self + .network_var_kind + .unwrap_or(GmodScriptedClassCallKind::NetworkVar); + Some(( + kind, + GmodNetworkVarCallRoles { + type_arg_idx: self + .network_var_type_roles + .first() + .and_then(|(param_idx, _)| { + param_idx_to_call_arg_idx(*param_idx, is_colon_call, self.is_colon_define) + }), + name_arg_idx: param_idx_to_call_arg_idx( + define_param_idx, + is_colon_call, + self.is_colon_define, + )?, + }, + )) + } + + fn vgui_panel_call( + &self, + is_colon_call: bool, + ) -> Option<(GmodScriptedClassCallKind, GmodVguiPanelCallRoles)> { + let (define_arg_idx, _) = *self.vgui_panel_define_roles.first()?; + Some(( + self.vgui_panel_kind + .unwrap_or(GmodScriptedClassCallKind::VguiRegister), + GmodVguiPanelCallRoles { + define_arg_idx: param_idx_to_call_arg_idx( + define_arg_idx, + is_colon_call, + self.is_colon_define, + )?, + table_arg_idx: self + .vgui_panel_table_roles + .first() + .and_then(|(param_idx, _)| { + param_idx_to_call_arg_idx(*param_idx, is_colon_call, self.is_colon_define) + }), + base_arg_idx: self + .vgui_panel_base_roles + .first() + .and_then(|(param_idx, _)| { + param_idx_to_call_arg_idx(*param_idx, is_colon_call, self.is_colon_define) + }), + }, + )) } - if matches_call_path(path, "timer.Create") { - return Some(GmodSystemCallKind::TimerCreate); + + fn derma_skin_call_roles(&self, is_colon_call: bool) -> Option { + let (define_arg_idx, _) = *self.derma_skin_define_roles.first()?; + Some(GmodDermaSkinCallRoles { + define_arg_idx: param_idx_to_call_arg_idx( + define_arg_idx, + is_colon_call, + self.is_colon_define, + )?, + }) } - if matches_call_path(path, "timer.Simple") { - return Some(GmodSystemCallKind::TimerSimple); +} + +fn param_idx_to_call_arg_idx( + param_idx: usize, + is_colon_call: bool, + is_colon_define: bool, +) -> Option { + if is_colon_call && !is_colon_define { + param_idx.checked_sub(1) + } else { + Some(param_idx) } - None } -fn matches_call_path(path: &str, target: &str) -> bool { - path == target || path.ends_with(&format!(".{target}")) || path.ends_with(&format!(":{target}")) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StaticArgTypeMatch { + Match, + Mismatch, + Unknown, } -fn extract_static_string_arg( - call_expr: LuaCallExpr, - arg_idx: usize, -) -> (Option, Option) { - let Some(arg_expr) = call_expr - .get_args_list() - .and_then(|args| args.get_args().nth(arg_idx)) - else { - return (None, None); +fn static_arg_matches_type(arg: &LuaExpr, param_type: &LuaType) -> StaticArgTypeMatch { + let Some(kind) = static_arg_kind(arg) else { + return StaticArgTypeMatch::Unknown; }; - let LuaExpr::LiteralExpr(literal_expr) = arg_expr else { - return (None, None); - }; + static_arg_kind_matches_type(kind, param_type) +} - match literal_expr.get_literal() { - Some(LuaLiteralToken::String(string_token)) => ( - Some(string_token.get_value()), - Some(string_token.get_range()), - ), - Some(_) => (None, Some(literal_expr.get_range())), - None => (None, Some(literal_expr.get_range())), +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StaticArgKind { + String, + Number, + Boolean, + Table, + Function, + Nil, +} + +fn static_arg_kind(arg: &LuaExpr) -> Option { + match arg { + LuaExpr::LiteralExpr(literal) => match literal.get_literal()? { + LuaLiteralToken::String(_) => Some(StaticArgKind::String), + LuaLiteralToken::Number(_) => Some(StaticArgKind::Number), + LuaLiteralToken::Bool(_) => Some(StaticArgKind::Boolean), + LuaLiteralToken::Nil(_) => Some(StaticArgKind::Nil), + LuaLiteralToken::Dots(_) | LuaLiteralToken::Question(_) => None, + }, + LuaExpr::TableExpr(_) => Some(StaticArgKind::Table), + LuaExpr::ClosureExpr(_) => Some(StaticArgKind::Function), + _ => None, + } +} + +fn static_arg_kind_matches_type(kind: StaticArgKind, param_type: &LuaType) -> StaticArgTypeMatch { + match param_type { + LuaType::Any | LuaType::Unknown => StaticArgTypeMatch::Unknown, + LuaType::String | LuaType::StringConst(_) | LuaType::DocStringConst(_) => { + match_bool(kind == StaticArgKind::String) + } + LuaType::Number + | LuaType::Integer + | LuaType::IntegerConst(_) + | LuaType::DocIntegerConst(_) => match_bool(kind == StaticArgKind::Number), + LuaType::Boolean | LuaType::BooleanConst(_) | LuaType::DocBooleanConst(_) => { + match_bool(kind == StaticArgKind::Boolean) + } + LuaType::Table | LuaType::Object(_) | LuaType::TableConst(_) => { + match_bool(kind == StaticArgKind::Table) + } + LuaType::DocFunction(_) | LuaType::Signature(_) | LuaType::Function => { + match_bool(kind == StaticArgKind::Function) + } + LuaType::Nil => match_bool(kind == StaticArgKind::Nil), + LuaType::Union(union) => { + let mut saw_unknown = false; + for typ in union.into_vec() { + match static_arg_kind_matches_type(kind, &typ) { + StaticArgTypeMatch::Match => return StaticArgTypeMatch::Match, + StaticArgTypeMatch::Unknown => saw_unknown = true, + StaticArgTypeMatch::Mismatch => {} + } + } + if saw_unknown { + StaticArgTypeMatch::Unknown + } else { + StaticArgTypeMatch::Mismatch + } + } + LuaType::MultiLineUnion(union) => { + let mut saw_unknown = false; + for (typ, _) in union.get_unions() { + match static_arg_kind_matches_type(kind, typ) { + StaticArgTypeMatch::Match => return StaticArgTypeMatch::Match, + StaticArgTypeMatch::Unknown => saw_unknown = true, + StaticArgTypeMatch::Mismatch => {} + } + } + if saw_unknown { + StaticArgTypeMatch::Unknown + } else { + StaticArgTypeMatch::Mismatch + } + } + LuaType::TypeGuard(inner) => static_arg_kind_matches_type(kind, inner), + LuaType::TableOf(inner) => static_arg_kind_matches_type(kind, inner), + LuaType::Instance(instance) => static_arg_kind_matches_type(kind, instance.get_base()), + LuaType::Variadic(variadic) => match variadic.as_ref() { + crate::db_index::VariadicType::Base(inner) => static_arg_kind_matches_type(kind, inner), + crate::db_index::VariadicType::Multi(types) => { + if types + .iter() + .any(|typ| static_arg_kind_matches_type(kind, typ) == StaticArgTypeMatch::Match) + { + StaticArgTypeMatch::Match + } else { + StaticArgTypeMatch::Unknown + } + } + }, + _ => StaticArgTypeMatch::Unknown, + } +} + +fn match_bool(matches: bool) -> StaticArgTypeMatch { + if matches { + StaticArgTypeMatch::Match + } else { + StaticArgTypeMatch::Mismatch + } +} + +impl AnnotatedGmodGlobalCallRoleMap { + fn build(db: &DbIndex) -> Self { + let mut role_map = Self::default(); + for (signature_id, signature) in db.get_signature_index().iter() { + if !signature.has_call_arg_roles() { + continue; + } + let Some(closure) = closure_from_signature_id(db, *signature_id) else { + continue; + }; + role_map.add_signature_closure(db, *signature_id, &closure); + } + role_map.rebuild_candidate_call_path_set(); + + role_map + } + + fn rebuild_candidate_call_path_set(&mut self) { + let mut call_paths = Vec::new(); + self.candidate_call_path_kinds.clear(); + + for (call_path, roles) in &self.roles_by_path { + let presence = roles.candidate_presence(); + if !presence.has_system + && !presence.has_net + && !presence.has_hook + && !presence.has_scripted_class + { + continue; + } + + call_paths.push(call_path.as_str()); + self.candidate_call_path_kinds.push(presence); + } + + self.candidate_call_path_matcher = if call_paths.is_empty() { + None + } else { + AhoCorasick::new(call_paths).ok() + }; + } + + fn candidate_call_paths_in_content(&self, content: &str) -> AnnotatedGmodCandidatePresence { + let mut presence = AnnotatedGmodCandidatePresence::default(); + let Some(matcher) = &self.candidate_call_path_matcher else { + return presence; + }; + + for mat in matcher.find_iter(content) { + let Some(candidate_presence) = + self.candidate_call_path_kinds.get(mat.pattern().as_usize()) + else { + continue; + }; + + presence.has_system |= candidate_presence.has_system; + presence.has_net |= candidate_presence.has_net; + presence.has_hook |= candidate_presence.has_hook; + presence.has_scripted_class |= candidate_presence.has_scripted_class; + + if presence.has_system + && presence.has_net + && presence.has_hook + && presence.has_scripted_class + { + break; + } + } + + presence + } + + fn add_signature_closure( + &mut self, + db: &DbIndex, + signature_id: LuaSignatureId, + closure: &LuaClosureExpr, + ) { + let Some(call_path) = global_call_path_for_signature_closure(db, signature_id, closure) + else { + return; + }; + if let Some(roles) = roles_from_signature(db, signature_id) { + self.roles_by_path.insert(call_path.clone(), roles.clone()); + if let Some(global_path) = call_path.strip_prefix("_G.") { + self.roles_by_path.insert(global_path.to_string(), roles); + } + } + } + + fn get(&self, call_path: &str) -> Option { + self.roles_by_path + .get(call_path) + .or_else(|| { + call_path + .strip_prefix("_G.") + .and_then(|global_path| self.roles_by_path.get(global_path)) + }) + .cloned() + } + + fn contains(&self, call_path: &str) -> bool { + self.roles_by_path.contains_key(call_path) + || call_path + .strip_prefix("_G.") + .is_some_and(|global_path| self.roles_by_path.contains_key(global_path)) + } +} + +impl<'a> AnnotatedGmodCallRoleMap<'a> { + fn build( + db: &DbIndex, + file_id: FileId, + root: &LuaChunk, + global_roles: &'a AnnotatedGmodGlobalCallRoleMap, + ) -> Self { + let mut role_map = Self { + global_roles, + local_roles_by_decl: HashMap::new(), + local_roles_by_path: HashMap::new(), + local_candidate_names: HashSet::new(), + }; + + for func_stat in root.descendants::() { + let Some(func_name) = func_stat.get_func_name() else { + continue; + }; + let Some(root_name_expr) = var_expr_root_name(&func_name) else { + continue; + }; + let Some(root_name) = root_name_expr.get_name_text() else { + continue; + }; + let Some(root_decl) = + db.get_decl_index() + .get_decl_tree(&file_id) + .and_then(|decl_tree| { + decl_tree.find_local_decl(&root_name, root_name_expr.get_position()) + }) + else { + continue; + }; + if !root_decl.is_local() { + continue; + } + let root_decl_id = root_decl.get_id(); + let Some(closure) = func_stat.get_closure() else { + continue; + }; + let signature_id = LuaSignatureId::from_closure(file_id, &closure); + let Some(roles) = roles_from_signature(db, signature_id) else { + continue; + }; + let Some(call_path) = func_name.get_access_path() else { + continue; + }; + role_map.add_local_path_roles(root_decl_id, call_path, roles); + } + + for local_func_stat in root.descendants::() { + let Some(name_token) = local_func_stat + .get_local_name() + .and_then(|local_name| local_name.get_name_token()) + else { + continue; + }; + let Some(closure) = local_func_stat.get_closure() else { + continue; + }; + let signature_id = LuaSignatureId::from_closure(file_id, &closure); + let Some(roles) = roles_from_signature(db, signature_id) else { + continue; + }; + role_map.add_local_decl_roles( + LuaDeclId::new(file_id, name_token.get_range().start()), + name_token.get_name_text().to_string(), + roles, + ); + } + + role_map + } + + fn add_local_decl_roles( + &mut self, + decl_id: LuaDeclId, + name: String, + roles: AnnotatedGmodCallRoles, + ) { + self.local_roles_by_decl.insert(decl_id, roles); + self.local_candidate_names.insert(name); + } + + fn add_local_path_roles( + &mut self, + root_decl_id: LuaDeclId, + call_path: String, + roles: AnnotatedGmodCallRoles, + ) { + self.local_candidate_names.insert(call_path.clone()); + self.local_roles_by_path + .insert((root_decl_id, call_path), roles); + } + + fn system_call( + &self, + db: &DbIndex, + file_id: FileId, + call_expr: &LuaCallExpr, + call_path: &str, + ) -> Option { + self.roles_for_call(db, file_id, call_expr, call_path) + .and_then(|roles| roles.system_call_site()) + } + + fn hook_call( + &self, + db: &DbIndex, + file_id: FileId, + call_expr: &LuaCallExpr, + call_path: &str, + ) -> Option<(GmodHookKind, usize, Option)> { + self.roles_for_call(db, file_id, call_expr, call_path) + .and_then(|roles| { + let (kind, param_idx, _) = roles.hook_roles.first()?; + Some(( + *kind, + *param_idx, + roles + .hook_callback_roles + .first() + .and_then(|(callback_param_idx, _)| { + param_idx_to_call_arg_idx( + *callback_param_idx, + call_expr.is_colon_call(), + roles.is_colon_define, + ) + }), + )) + }) + } + + fn vgui_panel_call( + &self, + db: &DbIndex, + file_id: FileId, + call_expr: &LuaCallExpr, + call_path: &str, + ) -> Option<(GmodScriptedClassCallKind, GmodVguiPanelCallRoles)> { + self.roles_for_call(db, file_id, call_expr, call_path) + .and_then(|roles| roles.vgui_panel_call(call_expr.is_colon_call())) + } + + fn inheritance_call( + &self, + db: &DbIndex, + file_id: FileId, + call_expr: &LuaCallExpr, + call_path: &str, + ) -> Option<(GmodScriptedClassCallKind, GmodNamedStringCallRoles)> { + self.roles_for_call(db, file_id, call_expr, call_path) + .and_then(|roles| roles.inheritance_call(call_expr.is_colon_call())) + } + + fn network_var_call( + &self, + db: &DbIndex, + file_id: FileId, + call_expr: &LuaCallExpr, + call_path: &str, + ) -> Option<(GmodScriptedClassCallKind, GmodNetworkVarCallRoles)> { + self.roles_for_call(db, file_id, call_expr, call_path) + .and_then(|roles| roles.network_var_call(call_expr.is_colon_call())) + } + + fn derma_skin_call( + &self, + db: &DbIndex, + file_id: FileId, + call_expr: &LuaCallExpr, + call_path: &str, + ) -> Option { + self.roles_for_call(db, file_id, call_expr, call_path) + .and_then(|roles| roles.derma_skin_call_roles(call_expr.is_colon_call())) + } + + fn roles_for_call( + &self, + db: &DbIndex, + file_id: FileId, + call_expr: &LuaCallExpr, + call_path: &str, + ) -> Option { + if let Some(local_path_roles) = + annotated_roles_from_local_call_path(self, db, file_id, call_expr, call_path) + { + return local_path_roles.and_then(|roles| roles.select_for_call(call_expr)); + } + + if self.global_roles.contains(call_path) { + if let Some(local_roles) = annotated_roles_from_local_call_prefix( + self, + db, + file_id, + call_expr.get_prefix_expr(), + ) { + return local_roles.and_then(|roles| roles.select_for_call(call_expr)); + } + + if call_expr_has_shadowing_local_root(db, file_id, call_expr) { + return None; + } + + return self + .global_roles + .get(call_path) + .and_then(|roles| roles.select_for_call(call_expr)); + } + + if !self.local_candidate_names.contains(call_path) { + return None; + } + + annotated_roles_from_local_call_prefix(self, db, file_id, call_expr.get_prefix_expr())? + .and_then(|roles| roles.select_for_call(call_expr)) + } +} + +fn roles_from_signature( + db: &DbIndex, + signature_id: LuaSignatureId, +) -> Option { + let signature = db.get_signature_index().get(&signature_id)?; + if !signature.has_call_arg_roles() { + return None; + } + + let mut roles = AnnotatedGmodCallRoles::from_signature_shape(signature); + for role in signature.call_arg_roles() { + roles.add_call_arg_role(&role); + } + + for overload in &signature.overloads { + if overload.get_call_arg_roles().is_empty() { + continue; + } + let mut overload_roles = AnnotatedGmodCallRoles::from_function_shape(overload); + for role in overload.get_call_arg_roles() { + overload_roles.add_call_arg_role(role); + } + overload_roles.sort_roles(); + if overload_roles.has_any_roles() { + roles.overloads.push(overload_roles); + } + } + + roles.sort_roles(); + + (roles.has_any_roles() || !roles.overloads.is_empty()).then_some(roles) +} + +fn closure_from_signature_id(db: &DbIndex, signature_id: LuaSignatureId) -> Option { + let root = db + .get_vfs() + .get_syntax_tree(&signature_id.get_file_id())? + .get_red_root(); + root.descendants() + .filter_map(LuaClosureExpr::cast) + .find(|closure| closure.get_position() == signature_id.get_position()) +} + +fn global_call_path_for_signature_closure( + db: &DbIndex, + signature_id: LuaSignatureId, + closure: &LuaClosureExpr, +) -> Option { + let file_id = signature_id.get_file_id(); + if let Some(func_stat) = closure.get_parent::() { + let func_name = func_stat.get_func_name()?; + return var_expr_has_global_root(db, file_id, &func_name) + .then(|| func_name.get_access_path())?; + } + + let assign_stat = closure.get_parent::()?; + let (vars, value_exprs) = assign_stat.get_var_and_expr_list(); + let value_idx = value_exprs + .iter() + .position(|expr| expr.get_position() == closure.get_position())?; + let var_expr = vars.get(value_idx)?; + var_expr_has_global_root(db, file_id, var_expr).then(|| var_expr.get_access_path())? +} + +fn var_expr_has_global_root(db: &DbIndex, file_id: FileId, var_expr: &LuaVarExpr) -> bool { + match var_expr { + LuaVarExpr::NameExpr(name_expr) => !name_expr_resolves_to_local(db, file_id, name_expr), + LuaVarExpr::IndexExpr(index_expr) => index_expr_root_name(index_expr) + .as_ref() + .is_none_or(|name_expr| !name_expr_resolves_to_local(db, file_id, name_expr)), + } +} + +fn call_expr_has_shadowing_local_root( + db: &DbIndex, + file_id: FileId, + call_expr: &LuaCallExpr, +) -> bool { + match call_expr.get_prefix_expr() { + Some(LuaExpr::NameExpr(name_expr)) => { + name_expr_resolves_to_shadowing_local(db, file_id, &name_expr) + } + Some(LuaExpr::IndexExpr(index_expr)) => index_expr_root_name(&index_expr) + .as_ref() + .is_some_and(|name_expr| name_expr_resolves_to_shadowing_local(db, file_id, name_expr)), + _ => false, + } +} + +fn index_expr_root_name(index_expr: &glua_parser::LuaIndexExpr) -> Option { + match index_expr.get_prefix_expr()? { + LuaExpr::NameExpr(name_expr) => Some(name_expr), + LuaExpr::IndexExpr(prefix_index_expr) => index_expr_root_name(&prefix_index_expr), + _ => None, + } +} + +fn var_expr_root_name(var_expr: &LuaVarExpr) -> Option { + match var_expr { + LuaVarExpr::NameExpr(name_expr) => Some(name_expr.clone()), + LuaVarExpr::IndexExpr(index_expr) => index_expr_root_name(index_expr), + } +} + +fn name_expr_resolves_to_local(db: &DbIndex, file_id: FileId, name_expr: &LuaNameExpr) -> bool { + name_expr_local_decl_id(db, file_id, name_expr).is_some() +} + +fn name_expr_resolves_to_shadowing_local( + db: &DbIndex, + file_id: FileId, + name_expr: &LuaNameExpr, +) -> bool { + let Some(decl_id) = name_expr_local_decl_id(db, file_id, name_expr) else { + return false; + }; + let Some(name) = name_expr.get_name_text() else { + return true; + }; + !local_decl_aliases_global_name(db, decl_id, &name) +} + +fn name_expr_local_decl_id( + db: &DbIndex, + file_id: FileId, + name_expr: &LuaNameExpr, +) -> Option { + db.get_reference_index() + .get_var_reference_decl(&file_id, name_expr.get_range()) + .filter(|decl_id| { + db.get_decl_index() + .get_decl(decl_id) + .is_some_and(|decl| decl.is_local()) + }) +} + +fn local_decl_aliases_global_name(db: &DbIndex, decl_id: LuaDeclId, global_name: &str) -> bool { + let Some((ret_idx, initializer)) = local_decl_initializer_expr(db, decl_id) else { + return false; + }; + if ret_idx != 0 { + return false; + } + + match initializer { + LuaExpr::NameExpr(name_expr) => { + name_expr.get_name_text().as_deref() == Some(global_name) + && !name_expr_resolves_to_local(db, decl_id.file_id, &name_expr) + } + LuaExpr::IndexExpr(index_expr) => { + index_expr + .get_access_path() + .as_deref() + .and_then(|path| path.strip_prefix("_G.")) + == Some(global_name) + && index_expr_root_name(&index_expr) + .as_ref() + .is_none_or(|root| !name_expr_resolves_to_local(db, decl_id.file_id, root)) + } + _ => false, + } +} + +fn local_decl_initializer_expr(db: &DbIndex, decl_id: LuaDeclId) -> Option<(usize, LuaExpr)> { + let decl = db.get_decl_index().get_decl(&decl_id)?; + let initializer = decl.get_initializer()?; + let root = db + .get_vfs() + .get_syntax_tree(&decl_id.file_id)? + .get_red_root(); + let node = initializer.get_expr_syntax_id().to_node_from_root(&root)?; + Some((initializer.get_ret_idx(), LuaExpr::cast(node)?)) +} + +fn annotated_roles_from_local_call_prefix( + role_map: &AnnotatedGmodCallRoleMap, + db: &DbIndex, + file_id: FileId, + prefix_expr: Option, +) -> Option> { + let LuaExpr::NameExpr(name_expr) = prefix_expr? else { + return None; + }; + annotated_roles_from_local_name_expr(role_map, db, file_id, &name_expr) +} + +fn annotated_roles_from_local_call_path( + role_map: &AnnotatedGmodCallRoleMap, + db: &DbIndex, + file_id: FileId, + call_expr: &LuaCallExpr, + call_path: &str, +) -> Option> { + let LuaExpr::IndexExpr(index_expr) = call_expr.get_prefix_expr()? else { + return None; + }; + let root_name_expr = index_expr_root_name(&index_expr)?; + let decl_id = db + .get_reference_index() + .get_var_reference_decl(&file_id, root_name_expr.get_range())?; + let decl = db.get_decl_index().get_decl(&decl_id)?; + if !decl.is_local() { + return None; + } + if root_name_expr + .get_name_text() + .is_some_and(|root_name| local_decl_aliases_global_name(db, decl_id, &root_name)) + { + return None; + } + + Some( + role_map + .local_roles_by_path + .get(&(decl_id, call_path.to_string())) + .cloned(), + ) +} + +fn annotated_roles_from_local_name_expr( + role_map: &AnnotatedGmodCallRoleMap, + db: &DbIndex, + file_id: FileId, + name_expr: &LuaNameExpr, +) -> Option> { + let decl_id = db + .get_reference_index() + .get_var_reference_decl(&file_id, name_expr.get_range())?; + let decl = db.get_decl_index().get_decl(&decl_id)?; + if !decl.is_local() { + return None; + } + if name_expr + .get_name_text() + .is_some_and(|name| local_decl_aliases_global_name(db, decl_id, &name)) + { + return None; + } + if let Some(roles) = role_map.local_roles_by_decl.get(&decl_id) { + return Some(Some(roles.clone())); } + + let Some(signature_id) = signature_id_from_decl_value(db, decl_id) else { + return Some(None); + }; + Some(roles_from_signature(db, signature_id)) +} + +fn signature_id_from_decl_value(db: &DbIndex, decl_id: LuaDeclId) -> Option { + let decl = db.get_decl_index().get_decl(&decl_id)?; + let value_syntax_id = decl.get_value_syntax_id()?; + let root = db + .get_vfs() + .get_syntax_tree(&decl_id.file_id)? + .get_red_root(); + let value_node = value_syntax_id.to_node_from_root(&root)?; + let closure = LuaClosureExpr::cast(value_node)?; + Some(LuaSignatureId::from_closure(decl_id.file_id, &closure)) } -fn extract_callback_arg(call_expr: LuaCallExpr, arg_idx: usize) -> GmodCallbackSiteMetadata { - let Some(callback_expr) = call_expr +fn collect_system_call_metadata( + db: &mut DbIndex, + file_id: FileId, + annotated_roles: &AnnotatedGmodCallRoleMap, + call_expr: LuaCallExpr, +) -> Option<()> { + let call_path = call_expr.get_access_path()?; + let call_site = annotated_roles.system_call(db, file_id, &call_expr, &call_path)?; + let kind = call_site.kind; + + match kind { + GmodSystemCallKind::AddNetworkString => { + let name_arg_idx = call_site.name_arg_idx?; + let (name, name_range) = extract_static_string_arg(call_expr.clone(), name_arg_idx); + db.get_gmod_infer_index_mut().add_net_message_registration( + file_id, + GmodNamedSiteMetadata { + syntax_id: call_expr.get_syntax_id(), + name, + name_range, + }, + ); + } + GmodSystemCallKind::NetStart => { + let name_arg_idx = call_site.name_arg_idx?; + let (name, name_range) = extract_static_string_arg(call_expr.clone(), name_arg_idx); + db.get_gmod_infer_index_mut().add_net_start_site( + file_id, + GmodNamedSiteMetadata { + syntax_id: call_expr.get_syntax_id(), + name, + name_range, + }, + ); + } + GmodSystemCallKind::NetReceive => { + let name_arg_idx = call_site.name_arg_idx?; + let (message_name, name_range) = + extract_static_string_arg(call_expr.clone(), name_arg_idx); + let callback = call_site + .callback_arg_idx + .and_then(|arg_idx| extract_callback_arg(call_expr.clone(), arg_idx)) + .or_else(|| extract_callback_arg(call_expr.clone(), name_arg_idx + 1)) + .unwrap_or_else(|| { + extract_first_callback_arg_after(call_expr.clone(), name_arg_idx) + }); + db.get_gmod_infer_index_mut().add_net_receive_site( + file_id, + GmodNetReceiveSiteMetadata { + syntax_id: call_expr.get_syntax_id(), + message_name, + name_range, + callback, + }, + ); + } + GmodSystemCallKind::ConcommandAdd => { + let name_arg_idx = call_site.name_arg_idx?; + let (command_name, name_range) = + extract_static_string_arg(call_expr.clone(), name_arg_idx); + let callback = call_site + .callback_arg_idx + .and_then(|arg_idx| extract_callback_arg(call_expr.clone(), arg_idx)) + .or_else(|| extract_callback_arg(call_expr.clone(), name_arg_idx + 1)) + .unwrap_or_else(|| { + extract_first_callback_arg_after(call_expr.clone(), name_arg_idx) + }); + db.get_gmod_infer_index_mut().add_concommand_site( + file_id, + GmodConcommandSiteMetadata { + syntax_id: call_expr.get_syntax_id(), + command_name, + name_range, + callback, + }, + ); + } + GmodSystemCallKind::CreateConVar | GmodSystemCallKind::CreateClientConVar => { + let name_arg_idx = call_site.name_arg_idx?; + let (convar_name, name_range) = + extract_static_string_arg(call_expr.clone(), name_arg_idx); + db.get_gmod_infer_index_mut().add_convar_site( + file_id, + GmodConVarSiteMetadata { + syntax_id: call_expr.get_syntax_id(), + kind: if kind == GmodSystemCallKind::CreateClientConVar { + GmodConVarKind::Client + } else { + GmodConVarKind::Server + }, + convar_name, + name_range, + }, + ); + } + GmodSystemCallKind::TimerCreate => { + let name_arg_idx = call_site.name_arg_idx?; + let (timer_name, name_range) = + extract_static_string_arg(call_expr.clone(), name_arg_idx); + let callback = call_site + .callback_arg_idx + .and_then(|arg_idx| extract_callback_arg(call_expr.clone(), arg_idx)) + .or_else(|| extract_first_callback_arg_after_opt(call_expr.clone(), name_arg_idx)) + .unwrap_or_default(); + db.get_gmod_infer_index_mut().add_timer_site( + file_id, + GmodTimerSiteMetadata { + syntax_id: call_expr.get_syntax_id(), + kind: GmodTimerKind::Create, + timer_name, + name_range, + callback, + }, + ); + } + GmodSystemCallKind::TimerSimple => { + let callback = call_site + .callback_arg_idx + .and_then(|arg_idx| extract_callback_arg(call_expr.clone(), arg_idx)) + .unwrap_or_default(); + db.get_gmod_infer_index_mut().add_timer_site( + file_id, + GmodTimerSiteMetadata { + syntax_id: call_expr.get_syntax_id(), + kind: GmodTimerKind::Simple, + timer_name: None, + name_range: None, + callback, + }, + ); + } + } + + Some(()) +} + +fn collect_annotated_scripted_class_call_metadata( + db: &mut DbIndex, + file_id: FileId, + annotated_roles: &AnnotatedGmodCallRoleMap, + call_expr: LuaCallExpr, +) -> Option<()> { + let call_path = call_expr.get_access_path()?; + + if let Some((kind, inheritance_roles)) = + annotated_roles.inheritance_call(db, file_id, &call_expr, &call_path) + { + let (literal_args, args) = extract_gmod_class_call_args(&call_expr); + db.get_gmod_class_metadata_index_mut().add_call( + file_id, + kind, + GmodScriptedClassCallMetadata { + syntax_id: call_expr.get_syntax_id(), + literal_args, + args, + inheritance_roles: Some(inheritance_roles), + network_var_roles: None, + vgui_panel_roles: None, + derma_skin_roles: None, + }, + ); + return Some(()); + } + + if let Some((kind, network_var_roles)) = + annotated_roles.network_var_call(db, file_id, &call_expr, &call_path) + { + let (literal_args, args) = extract_gmod_class_call_args(&call_expr); + db.get_gmod_class_metadata_index_mut().add_call( + file_id, + kind, + GmodScriptedClassCallMetadata { + syntax_id: call_expr.get_syntax_id(), + literal_args, + args, + inheritance_roles: None, + network_var_roles: Some(network_var_roles), + vgui_panel_roles: None, + derma_skin_roles: None, + }, + ); + return Some(()); + } + + if let Some((kind, vgui_panel_roles)) = + annotated_roles.vgui_panel_call(db, file_id, &call_expr, &call_path) + { + let (literal_args, args) = extract_gmod_class_call_args(&call_expr); + db.get_gmod_class_metadata_index_mut().add_call( + file_id, + kind, + GmodScriptedClassCallMetadata { + syntax_id: call_expr.get_syntax_id(), + literal_args, + args, + inheritance_roles: None, + network_var_roles: None, + vgui_panel_roles: Some(vgui_panel_roles), + derma_skin_roles: None, + }, + ); + return Some(()); + } + + if let Some(derma_skin_roles) = + annotated_roles.derma_skin_call(db, file_id, &call_expr, &call_path) + { + let (literal_args, args) = extract_gmod_class_call_args(&call_expr); + db.get_gmod_class_metadata_index_mut().add_call( + file_id, + GmodScriptedClassCallKind::DermaDefineSkin, + GmodScriptedClassCallMetadata { + syntax_id: call_expr.get_syntax_id(), + literal_args, + args, + inheritance_roles: None, + network_var_roles: None, + vgui_panel_roles: None, + derma_skin_roles: Some(derma_skin_roles), + }, + ); + return Some(()); + } + + None +} + +fn matches_configured_call_path(path: &str, target: &str) -> bool { + path == target + || path + .strip_suffix(target) + .is_some_and(|prefix| prefix.ends_with('.') || prefix.ends_with(':')) +} + +fn extract_static_string_arg( + call_expr: LuaCallExpr, + arg_idx: usize, +) -> (Option, Option) { + let Some(arg_expr) = call_expr .get_args_list() .and_then(|args| args.get_args().nth(arg_idx)) else { - return GmodCallbackSiteMetadata::default(); + return (None, None); + }; + + let LuaExpr::LiteralExpr(literal_expr) = arg_expr else { + return (None, None); + }; + + match literal_expr.get_literal() { + Some(LuaLiteralToken::String(string_token)) => ( + Some(string_token.get_value()), + Some(string_token.get_range()), + ), + Some(_) => (None, Some(literal_expr.get_range())), + None => (None, Some(literal_expr.get_range())), + } +} + +fn extract_gmod_class_call_args( + call_expr: &LuaCallExpr, +) -> ( + Vec>, + Vec, +) { + let Some(args_list) = call_expr.get_args_list() else { + return (Vec::new(), Vec::new()); }; - GmodCallbackSiteMetadata { + let mut literal_args = Vec::new(); + let mut args = Vec::new(); + for arg_expr in args_list.get_args() { + let syntax_id = arg_expr.get_syntax_id(); + let value = extract_gmod_class_literal_or_name(&arg_expr); + literal_args.push(value.clone()); + args.push(crate::GmodClassCallArg { syntax_id, value }); + } + + (literal_args, args) +} + +fn extract_gmod_class_literal_or_name(expr: &LuaExpr) -> Option { + match expr { + LuaExpr::LiteralExpr(literal_expr) => match literal_expr.get_literal()? { + LuaLiteralToken::String(string_token) => Some(GmodClassCallLiteral::String( + string_token.get_value().to_string(), + )), + LuaLiteralToken::Number(number_token) => match number_token.get_number_value() { + NumberResult::Int(value) => Some(GmodClassCallLiteral::Integer(value)), + NumberResult::Uint(value) => Some(GmodClassCallLiteral::Unsigned(value)), + NumberResult::Float(value) => Some(GmodClassCallLiteral::Float(value)), + }, + LuaLiteralToken::Bool(boolean_token) => { + Some(GmodClassCallLiteral::Boolean(boolean_token.is_true())) + } + LuaLiteralToken::Nil(_) => Some(GmodClassCallLiteral::Nil), + _ => None, + }, + LuaExpr::NameExpr(name_expr) => name_expr + .get_name_text() + .map(|name| GmodClassCallLiteral::NameRef(name.to_string())), + LuaExpr::ParenExpr(paren_expr) => { + let inner = paren_expr.get_expr()?; + extract_gmod_class_literal_or_name(&inner) + } + _ => None, + } +} + +fn extract_callback_arg( + call_expr: LuaCallExpr, + arg_idx: usize, +) -> Option { + let callback_expr = call_expr + .get_args_list() + .and_then(|args| args.get_args().nth(arg_idx))?; + + Some(GmodCallbackSiteMetadata { syntax_id: Some(callback_expr.get_syntax_id()), callback_range: Some(callback_expr.get_range()), - } + }) +} + +fn extract_first_callback_arg_after( + call_expr: LuaCallExpr, + arg_idx: usize, +) -> GmodCallbackSiteMetadata { + extract_first_callback_arg_after_opt(call_expr, arg_idx).unwrap_or_default() +} + +fn extract_first_callback_arg_after_opt( + call_expr: LuaCallExpr, + arg_idx: usize, +) -> Option { + let args_list = call_expr.get_args_list()?; + + args_list + .get_args() + .skip(arg_idx + 1) + .find(|arg_expr| matches!(arg_expr, LuaExpr::ClosureExpr(_))) + .map(|callback_expr| GmodCallbackSiteMetadata { + syntax_id: Some(callback_expr.get_syntax_id()), + callback_range: Some(callback_expr.get_range()), + }) } -fn collect_hook_call_site(db: &DbIndex, call_expr: LuaCallExpr) -> Option { +fn collect_hook_call_site( + db: &DbIndex, + file_id: FileId, + annotated_roles: &AnnotatedGmodCallRoleMap, + call_expr: LuaCallExpr, +) -> Option { let call_path = call_expr.get_access_path()?; - let mapped_hook = mapped_hook_for_emitter_call(db, &call_path, call_expr.clone()); - let kind = mapped_hook - .as_ref() - .map(|_| GmodHookKind::Emit) - .or_else(|| classify_hook_call_path(&call_path))?; - let (hook_name, name_range, name_issue) = mapped_hook.unwrap_or_else(|| { + let has_shadowing_local_root = call_expr_has_shadowing_local_root(db, file_id, &call_expr); + let annotated_hook = annotated_roles.hook_call(db, file_id, &call_expr, &call_path); + let mapped_hook = if has_shadowing_local_root { + None + } else { + mapped_hook_for_emitter_call(db, &call_path, call_expr.clone()) + }; + let (kind, name_arg_idx, callback_arg_idx, mapped_hook_data) = + if let Some((kind, name_arg_idx, callback_arg_idx)) = annotated_hook { + (kind, name_arg_idx, callback_arg_idx, None) + } else if let Some(mapped_hook) = mapped_hook { + (GmodHookKind::Emit, 0, None, Some(mapped_hook)) + } else { + return None; + }; + let (hook_name, name_range, name_issue) = mapped_hook_data.unwrap_or_else(|| { extract_static_hook_name( call_expr .get_args_list() - .and_then(|args| args.get_args().next()), + .and_then(|args| args.get_args().nth(name_arg_idx)), ) }); + let callback_arg_idx = if kind == GmodHookKind::Add { + callback_arg_idx.or_else(|| find_first_callback_arg_idx_after(&call_expr, name_arg_idx)) + } else { + callback_arg_idx + }; + Some(GmodHookSiteMetadata { syntax_id: call_expr.get_syntax_id(), kind, hook_name, name_range, name_issue, + callback_arg_idx, callback_params: if kind == GmodHookKind::Add { - extract_hook_callback_params_from_call(&call_expr) + extract_hook_callback_params_from_call(&call_expr, name_arg_idx, callback_arg_idx) } else { Vec::new() }, }) } -fn classify_hook_call_path(path: &str) -> Option { - if matches_call_path(path, "hook.Add") { - return Some(GmodHookKind::Add); - } - - if matches_call_path(path, "hook.Run") || matches_call_path(path, "hook.Call") { - return Some(GmodHookKind::Emit); - } - - None -} - fn mapped_hook_for_emitter_call( db: &DbIndex, call_path: &str, @@ -4158,7 +5660,7 @@ fn mapped_hook_for_emitter_call( Option, )> { for (emitter_path, mapped_hook) in &db.get_emmyrc().gmod.hook_mappings.emitter_to_hook { - if !matches_call_path(call_path, emitter_path) { + if !matches_configured_call_path(call_path, emitter_path) { continue; } @@ -4257,18 +5759,31 @@ fn collect_hook_method_site(db: &DbIndex, func_stat: LuaFuncStat) -> Option Vec { - let Some(callback_expr) = call_expr - .get_args_list() - .and_then(|args| args.get_args().nth(2)) - else { +fn extract_hook_callback_params_from_call( + call_expr: &LuaCallExpr, + name_arg_idx: usize, + callback_arg_idx: Option, +) -> Vec { + let Some(args_list) = call_expr.get_args_list() else { return Vec::new(); }; + let callback_expr = if let Some(callback_arg_idx) = callback_arg_idx { + args_list.get_args().nth(callback_arg_idx) + } else { + args_list + .get_args() + .skip(name_arg_idx + 1) + .find(|arg_expr| matches!(arg_expr, LuaExpr::ClosureExpr(_))) + }; + let Some(callback_expr) = callback_expr else { + return Vec::new(); + }; let LuaExpr::ClosureExpr(closure_expr) = callback_expr else { return Vec::new(); }; @@ -4276,6 +5791,16 @@ fn extract_hook_callback_params_from_call(call_expr: &LuaCallExpr) -> Vec Option { + call_expr + .get_args_list()? + .get_args() + .enumerate() + .skip(arg_idx + 1) + .find(|(_, arg_expr)| matches!(arg_expr, LuaExpr::ClosureExpr(_))) + .map(|(idx, _)| idx) +} + fn extract_hook_callback_params_from_method(func_stat: &LuaFuncStat) -> Vec { let Some(closure_expr) = func_stat.get_closure() else { return Vec::new(); diff --git a/crates/glua_code_analysis/src/compilation/analyzer/lua/call.rs b/crates/glua_code_analysis/src/compilation/analyzer/lua/call.rs index cdb00f4c3..c72b7ed3b 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/lua/call.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/lua/call.rs @@ -209,7 +209,6 @@ fn is_local_name_expr(db: &DbIndex, file_id: FileId, name_expr: &LuaNameExpr) -> pub(super) fn analyze_call(analyzer: &mut LuaAnalyzer, call_expr: LuaCallExpr) -> Option<()> { collect_gmod_scripted_class_call(analyzer, &call_expr); - collect_gmod_vgui_call(analyzer, &call_expr); collect_accessorfunc_annotated_call(analyzer, &call_expr); let special_call_reason = get_special_call_followup_reason(analyzer, &call_expr); @@ -1357,63 +1356,10 @@ fn collect_gmod_scripted_class_call(analyzer: &mut LuaAnalyzer, call_expr: &LuaC syntax_id: call_expr.get_syntax_id(), literal_args, args, - }, - ); -} - -fn collect_gmod_vgui_call(analyzer: &mut LuaAnalyzer, call_expr: &LuaCallExpr) { - if !analyzer.gmod_enabled { - return; - } - - // Fast path: vgui.Register / derma.DefineControl are always dotted accesses. - // Skip the expensive get_access_path() for the 99.9% of calls that can't match. - let Some(LuaExpr::IndexExpr(index_expr)) = call_expr.get_prefix_expr() else { - return; - }; - - let Some(LuaIndexKey::Name(key_token)) = index_expr.get_index_key() else { - return; - }; - let key_name = key_token.get_name_text(); - if key_name != "Register" && key_name != "DefineControl" { - return; - } - - // Check the base object: handle both direct (vgui.Register) and nested paths - let kind = match index_expr.get_prefix_expr() { - Some(LuaExpr::NameExpr(base)) => { - let base_name = base.get_name_token().map(|t| t.get_name_text().to_string()); - match base_name.as_deref() { - Some("vgui") if key_name == "Register" => GmodScriptedClassCallKind::VguiRegister, - Some("derma") if key_name == "DefineControl" => { - GmodScriptedClassCallKind::DermaDefineControl - } - _ => return, - } - } - Some(_) => { - // Nested path like something.vgui.Register - fall back to full path - let Some(call_path) = call_expr.get_access_path() else { - return; - }; - let Some(kind) = GmodScriptedClassCallKind::from_call_path(&call_path) else { - return; - }; - kind - } - None => return, - }; - - let (literal_args, args) = extract_call_args(call_expr); - - analyzer.db.get_gmod_class_metadata_index_mut().add_call( - analyzer.file_id, - kind, - GmodScriptedClassCallMetadata { - syntax_id: call_expr.get_syntax_id(), - literal_args, - args, + inheritance_roles: None, + network_var_roles: None, + vgui_panel_roles: None, + derma_skin_roles: None, }, ); } diff --git a/crates/glua_code_analysis/src/compilation/analyzer/lua/closure.rs b/crates/glua_code_analysis/src/compilation/analyzer/lua/closure.rs index cc06350ce..ac6c4c6b7 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/lua/closure.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/lua/closure.rs @@ -1,14 +1,15 @@ use std::ops::Deref; use glua_parser::{ - LuaAst, LuaAstNode, LuaCallArgList, LuaCallExpr, LuaClosureExpr, LuaComment, LuaDocTagReturn, - LuaFuncStat, LuaStat, LuaSyntaxKind, LuaVarExpr, + LuaAst, LuaAstNode, LuaBlock, LuaCallArgList, LuaCallExpr, LuaClosureExpr, LuaComment, + LuaDocTagReturn, LuaExpr, LuaFuncStat, LuaIfStat, LuaLiteralToken, LuaLocalStat, LuaReturnStat, + LuaStat, LuaSyntaxKind, LuaVarExpr, }; -use rowan::TextRange; +use rowan::{TextRange, TextSize}; use crate::{ - DbIndex, InferFailReason, LuaInferCache, LuaType, ReturnTypeKind, SignatureReturnStatus, - TypeOps, VariadicType, + DbIndex, InferFailReason, LuaDeclId, LuaInferCache, LuaType, ReturnTypeKind, + SignatureReturnStatus, TypeOps, VariadicType, compilation::analyzer::unresolve::{ UnResolveCallClosureParams, UnResolveClosureReturn, UnResolveParentAst, UnResolveParentClosureParams, UnResolveReturn, @@ -24,6 +25,7 @@ pub fn analyze_closure(analyzer: &mut LuaAnalyzer, closure: LuaClosureExpr) -> O analyze_colon_define(analyzer, &signature_id, &closure); analyze_lambda_params(analyzer, &signature_id, &closure); + analyze_require_guard_param(analyzer, &signature_id, &closure); analyze_return(analyzer, &signature_id, &closure); Some(()) } @@ -112,6 +114,335 @@ fn analyze_lambda_params( Some(()) } +fn analyze_require_guard_param( + analyzer: &mut LuaAnalyzer, + signature_id: &LuaSignatureId, + closure: &LuaClosureExpr, +) -> Option<()> { + let params = closure + .get_params_list()? + .get_params() + .filter_map(|param| { + if param.is_dots() { + Some("...".to_string()) + } else { + param + .get_name_token() + .map(|name| name.get_name_text().to_string()) + } + }) + .collect::>(); + + let block = closure.get_block()?; + let mut candidates = vec![]; + collect_require_guard_candidates(&block, ¶ms, &mut candidates); + + let param_idx = candidates + .into_iter() + .find(|candidate| { + !is_require_guard_local_mutable(analyzer, candidate.decl_pos) + && is_require_guard_return_shape(&block, &candidate.guard_name) + }) + .map(|candidate| candidate.param_idx); + + if let Some(param_idx) = param_idx { + let signature = analyzer + .db + .get_signature_index_mut() + .get_or_create(*signature_id); + signature.set_require_guard_param(param_idx); + } + + Some(()) +} + +#[derive(Debug, Clone)] +struct RequireGuardCandidate { + guard_name: String, + param_idx: usize, + decl_pos: TextSize, +} + +fn collect_require_guard_candidates( + block: &LuaBlock, + params: &[String], + candidates: &mut Vec, +) { + for stat in block.get_stats() { + match stat { + LuaStat::LocalStat(local) => { + if let Some(candidate) = get_require_guard_from_local_stat(&local, params) { + candidates.push(candidate); + } + } + LuaStat::IfStat(if_stat) => { + collect_require_guard_candidates_from_if(if_stat, params, candidates); + } + LuaStat::DoStat(do_stat) => { + if let Some(block) = do_stat.get_block() { + collect_require_guard_candidates(&block, params, candidates); + } + } + LuaStat::WhileStat(while_stat) => { + if let Some(block) = while_stat.get_block() { + collect_require_guard_candidates(&block, params, candidates); + } + } + LuaStat::RepeatStat(repeat_stat) => { + if let Some(block) = repeat_stat.get_block() { + collect_require_guard_candidates(&block, params, candidates); + } + } + LuaStat::ForStat(for_stat) => { + if let Some(block) = for_stat.get_block() { + collect_require_guard_candidates(&block, params, candidates); + } + } + LuaStat::ForRangeStat(for_range_stat) => { + if let Some(block) = for_range_stat.get_block() { + collect_require_guard_candidates(&block, params, candidates); + } + } + _ => {} + } + } +} + +fn collect_require_guard_candidates_from_if( + if_stat: LuaIfStat, + params: &[String], + candidates: &mut Vec, +) { + if let Some(block) = if_stat.get_block() { + collect_require_guard_candidates(&block, params, candidates); + } + for else_if in if_stat.get_else_if_clause_list() { + if let Some(block) = else_if.get_block() { + collect_require_guard_candidates(&block, params, candidates); + } + } + if let Some(else_clause) = if_stat.get_else_clause() { + if let Some(block) = else_clause.get_block() { + collect_require_guard_candidates(&block, params, candidates); + } + } +} + +fn get_require_guard_from_local_stat( + local: &LuaLocalStat, + params: &[String], +) -> Option { + let mut local_names = local.get_local_name_list(); + let local_name = local_names.next()?; + + let guard_name = local_name.get_name_token()?.get_name_text().to_string(); + + let mut value_exprs = local.get_value_exprs(); + let first = value_exprs.next()?; + let LuaExpr::CallExpr(call_expr) = first else { + return None; + }; + + let required_param = match_require_call(&call_expr, "pcall", "require")?; + let param_idx = params.iter().position(|param| param == &required_param)?; + + Some(RequireGuardCandidate { + guard_name, + param_idx, + decl_pos: local_name.get_position(), + }) +} + +fn match_require_call(call_expr: &LuaCallExpr, callee: &str, require_fn: &str) -> Option { + let prefix_expr = call_expr.get_prefix_expr()?; + let prefix_name = expr_name_text(&prefix_expr)?; + if prefix_name != callee { + return None; + } + + let args = call_expr.get_args_list()?; + let args = args.get_args().collect::>(); + if args.len() < 2 { + return None; + } + + let first_arg = expr_name_text(&args[0])?; + if first_arg != require_fn { + return None; + } + + expr_name_text(&args[1]) +} + +fn expr_name_text(expr: &LuaExpr) -> Option { + match expr { + LuaExpr::NameExpr(name_expr) => name_expr.get_name_text().map(|name| name.to_string()), + LuaExpr::ParenExpr(paren_expr) => { + paren_expr.get_expr().and_then(|expr| expr_name_text(&expr)) + } + _ => None, + } +} + +fn is_require_guard_local_mutable(analyzer: &LuaAnalyzer, local_decl_pos: TextSize) -> bool { + analyzer + .db + .get_reference_index() + .get_decl_references( + &analyzer.file_id, + &LuaDeclId::new(analyzer.file_id, local_decl_pos), + ) + .is_some_and(|decl_ref| decl_ref.mutable) +} + +fn is_require_guard_return_shape(block: &LuaBlock, guard_name: &str) -> bool { + is_block_return_shape_safe(block, guard_name, false) +} + +fn is_block_return_shape_safe(block: &LuaBlock, guard_name: &str, in_guard: bool) -> bool { + for stat in block.get_stats() { + match stat { + LuaStat::ReturnStat(return_stat) => { + if !is_return_exprs_safe(&return_stat, in_guard, guard_name) { + return false; + } + return true; + } + LuaStat::IfStat(if_stat) => { + if !is_if_return_shape_safe(if_stat, guard_name, in_guard) { + return false; + } + } + LuaStat::DoStat(do_stat) => { + if let Some(block) = do_stat.get_block() { + if !is_block_return_shape_safe(&block, guard_name, in_guard) { + return false; + } + } + } + LuaStat::WhileStat(while_stat) => { + if let Some(block) = while_stat.get_block() { + if !is_block_return_shape_safe(&block, guard_name, in_guard) { + return false; + } + } + } + LuaStat::RepeatStat(repeat_stat) => { + if let Some(block) = repeat_stat.get_block() { + if !is_block_return_shape_safe(&block, guard_name, in_guard) { + return false; + } + } + } + LuaStat::ForStat(for_stat) => { + if let Some(block) = for_stat.get_block() { + if !is_block_return_shape_safe(&block, guard_name, in_guard) { + return false; + } + } + } + LuaStat::ForRangeStat(for_range_stat) => { + if let Some(block) = for_range_stat.get_block() { + if !is_block_return_shape_safe(&block, guard_name, in_guard) { + return false; + } + } + } + _ => {} + } + } + true +} + +fn is_if_return_shape_safe(if_stat: LuaIfStat, guard_name: &str, in_guard: bool) -> bool { + let then_guard = if_stat + .get_condition_expr() + .is_some_and(|expr| is_expression_var(&expr, guard_name)); + + if let Some(block) = if_stat.get_block() { + if !is_block_return_shape_safe(&block, guard_name, in_guard || then_guard) { + return false; + } + } + + for else_if in if_stat.get_else_if_clause_list() { + let else_if_guard = else_if + .get_condition_expr() + .is_some_and(|expr| is_expression_var(&expr, guard_name)); + + if let Some(block) = else_if.get_block() { + if !is_block_return_shape_safe(&block, guard_name, in_guard || else_if_guard) { + return false; + } + } + } + + if let Some(else_clause) = if_stat.get_else_clause() { + if let Some(block) = else_clause.get_block() { + if !is_block_return_shape_safe(&block, guard_name, in_guard) { + return false; + } + } + } + + true +} + +fn is_return_exprs_safe(return_stat: &LuaReturnStat, in_guard: bool, guard_name: &str) -> bool { + let exprs = return_stat.get_expr_list().collect::>(); + match exprs.len() { + 0 => true, + 1 => is_single_return_expr_safe(&exprs[0], in_guard, guard_name), + _ => exprs.into_iter().all(|expr| is_false_or_nil_expr(&expr)), + } +} + +fn is_single_return_expr_safe(expr: &LuaExpr, in_guard: bool, guard_name: &str) -> bool { + if is_false_or_nil_expr(expr) { + return true; + } + + if is_expression_var(expr, guard_name) { + return true; + } + + if is_true_expr(expr) { + return in_guard; + } + + false +} + +fn is_false_or_nil_expr(expr: &LuaExpr) -> bool { + match expr { + LuaExpr::LiteralExpr(literal_expr) => match literal_expr.get_literal() { + Some(LuaLiteralToken::Nil(_)) => true, + Some(LuaLiteralToken::Bool(bool_token)) => !bool_token.is_true(), + _ => false, + }, + LuaExpr::ParenExpr(paren_expr) => paren_expr + .get_expr() + .is_some_and(|expr| is_false_or_nil_expr(&expr)), + _ => false, + } +} + +fn is_true_expr(expr: &LuaExpr) -> bool { + match expr { + LuaExpr::LiteralExpr(literal_expr) => { + matches!(literal_expr.get_literal(), Some(LuaLiteralToken::Bool(token)) if token.is_true()) + } + LuaExpr::ParenExpr(paren_expr) => paren_expr + .get_expr() + .is_some_and(|expr| is_true_expr(&expr)), + _ => false, + } +} + +fn is_expression_var(expr: &LuaExpr, name: &str) -> bool { + expr_name_text(expr).is_some_and(|var| var == name) +} + fn analyze_return( analyzer: &mut LuaAnalyzer, signature_id: &LuaSignatureId, @@ -274,7 +605,7 @@ pub fn analyze_return_point( } Ok(vec![LuaDocReturnInfo { - type_ref: return_type.unwrap_or(LuaType::Unknown), + type_ref: return_type.unwrap_or(LuaType::Never), default_value: None, description: None, name: None, diff --git a/crates/glua_code_analysis/src/compilation/analyzer/lua/stats.rs b/crates/glua_code_analysis/src/compilation/analyzer/lua/stats.rs index 6018eccf9..c63963b5f 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/lua/stats.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/lua/stats.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use crate::{ CacheEntry, FileId, GmodRealm, InFiled, InferFailReason, LuaArrayType, LuaMemberKey, - LuaSemanticDeclId, LuaSignatureId, LuaTypeCache, LuaTypeOwner, TypeOps, + LuaSemanticDeclId, LuaSignatureId, LuaTypeCache, LuaTypeOwner, LuaUnionType, TypeOps, compilation::{ analyzer::{ common::{add_member, bind_type}, @@ -112,7 +112,9 @@ pub fn analyze_local_stat(analyzer: &mut LuaAnalyzer, local_stat: LuaLocalStat) .add_unresolve(unresolve.into(), InferFailReason::FieldNotFound); continue; } - if should_defer_nil_gmod_index_alias(analyzer, &expr, &expr_type) { + if should_defer_nil_gmod_index_alias(analyzer, &expr, &expr_type) + || should_defer_weak_gmod_dynamic_index_alias(analyzer, &expr, &expr_type) + { analyzer.context.request_stabilization(analyzer.file_id); clear_index_expr_type_cache(analyzer, &expr); let unresolve = UnResolveDecl { @@ -324,6 +326,28 @@ fn should_defer_nil_gmod_index_alias( && matches!(expr, LuaExpr::IndexExpr(_)) } +fn should_defer_weak_gmod_dynamic_index_alias( + analyzer: &LuaAnalyzer, + expr: &LuaExpr, + expr_type: &LuaType, +) -> bool { + analyzer.gmod_enabled + && analyzer.db.get_emmyrc().gmod.infer_dynamic_fields + && matches!(expr, LuaExpr::IndexExpr(index_expr) if matches!(index_expr.get_index_key(), Some(LuaIndexKey::Expr(_)))) + && is_weak_dynamic_index_alias_type(expr_type) +} + +fn is_weak_dynamic_index_alias_type(expr_type: &LuaType) -> bool { + match expr_type { + LuaType::Any | LuaType::Unknown => true, + LuaType::Union(union) => match union.as_ref() { + LuaUnionType::Nullable(inner) => inner.is_any() || inner.is_unknown(), + LuaUnionType::Multi(_) => false, + }, + _ => false, + } +} + fn should_defer_gmod_self_index(analyzer: &LuaAnalyzer, expr: &LuaExpr) -> bool { if !analyzer.gmod_enabled || !analyzer.db.get_emmyrc().gmod.infer_dynamic_fields { return false; @@ -378,7 +402,7 @@ fn clear_index_expr_type_cache(analyzer: &mut LuaAnalyzer, expr: &LuaExpr) { let mut current_expr = expr.clone(); while let LuaExpr::IndexExpr(index_expr) = current_expr { let syntax_id = index_expr.get_syntax_id(); - if matches!(cache.expr_cache.get(&syntax_id), Some(CacheEntry::Cache(typ)) if typ.is_nil()) + if matches!(cache.expr_cache.get(&syntax_id), Some(CacheEntry::Cache(typ)) if typ.is_nil() || is_weak_dynamic_index_alias_type(typ)) { cache.expr_cache.remove(&syntax_id); cache.expr_var_ref_id_cache.remove(&syntax_id); @@ -768,7 +792,15 @@ pub fn analyze_assign_stat(analyzer: &mut LuaAnalyzer, assign_stat: LuaAssignSta record_assign_elapsed(analyzer, step_start, AssignProfileStep::GetOwner); let step_start = profile_enabled.then(std::time::Instant::now); - if special_assign_pattern(analyzer, type_owner.clone(), var.clone(), expr.clone()).is_some() + let assign_stat_range = assign_stat.get_range(); + if special_assign_pattern( + analyzer, + type_owner.clone(), + var.clone(), + expr.clone(), + assign_stat_range, + ) + .is_some() { record_assign_elapsed(analyzer, step_start, AssignProfileStep::Special); if profile_enabled { @@ -2194,11 +2226,23 @@ pub fn analyze_table_field(analyzer: &mut LuaAnalyzer, field: LuaTableField) -> Some(()) } +/// Extract a string literal value from an expression, if it is a literal string. +fn extract_string_literal_from_expr(expr: &LuaExpr) -> Option { + match expr { + LuaExpr::LiteralExpr(literal_expr) => match literal_expr.get_literal()? { + LuaLiteralToken::String(string_token) => Some(string_token.get_value().to_string()), + _ => None, + }, + _ => None, + } +} + fn special_assign_pattern( analyzer: &mut LuaAnalyzer, type_owner: LuaTypeOwner, var: LuaVarExpr, expr: LuaExpr, + assign_stat_range: rowan::TextRange, ) -> Option<()> { let access_path = var.get_access_path()?; let binary_expr = if let LuaExpr::BinaryExpr(binary_expr) = expr { @@ -2236,6 +2280,28 @@ fn special_assign_pattern( if guarded_table_expr { set_index_expr_owner(analyzer, var); } + + // Register inferred string default for `x = x or "literal"`. + // This is a SIBLING branch to the table-guard path: only fires + // when the RHS is NOT a TableExpr and IS a string literal, + // and the type_owner is a plain Decl. Completely disjoint from + // the table-guard path. + if !guarded_table_expr { + if let LuaTypeOwner::Decl(decl_id) = &type_owner { + if let Some(string_value) = extract_string_literal_from_expr(&right) { + analyzer + .db + .get_property_index_mut() + .add_inferred_string_default( + analyzer.file_id, + *decl_id, + smol_str::SmolStr::new(string_value), + assign_stat_range, + ); + } + } + } + assign_merge_type_owner_and_expr_type( analyzer, type_owner, @@ -2267,6 +2333,9 @@ fn infer_guarded_table_assignment_type( if left_type.is_nil() || left_type.is_unknown() || left_type.is_never() { return Ok(right_type); } + if should_prefer_guarded_dynamic_index_rhs(analyzer, left, &left_type) { + return Ok(right_type); + } if !(left_type.is_any() || left_type.is_table()) { return analyzer.infer_expr(binary_expr); } @@ -2274,6 +2343,17 @@ fn infer_guarded_table_assignment_type( Ok(TypeOps::Union.apply(analyzer.db, &left_type, &right_type)) } +fn should_prefer_guarded_dynamic_index_rhs( + analyzer: &LuaAnalyzer, + left: &LuaExpr, + left_type: &LuaType, +) -> bool { + analyzer.gmod_enabled + && analyzer.db.get_emmyrc().gmod.infer_dynamic_fields + && left_type.is_any() + && matches!(left, LuaExpr::IndexExpr(index_expr) if matches!(index_expr.get_index_key(), Some(LuaIndexKey::Expr(_)))) +} + fn has_delayed_definition_attribute(analyzer: &LuaAnalyzer, decl_id: LuaDeclId) -> bool { if let Some(property) = analyzer .db diff --git a/crates/glua_code_analysis/src/compilation/analyzer/mod.rs b/crates/glua_code_analysis/src/compilation/analyzer/mod.rs index ba5fe07a9..dd9d1714a 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/mod.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/mod.rs @@ -50,15 +50,16 @@ pub fn analyze( run_analysis::(db, &mut context); run_analysis::(db, &mut context); - run_analysis::(db, &mut context); // Gmod pre-analysis: collect realm metadata, scripted class types, hooks, - // and network flow BEFORE lua_analyze. This ensures flow analysis uses + // and network flow before flow/lua analysis. This ensures flow analysis uses // correct realm keys (Client/Server/Shared) from the start, avoiding the // previous problem where all flow caches used realm=Unknown and had to be // fully recomputed in the unresolve phase. run_analysis::(db, &mut context); + run_analysis::(db, &mut context); + run_analysis::(db, &mut context); // Gmod post-analysis: synthesize members that depend on metadata collected diff --git a/crates/glua_code_analysis/src/compilation/analyzer/unresolve/mod.rs b/crates/glua_code_analysis/src/compilation/analyzer/unresolve/mod.rs index 482aa9036..a230b3cb8 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/unresolve/mod.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/unresolve/mod.rs @@ -29,7 +29,9 @@ use resolve_closure::{ pub(crate) use resolve::get_wrapped_callable_target_expr; pub use resolve_closure::extract_hook_name; -pub use resolve_closure::resolve_gmod_hook_add_callback_doc_function; +pub use resolve_closure::{ + resolve_gmod_hook_add_callback_doc_function, resolve_gmod_hook_callback_doc_function, +}; use rowan::TextRange; use super::{AnalyzeContext, infer_cache_manager::InferCacheManager, lua::LuaReturnPoint}; diff --git a/crates/glua_code_analysis/src/compilation/analyzer/unresolve/resolve_closure.rs b/crates/glua_code_analysis/src/compilation/analyzer/unresolve/resolve_closure.rs index 162810d5c..7032b1d77 100644 --- a/crates/glua_code_analysis/src/compilation/analyzer/unresolve/resolve_closure.rs +++ b/crates/glua_code_analysis/src/compilation/analyzer/unresolve/resolve_closure.rs @@ -1,15 +1,15 @@ use std::sync::Arc; use glua_parser::{ - LuaAstNode, LuaCallExpr, LuaExpr, LuaIndexMemberExpr, LuaLiteralToken, LuaTableExpr, - LuaVarExpr, PathTrait, + LuaAstNode, LuaCallExpr, LuaExpr, LuaIndexMemberExpr, LuaLiteralToken, LuaTableExpr, LuaVarExpr, }; use crate::{ - DbIndex, InferFailReason, InferGuard, InferGuardRef, LuaDocParamInfo, LuaDocReturnInfo, - LuaFunctionType, LuaInferCache, LuaMemberIndexItem, LuaMemberKey, LuaMemberOwner, LuaSignature, - LuaSignatureId, LuaType, LuaTypeDeclId, RenderLevel, ReturnTypeKind, SignatureReturnStatus, - TypeOps, get_real_type, humanize_type, infer_call_expr_func, infer_expr, infer_table_should_be, + DbIndex, GmodHookKind, InferFailReason, InferGuard, InferGuardRef, LuaDocParamInfo, + LuaDocReturnInfo, LuaFunctionType, LuaInferCache, LuaMemberIndexItem, LuaMemberKey, + LuaMemberOwner, LuaSignature, LuaSignatureId, LuaType, LuaTypeDeclId, RenderLevel, + ReturnTypeKind, SignatureReturnStatus, TypeOps, get_real_type, humanize_type, + infer_call_expr_func, infer_expr, infer_table_should_be, }; use super::{ @@ -237,17 +237,44 @@ pub fn resolve_gmod_hook_add_callback_doc_function( origin_signature_id: Option, call_file_id: crate::FileId, ) -> Option> { - if !db.get_emmyrc().gmod.enabled || param_idx != 2 { - return None; - } + resolve_gmod_hook_callback_doc_function( + db, + call_expr, + param_idx, + origin_signature_id, + call_file_id, + ) + .map(|resolved| resolved.function) +} + +pub struct GmodHookCallbackDocFunction { + pub hook_name: String, + pub function: Arc, +} - let call_path = call_expr.get_access_path()?; - if !matches_call_path(&call_path, "hook.Add") { +pub fn resolve_gmod_hook_callback_doc_function( + db: &DbIndex, + call_expr: &LuaCallExpr, + param_idx: usize, + origin_signature_id: Option, + call_file_id: crate::FileId, +) -> Option { + if !db.get_emmyrc().gmod.enabled { return None; } - let hook_name = extract_hook_name(call_expr)?; - let member_key = LuaMemberKey::Name(hook_name.into()); + let hook_site = db + .get_gmod_infer_index() + .get_hook_file_metadata(&call_file_id)? + .sites + .iter() + .find(|site| { + site.syntax_id == call_expr.get_syntax_id() + && site.kind == GmodHookKind::Add + && site.callback_arg_idx == Some(param_idx) + })?; + let hook_name = hook_site.hook_name.as_ref()?.clone(); + let member_key = LuaMemberKey::Name(hook_name.clone().into()); let call_realm = db .get_gmod_infer_index() .get_realm_at_offset(&call_file_id, call_expr.get_range().start()); @@ -289,7 +316,14 @@ pub fn resolve_gmod_hook_add_callback_doc_function( }) }, ); - candidates.into_iter().map(|(_, func)| func).next() + let function = candidates + .into_iter() + .map(|(_, function)| function) + .next()?; + Some(GmodHookCallbackDocFunction { + hook_name, + function, + }) } fn iter_hook_owner_names(db: &DbIndex) -> Vec { @@ -345,14 +379,6 @@ pub fn extract_hook_name(call_expr: &LuaCallExpr) -> Option { } } -fn matches_call_path(path: &str, target: &str) -> bool { - // All call sites pass a fully-qualified target (e.g. "hook.Add"). - // get_access_path() also returns the full qualified path, so an exact equality check - // is both necessary and sufficient. A suffix check would produce false positives for - // paths like "mylib.hook.Add" when the target is "hook.Add". - path == target -} - fn is_realm_compatible(call_realm: crate::GmodRealm, item_realm: crate::GmodRealm) -> bool { !matches!( (call_realm, item_realm), diff --git a/crates/glua_code_analysis/src/compilation/mod.rs b/crates/glua_code_analysis/src/compilation/mod.rs index f18c62cb4..27515cfab 100644 --- a/crates/glua_code_analysis/src/compilation/mod.rs +++ b/crates/glua_code_analysis/src/compilation/mod.rs @@ -5,6 +5,7 @@ pub use analyzer::gmod::get_scripted_class_info_for_file; pub(crate) use analyzer::gmod::get_scripted_class_type_decl_id; pub use analyzer::unresolve::extract_hook_name; pub use analyzer::unresolve::resolve_gmod_hook_add_callback_doc_function; +pub use analyzer::unresolve::resolve_gmod_hook_callback_doc_function; use std::sync::Arc; diff --git a/crates/glua_code_analysis/src/compilation/test/and_or_test.rs b/crates/glua_code_analysis/src/compilation/test/and_or_test.rs index bab6807dd..166e7e4ae 100644 --- a/crates/glua_code_analysis/src/compilation/test/and_or_test.rs +++ b/crates/glua_code_analysis/src/compilation/test/and_or_test.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod test { use crate::{DiagnosticCode, VirtualWorkspace}; + use googletest::prelude::*; #[test] fn test_issue_221() { @@ -136,4 +137,45 @@ mod test { "#, )); } + + // ── Inferred string default: type must stay string ───────────────── + + #[gtest] + fn test_string_or_literal_keeps_string_type_annotated() { + // With `---@param panelClass string|nil` then `panelClass = panelClass or "DScrollPanel"`, + // the inferred type must be `string` (not the literal, not a union). + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@param panelClass string|nil + function foo(panelClass) + panelClass = panelClass or "DScrollPanel" + a = panelClass + end + "#, + ); + + let a = ws.expr_ty("a"); + let desc = ws.humanize_type(a); + assert_eq!(desc, "string"); + } + + #[gtest] + fn test_string_or_literal_keeps_string_type_unannotated() { + // Without annotation, `panelClass = panelClass or "DScrollPanel"` + // must still infer `string`. + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + function foo(panelClass) + panelClass = panelClass or "DScrollPanel" + a = panelClass + end + "#, + ); + + let a = ws.expr_ty("a"); + let desc = ws.humanize_type(a); + assert_eq!(desc, "string"); + } } diff --git a/crates/glua_code_analysis/src/compilation/test/closure_param_infer_test.rs b/crates/glua_code_analysis/src/compilation/test/closure_param_infer_test.rs index 4e74f7023..d68815c1e 100644 --- a/crates/glua_code_analysis/src/compilation/test/closure_param_infer_test.rs +++ b/crates/glua_code_analysis/src/compilation/test/closure_param_infer_test.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod test { - use crate::{LuaType, VirtualWorkspace}; + use crate::{GmodHookKind, LuaType, VirtualWorkspace}; #[test] fn test_closure_param_infer() { @@ -589,8 +589,9 @@ mod test { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); - ws.def( + let file_id = ws.def( r#" ---@class Entity local Entity = {} @@ -607,12 +608,6 @@ mod test { ---@return boolean function GM:AcceptInput(ent, input, activator, caller, value) end - hook = {} - ---@param eventName string - ---@param identifier any - ---@param func function - function hook.Add(eventName, identifier, func) end - hook.Add("AcceptInput", "Test", function(ent, input, activator, caller, value) gmod_hook_ent = ent gmod_hook_input = input @@ -623,6 +618,23 @@ mod test { "#, ); + { + let metadata = ws + .get_db_mut() + .get_gmod_infer_index() + .get_hook_file_metadata(&file_id) + .expect("expected hook metadata for annotated hook.Add"); + let site = metadata + .sites + .iter() + .find(|site| { + site.kind == GmodHookKind::Add + && site.hook_name.as_deref() == Some("AcceptInput") + }) + .expect("expected AcceptInput hook metadata"); + assert_eq!(site.callback_arg_idx, Some(2)); + } + let hook_ent = ws.expr_ty("gmod_hook_ent"); let hook_input = ws.expr_ty("gmod_hook_input"); let hook_activator = ws.expr_ty("gmod_hook_activator"); @@ -644,4 +656,41 @@ mod test { assert_eq!(ws.humanize_type(hook_caller), ws.humanize_type(entity_type)); assert_eq!(ws.humanize_type(hook_value), ws.humanize_type(any_type)); } + + #[test] + fn test_gmod_hook_callback_params_infer_from_annotated_wrapper() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = ws.get_emmyrc(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + ws.def( + r#" + ---@class Player + local Player = {} + + ---@class GM + GM = {} + + ---@param ply Player + ---@return boolean + function GM:PlayerSpawn(ply) end + + ---@[call_arg("gmod.hook", "add")] + ---@param eventName string + ---@[call_arg("gmod.hook", "callback")] + ---@param callback function + local function add_hook(eventName, callback, identifier) end + + add_hook("PlayerSpawn", function(ply) + gmod_wrapper_hook_ply = ply + end, "Test") + "#, + ); + + let hook_ply = ws.expr_ty("gmod_wrapper_hook_ply"); + let player_type = ws.ty("Player"); + assert_eq!(ws.humanize_type(hook_ply), ws.humanize_type(player_type)); + } } diff --git a/crates/glua_code_analysis/src/compilation/test/flow.rs b/crates/glua_code_analysis/src/compilation/test/flow.rs index 489585c32..1e3d90154 100644 --- a/crates/glua_code_analysis/src/compilation/test/flow.rs +++ b/crates/glua_code_analysis/src/compilation/test/flow.rs @@ -3955,6 +3955,239 @@ _2 = a[1] )); } + #[gtest] + fn test_elseif_type_guard_narrows_after_previous_type_guard_false_branch() { + let mut ws = VirtualWorkspace::new(); + ws.def_gmod_type_predicates(); + ws.def( + r#" + ---@param value table + local function RequiresTable(value) end + "#, + ); + + assert!(ws.check_code_for( + DiagnosticCode::ParamTypeMismatch, + r#" + ---@param sfmeshdata string|table + local function test(sfmeshdata) + if isstring(sfmeshdata) then + return + elseif istable(sfmeshdata) then + RequiresTable(sfmeshdata) + end + end + "#, + )); + } + + #[gtest] + fn test_starfall_mesh_convexes_do_not_keep_string_after_elseif_istable_guard() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + set_gmod_enabled(&mut ws); + ws.def_gmod_type_predicates(); + + let file_id = ws.def_file( + "lua/entities/starfall_prop/init.lua", + r#" + SF = {} + + function SF.Throw(msg, level, uncatchable, userdata) + local level = 1 + (level or 1) + error(msg, level) + end + + util = {} + + ---@param str string + ---@return string + function util.Compress(str) end + + ---@param compressedString string + ---@param maxSize? number + ---@return string|nil + function util.Decompress(compressedString, maxSize) end + + ---@class Vector + + ---@return Vector + function Vector(x, y, z) end + + local function streamToMesh(meshdata) + local meshConvexes = {} + + meshdata = SF.StringStream(util.Decompress(meshdata, 65536)) + local nConvexes = meshdata:readInt32() + if nConvexes > maxConvexesPerProp then SF.Throw("Exceeded", 2) end + for iConvex = 1, nConvexes do + local nVertices = meshdata:readInt32() + if nVertices > maxVerticesPerConvex then SF.Throw("Exceeded", 2) end + local convex = {} + for iVertex = 1, nVertices do + convex[iVertex] = Vector(meshdata:readFloat(), meshdata:readFloat(), meshdata:readFloat()) + end + meshConvexes[iConvex] = convex + end + + return meshConvexes + end + + local function meshToStream(meshConvexes) + local meshdata = SF.StringStream() + meshdata:writeInt32(#meshConvexes) + for _, convex in ipairs(meshConvexes) do + meshdata:writeInt32(#convex) + for _, vertex in ipairs(convex) do + meshdata:writeFloat(vertex[1]) meshdata:writeFloat(vertex[2]) meshdata:writeFloat(vertex[3]) + end + end + return util.Compress(meshdata:getString()) + end + + local function checkMesh(ply, meshConvexes) + if #meshConvexes > maxConvexesPerProp then SF.Throw("Exceeded", 2) end + if #meshConvexes <= 0 then SF.Throw("Invalid", 2) end + + local totalVertices = 0 + for _, convex in ipairs(meshConvexes) do + if #convex > maxVerticesPerConvex then SF.Throw("Exceeded", 2) end + if #convex < 4 then SF.Throw("Invalid", 2) end + + totalVertices = totalVertices + #convex + customPropVertexLimit:checkuse(ply, totalVertices) + + for k, vertex in ipairs(convex) do + for i = 1, k - 1 do + if convex[i]:DistToSqr(vertex) < mindist then + SF.Throw("No two vertices can have a distance less", 2) + end + end + end + end + end + + local function createCustomProp(ply, pos, ang, sfmeshdata) + local meshConvexes + if isstring(sfmeshdata) then + meshConvexes = streamToMesh(sfmeshdata) + elseif istable(sfmeshdata) then + meshConvexes = sfmeshdata + sfmeshdata = meshToStream(meshConvexes) + else + SF.Throw("Invalid sfmeshdata", 2) + end + if #sfmeshdata > 65536 then + SF.Throw("sfmeshdata is too long!", 2) + end + + checkMesh(ply, meshConvexes) + SF.NetBurst:use(ply, #sfmeshdata * 8) + + local propent = ents.Create("starfall_prop") + propent.sf_physmesh = meshConvexes + + propent.sfmeshdata = sfmeshdata + propent:Spawn() + + local totalVertices = 0 + for k, v in ipairs(meshConvexes) do + totalVertices = totalVertices + #v + end + + return propent + end + "#, + ); + + assert!(!file_has_diagnostic( + &mut ws, + file_id, + DiagnosticCode::ParamTypeMismatch, + )); + + let mesh_convexes_ty = nth_name_expr_type_from_end(&mut ws, file_id, "meshConvexes", 0); + assert_that!(ws.humanize_type(mesh_convexes_ty), eq("table")); + } + + #[gtest] + fn test_never_returning_call_branch_does_not_leave_uninitialized_local_nil() { + let mut ws = VirtualWorkspace::new(); + let file_id = ws.def( + r#" + ---@return never + local function Throw() end + + local value + if condition then + value = "ok" + else + Throw() + end + + local result = value + "#, + ); + + let value_ty = nth_name_expr_type_from_end(&mut ws, file_id, "value", 0); + assert_that!(ws.humanize_type(value_ty), eq("\"ok\"")); + } + + #[gtest] + fn test_error_wrapper_call_branch_does_not_leave_uninitialized_local_nil() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let file_id = ws.def( + r#" + local function Throw(message) + error(message, 2) + end + + local value + if condition then + value = "ok" + else + Throw("invalid") + end + + local result = value + "#, + ); + + let value_ty = nth_name_expr_type_from_end(&mut ws, file_id, "value", 0); + assert_that!(ws.humanize_type(value_ty), eq("\"ok\"")); + } + + #[gtest] + fn test_shadowed_global_never_member_call_does_not_mark_branch_unreachable() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + GlobalApi = {} + + ---@return never + function GlobalApi.Throw() end + "#, + ); + + let file_id = ws.def( + r#" + local GlobalApi = {} + function GlobalApi.Throw() end + + local value + if condition then + value = "ok" + else + GlobalApi.Throw() + end + + local result = value + "#, + ); + + let value_ty = nth_name_expr_type_from_end(&mut ws, file_id, "value", 0); + assert_that!(ws.humanize_type(value_ty), eq("\"ok\"?")); + } + #[gtest] fn test_realistic_registry_lookup_keeps_value_type_for_followup_field_access() { let mut ws = VirtualWorkspace::new(); @@ -4678,4 +4911,168 @@ _2 = a[1] let not_narrowed = nth_name_expr_type_from_end(&mut ws, file_id, "y", 0); assert_eq!(not_narrowed, LuaType::Unknown); } + + // ── Table-guard / self-coalescing regression guards ──────────────────── + + #[gtest] + fn test_self_coalescing_table_or_keeps_table_behavior() { + // `opts = opts or {}` then `opts.foo = "x"` must keep table behavior; + // opts must NOT collapse to string after the second assignment. + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@param opts table|nil + function foo(opts) + opts = opts or {} + opts.foo = "x" + a = opts + end + "#, + ); + + let a = ws.expr_ty("a"); + let desc = ws.humanize_type(a); + assert_that!( + desc, + not(contains_substring("string")), + "table-guard self-coalescing must not leak string into opts: {}", + desc + ); + } + + #[gtest] + fn test_self_coalescing_registry_table_of_over_bare_table_unchanged() { + // `Glide = Glide or {}` and `Glide.Registry = Glide.Registry or {}` + // must keep the table-of-over-bare-table preference unchanged. + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + Glide = Glide or {} + Glide.Registry = Glide.Registry or {} + a = Glide + b = Glide.Registry + "#, + ); + + let a_ty = ws.expr_ty("a"); + let a_desc = ws.humanize_type(a_ty); + assert_that!( + a_desc, + not(contains_substring("string")), + "Glide self-coalescing must keep table type: {}", + a_desc + ); + + let b_ty = ws.expr_ty("b"); + let b_desc = ws.humanize_type(b_ty); + assert_that!( + b_desc, + not(contains_substring("string")), + "Glide.Registry self-coalescing must keep table type: {}", + b_desc + ); + } + + /// Regression: a wrong-realm assignment (e.g. inside `if SERVER`) must NOT + /// kill an inferred default for a client-side use site when used as a + /// generic string template argument. + #[gtest] + fn test_inferred_default_survives_wrong_realm_assignment() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + ws.def( + r#" + ---@class Panel + ---@class DPanel: Panel + ---@class ServerPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + let file_id = ws.def_file( + "lua/autorun/client/test.lua", + r#" + ---@param panelClass string|nil + local function create(panelClass) + panelClass = panelClass or "DPanel" + if SERVER then + panelClass = "ServerPanel" + end + result = create_panel(panelClass) + end + "#, + ); + + // At the client use site, panelClass's inferred default "DPanel" + // should survive because the SERVER-only reassignment must be + // filtered out by realm-aware reachability. + let ty = nth_name_expr_type_from_end(&mut ws, file_id, "result", 0); + let desc = ws.humanize_type(ty); + assert_that!( + desc, + contains_substring("DPanel"), + "inferred default must survive wrong-realm assignment for generic binding, got: {}", + desc + ); + } + + /// Regression: a wrong-realm assignment must NOT kill an explicit param + /// default for a client-side use site when used as a generic string + /// template argument. + #[gtest] + fn test_explicit_default_survives_wrong_realm_assignment() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + ws.def( + r#" + ---@class Panel + ---@class DPanel: Panel + ---@class ServerPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + let file_id = ws.def_file( + "lua/autorun/client/test.lua", + r#" + ---@param panelClass string="DPanel" + local function create(panelClass) + if SERVER then + panelClass = "ServerPanel" + end + result = create_panel(panelClass) + end + "#, + ); + + // Inside the function body, at the client use site, panelClass's + // explicit default "DPanel" should survive because the SERVER-only + // reassignment must be filtered out. + let ty = nth_name_expr_type_from_end(&mut ws, file_id, "result", 0); + let desc = ws.humanize_type(ty); + assert_that!( + desc, + contains_substring("DPanel"), + "explicit default must survive wrong-realm assignment for generic binding, got: {}", + desc + ); + } } diff --git a/crates/glua_code_analysis/src/compilation/test/generic_test.rs b/crates/glua_code_analysis/src/compilation/test/generic_test.rs index 67b4e7dff..b72d6b568 100644 --- a/crates/glua_code_analysis/src/compilation/test/generic_test.rs +++ b/crates/glua_code_analysis/src/compilation/test/generic_test.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod test { - use crate::{DiagnosticCode, Emmyrc, VirtualWorkspace}; + use crate::{DiagnosticCode, Emmyrc, LuaType, VirtualWorkspace}; #[test] fn test_issue_586() { @@ -953,4 +953,32 @@ mod test { let result_ty = ws.expr_ty("result"); assert_eq!(ws.humanize_type(result_ty), "string[]"); } + + #[test] + fn test_generic_definition_return_uses_class_type_with_gmod_enabled() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + + ws.def( + r#" + ---@class Entity + local Entity = {} + + ---@generic T + ---@param object T + ---@return (definition) T + local function get_meta(object) end + + ---@type Entity + local ent + result = get_meta(ent) + "#, + ); + + let result_ty = ws.expr_ty("result"); + assert_eq!(ws.humanize_type(result_ty.clone()), "Entity"); + assert!(matches!(result_ty, LuaType::Def(_))); + } } diff --git a/crates/glua_code_analysis/src/compilation/test/gmod_realm_hook_test.rs b/crates/glua_code_analysis/src/compilation/test/gmod_realm_hook_test.rs index 97ac66531..0b79915de 100644 --- a/crates/glua_code_analysis/src/compilation/test/gmod_realm_hook_test.rs +++ b/crates/glua_code_analysis/src/compilation/test/gmod_realm_hook_test.rs @@ -12,6 +12,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); } #[gtest] @@ -394,6 +395,459 @@ mod test { })); } + #[gtest] + fn test_system_metadata_detection_uses_annotated_net_message_call_arg_roles() { + let mut ws = VirtualWorkspace::new(); + set_gmod_enabled(&mut ws); + let file_id = ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.net_message", "define")] + ---@param name string + function RegisterNetMessage(name) end + + ---@[call_arg("gmod.net_message", "start")] + ---@param name string + function StartWrappedNet(name) end + + ---@[call_arg("gmod.net_message", "receive")] + ---@param name string + function ReceiveWrappedNet(name, callback) end + + ---@[call_arg("gmod.net_message", "start")] + ---@param name string + local function StartLocalWrappedNet(name) end + + RegisterNetMessage("wrapped.net") + StartWrappedNet("wrapped.net") + ReceiveWrappedNet("wrapped.net", function(_, _) end) + StartLocalWrappedNet("local.wrapped.net") + do + local StartWrappedNet = function(name) end + StartWrappedNet("shadowed.net") + end + "#, + ); + + let metadata = ws + .get_db_mut() + .get_gmod_infer_index() + .get_system_file_metadata(&file_id) + .cloned() + .expect("expected system metadata"); + + assert!( + metadata + .net_add_string_calls + .iter() + .any(|site| site.name.as_deref() == Some("wrapped.net")) + ); + assert!( + metadata + .net_start_calls + .iter() + .any(|site| site.name.as_deref() == Some("wrapped.net")) + ); + assert!( + metadata + .net_start_calls + .iter() + .any(|site| site.name.as_deref() == Some("local.wrapped.net")) + ); + assert!(metadata.net_receive_calls.iter().any(|site| { + site.message_name.as_deref() == Some("wrapped.net") && site.callback.syntax_id.is_some() + })); + assert!( + !metadata + .net_start_calls + .iter() + .any(|site| site.name.as_deref() == Some("shadowed.net")) + ); + } + + #[gtest] + fn test_hook_detection_uses_annotated_hook_call_arg_roles() { + let mut ws = VirtualWorkspace::new(); + set_gmod_enabled(&mut ws); + let file_id = ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.hook", "add")] + ---@param name string + function AddWrappedHook(name, identifier, callback) end + + ---@[call_arg("gmod.hook", "emit")] + ---@param name string + function RunWrappedHook(name) end + + AddWrappedHook("WrappedThink", "id", function(ply, data) end) + RunWrappedHook("WrappedEmit") + "#, + ); + + let metadata = ws + .get_db_mut() + .get_gmod_infer_index() + .get_hook_file_metadata(&file_id) + .cloned() + .expect("expected hook metadata"); + + assert!(metadata.sites.iter().any(|site| { + site.kind == GmodHookKind::Add + && site.hook_name.as_deref() == Some("WrappedThink") + && site.callback_params == vec!["ply".to_string(), "data".to_string()] + })); + assert!(metadata.sites.iter().any(|site| { + site.kind == GmodHookKind::Emit && site.hook_name.as_deref() == Some("WrappedEmit") + })); + } + + #[gtest] + fn test_gmod_system_detection_does_not_match_lookalike_builtin_paths() { + let mut ws = VirtualWorkspace::new(); + set_gmod_enabled(&mut ws); + let file_id = ws.def( + r#" + mylib = { hook = {}, net = {}, timer = {}, concommand = {} } + function mylib.hook.Add(name, id, callback) end + function mylib.hook.Run(name) end + function mylib.net.Start(name) end + function mylib.net.Receive(name, callback) end + function mylib.timer.Create(name, delay, repetitions, callback) end + function mylib.concommand.Add(name, callback) end + + mylib.hook.Add("LookalikeAdd", "id", function() end) + mylib.hook.Run("LookalikeRun") + mylib.net.Start("lookalike.net") + mylib.net.Receive("lookalike.net", function() end) + mylib.timer.Create("lookalike.timer", 1, 1, function() end) + mylib.concommand.Add("lookalike_command", function() end) + + do + local hook = { Add = function() end, Run = function() end } + local net = { Start = function() end, Receive = function() end } + local timer = { Create = function() end } + local concommand = { Add = function() end } + local CreateConVar = function() end + local CreateClientConVar = function() end + + hook.Add("ShadowedAdd", "id", function() end) + hook.Run("ShadowedRun") + net.Start("shadowed.net") + net.Receive("shadowed.net", function() end) + timer.Create("shadowed.timer", 1, 1, function() end) + concommand.Add("shadowed_command", function() end) + CreateConVar("shadowed_server_convar") + CreateClientConVar("shadowed_client_convar") + end + "#, + ); + + let db = ws.get_db_mut(); + assert!( + db.get_gmod_infer_index() + .get_hook_file_metadata(&file_id) + .is_none() + ); + let system_metadata = db + .get_gmod_infer_index() + .get_system_file_metadata(&file_id) + .cloned(); + assert!( + system_metadata.is_none_or(|metadata| { + metadata.net_start_calls.is_empty() + && metadata.net_receive_calls.is_empty() + && metadata.timer_calls.is_empty() + && metadata.concommand_add_calls.is_empty() + && metadata.convar_create_calls.is_empty() + }), + "lookalike builtin paths should not be collected as GMod system metadata" + ); + } + + #[gtest] + fn test_gmod_system_detection_keeps_local_aliases_to_builtin_globals() { + let mut ws = VirtualWorkspace::new(); + set_gmod_enabled(&mut ws); + let file_id = ws.def( + r#" + do + local hook = hook + local net = net + local timer = timer + local concommand = concommand + local CreateConVar = CreateConVar + local CreateClientConVar = _G.CreateClientConVar + + hook.Add("AliasAdd", "id", function() end) + hook.Run("AliasRun") + net.Start("alias.start") + net.Receive("alias.receive", function() end) + timer.Create("alias.timer", 1, 1, function() end) + concommand.Add("alias_command", function() end) + CreateConVar("alias_server_convar") + CreateClientConVar("alias_client_convar") + end + "#, + ); + + let db = ws.get_db_mut(); + let hook_metadata = db + .get_gmod_infer_index() + .get_hook_file_metadata(&file_id) + .cloned() + .expect("expected hook metadata"); + assert!(hook_metadata.sites.iter().any(|site| { + site.kind == GmodHookKind::Add && site.hook_name.as_deref() == Some("AliasAdd") + })); + assert!(hook_metadata.sites.iter().any(|site| { + site.kind == GmodHookKind::Emit && site.hook_name.as_deref() == Some("AliasRun") + })); + + let system_metadata = db + .get_gmod_infer_index() + .get_system_file_metadata(&file_id) + .cloned() + .expect("expected system metadata"); + assert!( + system_metadata + .net_start_calls + .iter() + .any(|site| site.name.as_deref() == Some("alias.start")) + ); + assert!( + system_metadata + .net_receive_calls + .iter() + .any(|site| site.message_name.as_deref() == Some("alias.receive")) + ); + assert!( + system_metadata + .timer_calls + .iter() + .any(|site| site.timer_name.as_deref() == Some("alias.timer")) + ); + assert!( + system_metadata + .concommand_add_calls + .iter() + .any(|site| site.command_name.as_deref() == Some("alias_command")) + ); + assert!( + system_metadata + .convar_create_calls + .iter() + .any(|site| site.kind == GmodConVarKind::Server + && site.convar_name.as_deref() == Some("alias_server_convar")) + ); + assert!( + system_metadata + .convar_create_calls + .iter() + .any(|site| site.kind == GmodConVarKind::Client + && site.convar_name.as_deref() == Some("alias_client_convar")) + ); + } + + #[gtest] + fn test_gmod_system_detection_keeps_global_owner_prefixed_builtin_paths() { + let mut ws = VirtualWorkspace::new(); + set_gmod_enabled(&mut ws); + let file_id = ws.def( + r#" + _G.util.AddNetworkString("global.add") + _G.net.Start("global.start") + _G.net.Receive("global.receive", function() end) + _G.hook.Add("GlobalAdd", "id", function() end) + _G.hook.Run("GlobalRun") + _G.timer.Create("global.timer", 1, 1, function() end) + _G.concommand.Add("global_command", function() end) + "#, + ); + + let db = ws.get_db_mut(); + let hook_metadata = db + .get_gmod_infer_index() + .get_hook_file_metadata(&file_id) + .cloned() + .expect("expected hook metadata"); + assert!(hook_metadata.sites.iter().any(|site| { + site.kind == GmodHookKind::Add && site.hook_name.as_deref() == Some("GlobalAdd") + })); + assert!(hook_metadata.sites.iter().any(|site| { + site.kind == GmodHookKind::Emit && site.hook_name.as_deref() == Some("GlobalRun") + })); + + let system_metadata = db + .get_gmod_infer_index() + .get_system_file_metadata(&file_id) + .cloned() + .expect("expected system metadata"); + assert!( + system_metadata + .net_add_string_calls + .iter() + .any(|site| site.name.as_deref() == Some("global.add")) + ); + assert!( + system_metadata + .net_start_calls + .iter() + .any(|site| site.name.as_deref() == Some("global.start")) + ); + assert!( + system_metadata + .net_receive_calls + .iter() + .any(|site| site.message_name.as_deref() == Some("global.receive")) + ); + assert!( + system_metadata + .timer_calls + .iter() + .any(|site| site.timer_name.as_deref() == Some("global.timer")) + ); + assert!( + system_metadata + .concommand_add_calls + .iter() + .any(|site| site.command_name.as_deref() == Some("global_command")) + ); + } + + #[gtest] + fn test_system_metadata_detection_uses_cross_file_annotated_net_message_roles() { + let mut ws = VirtualWorkspace::new(); + set_gmod_enabled(&mut ws); + ws.def_file( + "lua/autorun/net_wrappers.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.net_message", "define")] + ---@param name string + function RegisterGlobalNetMessage(name) end + + API = API or {} + + ---@[call_arg("gmod.net_message", "start")] + ---@param name string + function API.StartGlobalNet(name) end + "#, + ); + let file_id = ws.def_file( + "lua/autorun/use_net_wrappers.lua", + r#" + RegisterGlobalNetMessage("cross.file.net") + API.StartGlobalNet("cross.file.indexed.net") + + do + local API = {} + API.StartGlobalNet("shadowed.index.net") + end + "#, + ); + + let metadata = ws + .get_db_mut() + .get_gmod_infer_index() + .get_system_file_metadata(&file_id) + .cloned() + .expect("expected system metadata"); + + assert!( + metadata + .net_add_string_calls + .iter() + .any(|site| site.name.as_deref() == Some("cross.file.net")) + ); + assert!( + metadata + .net_start_calls + .iter() + .any(|site| site.name.as_deref() == Some("cross.file.indexed.net")) + ); + assert!( + !metadata + .net_start_calls + .iter() + .any(|site| site.name.as_deref() == Some("shadowed.index.net")) + ); + } + + #[gtest] + fn test_system_metadata_detection_uses_annotated_concommand_convar_and_timer_roles() { + let mut ws = VirtualWorkspace::new(); + set_gmod_enabled(&mut ws); + let file_id = ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.concommand", "define")] + ---@param name string + ---@[call_arg("gmod.concommand", "callback")] + ---@param callback function + function RegisterCommand(name, callback) end + + ---@[call_arg("gmod.convar", "define_server")] + ---@param name string + function CreateServerCVar(name, value) end + + ---@[call_arg("gmod.convar", "define_client")] + ---@param name string + function CreateClientCVar(name, value) end + + ---@[call_arg("gmod.timer", "define")] + ---@param identifier string + ---@[call_arg("gmod.timer", "callback")] + ---@param callback function + function CreateWrappedTimer(identifier, delay, repetitions, callback) end + + ---@param delay number + ---@[call_arg("gmod.timer", "simple")] + ---@param callback function + function SimpleWrappedTimer(delay, callback) end + + RegisterCommand("wrapped_cmd", function() end) + CreateServerCVar("wrapped_server_cvar", "1") + CreateClientCVar("wrapped_client_cvar", "1") + CreateWrappedTimer("wrapped_timer", 1, 0, function() end) + SimpleWrappedTimer(0.25, function() end) + "#, + ); + + let metadata = ws + .get_db_mut() + .get_gmod_infer_index() + .get_system_file_metadata(&file_id) + .cloned() + .expect("expected system metadata"); + + assert!(metadata.concommand_add_calls.iter().any(|site| { + site.command_name.as_deref() == Some("wrapped_cmd") && site.callback.syntax_id.is_some() + })); + assert!(metadata.convar_create_calls.iter().any(|site| { + site.kind == GmodConVarKind::Server + && site.convar_name.as_deref() == Some("wrapped_server_cvar") + })); + assert!(metadata.convar_create_calls.iter().any(|site| { + site.kind == GmodConVarKind::Client + && site.convar_name.as_deref() == Some("wrapped_client_cvar") + })); + assert!(metadata.timer_calls.iter().any(|site| { + site.kind == GmodTimerKind::Create + && site.timer_name.as_deref() == Some("wrapped_timer") + && site.callback.syntax_id.is_some() + })); + assert!(metadata.timer_calls.iter().any(|site| { + site.kind == GmodTimerKind::Simple + && site.timer_name.is_none() + && site.callback.syntax_id.is_some() + })); + } + #[gtest] fn test_realm_inference_respects_default_realm_config() { let mut ws = VirtualWorkspace::new(); @@ -425,12 +879,17 @@ mod test { .gmod .hook_mappings .emitter_to_hook - .insert("myhooks.Emit".to_string(), "*".to_string()); + .insert("Emit".to_string(), "*".to_string()); ws.update_emmyrc(emmyrc); let file_id = ws.def( r#" function PLUGIN:PlayerConnect(ply) end myhooks.Emit("MappedEmit") + + do + local myhooks = { Emit = function() end } + myhooks.Emit("ShadowedMappedEmit") + end "#, ); @@ -448,6 +907,10 @@ mod test { assert!(metadata.sites.iter().any(|site| { site.kind == GmodHookKind::Emit && site.hook_name.as_deref() == Some("MappedEmit") })); + assert!(!metadata.sites.iter().any(|site| { + site.kind == GmodHookKind::Emit + && site.hook_name.as_deref() == Some("ShadowedMappedEmit") + })); } #[gtest] diff --git a/crates/glua_code_analysis/src/compilation/test/gmod_scripted_class_test.rs b/crates/glua_code_analysis/src/compilation/test/gmod_scripted_class_test.rs index 524e1f758..83e0b1b90 100644 --- a/crates/glua_code_analysis/src/compilation/test/gmod_scripted_class_test.rs +++ b/crates/glua_code_analysis/src/compilation/test/gmod_scripted_class_test.rs @@ -203,6 +203,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "lua/entities/entities_test.lua", r#" @@ -254,6 +255,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_name = "lua/entities/scripted.lua"; let file_id = ws.def_file(file_name, r#"DEFINE_BASECLASS("base_anim")"#); @@ -295,6 +297,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("lua/entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let allowed_file_id = ws.def_file( "lua/entities/entities_test.lua", @@ -339,6 +342,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "addons/mygamemode/gamemode/entities/entities_test/shared.lua", @@ -360,6 +364,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("plugins/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "plugins/vehicles/sh_plugin.lua", @@ -427,6 +432,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("plugins/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "plugins/vehicles/sh_plugin.lua", @@ -487,6 +493,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "plugins/vehicles/sh_plugin.lua", @@ -527,6 +534,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "plugins/vehicles/entities/entities/vehicles_money/sh_init.lua", @@ -560,6 +568,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "entities/entities/cityrp_money/sh_init.lua", @@ -612,6 +621,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "entities/entities/shadowed_ent/init.lua", @@ -646,6 +656,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "entities/entities/shadowed_method_ent/init.lua", @@ -685,6 +696,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "entities/entities/cityrp_inventory/init.lua", @@ -733,6 +745,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "entities/entities/cityrp_inventory/init.lua", @@ -777,6 +790,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/my_entity/init.lua", @@ -837,6 +851,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/typed_ent/init.lua", @@ -872,6 +887,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/nw_entity/init.lua", @@ -929,6 +945,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/derived_ent/init.lua", @@ -958,6 +975,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedGlobal); let file_id = ws.def_file( @@ -1014,6 +1032,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedGlobal); let file_id = ws.def_file( @@ -1080,6 +1099,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "lua/vgui/baseclass_source_anchor.lua", @@ -1121,6 +1141,417 @@ mod test { ); } + #[gtest] + fn test_annotated_vgui_registration_wrapper_creates_panel_class() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + let file_id = ws.def_file( + "lua/vgui/annotated_wrapper.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + ---@class Panel + + ---@generic T: Panel + ---@[call_arg("gmod.vgui_panel", "define")] + ---@param name string + ---@[call_arg("gmod.vgui_panel", "table")] + ---@param panelTable T + ---@[call_arg("gmod.vgui_panel", "base")] + ---@param baseName string + ---@return T + local function register_panel(name, panelTable, baseName) + return panelTable + end + + local PANEL = {} + function PANEL:Paint() end + + register_panel("AnnotatedPanel", PANEL, "DFrame") + "#, + ); + + let db = ws.get_db_mut(); + let definitions = db + .get_gmod_class_metadata_index() + .find_vgui_panel_definitions("AnnotatedPanel"); + assert_eq!(definitions.len(), 1); + assert_eq!( + db.get_gmod_class_metadata_index() + .get_vgui_panel_base("AnnotatedPanel"), + Some(Some("DFrame".to_string())) + ); + + let paint_member = db.get_member_index().get_member_item( + &LuaMemberOwner::Type(LuaTypeDeclId::global("AnnotatedPanel")), + &LuaMemberKey::Name("Paint".into()), + ); + assert!( + paint_member.is_some(), + "expected annotated wrapper to synthesize panel members for file {file_id:?}" + ); + } + + #[gtest] + fn test_cross_file_annotated_vgui_registration_wrapper_creates_panel_class() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "lua/includes/modules/panel_registry.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + PanelRegistry = {} + + ---@generic T: Panel + ---@[call_arg("gmod.vgui_panel", "define")] + ---@param name string + ---@[call_arg("gmod.vgui_panel", "table")] + ---@param panelTable T + ---@[call_arg("gmod.vgui_panel", "base")] + ---@param baseName string + ---@return T + function PanelRegistry.Register(name, panelTable, baseName) + return panelTable + end + "#, + ); + + ws.def_file( + "lua/vgui/cross_file_annotated_wrapper.lua", + r#" + local PANEL = {} + function PANEL:Paint() end + + PanelRegistry.Register("CrossFilePanel", PANEL, "DFrame") + "#, + ); + + let db = ws.get_db_mut(); + let definitions = db + .get_gmod_class_metadata_index() + .find_vgui_panel_definitions("CrossFilePanel"); + assert_eq!(definitions.len(), 1); + assert_eq!( + db.get_gmod_class_metadata_index() + .get_vgui_panel_base("CrossFilePanel"), + Some(Some("DFrame".to_string())) + ); + + let paint_member = db.get_member_index().get_member_item( + &LuaMemberOwner::Type(LuaTypeDeclId::global("CrossFilePanel")), + &LuaMemberKey::Name("Paint".into()), + ); + assert!( + paint_member.is_some(), + "expected cross-file annotated wrapper to synthesize panel members" + ); + } + + #[gtest] + fn test_colon_annotated_vgui_registration_wrapper_maps_self_parameter() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "lua/vgui/colon_annotated_wrapper.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + local registry = {} + + ---@generic T: Panel + ---@[call_arg("gmod.vgui_panel", "define")] + ---@param name string + ---@[call_arg("gmod.vgui_panel", "table")] + ---@param panelTable T + ---@[call_arg("gmod.vgui_panel", "base")] + ---@param baseName string + ---@return T + function registry:Register(name, panelTable, baseName) + return panelTable + end + + local PANEL = {} + function PANEL:Paint() end + + registry:Register("ColonPanel", PANEL, "DFrame") + "#, + ); + + let db = ws.get_db_mut(); + let definitions = db + .get_gmod_class_metadata_index() + .find_vgui_panel_definitions("ColonPanel"); + assert_eq!(definitions.len(), 1); + assert_eq!( + db.get_gmod_class_metadata_index() + .get_vgui_panel_base("ColonPanel"), + Some(Some("DFrame".to_string())) + ); + + let paint_member = db.get_member_index().get_member_item( + &LuaMemberOwner::Type(LuaTypeDeclId::global("ColonPanel")), + &LuaMemberKey::Name("Paint".into()), + ); + assert!( + paint_member.is_some(), + "expected colon-call annotated wrapper to synthesize panel members" + ); + } + + #[gtest] + fn test_annotated_network_var_wrapper_synthesizes_get_set_with_reordered_args() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "lua/entities/annotated_network/shared.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.network_var", "define")] + ---@param name string + ---@[call_arg("gmod.network_var", "type")] + ---@param typeName string + local function addNetworkVar(name, typeName) + end + + function ENT:SetupDataTables() + addNetworkVar("Enabled", "Bool") + end + "#, + ); + + let db = ws.get_db_mut(); + let owner = LuaMemberOwner::Type(LuaTypeDeclId::global("annotated_network")); + let getter = db + .get_member_index() + .get_member_item(&owner, &LuaMemberKey::Name("GetEnabled".into())); + let setter = db + .get_member_index() + .get_member_item(&owner, &LuaMemberKey::Name("SetEnabled".into())); + + assert!(getter.is_some(), "expected annotated wrapper getter"); + assert!(setter.is_some(), "expected annotated wrapper setter"); + } + + #[gtest] + fn test_annotated_network_var_overload_roles_synthesize_omitted_slot_form() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "lua/entities/annotated_network_overload/shared.lua", + r#" + ---@attribute overload_call_arg(param: integer, domain: string, role: string, priority: integer?) + + ---@[overload_call_arg(0, "gmod.network_var", "type")] + ---@[overload_call_arg(1, "gmod.network_var", "define")] + ---@overload fun(type: string, name: string, extended?: table) + ---@[call_arg("gmod.network_var", "type")] + ---@param type string + ---@param slot number + ---@[call_arg("gmod.network_var", "define")] + ---@param name string + ---@param extended? table + local function DefineNetworkVar(type, slot, name, extended) + end + + function ENT:SetupDataTables() + DefineNetworkVar("Bool", "Active") + DefineNetworkVar("Float", 0, "Speed") + end + "#, + ); + + let db = ws.get_db_mut(); + let owner = LuaMemberOwner::Type(LuaTypeDeclId::global("annotated_network_overload")); + for name in ["GetActive", "SetActive", "GetSpeed", "SetSpeed"] { + let member = db + .get_member_index() + .get_member_item(&owner, &LuaMemberKey::Name(name.into())); + assert!( + member.is_some(), + "expected annotated NetworkVar overload to synthesize {name}" + ); + } + } + + #[gtest] + fn test_annotated_network_var_element_wrapper_synthesizes_number_type() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + let file_id = ws.def_file( + "lua/entities/annotated_network_element/shared.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.network_var", "define_element")] + ---@param name string + ---@[call_arg("gmod.network_var", "type")] + ---@param typeName string + local function addNetworkElement(name, typeName) + end + + function ENT:SetupDataTables() + addNetworkElement("VelocityX", "Vector") + end + + local value = ENT:GetVelocityX() + "#, + ); + + let value_type = local_name_type(&mut ws, file_id, "value"); + assert!( + matches!(value_type, LuaType::Number | LuaType::Integer), + "NetworkVarElement wrapper should synthesize numeric getter, got {value_type:?}" + ); + } + + #[gtest] + fn test_annotated_network_var_element_overload_roles_synthesize_omitted_slot_form() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + let file_id = ws.def_file( + "lua/entities/annotated_network_element_overload/shared.lua", + r#" + ---@attribute overload_call_arg(param: integer, domain: string, role: string, priority: integer?) + + ---@[overload_call_arg(0, "gmod.network_var", "type")] + ---@[overload_call_arg(2, "gmod.network_var", "define_element")] + ---@overload fun(type: string, element: string, name: string, extended?: table) + ---@[call_arg("gmod.network_var", "type")] + ---@param type string + ---@param slot number + ---@param element string + ---@[call_arg("gmod.network_var", "define_element")] + ---@param name string + ---@param extended? table + local function DefineNetworkVarElement(type, slot, element, name, extended) + end + + function ENT:SetupDataTables() + DefineNetworkVarElement("Vector", "x", "OffsetX") + DefineNetworkVarElement("Vector", 0, "y", "OffsetY") + end + + local x = ENT:GetOffsetX() + local y = ENT:GetOffsetY() + "#, + ); + + for name in ["x", "y"] { + let value_type = local_name_type(&mut ws, file_id, name); + assert!( + matches!(value_type, LuaType::Number | LuaType::Integer), + "NetworkVarElement overload wrapper should synthesize numeric getter for {name}, got {value_type:?}" + ); + } + } + + #[gtest] + fn test_annotated_derma_skin_wrapper_records_skin_definition() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "lua/skins/annotated_skin_wrapper.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + ---@[call_arg("gmod.derma_skin", "define")] + ---@param skinName string + ---@param description string + ---@param skin table + local function define_skin(skinName, description, skin) end + + define_skin("AnnotatedSkin", "test skin", {}) + "#, + ); + + let definitions = ws + .get_db_mut() + .get_gmod_class_metadata_index() + .find_derma_skin_definitions("AnnotatedSkin"); + + assert_eq!(definitions.len(), 1); + } + + #[gtest] + fn test_scripted_class_detection_does_not_match_lookalike_builtin_paths() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + let file_id = ws.def_file( + "lua/vgui/lookalike_paths.lua", + r#" + mylib = { vgui = {}, derma = {} } + function mylib.vgui.Register(name, panel, base) end + function mylib.derma.DefineControl(name, description, panel, base) end + function mylib.derma.DefineSkin(name, description, skin) end + + local PANEL = {} + mylib.vgui.Register("LookalikePanel", PANEL, "DPanel") + mylib.derma.DefineControl("LookalikeControl", "desc", PANEL, "DPanel") + mylib.derma.DefineSkin("LookalikeSkin", "desc", {}) + "#, + ); + + let db = ws.get_db_mut(); + assert!( + db.get_gmod_class_metadata_index() + .get_file_metadata(&file_id) + .is_none() + ); + assert!( + db.get_gmod_class_metadata_index() + .find_vgui_panel_definitions("LookalikePanel") + .is_empty() + ); + assert!( + db.get_gmod_class_metadata_index() + .find_vgui_panel_definitions("LookalikeControl") + .is_empty() + ); + assert!( + db.get_gmod_class_metadata_index() + .find_derma_skin_definitions("LookalikeSkin") + .is_empty() + ); + } + #[gtest] fn test_ent_base_from_shared_file_sets_folder_class_super_type() { let mut ws = VirtualWorkspace::new(); @@ -1128,6 +1559,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "entities/entities/cityrp_money/sh_init.lua", @@ -1183,6 +1615,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/mapped_ent/shared.lua", @@ -1216,6 +1649,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::TypeNotFound); ws.def_files(vec![ @@ -1263,6 +1697,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/vgui/my_panel.lua", @@ -1305,6 +1740,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/vgui/multi_panel.lua", @@ -1404,6 +1840,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/vgui/reassigned_panel.lua", @@ -1469,6 +1906,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/vgui/reassigned_panel_accessors.lua", @@ -1534,6 +1972,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/vgui/reassigned_panel_stress.lua", @@ -1619,6 +2058,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/vgui/panel_do_scope.lua", @@ -1691,6 +2131,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/vgui/panel_orphan_scope.lua", @@ -1735,6 +2176,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "lua/vgui/panel_hover_type.lua", @@ -1826,6 +2268,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "lua/vgui/reassigned_panel_regions.lua", @@ -1895,6 +2338,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "lua/vgui/reassigned_panel_decl_type.lua", @@ -1954,6 +2398,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = false; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); let file_id = ws.def_file( @@ -1998,6 +2443,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = false; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); let file_id = ws.def_file( @@ -2037,6 +2483,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); let file_id = ws.def_file( @@ -2076,6 +2523,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def( @@ -2134,6 +2582,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def( @@ -2181,6 +2630,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.enable_check(DiagnosticCode::NeedCheckNil); ws.enable_check(DiagnosticCode::MissingParameter); @@ -2267,6 +2717,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/two_arg_nw/init.lua", @@ -2315,6 +2766,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "lua/entities/nve_entity/init.lua", @@ -2378,6 +2830,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/nve_short/init.lua", @@ -2417,6 +2870,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_files(vec![ ( @@ -2483,6 +2937,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/mixed_nw/init.lua", @@ -2542,6 +2997,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "lua/entities/nve_meta_test.lua", @@ -2575,6 +3031,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/wrapper_ent/init.lua", @@ -2628,6 +3085,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/wrapper_fixed/init.lua", @@ -2681,6 +3139,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/elem_wrap/init.lua", @@ -2726,6 +3185,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def_files(vec![ @@ -2779,6 +3239,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_files(vec![ ( @@ -2839,6 +3300,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def_files(vec![ @@ -2898,6 +3360,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/local_wrap_ent/init.lua", @@ -2950,6 +3413,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def_files(vec![ @@ -3003,6 +3467,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def_files(vec![ @@ -3139,6 +3604,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); // First batch: shared.lua analyzed alone @@ -3188,6 +3654,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def_files(vec![ @@ -3548,6 +4015,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def_files(vec![ @@ -3712,6 +4180,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def_file( @@ -3831,6 +4300,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/includes/custom_classes.lua", @@ -3913,6 +4383,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); // class.TOOL.lua - exact real annotation structure @@ -4061,6 +4532,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); let file_id = ws.def_file( @@ -4117,6 +4589,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); // Mimics the real pattern: function param with @type re-annotation @@ -4165,6 +4638,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); // Class defined in file A @@ -4226,6 +4700,7 @@ mod test { emmyrc.gmod.infer_dynamic_fields = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); let file_id = ws.def_file( @@ -4309,6 +4784,7 @@ mod test { emmyrc.gmod.infer_dynamic_fields = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "cityrp/entities/entities/glide_wheel/init.lua", @@ -4370,6 +4846,7 @@ mod test { emmyrc.gmod.infer_dynamic_fields = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UncheckedNilAccess); let file_id = ws.def_file( @@ -4464,6 +4941,7 @@ mod test { emmyrc.gmod.infer_dynamic_fields = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "cityrp/entities/entities/base_glide/sv_input.lua", @@ -4527,6 +5005,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def_file( @@ -4588,6 +5067,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.enable_check(DiagnosticCode::UncheckedNilAccess); @@ -4680,6 +5160,7 @@ mod test { emmyrc.gmod.infer_dynamic_fields = true; emmyrc.strict.array_index = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "annotations/vehicle.lua", @@ -4913,6 +5394,7 @@ mod test { emmyrc.gmod.infer_dynamic_fields = true; emmyrc.strict.array_index = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let annotations_code = r#" ---@class Entity @@ -5072,6 +5554,7 @@ mod test { emmyrc.gmod.infer_dynamic_fields = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("lua/entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.analysis .diagnostic .enable_only(DiagnosticCode::UndefinedField); @@ -5252,6 +5735,7 @@ mod test { emmyrc.gmod.infer_dynamic_fields = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("lua/entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.analysis .diagnostic .enable_only(DiagnosticCode::UncheckedNilAccess); @@ -5445,6 +5929,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let consumer_path = "lua/weapons/glide_refueler_base.lua"; let consumer_code = r#" @@ -5593,6 +6078,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_ids = ws.def_files(vec![ ( @@ -5791,6 +6277,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "lua/weapons/glide_refueler_base.lua", @@ -5823,6 +6310,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_ids = ws.def_files(vec![ ( @@ -5911,6 +6399,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.infer_dynamic_fields = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def( r#" @@ -5945,6 +6434,7 @@ mod test { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def( r#" @@ -5970,6 +6460,7 @@ mod test { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def( r#" @@ -5998,6 +6489,7 @@ mod test { emmyrc.gmod.infer_dynamic_fields = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("lua/entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let code = r#" ---@return unknown @@ -6158,6 +6650,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/dual_realm_ent/init.lua", @@ -6213,6 +6706,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/vgui/test_derma_panel.lua", @@ -6265,6 +6759,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_files(vec![ ( @@ -6346,6 +6841,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::ParamTypeMismatch); let file_id = ws.def_file( @@ -6410,6 +6906,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let shared_path = "lua/weapons/weapon_mad_deagle/shared.lua"; let shared_text = r#" @@ -6481,6 +6978,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "gamemodes/sandbox/gamemode/init.lua", @@ -6518,6 +7016,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "gamemodes/darkrp/gamemode/entities/entities/my_ent/shared.lua", @@ -6555,6 +7054,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.analysis .diagnostic .enable_only(DiagnosticCode::UndefinedField); @@ -6640,6 +7140,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/entities/base_thing/init.lua", @@ -6685,6 +7186,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "gamemodes/weirdgame/gamemode/init.lua", @@ -6718,6 +7220,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "gamemodes/sandbox/gamemode/player.lua", @@ -6759,6 +7262,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); add_gamemode_gm_library(&mut ws); ws.def_file( @@ -6797,6 +7301,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); add_gamemode_gm_library(&mut ws); ws.def_file( @@ -6822,12 +7327,79 @@ mod test { assert_eq!(count, 1); } + #[gtest] + fn test_annotated_define_baseclass_wrapper_synthesizes_parent_alias() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + add_gamemode_gm_library(&mut ws); + + ws.def_file( + "gamemodes/sandbox/gamemode/player.lua", + r#"function GM:SetupMove(ply, mv, cmd) end"#, + ); + + ws.def_file( + "gamemodes/darkrp/gamemode/init.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + ---@[call_arg("gmod.class_base", "reference")] + ---@param base string + local function setBaseClass(base) end + + setBaseClass("gamemode_sandbox") + "#, + ); + + let owner = LuaMemberOwner::Type(LuaTypeDeclId::global("gamemode_darkrp")); + let member_item = ws + .get_db_mut() + .get_member_index() + .get_member_item(&owner, &LuaMemberKey::Name("Sandbox".into())); + + assert!( + member_item.is_some(), + "annotated class_base wrapper should synthesize Sandbox alias" + ); + } + + #[gtest] + fn test_annotated_derive_gamemode_wrapper_adds_prefixed_super_type() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + add_gamemode_gm_library(&mut ws); + + ws.def_file( + "gamemodes/darkrp/gamemode/init.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + ---@[call_arg("gmod.gamemode", "reference")] + ---@param base string + local function derive(base) end + + derive("sandbox") + "#, + ); + + let super_types = super_types_of(&mut ws, "gamemode_darkrp"); + assert!( + super_types.contains(&LuaType::Ref(LuaTypeDeclId::global("gamemode_sandbox"))), + "annotated gamemode wrapper should add gamemode_sandbox super type, got {super_types:?}" + ); + } + #[gtest] fn test_derive_gamemode_and_define_baseclass_together() { let mut ws = VirtualWorkspace::new(); let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); add_gamemode_gm_library(&mut ws); ws.def_file( @@ -6859,6 +7431,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "gamemodes/darkrp/gamemode/init.lua", @@ -6885,6 +7458,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); add_gamemode_gm_library(&mut ws); ws.def_file( @@ -6907,6 +7481,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "lua/autorun/test.lua", @@ -6935,6 +7510,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); add_gamemode_gm_library(&mut ws); let file_a = ws.def_file( @@ -6970,6 +7546,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); add_gamemode_gm_library(&mut ws); let file_id = ws.def_file( @@ -7028,6 +7605,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); // Simulate the real edit_sky / env_skypaint pattern ws.def_files(vec![ @@ -7098,6 +7676,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.enable_check(DiagnosticCode::UndefinedField); ws.def_files(vec![ @@ -7159,6 +7738,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); // Test direct call: scripted_ents.GetMember("class", "method")(self) ws.def_files(vec![ @@ -7209,6 +7789,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); // Verify the target entity still has its own NetworkVar members ws.def_files(vec![ @@ -7273,6 +7854,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_files(vec![ ( @@ -7323,6 +7905,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_files(vec![ ( @@ -7380,6 +7963,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_files(vec![ ( @@ -7446,6 +8030,7 @@ mod test { emmyrc.gmod.enabled = true; emmyrc.gmod.scripted_class_scopes.include = vec![legacy_scope("entities/**")]; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); // Delegation to a nonexistent class should not crash ws.def_file( @@ -7491,6 +8076,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let file_id = ws.def_file( "lua/vgui/self_field_collapse.lua", @@ -7563,6 +8149,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/vgui/alias_after_reassign.lua", diff --git a/crates/glua_code_analysis/src/compilation/test/infer_str_tpl_test.rs b/crates/glua_code_analysis/src/compilation/test/infer_str_tpl_test.rs index e02173f64..99abd6a2a 100644 --- a/crates/glua_code_analysis/src/compilation/test/infer_str_tpl_test.rs +++ b/crates/glua_code_analysis/src/compilation/test/infer_str_tpl_test.rs @@ -1169,4 +1169,427 @@ mod test { let ply_field_type = ws.expr_ty("scenario_ply_test_var"); assert!(ws.check_type(&ply_field_type, &bool_type)); } + + // ── Inferred string default binding tests ─────────────────────────── + + #[gtest] + fn test_inferred_str_default_binds_str_tpl_generic() { + // `panelClass = panelClass or "DScrollPanel"` then + // `local p = fn(panelClass)` ⇒ p is DScrollPanel. + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DScrollPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + ---@param panelClass string|nil + function foo(panelClass) + panelClass = panelClass or "DScrollPanel" + local p = create_panel(panelClass) + a = p + end + "#, + ); + + let a_ty = ws.expr_ty("a"); + let expected = ws.ty("DScrollPanel"); + assert_eq!(a_ty, expected); + } + + #[gtest] + fn test_no_or_default_does_not_bind_str_tpl() { + // `---@param panelClass string` with NO or-default ⇒ + // `fn(panelClass)` ⇒ Panel (no binding). + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DScrollPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + ---@param panelClass string + function foo(panelClass) + local p = create_panel(panelClass) + a = p + end + "#, + ); + + let a_ty = ws.expr_ty("a"); + let expected = ws.ty("Panel"); + assert_eq!(a_ty, expected); + } + + #[gtest] + fn test_non_self_or_does_not_bind_from_default_metadata() { + // `local y = panelClass or "DScrollPanel"; fn(y)` ⇒ + // y is a different decl with no registered default ⇒ Panel. + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DScrollPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + ---@param panelClass string|nil + function foo(panelClass) + local y = panelClass or "DScrollPanel" + local p = create_panel(y) + a = p + end + "#, + ); + + let a_ty = ws.expr_ty("a"); + let expected = ws.ty("Panel"); + assert_eq!(a_ty, expected); + } + + #[gtest] + fn test_inferred_str_default_binds_unannotated_variable() { + // Without any annotation, `panelClass = panelClass or "DScrollPanel"` + // still carries the inferred default and binds. + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DScrollPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + function foo(panelClass) + panelClass = panelClass or "DScrollPanel" + local p = create_panel(panelClass) + a = p + end + "#, + ); + + let a_ty = ws.expr_ty("a"); + let expected = ws.ty("DScrollPanel"); + assert_eq!(a_ty, expected); + } + + // ── Flow-sensitive inferred default tests ────────────────────────── + + #[gtest] + fn test_inferred_str_default_is_killed_by_later_reassignment() { + // After `panelClass = panelClass or "DScrollPanel"` followed by + // `panelClass = otherClass`, the default is dead at the use site. + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DScrollPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + ---@param panelClass string|nil + ---@param otherClass string + function AddTab(panelClass, otherClass) + panelClass = panelClass or "DScrollPanel" + panelClass = otherClass + local p = create_panel(panelClass) + a = p + end + "#, + ); + + let a_ty = ws.expr_ty("a"); + let expected = ws.ty("Panel"); + assert_eq!(a_ty, expected); + } + + #[gtest] + fn test_inferred_str_default_inside_conditional_does_not_bind() { + // The default is inside a conditional — it does not dominate the use, + // so it must NOT bind. + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DScrollPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + ---@param panelClass string|nil + ---@param cond boolean + function AddTab(panelClass, cond) + if cond then + panelClass = panelClass or "DScrollPanel" + end + local p = create_panel(panelClass) + a = p + end + "#, + ); + + let a_ty = ws.expr_ty("a"); + let expected = ws.ty("Panel"); + assert_eq!(a_ty, expected); + } + + #[gtest] + fn test_inferred_str_default_before_branch_still_binds() { + // The default is before the branch and no reassignment happens, + // so it MUST still bind. + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DScrollPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + ---@param panelClass string|nil + ---@param cond boolean + function AddTab(panelClass, cond) + panelClass = panelClass or "DScrollPanel" + if cond then + local x = 1 + end + local p = create_panel(panelClass) + a = p + end + "#, + ); + + let a_ty = ws.expr_ty("a"); + let expected = ws.ty("DScrollPanel"); + assert_eq!(a_ty, expected); + } + + #[gtest] + fn test_inferred_str_default_branch_reassignment_kills_binding() { + // Default before branch, but branch reassigns → default is dead. + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DScrollPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + ---@param panelClass string|nil + ---@param cond boolean + ---@param otherClass string + function AddTab(panelClass, cond, otherClass) + panelClass = panelClass or "DScrollPanel" + if cond then + panelClass = otherClass + end + local p = create_panel(panelClass) + a = p + end + "#, + ); + + let a_ty = ws.expr_ty("a"); + let expected = ws.ty("Panel"); + assert_eq!(a_ty, expected); + } + + #[gtest] + fn test_self_coalescing_assignment_kills_explicit_default() { + // When a function has `---@param panelClass string = "DPanel"` AND + // then `panelClass = panelClass or "DScrollPanel"`, + // the self-coalescing assignment kills the explicit default. + // The inferred default "DScrollPanel" should bind downstream. + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DScrollPanel: Panel + ---@class DPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + ---@param panelClass string|nil + function foo(panelClass) + panelClass = panelClass or "DScrollPanel" + ---@cast panelClass string + local p = create_panel(panelClass) + a = p + end + + ---@param panelClass string = "DPanel" + function bar(panelClass) + panelClass = panelClass or "DScrollPanel" + local p = create_panel(panelClass) + b = p + end + "#, + ); + + // foo: no explicit default → inferred "DScrollPanel" binds + let a_ty = ws.expr_ty("a"); + let expected_inferred = ws.ty("DScrollPanel"); + assert_eq!(a_ty, expected_inferred); + + // bar: self-coalescing assignment kills explicit default, + // inferred "DScrollPanel" binds + let b_ty = ws.expr_ty("b"); + let expected_inferred = ws.ty("DScrollPanel"); + assert_eq!(b_ty, expected_inferred); + } + + // ── Explicit param default flow-validity tests ───────────────────── + + #[gtest] + fn test_explicit_param_default_is_killed_by_reassignment() { + // When a function parameter has `---@param panelClass string = "DPanel"` + // but `panelClass = otherClass` reassigns it before the use site, + // the explicit default must be killed. Expected: `p` is `Panel`. + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + ---@param panelClass string = "DPanel" + ---@param otherClass string + function AddTab(panelClass, otherClass) + panelClass = otherClass + local p = create_panel(panelClass) + a = p + end + "#, + ); + + let a_ty = ws.expr_ty("a"); + let expected = ws.ty("Panel"); + assert_eq!(a_ty, expected); + } + + #[gtest] + fn test_non_string_explicit_param_default_does_not_bind_str_tpl() { + // When a function parameter has a non-string explicit default (e.g. boolean), + // the string-template default resolver must NOT bind from it. + // Expected: `p` is `Panel` (no binding from boolean default). + // No reassignment — isolates the non-string-default behavior. + let mut ws = VirtualWorkspace::new(); + + ws.def( + r#" + ---@class Panel + ---@class DPanel: Panel + + ---@generic T: Panel + ---@param classname `T` + ---@return T + function create_panel(classname) + end + "#, + ); + + ws.def( + r#" + ---@param panelClass string = true + function AddTab(panelClass) + local p = create_panel(panelClass) + a = p + end + "#, + ); + + let a_ty = ws.expr_ty("a"); + let expected = ws.ty("Panel"); + assert_eq!(a_ty, expected); + } } diff --git a/crates/glua_code_analysis/src/compilation/test/member_infer_test.rs b/crates/glua_code_analysis/src/compilation/test/member_infer_test.rs index acc06d607..089a38c77 100644 --- a/crates/glua_code_analysis/src/compilation/test/member_infer_test.rs +++ b/crates/glua_code_analysis/src/compilation/test/member_infer_test.rs @@ -75,6 +75,239 @@ mod test { .expect("expected semantic info for local name") } + fn setup_vehicle_weapon_registry_same_file_fixture() -> (VirtualWorkspace, crate::FileId) { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + emmyrc.gmod.infer_dynamic_fields = true; + ws.update_emmyrc(emmyrc); + + ws.def_file( + "lua/autorun/sh_glide.lua", + r#" + ---@class Glide + Glide = Glide or {} + "#, + ); + + let file_id = ws.def_file( + "lua/glide/sh_vsweps.lua", + r##" + Glide.WeaponRegistry = Glide.WeaponRegistry or {} + + local function RunWeaponScript(path, className) + VSWEP.ClassName = className + VSWEP.Name = "#glide.weapons.mgs" + VSWEP.Icon = "glide/icons/bullets.png" + VSWEP.Base = "base" + end + + function Glide.ReloadWeaponScript(className) + local registry = Glide.WeaponRegistry + + if registry[className] then + VSWEP = registry[className] + else + VSWEP = {} + end + + RunWeaponScript("glide/vsweps/" .. className .. ".lua", className) + registry[className] = VSWEP + VSWEP = nil + end + + Glide.ReloadWeaponScript("base") + Glide.ReloadWeaponScript("child") + Owner = Glide.WeaponRegistry + + local function RefreshInheritance(className) + if className == "base" then return end + + local class = Glide.WeaponRegistry[className] + local baseClassName = class.Base + + if type(baseClassName) ~= "string" then + return + end + + local baseClass = Glide.WeaponRegistry[baseClassName] + + if baseClass == nil then + return + end + + class.BaseClass = baseClass + setmetatable(class, { __index = baseClass }) + end + "##, + ); + + (ws, file_id) + } + + fn setup_vehicle_weapon_registry_real_multifile_fixture() -> (VirtualWorkspace, crate::FileId) { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + emmyrc.gmod.infer_dynamic_fields = true; + ws.update_emmyrc(emmyrc); + + ws.def_file( + "lua/autorun/sh_glide.lua", + r#" + SERVER = true + CLIENT = false + + ---@class Glide + Glide = Glide or {} + + file = file or {} + concommand = concommand or {} + net = net or {} + + function Glide.IncludeDir(path, sv, cl) end + function Glide.Print(...) end + function Glide.PrintDev(...) end + function Glide.StartCommand(...) end + + function file.Exists(path, realm) return true end + function file.Find(path, realm) return {"base.lua", "missile_launcher.lua"} end + + function CompileFile(path) + return function() end + end + + function ProtectedCall(fn, ...) + fn(...) + return true + end + + function ErrorNoHalt(...) end + + function concommand.Add(name, fn, autocomplete, help) end + function net.WriteString(value) end + function net.Broadcast() end + "#, + ); + + ws.def_file( + "lua/glide/vsweps/base.lua", + r##" + VSWEP.Name = "#glide.weapons.mgs" + VSWEP.Icon = "glide/icons/bullets.png" + VSWEP.FireDelay = 0.5 + VSWEP.ReloadDelay = 1 + VSWEP.EnableLockOn = false + "##, + ); + + ws.def_file( + "lua/glide/vsweps/missile_launcher.lua", + r##" + VSWEP.Base = "base" + VSWEP.Name = "#glide.weapons.missiles" + VSWEP.Icon = "glide/icons/rocket.png" + VSWEP.FireDelay = 1 + VSWEP.EnableLockOn = false + "##, + ); + + let file_id = ws.def_file( + "lua/glide/sh_vsweps.lua", + r#" + if SERVER then + Glide.IncludeDir("glide/vsweps/", false, true) + end + + Glide.WeaponRegistry = Glide.WeaponRegistry or {} + + local function ValidateTableKey(tbl, key, expectedType) + local value = tbl[key] + if value == nil then return end + + local actualType = type(value) + assert(actualType == expectedType) + end + + local function RunWeaponScript(path, className) + local func = CompileFile(path) + if not func then + Glide.Print("failed", className) + return + end + func() + + VSWEP.ClassName = className + + if CLIENT then + ValidateTableKey(VSWEP, "Name", "string") + ValidateTableKey(VSWEP, "Icon", "string") + end + + if SERVER then + ValidateTableKey(VSWEP, "FireDelay", "number") + ValidateTableKey(VSWEP, "ReloadDelay", "number") + ValidateTableKey(VSWEP, "EnableLockOn", "boolean") + end + end + + local function RefreshInheritance(className) + if className == "base" then return end + + OwnerInFunction = Glide.WeaponRegistry + local class = Glide.WeaponRegistry[className] + RawClassExpr = Glide.WeaponRegistry[className] + local baseClassName = class.Base + + if type(baseClassName) ~= "string" then + ErrorNoHalt(className .. ": Invalid base class type! (string expected, got " .. type(baseClassName) .. ")\n") + return + end + + local baseClass = Glide.WeaponRegistry[baseClassName] + + if baseClass == nil then + ErrorNoHalt(className .. ": Invalid base class: " .. baseClassName .. "\n") + return + end + + class.BaseClass = baseClass + setmetatable(class, { __index = baseClass }) + end + + function Glide.ReloadWeaponScript(className) + local path = "glide/vsweps/" .. className .. ".lua" + + if not file.Exists(path, "LUA") then + Glide.Print("missing", className) + return + end + + local registry = Glide.WeaponRegistry + if registry[className] then + VSWEP = registry[className] + else + VSWEP = {} + end + + local success = ProtectedCall(RunWeaponScript, path, className) + if success then + registry[className] = VSWEP + end + + VSWEP = nil + end + + Glide.ReloadWeaponScript("base") + Glide.ReloadWeaponScript("missile_launcher") + Owner = Glide.WeaponRegistry + RefreshInheritance("missile_launcher") + "#, + ); + + (ws, file_id) + } + fn index_expr_type( ws: &mut VirtualWorkspace, file_id: crate::FileId, @@ -517,6 +750,193 @@ mod test { ); } + #[gtest] + fn test_vehicle_weapon_registry_same_file_key_should_not_infer_unknown() { + let (mut ws, _) = setup_vehicle_weapon_registry_same_file_fixture(); + + let owner_ty = ws.expr_ty("Owner"); + let owner_detailed = ws.humanize_type_detailed(owner_ty); + assert_that!( + owner_detailed.as_str(), + not(contains_substring("[unknown]")), + "registry key inference should not use the invalid `unknown` key type: {}", + owner_detailed + ); + } + + #[gtest] + fn test_vehicle_weapon_registry_same_file_class_read_should_use_value_shape_and_nil_union() { + let (mut ws, file_id) = setup_vehicle_weapon_registry_same_file_fixture(); + + let class_ty = local_name_type(&mut ws, file_id, "class"); + let class_detailed = ws.humanize_type_detailed(class_ty.clone()); + assert_that!( + class_detailed.as_str(), + all!( + not(eq("any?")), + contains_substring("ClassName"), + contains_substring("Name"), + contains_substring("Icon"), + contains_substring("?") + ), + "`local class = Glide.WeaponRegistry[className]` should use the registry value shape with nil union: {}", + class_detailed + ); + + let base_class_name_ty = local_name_type(&mut ws, file_id, "baseClassName"); + let string_ty = ws.ty("string"); + assert_that!( + ws.check_type(&base_class_name_ty, &string_ty), + eq(true), + "`class.Base` should preserve the string key before the second registry read, got: {}", + ws.humanize_type_detailed(base_class_name_ty.clone()) + ); + + let base_class_read_ty = + index_expr_type(&mut ws, file_id, "Glide.WeaponRegistry[baseClassName]"); + let base_class_read_display = ws.humanize_type_detailed(base_class_read_ty.clone()); + assert_that!( + base_class_read_display.as_str(), + all!( + not(eq("any?")), + not(eq("nil")), + contains_substring("Name"), + contains_substring("Icon") + ), + "`Glide.WeaponRegistry[baseClassName]` should also use the registry value shape instead of degrading: {}", + base_class_read_display + ); + } + + #[gtest] + fn test_vehicle_weapon_registry_real_multifile_local_binding_does_not_degrade_to_any() { + let (mut ws, file_id) = setup_vehicle_weapon_registry_real_multifile_fixture(); + + let owner_ty = ws.expr_ty("Owner"); + let owner_display = ws.humanize_type_detailed(owner_ty); + assert_that!( + owner_display.as_str(), + contains_substring("[dynamic]"), + "expected real-style registry fixture to preserve dynamic owner shape: {}", + owner_display + ); + + let raw_expr_ty = index_expr_type(&mut ws, file_id, "Glide.WeaponRegistry[className]"); + let raw_expr_display = ws.humanize_type_detailed(raw_expr_ty.clone()); + assert_that!( + raw_expr_display.as_str(), + all!( + not(eq("any?")), + contains_substring("Name"), + contains_substring("Icon"), + contains_substring("?") + ), + "raw index expr should preserve registry value shape in the real-style fixture: {}", + raw_expr_display + ); + + let class_ty = local_name_type(&mut ws, file_id, "class"); + let class_display = ws.humanize_type_detailed(class_ty.clone()); + assert_that!( + ws.check_type(&class_ty, &raw_expr_ty), + eq(true), + "local binding for `class` should be compatible with the paired raw index expr: local={}, raw={}", + class_display, + raw_expr_display + ); + assert_that!( + class_display.as_str(), + all!( + not(eq("any?")), + contains_substring("Name"), + contains_substring("Icon") + ), + "local binding for `class` should not degrade beyond the paired raw index expr: {}", + class_display + ); + } + + #[gtest] + fn test_guarded_dynamic_key_assignment_preserves_shaped_table_value() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + emmyrc.gmod.infer_dynamic_fields = true; + ws.update_emmyrc(emmyrc); + + let file_id = ws.def_file( + "lua/glide/client/debugging.lua", + r#" + ---@class Vector + local Vector = {} + ---@param other Vector + function Vector:Add(other) + end + + ---@return Vector + local function vec() + end + + ---@return any + local function getVid() + end + + local vehicleContacts = {} + local vid = getVid() + local contactPos = vec() + local d = { contactPos = true, contactNormal = true } + + if d.contactPos then + vehicleContacts[vid] = vehicleContacts[vid] or { + sum = vec(), + n = 0, + normalSum = vec(), + normalN = 0, + } + vehicleContacts[vid].sum:Add(contactPos) + vehicleContacts[vid].n = vehicleContacts[vid].n + 1 + if d.contactNormal then + vehicleContacts[vid].normalSum:Add(vec()) + vehicleContacts[vid].normalN = vehicleContacts[vid].normalN + 1 + end + end + + local contacts = vehicleContacts + local contact = vehicleContacts[vid] + "#, + ); + + let contacts_ty = local_name_type(&mut ws, file_id, "contacts"); + let contacts_display = ws.humanize_type_detailed(contacts_ty.clone()); + assert_that!( + contacts_display.as_str(), + all!( + contains_substring("sum"), + contains_substring("n"), + contains_substring("normalSum"), + contains_substring("normalN"), + not(contains_substring(": any")) + ), + "guarded dynamic-key table should preserve the shaped value type on `vehicleContacts`: {}", + contacts_display + ); + + let contact_ty = local_name_type(&mut ws, file_id, "contact"); + let contact_display = ws.humanize_type_detailed(contact_ty.clone()); + assert_that!( + contact_display.as_str(), + all!( + contains_substring("sum"), + contains_substring("n"), + contains_substring("normalSum"), + contains_substring("normalN"), + not(eq("any?")) + ), + "guarded dynamic-key read should preserve the shaped contact value: {}", + contact_display + ); + } + #[gtest] fn test_dynamic_key_read_from_known_table_fields_returns_child_table_value() { let mut ws = VirtualWorkspace::new_with_init_std_lib(); diff --git a/crates/glua_code_analysis/src/config/pre_process.rs b/crates/glua_code_analysis/src/config/pre_process.rs index c8e9024bf..b001884e5 100644 --- a/crates/glua_code_analysis/src/config/pre_process.rs +++ b/crates/glua_code_analysis/src/config/pre_process.rs @@ -73,6 +73,15 @@ impl PreProcessContext { path = self.replace_placeholders(&path, workspace_str); + // Guard: after env-var expansion + placeholder replacement the path may + // be empty (e.g. unset `$GLUA_SNIPPETS_PATH` expanding to ""). An empty + // path would silently join to the workspace root and cause an unbounded + // re-scan of the entire workspace — return it empty so downstream + // ingestion filters can drop it. + if path.trim().is_empty() { + return path; + } + if path.starts_with('~') { let home_dir = match dirs::home_dir() { Some(path) => path, diff --git a/crates/glua_code_analysis/src/config/test.rs b/crates/glua_code_analysis/src/config/test.rs index 0d0472811..ae73f480e 100644 --- a/crates/glua_code_analysis/src/config/test.rs +++ b/crates/glua_code_analysis/src/config/test.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod test { use crate::config::Emmyrc; + use std::path::PathBuf; #[test] fn test_default_ignore_dir_defaults_resolve_to_4_globs() { @@ -199,4 +200,74 @@ mod test { let emmyrc: Emmyrc = serde_json::from_str(json).unwrap(); assert_eq!(emmyrc.gmod.auto_load_annotations, None); } + + #[test] + fn test_pre_process_empty_library_path_stays_empty() { + use crate::config::configs::EmmyLibraryItem; + + let mut emmyrc = Emmyrc::default(); + emmyrc + .workspace + .library + .push(EmmyLibraryItem::Path(String::new())); + + let workspace = PathBuf::from("/some/workspace"); + emmyrc.pre_process_emmyrc(&workspace); + + // An empty library path must NOT resolve to the workspace root. + // It should remain empty so downstream ingestion can drop it. + assert_eq!( + emmyrc.workspace.library.len(), + 1, + "expected 1 library entry" + ); + let processed = emmyrc.workspace.library[0].get_path(); + assert!( + processed.is_empty(), + "empty library path should stay empty after preprocessing, got: {:?}", + processed + ); + } + + #[test] + fn test_pre_process_whitespace_library_path_stays_empty() { + use crate::config::configs::EmmyLibraryItem; + + let mut emmyrc = Emmyrc::default(); + emmyrc + .workspace + .library + .push(EmmyLibraryItem::Path(" ".to_string())); + + let workspace = PathBuf::from("/some/workspace"); + emmyrc.pre_process_emmyrc(&workspace); + + let processed = emmyrc.workspace.library[0].get_path(); + assert!( + processed.trim().is_empty(), + "whitespace-only library path should stay empty after preprocessing, got: {:?}", + processed + ); + } + + #[test] + fn test_pre_process_unset_env_var_library_path_stays_empty() { + use crate::config::configs::EmmyLibraryItem; + + // Use an env var name that is extremely unlikely to be set. + let mut emmyrc = Emmyrc::default(); + emmyrc.workspace.library.push(EmmyLibraryItem::Path( + "$__GLUA_LS_TEST_UNSET_VAR_12345__".to_string(), + )); + + let workspace = PathBuf::from("/some/workspace"); + emmyrc.pre_process_emmyrc(&workspace); + + let processed = emmyrc.workspace.library[0].get_path(); + assert!( + processed.trim().is_empty(), + "unset env var library path should expand to empty and stay empty, got: {:?}", + processed + ); + } } diff --git a/crates/glua_code_analysis/src/db_index/gmod_class/mod.rs b/crates/glua_code_analysis/src/db_index/gmod_class/mod.rs index 61fab6394..f1c453848 100644 --- a/crates/glua_code_analysis/src/db_index/gmod_class/mod.rs +++ b/crates/glua_code_analysis/src/db_index/gmod_class/mod.rs @@ -15,6 +15,7 @@ pub enum GmodScriptedClassCallKind { NetworkVarElement, VguiRegister, DermaDefineControl, + DermaDefineSkin, } impl GmodScriptedClassCallKind { @@ -28,16 +29,6 @@ impl GmodScriptedClassCallKind { _ => None, } } - - pub fn from_call_path(path: &str) -> Option { - if path == "vgui.Register" || path.ends_with(".vgui.Register") { - return Some(Self::VguiRegister); - } - if path == "derma.DefineControl" || path.ends_with(".derma.DefineControl") { - return Some(Self::DermaDefineControl); - } - None - } } #[derive(Debug, Clone, PartialEq)] @@ -62,6 +53,92 @@ pub struct GmodScriptedClassCallMetadata { pub syntax_id: LuaSyntaxId, pub literal_args: Vec>, pub args: Vec, + pub inheritance_roles: Option, + pub network_var_roles: Option, + pub vgui_panel_roles: Option, + pub derma_skin_roles: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GmodNamedStringCallRoles { + pub name_arg_idx: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GmodNetworkVarCallRoles { + pub type_arg_idx: Option, + pub name_arg_idx: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GmodVguiPanelCallRoles { + pub define_arg_idx: usize, + pub table_arg_idx: Option, + pub base_arg_idx: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GmodDermaSkinCallRoles { + pub define_arg_idx: usize, +} + +impl GmodScriptedClassCallMetadata { + pub fn inheritance_name_arg_idx(&self) -> usize { + self.inheritance_roles + .map(|roles| roles.name_arg_idx) + .unwrap_or(0) + } + + pub fn network_var_type_arg_idx(&self) -> Option { + self.network_var_roles + .and_then(|roles| roles.type_arg_idx) + .or(Some(0)) + } + + pub fn network_var_name_arg_idx(&self) -> Option { + self.network_var_roles.map(|roles| roles.name_arg_idx) + } + + pub fn vgui_panel_define_arg_idx(&self) -> usize { + self.vgui_panel_roles + .map(|roles| roles.define_arg_idx) + .unwrap_or(0) + } + + pub fn vgui_panel_table_arg_idx(&self, default_arg_idx: usize) -> usize { + self.vgui_panel_roles + .and_then(|roles| roles.table_arg_idx) + .unwrap_or(default_arg_idx) + } + + pub fn vgui_panel_base_arg_idx(&self, default_arg_idx: Option) -> Option { + self.vgui_panel_roles + .and_then(|roles| roles.base_arg_idx) + .or(default_arg_idx) + } + + pub fn derma_skin_define_arg_idx(&self) -> usize { + self.derma_skin_roles + .map(|roles| roles.define_arg_idx) + .unwrap_or(0) + } + + pub fn define_arg_range(&self, kind: GmodScriptedClassCallKind) -> rowan::TextRange { + let arg_idx = match kind { + GmodScriptedClassCallKind::DefineBaseClass + | GmodScriptedClassCallKind::DeriveGamemode => self.inheritance_name_arg_idx(), + GmodScriptedClassCallKind::NetworkVar + | GmodScriptedClassCallKind::NetworkVarElement => { + self.network_var_name_arg_idx().unwrap_or(0) + } + GmodScriptedClassCallKind::DermaDefineSkin => self.derma_skin_define_arg_idx(), + _ => self.vgui_panel_define_arg_idx(), + }; + self.args + .get(arg_idx) + .map(|arg| arg.syntax_id.get_range()) + .unwrap_or_else(|| self.syntax_id.get_range()) + } } #[derive(Debug, Clone, Default, PartialEq)] @@ -73,19 +150,19 @@ pub struct GmodScriptedClassFileMetadata { pub network_var_element_calls: Vec, pub vgui_register_calls: Vec, pub derma_define_control_calls: Vec, + pub derma_define_skin_calls: Vec, } impl GmodScriptedClassFileMetadata { pub fn get_define_baseclass_name(&self) -> Option<&str> { - self.define_baseclass_calls - .iter() - .rev() - .find_map(|call| match call.literal_args.first() { + self.define_baseclass_calls.iter().rev().find_map(|call| { + match call.literal_args.get(call.inheritance_name_arg_idx()) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => { Some(name.as_str()) } _ => None, - }) + } + }) } fn calls_by_kind_mut( @@ -100,6 +177,7 @@ impl GmodScriptedClassFileMetadata { GmodScriptedClassCallKind::NetworkVarElement => &mut self.network_var_element_calls, GmodScriptedClassCallKind::VguiRegister => &mut self.vgui_register_calls, GmodScriptedClassCallKind::DermaDefineControl => &mut self.derma_define_control_calls, + GmodScriptedClassCallKind::DermaDefineSkin => &mut self.derma_define_skin_calls, } } } @@ -108,6 +186,7 @@ impl GmodScriptedClassFileMetadata { pub struct GmodClassMetadataIndex { file_metadata: HashMap, vgui_panels: HashMap>, + derma_skins: HashMap>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -117,11 +196,18 @@ struct VguiPanelDefinition { base_name: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct DermaSkinDefinition { + file_id: FileId, + range_start: TextSize, +} + impl GmodClassMetadataIndex { pub fn new() -> Self { Self { file_metadata: HashMap::new(), vgui_panels: HashMap::new(), + derma_skins: HashMap::new(), } } @@ -147,14 +233,18 @@ impl GmodClassMetadataIndex { kind: GmodScriptedClassCallKind, call_metadata: &GmodScriptedClassCallMetadata, ) -> Option<(String, Option)> { - let base_arg_index = match kind { - GmodScriptedClassCallKind::VguiRegister => 2, - GmodScriptedClassCallKind::DermaDefineControl => 3, - _ => return None, + let default_base_arg_index = match kind { + GmodScriptedClassCallKind::VguiRegister => Some(2), + GmodScriptedClassCallKind::DermaDefineControl => Some(3), + _ => None, }; - let panel_name = Self::extract_non_empty_string_arg(call_metadata, 0)?; - let base_name = Self::extract_non_empty_string_arg(call_metadata, base_arg_index); + let define_arg_index = call_metadata.vgui_panel_define_arg_idx(); + let base_arg_index = call_metadata.vgui_panel_base_arg_idx(default_base_arg_index); + + let panel_name = Self::extract_non_empty_string_arg(call_metadata, define_arg_index)?; + let base_name = base_arg_index + .and_then(|arg_index| Self::extract_non_empty_string_arg(call_metadata, arg_index)); Some((panel_name, base_name)) } @@ -188,8 +278,41 @@ impl GmodClassMetadataIndex { Self::insert_vgui_panel_from_call(&mut self.vgui_panels, file_id, kind, call_metadata); } + fn insert_derma_skin_from_call( + derma_skins: &mut HashMap>, + file_id: FileId, + call_metadata: &GmodScriptedClassCallMetadata, + ) { + let Some(skin_name) = Self::extract_non_empty_string_arg( + call_metadata, + call_metadata.derma_skin_define_arg_idx(), + ) else { + return; + }; + + derma_skins + .entry(skin_name) + .or_default() + .push(DermaSkinDefinition { + file_id, + range_start: call_metadata.syntax_id.get_range().start(), + }); + } + + fn update_derma_skins_from_call( + &mut self, + file_id: FileId, + kind: GmodScriptedClassCallKind, + call_metadata: &GmodScriptedClassCallMetadata, + ) { + if kind == GmodScriptedClassCallKind::DermaDefineSkin { + Self::insert_derma_skin_from_call(&mut self.derma_skins, file_id, call_metadata); + } + } + fn recompute_vgui_panels(&mut self) { let mut vgui_panels = HashMap::new(); + let mut derma_skins = HashMap::new(); for (file_id, file_metadata) in &self.file_metadata { for call in &file_metadata.vgui_register_calls { @@ -208,9 +331,13 @@ impl GmodClassMetadataIndex { call, ); } + for call in &file_metadata.derma_define_skin_calls { + Self::insert_derma_skin_from_call(&mut derma_skins, *file_id, call); + } } self.vgui_panels = vgui_panels; + self.derma_skins = derma_skins; } pub fn add_call( @@ -219,8 +346,24 @@ impl GmodClassMetadataIndex { kind: GmodScriptedClassCallKind, call_metadata: GmodScriptedClassCallMetadata, ) { - self.update_vgui_panels_from_call(file_id, kind, &call_metadata); + { + let calls = self + .file_metadata + .entry(file_id) + .or_default() + .calls_by_kind_mut(kind); + if let Some(existing) = calls + .iter_mut() + .find(|existing| existing.syntax_id == call_metadata.syntax_id) + { + *existing = call_metadata; + self.recompute_vgui_panels(); + return; + } + } + self.update_vgui_panels_from_call(file_id, kind, &call_metadata); + self.update_derma_skins_from_call(file_id, kind, &call_metadata); self.file_metadata .entry(file_id) .or_default() @@ -257,8 +400,9 @@ impl GmodClassMetadataIndex { .iter() .chain(file_metadata.derma_define_control_calls.iter()) { + let define_arg_index = call.vgui_panel_define_arg_idx(); let Some(Some(GmodClassCallLiteral::String(panel_name))) = - call.literal_args.first() + call.literal_args.get(define_arg_index) else { continue; }; @@ -273,6 +417,34 @@ impl GmodClassMetadataIndex { definitions } + pub fn find_derma_skin_definitions( + &self, + name: &str, + ) -> Vec<(FileId, &GmodScriptedClassCallMetadata)> { + if name.trim().is_empty() { + return Vec::new(); + } + + let mut definitions = Vec::new(); + for (file_id, file_metadata) in &self.file_metadata { + for call in &file_metadata.derma_define_skin_calls { + let define_arg_index = call.derma_skin_define_arg_idx(); + let Some(Some(GmodClassCallLiteral::String(skin_name))) = + call.literal_args.get(define_arg_index) + else { + continue; + }; + + if skin_name == name { + definitions.push((*file_id, call)); + } + } + } + + definitions.sort_by_key(|(file_id, call)| (file_id.id, call.syntax_id.get_range().start())); + definitions + } + pub fn get_vgui_panel_base(&self, name: &str) -> Option> { self.vgui_panels.get(name).map(|definitions| { definitions @@ -336,6 +508,10 @@ mod tests { Some(GmodClassCallLiteral::String(base.to_string())), ), ], + inheritance_roles: None, + network_var_roles: None, + vgui_panel_roles: None, + derma_skin_roles: None, } } diff --git a/crates/glua_code_analysis/src/db_index/gmod_infer/mod.rs b/crates/glua_code_analysis/src/db_index/gmod_infer/mod.rs index c822d6622..2b62ce856 100644 --- a/crates/glua_code_analysis/src/db_index/gmod_infer/mod.rs +++ b/crates/glua_code_analysis/src/db_index/gmod_infer/mod.rs @@ -49,6 +49,7 @@ pub struct GmodHookSiteMetadata { pub hook_name: Option, pub name_range: Option, pub name_issue: Option, + pub callback_arg_idx: Option, pub callback_params: Vec, } diff --git a/crates/glua_code_analysis/src/db_index/property/mod.rs b/crates/glua_code_analysis/src/db_index/property/mod.rs index bdb59ae1d..bd2d396b3 100644 --- a/crates/glua_code_analysis/src/db_index/property/mod.rs +++ b/crates/glua_code_analysis/src/db_index/property/mod.rs @@ -8,13 +8,24 @@ pub use decl_feature::{DeclFeatureFlag, PropertyDeclFeature}; use glua_parser::{LuaAstNode, LuaDocTagField, LuaDocType, LuaVersionCondition, VisibilityKind}; pub use property::LuaCommonProperty; pub use property::{LuaDeprecated, LuaExport, LuaExportScope, LuaPropertyId}; +use rowan::TextRange; +use smol_str::SmolStr; use crate::LuaDocDefaultValue; pub use crate::db_index::property::property::LuaAttributeUse; -use crate::{DbIndex, FileId, LuaMember, LuaSignatureId}; +use crate::{DbIndex, FileId, LuaDeclId, LuaMember, LuaSignatureId}; use super::{LuaSemanticDeclId, traits::LuaIndex}; +/// An inferred string default from a self-coalescing `x = x or "literal"` pattern. +#[derive(Debug, Clone)] +pub struct LuaInferredStringDefault { + /// The string value (e.g. "DScrollPanel"). + pub value: SmolStr, + /// The source range of the assignment statement that produced this default. + pub source_range: TextRange, +} + #[derive(Debug)] pub struct LuaPropertyIndex { properties: HashMap, @@ -23,6 +34,13 @@ pub struct LuaPropertyIndex { id_count: u32, in_filed_owner: HashMap>, + + /// Inferred string defaults from `x = x or "literal"` patterns. + /// Keyed by `LuaDeclId`; one decl can have multiple candidates (e.g. + /// multiple self-coalescing assignments in different scopes of the same + /// function). File ownership is tracked for cleanup on reindex. + inferred_string_defaults: HashMap>, + inferred_string_defaults_file_owners: HashMap>, } impl Default for LuaPropertyIndex { @@ -39,6 +57,8 @@ impl LuaPropertyIndex { properties: HashMap::new(), property_owners_map: HashMap::new(), signature_owner_by_property: HashMap::new(), + inferred_string_defaults: HashMap::new(), + inferred_string_defaults_file_owners: HashMap::new(), } } @@ -309,6 +329,38 @@ impl LuaPropertyIndex { .map(|property| (owner_id, property)) }) } + + /// Register an inferred string default from `x = x or "literal"`. + pub fn add_inferred_string_default( + &mut self, + file_id: FileId, + decl_id: LuaDeclId, + value: SmolStr, + source_range: TextRange, + ) { + let entry = LuaInferredStringDefault { + value, + source_range, + }; + self.inferred_string_defaults + .entry(decl_id) + .or_default() + .push(entry); + self.inferred_string_defaults_file_owners + .entry(file_id) + .or_default() + .insert(decl_id); + } + + /// Get all inferred string default candidates for a declaration. + pub fn get_inferred_string_defaults( + &self, + decl_id: &LuaDeclId, + ) -> Option<&[LuaInferredStringDefault]> { + self.inferred_string_defaults + .get(decl_id) + .map(|v| v.as_slice()) + } } impl LuaIndex for LuaPropertyIndex { @@ -321,6 +373,12 @@ impl LuaIndex for LuaPropertyIndex { } } } + // Clean up inferred string defaults owned by this file. + if let Some(decl_ids) = self.inferred_string_defaults_file_owners.remove(&file_id) { + for decl_id in decl_ids { + self.inferred_string_defaults.remove(&decl_id); + } + } } fn clear(&mut self) { @@ -328,6 +386,8 @@ impl LuaIndex for LuaPropertyIndex { self.property_owners_map.clear(); self.signature_owner_by_property.clear(); self.in_filed_owner.clear(); + self.inferred_string_defaults.clear(); + self.inferred_string_defaults_file_owners.clear(); self.id_count = 0; } } diff --git a/crates/glua_code_analysis/src/db_index/signature/mod.rs b/crates/glua_code_analysis/src/db_index/signature/mod.rs index 5d3213548..a3a36683a 100644 --- a/crates/glua_code_analysis/src/db_index/signature/mod.rs +++ b/crates/glua_code_analysis/src/db_index/signature/mod.rs @@ -6,8 +6,10 @@ use std::collections::{HashMap, HashSet}; pub use async_state::AsyncState; pub use signature::{ - LuaDocDefaultValue, LuaDocParamInfo, LuaDocReturnInfo, LuaGenericParamInfo, LuaNoDiscard, - LuaOutParamInfo, LuaSignature, LuaSignatureId, ReturnTypeKind, SignatureReturnStatus, + CALL_ARG_ATTRIBUTE, LuaCallArgRole, LuaDocDefaultValue, LuaDocParamInfo, LuaDocReturnInfo, + LuaGenericParamInfo, LuaNoDiscard, LuaOutParamInfo, LuaSignature, LuaSignatureId, + OVERLOAD_CALL_ARG_ATTRIBUTE, ReturnTypeKind, SignatureReturnStatus, + find_call_arg_role_from_type, visit_call_arg_roles_from_type, }; use crate::FileId; diff --git a/crates/glua_code_analysis/src/db_index/signature/signature.rs b/crates/glua_code_analysis/src/db_index/signature/signature.rs index b53c6358c..947d8cf13 100644 --- a/crates/glua_code_analysis/src/db_index/signature/signature.rs +++ b/crates/glua_code_analysis/src/db_index/signature/signature.rs @@ -7,11 +7,14 @@ use glua_parser::{LuaAstNode, LuaClosureExpr, LuaDocFuncType}; use rowan::TextSize; use crate::db_index::signature::async_state::AsyncState; +use crate::{DbIndex, LuaAttributeUse, SemanticModel, VariadicType, first_param_may_not_self}; use crate::{ FileId, db_index::{LuaFunctionType, LuaType}, }; -use crate::{LuaAttributeUse, SemanticModel, VariadicType, first_param_may_not_self}; + +pub const CALL_ARG_ATTRIBUTE: &str = "call_arg"; +pub const OVERLOAD_CALL_ARG_ATTRIBUTE: &str = "overload_call_arg"; #[derive(Debug)] pub struct LuaSignature { @@ -26,6 +29,15 @@ pub struct LuaSignature { pub async_state: AsyncState, pub nodiscard: Option, pub is_vararg: bool, + require_guard_param: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LuaCallArgRole { + pub param_idx: usize, + pub domain: String, + pub role: String, + pub priority: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -54,9 +66,18 @@ impl LuaSignature { async_state: AsyncState::None, nodiscard: None, is_vararg: false, + require_guard_param: None, } } + pub fn require_guard_param(&self) -> Option { + self.require_guard_param + } + + pub fn set_require_guard_param(&mut self, param_idx: usize) { + self.require_guard_param = Some(param_idx); + } + pub fn is_generic(&self) -> bool { !self.generic_params.is_empty() } @@ -76,6 +97,61 @@ impl LuaSignature { .any(|overload| overload_has_special_call_params(overload.as_ref())) } + pub fn has_call_arg_roles(&self) -> bool { + self.param_docs.values().any(|param_info| { + param_info + .get_attribute_by_name(CALL_ARG_ATTRIBUTE) + .is_some() + }) || self + .overloads + .iter() + .any(|overload| !overload.get_call_arg_roles().is_empty()) + } + + pub fn call_arg_roles_for_param(&self, param_idx: usize) -> Vec { + let mut roles = Vec::new(); + self.visit_call_arg_roles_for_param(param_idx, &mut |role| roles.push(role.clone())); + roles + } + + pub fn visit_call_arg_roles_for_param(&self, param_idx: usize, visitor: &mut F) + where + F: FnMut(&LuaCallArgRole), + { + if let Some(param_info) = self.get_param_info_by_id(param_idx) { + visit_call_arg_roles_from_param_attribute(param_idx, param_info, visitor); + } + for overload in &self.overloads { + for role in overload.get_call_arg_roles() { + if role.param_idx == param_idx { + visitor(role); + } + } + } + } + + pub fn call_arg_roles(&self) -> Vec { + let mut roles = Vec::new(); + for (param_idx, param_info) in &self.param_docs { + visit_call_arg_roles_from_param_attribute(*param_idx, param_info, &mut |role| { + roles.push(role.clone()); + }); + } + roles.sort_by_key(|role| { + ( + role.param_idx, + std::cmp::Reverse(role.priority.unwrap_or(0)), + ) + }); + roles + } + + pub fn overload_call_arg_roles(&self) -> impl Iterator { + self.overloads + .iter() + .flat_map(|overload| overload.get_call_arg_roles().iter()) + } + pub fn get_type_params(&self) -> Vec<(String, Option)> { let mut type_params = Vec::new(); for (idx, param_name) in self.params.iter().enumerate() { @@ -212,6 +288,114 @@ impl LuaSignature { } } +fn visit_call_arg_roles_from_param_attribute( + param_idx: usize, + param_info: &LuaDocParamInfo, + visitor: &mut F, +) where + F: FnMut(&LuaCallArgRole), +{ + for attribute_use in param_info.iter_attributes_by_name(CALL_ARG_ATTRIBUTE) { + let Some(domain) = attribute_string_arg(attribute_use, "domain") else { + continue; + }; + let Some(role) = attribute_string_arg(attribute_use, "role") else { + continue; + }; + let priority = attribute_integer_arg(attribute_use, "priority"); + visitor(&LuaCallArgRole { + param_idx, + domain, + role, + priority, + }); + } +} + +fn attribute_string_arg(attribute_use: &LuaAttributeUse, name: &str) -> Option { + match attribute_use.get_param_by_name(name)? { + LuaType::DocStringConst(value) | LuaType::StringConst(value) => Some(value.to_string()), + _ => None, + } +} + +fn attribute_integer_arg(attribute_use: &LuaAttributeUse, name: &str) -> Option { + match attribute_use.get_param_by_name(name)? { + LuaType::DocIntegerConst(value) | LuaType::IntegerConst(value) => Some(*value), + _ => None, + } +} + +pub fn visit_call_arg_roles_from_type( + db: &DbIndex, + typ: &LuaType, + arg_idx: usize, + visitor: &mut F, +) where + F: FnMut(&LuaCallArgRole), +{ + match typ { + LuaType::Signature(signature_id) => { + if let Some(signature) = db.get_signature_index().get(signature_id) { + signature.visit_call_arg_roles_for_param(arg_idx, visitor); + } + } + LuaType::TypeGuard(inner) => { + visit_call_arg_roles_from_type(db, inner, arg_idx, visitor); + } + LuaType::TableOf(inner) => { + visit_call_arg_roles_from_type(db, inner, arg_idx, visitor); + } + LuaType::Instance(instance) => { + visit_call_arg_roles_from_type(db, instance.get_base(), arg_idx, visitor); + } + LuaType::Union(union) => match union.as_ref() { + crate::db_index::LuaUnionType::Nullable(inner) => { + visit_call_arg_roles_from_type(db, inner, arg_idx, visitor); + } + crate::db_index::LuaUnionType::Multi(types) => { + for typ in types { + visit_call_arg_roles_from_type(db, typ, arg_idx, visitor); + } + } + }, + LuaType::Intersection(intersection) => { + for typ in intersection.get_types() { + visit_call_arg_roles_from_type(db, typ, arg_idx, visitor); + } + } + LuaType::MultiLineUnion(union) => { + for (typ, _) in union.get_unions() { + visit_call_arg_roles_from_type(db, typ, arg_idx, visitor); + } + } + _ => {} + } +} + +pub fn find_call_arg_role_from_type( + db: &DbIndex, + typ: &LuaType, + arg_idx: usize, + domain: &str, + roles: &[&str], +) -> Option { + let mut best: Option = None; + visit_call_arg_roles_from_type(db, typ, arg_idx, &mut |role| { + if role.domain != domain || !roles.iter().any(|candidate| *candidate == role.role) { + return; + } + + if best + .as_ref() + .is_none_or(|current| role.priority.unwrap_or(0) > current.priority.unwrap_or(0)) + { + best = Some(role.clone()); + } + }); + best +} + fn type_contains_str_tpl_ref(typ: &LuaType) -> bool { match typ { LuaType::StrTplRef(_) => true, @@ -255,6 +439,69 @@ impl LuaDocParamInfo { .flatten() .find(|attr| attr.id.get_name() == name) } + + pub fn iter_attributes_by_name<'a>( + &'a self, + name: &'a str, + ) -> impl Iterator + 'a { + self.attributes + .iter() + .flatten() + .filter(move |attr| attr.id.get_name() == name) + } +} + +#[cfg(test)] +mod tests { + use super::{CALL_ARG_ATTRIBUTE, LuaDocParamInfo, LuaSignature}; + use crate::{LuaAttributeUse, LuaType, LuaTypeDeclId}; + use smol_str::SmolStr; + + fn call_arg_attribute(domain: &str, role: &str) -> LuaAttributeUse { + LuaAttributeUse::new( + LuaTypeDeclId::global(CALL_ARG_ATTRIBUTE), + vec![ + ( + "domain".to_string(), + Some(LuaType::DocStringConst(SmolStr::new(domain).into())), + ), + ( + "role".to_string(), + Some(LuaType::DocStringConst(SmolStr::new(role).into())), + ), + ], + ) + } + + #[test] + fn call_arg_roles_for_param_keeps_multiple_attributes() { + let mut signature = LuaSignature::new(); + signature.params.push("name".to_string()); + signature.param_docs.insert( + 0, + LuaDocParamInfo { + name: "name".to_string(), + type_ref: LuaType::String, + default_value: None, + nullable: false, + description: None, + attributes: Some(vec![ + call_arg_attribute("gmod.vgui_panel", "define"), + call_arg_attribute("gmod.derma_skin", "reference"), + ]), + }, + ); + + let roles = signature.call_arg_roles_for_param(0); + + assert_eq!(roles.len(), 2); + assert_eq!(roles[0].domain, "gmod.vgui_panel"); + assert_eq!(roles[0].role, "define"); + assert_eq!(roles[0].param_idx, 0); + assert_eq!(roles[1].domain, "gmod.derma_skin"); + assert_eq!(roles[1].role, "reference"); + assert_eq!(roles[1].param_idx, 0); + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/glua_code_analysis/src/db_index/type/humanize_type.rs b/crates/glua_code_analysis/src/db_index/type/humanize_type.rs index c79170eff..1c5d18bf5 100644 --- a/crates/glua_code_analysis/src/db_index/type/humanize_type.rs +++ b/crates/glua_code_analysis/src/db_index/type/humanize_type.rs @@ -110,6 +110,12 @@ pub fn humanize_type(db: &DbIndex, ty: &LuaType, level: RenderLevel) -> String { LuaType::DocBooleanConst(b) => b.to_string(), LuaType::Ref(id) => { if let Some(type_decl) = db.get_type_index().get_type_decl(id) { + if type_decl.is_alias() { + if let Some(alias_type) = humanize_alias_ref_type(db, ty, level) { + return alias_type; + } + } + let name = type_decl.get_full_name().to_string(); humanize_simple_type(db, id, &name, level).unwrap_or(name) } else { @@ -163,6 +169,27 @@ pub fn humanize_type(db: &DbIndex, ty: &LuaType, level: RenderLevel) -> String { } } +fn humanize_alias_ref_type(db: &DbIndex, ty: &LuaType, level: RenderLevel) -> Option { + if !matches!( + level, + RenderLevel::Documentation | RenderLevel::DetailedCount(_) | RenderLevel::Detailed + ) { + return None; + } + + let resolved = super::resolve_alias_type(db, ty); + let alias_id = resolved.alias_id?; + if resolved.typ == *ty { + return None; + } + + Some(format!( + "(alias) {} = {}", + alias_id.get_simple_name(), + humanize_type(db, &resolved.typ, level) + )) +} + fn humanize_def_type(db: &DbIndex, id: &LuaTypeDeclId, level: RenderLevel) -> String { let type_decl = match db.get_type_index().get_type_decl(id) { Some(type_decl) => type_decl, @@ -959,6 +986,9 @@ fn build_table_member_string( LuaMemberKey::Integer(i) => format!("[{i}]{separator}{member_value}"), LuaMemberKey::None => member_value, LuaMemberKey::ExprType(LuaType::Integer) => member_value, + LuaMemberKey::ExprType(typ) if typ.is_unknown() => { + format!("[dynamic]{separator}{member_value}") + } LuaMemberKey::ExprType(typ) => { let key_type = humanize_type(db, typ, level.next_level()); format!("[{key_type}]{separator}{member_value}") diff --git a/crates/glua_code_analysis/src/db_index/type/mod.rs b/crates/glua_code_analysis/src/db_index/type/mod.rs index ed7d50d73..cf4a0d1f4 100644 --- a/crates/glua_code_analysis/src/db_index/type/mod.rs +++ b/crates/glua_code_analysis/src/db_index/type/mod.rs @@ -24,6 +24,55 @@ pub use type_owner::{LuaTypeCache, LuaTypeOwner}; pub use type_visit_trait::TypeVisitTrait; pub use types::*; +#[derive(Debug, Clone)] +pub struct LuaResolvedAliasType { + pub alias_id: Option, + pub typ: LuaType, +} + +pub fn resolve_alias_type(db: &DbIndex, typ: &LuaType) -> LuaResolvedAliasType { + let mut visited_aliases = HashSet::new(); + resolve_alias_type_inner(db, typ, None, &mut visited_aliases) +} + +fn resolve_alias_type_inner( + db: &DbIndex, + typ: &LuaType, + alias_id: Option, + visited_aliases: &mut HashSet, +) -> LuaResolvedAliasType { + let (LuaType::Ref(type_id) | LuaType::Def(type_id)) = typ else { + return LuaResolvedAliasType { + alias_id, + typ: typ.clone(), + }; + }; + + let Some(type_decl) = db.get_type_index().get_type_decl(type_id) else { + return LuaResolvedAliasType { + alias_id, + typ: typ.clone(), + }; + }; + + if !type_decl.is_alias() || !visited_aliases.insert(type_id.clone()) { + return LuaResolvedAliasType { + alias_id, + typ: typ.clone(), + }; + } + + let Some(origin) = type_decl.get_alias_origin(db, None) else { + return LuaResolvedAliasType { + alias_id, + typ: typ.clone(), + }; + }; + + let alias_id = alias_id.or_else(|| Some(type_id.clone())); + resolve_alias_type_inner(db, &origin, alias_id, visited_aliases) +} + fn replace_table_const_in_type( typ: &LuaType, table_range: &InFiled, diff --git a/crates/glua_code_analysis/src/db_index/type/test.rs b/crates/glua_code_analysis/src/db_index/type/test.rs index 3f786127a..fa3614d07 100644 --- a/crates/glua_code_analysis/src/db_index/type/test.rs +++ b/crates/glua_code_analysis/src/db_index/type/test.rs @@ -5,12 +5,48 @@ mod test { use crate::db_index::traits::LuaIndex; use crate::db_index::r#type::LuaTypeIndex; use crate::db_index::{LuaDeclTypeKind, LuaTypeFlag}; - use crate::{FileId, LuaTypeDecl, LuaTypeDeclId}; + use crate::{DbIndex, FileId, LuaType, LuaTypeDecl, LuaTypeDeclId, resolve_alias_type}; fn create_type_index() -> LuaTypeIndex { LuaTypeIndex::new() } + #[test] + fn test_resolve_alias_type_handles_def_alias() { + let mut db = DbIndex::default(); + let file_id = FileId { id: 1 }; + let origin_id = LuaTypeDeclId::global("Test2"); + let alias_id = LuaTypeDeclId::global("TestAlias"); + + db.get_type_index_mut().add_type_decl( + file_id, + LuaTypeDecl::new( + file_id, + TextRange::new(0.into(), 0.into()), + "Test2".to_string(), + LuaDeclTypeKind::Class, + LuaTypeFlag::None.into(), + origin_id.clone(), + ), + ); + + let mut alias = LuaTypeDecl::new( + file_id, + TextRange::new(0.into(), 0.into()), + "TestAlias".to_string(), + LuaDeclTypeKind::Alias, + LuaTypeFlag::None.into(), + alias_id.clone(), + ); + alias.add_alias_origin(LuaType::Ref(origin_id.clone())); + db.get_type_index_mut().add_type_decl(file_id, alias); + + let resolved = resolve_alias_type(&db, &LuaType::Def(alias_id.clone())); + + assert_eq!(resolved.alias_id, Some(alias_id)); + assert_eq!(resolved.typ, LuaType::Ref(origin_id)); + } + #[test] fn test_namespace() { let mut index = create_type_index(); diff --git a/crates/glua_code_analysis/src/db_index/type/types.rs b/crates/glua_code_analysis/src/db_index/type/types.rs index a517184be..4580bfff0 100644 --- a/crates/glua_code_analysis/src/db_index/type/types.rs +++ b/crates/glua_code_analysis/src/db_index/type/types.rs @@ -9,7 +9,7 @@ use std::{ }; use crate::{ - AsyncState, DbIndex, FileId, InFiled, SemanticModel, + AsyncState, DbIndex, FileId, InFiled, LuaCallArgRole, SemanticModel, db_index::{LuaMemberKey, LuaSignatureId, r#type::type_visit_trait::TypeVisitTrait}, first_param_may_not_self, }; @@ -733,6 +733,7 @@ pub struct LuaFunctionType { is_variadic: bool, params: Vec<(String, Option)>, optional_params: Vec, + call_arg_roles: Vec, ret: LuaType, } @@ -765,6 +766,7 @@ impl LuaFunctionType { is_variadic, params, optional_params, + call_arg_roles: Vec::new(), ret, } } @@ -775,6 +777,15 @@ impl LuaFunctionType { self } + pub fn with_call_arg_roles(mut self, call_arg_roles: Vec) -> Self { + self.call_arg_roles = call_arg_roles; + self + } + + pub fn get_call_arg_roles(&self) -> &[LuaCallArgRole] { + &self.call_arg_roles + } + pub fn get_async_state(&self) -> AsyncState { self.async_state } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/access_invisible.rs b/crates/glua_code_analysis/src/diagnostic/checker/access_invisible.rs index 3a372eb83..adfd14ace 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/access_invisible.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/access_invisible.rs @@ -177,8 +177,8 @@ fn report_reason( let version_number = emmyrc.runtime.version.to_lua_version_number(); let visible = version_conds.iter().any(|cond| cond.check(&version_number)); if !visible { - let message = t!( - "The current Lua version %{version} is not accessible; expected %{conds}.", + let message = format!( + "The current Lua version {version} is not accessible; expected {conds}.", version = version_number, conds = version_conds .iter() @@ -199,16 +199,17 @@ fn report_reason( let message = match property.visibility { VisibilityKind::Protected => { - t!("The property is protected and cannot be accessed outside its subclasses.") + "The property is protected and cannot be accessed outside its subclasses.".to_string() } VisibilityKind::Private => { - t!("The property is private and cannot be accessed outside the class.") + "The property is private and cannot be accessed outside the class.".to_string() } VisibilityKind::Package => { - t!("The property is package-private and cannot be accessed outside the package.") + "The property is package-private and cannot be accessed outside the package." + .to_string() } VisibilityKind::Internal => { - t!("The property is internal and cannot be accessed outside the module.") + "The property is internal and cannot be accessed outside the module.".to_string() } _ => { return None; diff --git a/crates/glua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs b/crates/glua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs index 696ec6fdd..8a1b878bd 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/assign_type_mismatch.rs @@ -907,15 +907,15 @@ fn add_type_check_diagnostic( Err(reason) => { let reason_message = match reason { TypeCheckFailReason::TypeNotMatchWithReason(reason) => reason, - TypeCheckFailReason::TypeRecursion => t!("type recursion").to_string(), + TypeCheckFailReason::TypeRecursion => "type recursion".to_string(), _ => "".to_string(), }; context.add_diagnostic( DiagnosticCode::AssignTypeMismatch, range, - t!( - "Cannot assign `%{value}` to `%{source}`. %{reason}", + format!( + "Cannot assign `{value}` to `{source}`. {reason}", value = humanize_lint_type(db, value_type), source = humanize_lint_type(db, source_type), reason = reason_message diff --git a/crates/glua_code_analysis/src/diagnostic/checker/attribute_check.rs b/crates/glua_code_analysis/src/diagnostic/checker/attribute_check.rs index 5eb8856b1..ba2ebf3af 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/attribute_check.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/attribute_check.rs @@ -87,8 +87,8 @@ fn check_param_count( Some(arg) => arg.get_range(), None => attribute_use.get_range(), }, - t!( - "expected %{num} parameters but found %{found_num}", + format!( + "expected {num} parameters but found {found_num}", num = def_params.len(), found_num = call_args_count ) @@ -109,8 +109,8 @@ fn check_param_count( context.add_diagnostic( DiagnosticCode::AttributeRedundantParameter, arg.get_range(), - t!( - "expected %{num} parameters but found %{found_num}", + format!( + "expected {num} parameters but found {found_num}", num = def_params.len(), found_num = call_args_count ) @@ -200,8 +200,8 @@ fn add_type_check_diagnostic( context.add_diagnostic( DiagnosticCode::AttributeParamTypeMismatch, range, - t!( - "expected `%{source}` but found `%{found}`. %{reason}", + format!( + "expected `{source}` but found `{found}`. {reason}", source = humanize_lint_type(db, param_type), found = humanize_lint_type(db, expr_type), reason = reason_message diff --git a/crates/glua_code_analysis/src/diagnostic/checker/await_in_sync.rs b/crates/glua_code_analysis/src/diagnostic/checker/await_in_sync.rs index b77faa70b..50a68903f 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/await_in_sync.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/await_in_sync.rs @@ -357,7 +357,7 @@ fn check_call_in_async( context.add_diagnostic( DiagnosticCode::AwaitInSync, prefix_expr.get_range(), - t!("Async function can only be called in async function.").to_string(), + "Async function can only be called in async function.".to_string(), None, ); } @@ -475,7 +475,7 @@ fn check_call_as_arg( context.add_diagnostic( DiagnosticCode::AwaitInSync, arg.get_range(), - t!("Async function can only be called in async function.").to_string(), + "Async function can only be called in async function.".to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/call_non_callable.rs b/crates/glua_code_analysis/src/diagnostic/checker/call_non_callable.rs index ff2fa8b4e..df1b6b852 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/call_non_callable.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/call_non_callable.rs @@ -58,15 +58,15 @@ fn check_call_expr( } let message = if !non_callable_types.is_empty() { - t!( - "Cannot call expression of type `%{full}`; non-callable type(s): %{types}.", + format!( + "Cannot call expression of type `{full}`; non-callable type(s): {types}.", full = humanize_type(db, &call_expr_type, RenderLevel::Detailed), types = non_callable_types.join(", "), ) .to_string() } else { - t!( - "Cannot call expression of type `%{typ}`.", + format!( + "Cannot call expression of type `{typ}`.", typ = humanize_lint_type(db, &call_expr_type), ) .to_string() diff --git a/crates/glua_code_analysis/src/diagnostic/checker/cast_type_mismatch.rs b/crates/glua_code_analysis/src/diagnostic/checker/cast_type_mismatch.rs index a56f5e3e0..d335d5e20 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/cast_type_mismatch.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/cast_type_mismatch.rs @@ -119,14 +119,14 @@ fn add_cast_type_mismatch_diagnostic( TypeCheckFailReason::TypeNotMatch | TypeCheckFailReason::DonotCheck => { "".to_string() } - TypeCheckFailReason::TypeRecursion => t!("type recursion").to_string(), + TypeCheckFailReason::TypeRecursion => "type recursion".to_string(), }; context.add_diagnostic( DiagnosticCode::CastTypeMismatch, range, - t!( - "Cannot cast `%{original}` to `%{target}`. %{reason}", + format!( + "Cannot cast `{original}` to `{target}`. {reason}", original = humanize_lint_type(db, origin_type), target = humanize_lint_type(db, target_type), reason = reason_message diff --git a/crates/glua_code_analysis/src/diagnostic/checker/check_export.rs b/crates/glua_code_analysis/src/diagnostic/checker/check_export.rs index cb6f0ddaa..5c91f6d41 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/check_export.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/check_export.rs @@ -100,8 +100,8 @@ fn check_export_index_expr( context.add_diagnostic( DiagnosticCode::InjectField, index_key.get_range()?, - t!( - "Fields cannot be injected into the reference of `%{class}` for `%{field}`. ", + format!( + "Fields cannot be injected into the reference of `{class}` for `{field}`. ", class = humanize_lint_type(db, export_typ), field = index_name, ) @@ -113,7 +113,7 @@ fn check_export_index_expr( context.add_diagnostic( DiagnosticCode::UndefinedField, index_key.get_range()?, - t!("Undefined field `%{field}`. ", field = index_name,).to_string(), + format!("Undefined field `{field}`. ", field = index_name,).to_string(), None, ); } @@ -177,7 +177,7 @@ fn check_export_index_expr( context.add_diagnostic( DiagnosticCode::UndefinedField, index_key.get_range()?, - t!("Undefined field `%{field}`. ", field = index_name,).to_string(), + format!("Undefined field `{field}`. ", field = index_name,).to_string(), None, ); diff --git a/crates/glua_code_analysis/src/diagnostic/checker/check_field.rs b/crates/glua_code_analysis/src/diagnostic/checker/check_field.rs index 17242b489..f8deaee9d 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/check_field.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/check_field.rs @@ -260,8 +260,8 @@ fn check_index_expr( context.add_diagnostic( DiagnosticCode::UndefinedField, index_key.get_range()?, - t!( - "Cannot call methods via `:` on a table returned by GetTable(). Use dot-access `.%{field}` instead. ", + format!( + "Cannot call methods via `:` on a table returned by GetTable(). Use dot-access `.{field}` instead. ", field = index_key.get_path_part(), ) .to_string(), @@ -329,8 +329,8 @@ fn check_index_expr( context.add_diagnostic( DiagnosticCode::InjectField, index_key.get_range()?, - t!( - "Fields cannot be injected into the reference of `%{class}` for `%{field}`. ", + format!( + "Fields cannot be injected into the reference of `{class}` for `{field}`. ", class = humanize_lint_type(db, &prefix_typ), field = field_name, ) @@ -345,7 +345,7 @@ fn check_index_expr( context.add_diagnostic( DiagnosticCode::UndefinedField, index_key.get_range()?, - t!("Undefined field `%{field}`. ", field = field_name,).to_string(), + format!("Undefined field `{field}`. ", field = field_name,).to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/check_param_count.rs b/crates/glua_code_analysis/src/diagnostic/checker/check_param_count.rs index ead84226f..d8c6ad867 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/check_param_count.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/check_param_count.rs @@ -91,8 +91,8 @@ fn check_closure_expr( context.add_diagnostic( DiagnosticCode::RedundantParameter, param.get_range(), - t!( - "expected %{num} parameters but found %{found_num}", + format!( + "expected {num} parameters but found {found_num}", num = source_params_len, found_num = params.len(), ) @@ -182,7 +182,8 @@ fn check_call_expr( && !is_nullable(context.db, &typ) && !fake_param_optional.get(i).copied().unwrap_or(false) { - miss_parameter_info.push(t!("missing parameter: %{name}", name = param_info.0,)); + miss_parameter_info + .push(format!("missing parameter: {name}", name = param_info.0,)); } } @@ -198,8 +199,8 @@ fn check_call_expr( context.add_diagnostic( DiagnosticCode::MissingParameter, right_paren.get_range(), - t!( - "expected %{num} parameters but found %{found_num}. %{infos}", + format!( + "expected {num} parameters but found {found_num}. {infos}", num = fake_params.len(), found_num = call_args_count, infos = miss_parameter_info.join(" \n ") @@ -256,8 +257,8 @@ fn check_call_expr( context.add_diagnostic( DiagnosticCode::RedundantParameter, arg.get_range(), - t!( - "expected %{num} parameters but found %{found_num}", + format!( + "expected {num} parameters but found {found_num}", num = fake_params.len(), found_num = min_call_args_count, ) diff --git a/crates/glua_code_analysis/src/diagnostic/checker/check_return_count.rs b/crates/glua_code_analysis/src/diagnostic/checker/check_return_count.rs index 1de07751a..add4f5b98 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/check_return_count.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/check_return_count.rs @@ -124,7 +124,7 @@ fn check_missing_return( context.add_diagnostic( DiagnosticCode::MissingReturn, range, - t!("Annotations specify that a return value is required here.").to_string(), + "Annotations specify that a return value is required here.".to_string(), None, ); } @@ -304,8 +304,8 @@ fn check_return_count( context.add_diagnostic( DiagnosticCode::MissingReturnValue, return_stat.get_range(), - t!( - "Annotations specify that at least %{min} return value(s) are required, found %{rmin} returned here instead.", + format!( + "Annotations specify that at least {min} return value(s) are required, found {rmin} returned here instead.", min = min_expected_return_count, rmin = total_return_count ) @@ -319,8 +319,8 @@ fn check_return_count( context.add_diagnostic( DiagnosticCode::RedundantReturnValue, range, - t!( - "Annotations specify that at most %{max} return value(s) are required, found %{rmax} returned here instead.", + format!( + "Annotations specify that at most {max} return value(s) are required, found {rmax} returned here instead.", max = max_expected_return_count?, rmax = total_return_count ) diff --git a/crates/glua_code_analysis/src/diagnostic/checker/circle_doc_class.rs b/crates/glua_code_analysis/src/diagnostic/checker/circle_doc_class.rs index e3ec9a4f0..646bb3316 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/circle_doc_class.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/circle_doc_class.rs @@ -68,7 +68,7 @@ fn check_doc_tag_class( context.add_diagnostic( DiagnosticCode::CircleDocClass, get_lint_range(tag).unwrap_or(tag.get_range()), - t!("Circularly inherited classes.").to_string(), + "Circularly inherited classes.".to_string(), None, ); return Some(()); diff --git a/crates/glua_code_analysis/src/diagnostic/checker/code_style/invert_if.rs b/crates/glua_code_analysis/src/diagnostic/checker/code_style/invert_if.rs index 30441495a..2012dd457 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/code_style/invert_if.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/code_style/invert_if.rs @@ -93,7 +93,7 @@ fn check_early_return_pattern(context: &mut DiagnosticContext, if_statement: &Lu context.add_diagnostic( DiagnosticCode::InvertIf, if_token.get_range(), - t!("Consider inverting 'if' statement to reduce nesting").to_string(), + "Consider inverting 'if' statement to reduce nesting".to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/code_style/non_literal_expressions_in_assert.rs b/crates/glua_code_analysis/src/diagnostic/checker/code_style/non_literal_expressions_in_assert.rs index 085d38173..a4ff916e4 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/code_style/non_literal_expressions_in_assert.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/code_style/non_literal_expressions_in_assert.rs @@ -55,7 +55,7 @@ fn check_assert_rule( context.add_diagnostic( DiagnosticCode::NonLiteralExpressionsInAssert, range, - t!("codestyle.NonLiteralExpressionsInAssert").to_string(), + "Using an assert call with an expensive (non-literal) message expression may cause serious performance regressions.\nThe assert macro is only allowed if the error message is a fixed string literal.\nPlease refactor your code to separate the condition check and error handling.\n\nInstead of:\n local a = assert(foo(), expensive_msg_expression)\n\nUse one of the following forms:\n local a = foo()\n if not a then\n error(expensive_msg_expression)\n end\n".to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/code_style/preferred_local_alias.rs b/crates/glua_code_analysis/src/diagnostic/checker/code_style/preferred_local_alias.rs index 62072977d..0687370a2 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/code_style/preferred_local_alias.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/code_style/preferred_local_alias.rs @@ -300,8 +300,8 @@ fn check_index_expr_preference( context.add_diagnostic( DiagnosticCode::PreferredLocalAlias, index_expr.get_range(), - t!( - "Prefer use local alias variable '%{name}'", + format!( + "Prefer use local alias variable '{name}'", name = alias_info.preferred_name ) .to_string(), diff --git a/crates/glua_code_analysis/src/diagnostic/checker/duplicate_field.rs b/crates/glua_code_analysis/src/diagnostic/checker/duplicate_field.rs index 7868e46d1..3df04f979 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/duplicate_field.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/duplicate_field.rs @@ -185,7 +185,7 @@ fn check_decl_duplicate_field( context.add_diagnostic( DiagnosticCode::DuplicateSetField, signature.member.get_range(), - t!("Duplicate field `%{name}`.", name = key.to_path()).to_string(), + format!("Duplicate field `{name}`.", name = key.to_path()).to_string(), None, ); } @@ -214,7 +214,7 @@ fn check_decl_duplicate_field( DiagnosticCode::DuplicateDocField, // TODO: 范围缩小到名称而不是整个 ---@field field_decl.member.get_range(), - t!("Duplicate field `%{name}`.", name = key.to_path()).to_string(), + format!("Duplicate field `{name}`.", name = key.to_path()).to_string(), None, ); } @@ -289,7 +289,7 @@ fn check_one_member( context.add_diagnostic( DiagnosticCode::DuplicateSetField, in_filed.value.get_range(), - t!("Duplicate field `%{name}`.", name = key.to_path()).to_string(), + format!("Duplicate field `{name}`.", name = key.to_path()).to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/duplicate_index.rs b/crates/glua_code_analysis/src/diagnostic/checker/duplicate_index.rs index ec33b2d26..a20a1355d 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/duplicate_index.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/duplicate_index.rs @@ -66,7 +66,7 @@ fn check_table_duplicate_index( context.add_diagnostic( DiagnosticCode::DuplicateIndex, range, - t!("Duplicate index `%{name}`.", name = name).to_string(), + format!("Duplicate index `{name}`.", name = name).to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/duplicate_require.rs b/crates/glua_code_analysis/src/diagnostic/checker/duplicate_require.rs index 1bcf8d1ab..5925925c3 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/duplicate_require.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/duplicate_require.rs @@ -46,7 +46,7 @@ fn check_require_call_expr( context.add_diagnostic( DiagnosticCode::DuplicateRequire, call_expr.get_range(), - t!("The same file is required multiple times.").to_string(), + "The same file is required multiple times.".to_string(), None, ); return Some(()); diff --git a/crates/glua_code_analysis/src/diagnostic/checker/duplicate_type.rs b/crates/glua_code_analysis/src/diagnostic/checker/duplicate_type.rs index ecaf62bf9..0493ed2c8 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/duplicate_type.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/duplicate_type.rs @@ -62,14 +62,14 @@ fn check_duplicate_class(context: &mut DiagnosticContext, class_tag: LuaDocTagCl context.add_diagnostic( DiagnosticCode::DuplicateType, range, - t!("Duplicate class '%{name}', if this is intentional, please add the 'partial' attribute for every class define", name = name).to_string(), + format!("Duplicate class '{name}', if this is intentional, please add the 'partial' attribute for every class define", name = name).to_string(), None, ); } else if type_times > 0 && partial_times > 0 { context.add_diagnostic( DiagnosticCode::DuplicateType, range, - t!("Duplicate class '%{name}'. The class %{name} is defined as both partial and non-partial.", name = name).to_string(), + format!("Duplicate class '{name}'. The class {name} is defined as both partial and non-partial.", name = name).to_string(), None, ); } @@ -77,8 +77,8 @@ fn check_duplicate_class(context: &mut DiagnosticContext, class_tag: LuaDocTagCl context.add_diagnostic( DiagnosticCode::DuplicateType, range, - t!( - "Duplicate class constructor '%{name}'. constructor must have only one.", + format!( + "Duplicate class constructor '{name}'. constructor must have only one.", name = name ) .to_string(), @@ -119,14 +119,14 @@ fn check_duplicate_enum(context: &mut DiagnosticContext, enum_tag: LuaDocTagEnum context.add_diagnostic( DiagnosticCode::DuplicateType, range, - t!("Duplicate enum '%{name}', if this is intentional, please add the 'partial' attribute for every enum define", name = name).to_string(), + format!("Duplicate enum '{name}', if this is intentional, please add the 'partial' attribute for every enum define", name = name).to_string(), None, ); } else if type_times > 0 && partial_times > 0 { context.add_diagnostic( DiagnosticCode::DuplicateType, range, - t!("Duplicate enum '%{name}'. The enum %{name} is defined as both partial and non-partial.", name = name).to_string(), + format!("Duplicate enum '{name}'. The enum {name} is defined as both partial and non-partial.", name = name).to_string(), None, ); } @@ -158,7 +158,7 @@ fn check_duplicate_alias(context: &mut DiagnosticContext, alias_tag: LuaDocTagAl context.add_diagnostic( DiagnosticCode::DuplicateType, range, - t!( + format!( "Duplicate alias '{name}'. Alias definitions cannot be partial.", name = name ) diff --git a/crates/glua_code_analysis/src/diagnostic/checker/enum_value_mismatch.rs b/crates/glua_code_analysis/src/diagnostic/checker/enum_value_mismatch.rs index 403c53e9a..c4fd156fe 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/enum_value_mismatch.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/enum_value_mismatch.rs @@ -92,8 +92,8 @@ fn check_enum_value_pair( context.add_diagnostic( DiagnosticCode::EnumValueMismatch, value_expr.get_range(), - t!( - "Value '%{value}' does not match any enum value. Expected one of: %{enum_values}", + format!( + "Value '{value}' does not match any enum value. Expected one of: {enum_values}", value = constant_value_str, enum_values = enum_values_str.join(", ") ) diff --git a/crates/glua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs b/crates/glua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs index 05f35e5e9..dadefafc0 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/generic/generic_constraint_mismatch.rs @@ -278,8 +278,7 @@ fn validate_str_tpl_ref( context.add_diagnostic( DiagnosticCode::GenericConstraintMismatch, range, - t!("the string template type does not match any type declaration") - .to_string(), + "the string template type does not match any type declaration".to_string(), None, ); } @@ -310,7 +309,7 @@ fn validate_str_tpl_ref( context.add_diagnostic( DiagnosticCode::GenericConstraintMismatch, range, - t!("the string template type must be a string constant").to_string(), + "the string template type must be a string constant".to_string(), None, ); } @@ -364,8 +363,8 @@ fn add_type_check_diagnostic( context.add_diagnostic( DiagnosticCode::GenericConstraintMismatch, range, - t!( - "type `%{found}` does not satisfy the constraint `%{source}`. %{reason}", + format!( + "type `{found}` does not satisfy the constraint `{source}`. {reason}", source = humanize_type(db, extend_type, RenderLevel::Simple), found = humanize_type(db, expr_type, RenderLevel::Simple), reason = reason_message diff --git a/crates/glua_code_analysis/src/diagnostic/checker/global_non_module.rs b/crates/glua_code_analysis/src/diagnostic/checker/global_non_module.rs index bab0027d3..2568cd1cc 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/global_non_module.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/global_non_module.rs @@ -34,7 +34,7 @@ fn check_assign_stat( context.add_diagnostic( DiagnosticCode::GlobalInNonModule, var.get_range(), - t!("Global variable should only be defined in module scope").to_string(), + "Global variable should only be defined in module scope".to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/gmod_hook_name.rs b/crates/glua_code_analysis/src/diagnostic/checker/gmod_hook_name.rs index d8ec28210..a27225f25 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/gmod_hook_name.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/gmod_hook_name.rs @@ -26,9 +26,9 @@ impl Checker for GmodHookNameChecker { }; let message = match name_issue { - GmodHookNameIssue::Empty => t!("Hook name should not be empty.").to_string(), + GmodHookNameIssue::Empty => "Hook name should not be empty.".to_string(), GmodHookNameIssue::NonStringLiteral => { - t!("Hook name should be a string literal when static.").to_string() + "Hook name should be a string literal when static.".to_string() } }; diff --git a/crates/glua_code_analysis/src/diagnostic/checker/gmod_network.rs b/crates/glua_code_analysis/src/diagnostic/checker/gmod_network.rs index 3367c55bf..35437946f 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/gmod_network.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/gmod_network.rs @@ -133,8 +133,8 @@ fn check_read_write_mismatch( ( DiagnosticCode::GmodNetReadWriteOrderMismatch, receive_flow.receive_range, - t!( - "Read/write structure mismatch for `%{name}`: writer and receiver flows cannot be aligned.", + format!( + "Read/write structure mismatch for `{name}`: writer and receiver flows cannot be aligned.", name = receive_flow.message_name, ) .to_string(), @@ -283,8 +283,8 @@ fn check_missing_send_counterpart( context.add_diagnostic( DiagnosticCode::GmodNetMissingNetworkCounterpart, send_flow.start_range, - t!( - "No `net.Receive` counterpart found for `%{name}` in %{realm} realm.", + format!( + "No `net.Receive` counterpart found for `{name}` in {realm} realm.", name = send_flow.message_name, realm = realm_label(expected_realm), ) @@ -324,8 +324,8 @@ fn check_missing_receive_counterpart( context.add_diagnostic( DiagnosticCode::GmodNetMissingNetworkCounterpart, receive_flow.receive_range, - t!( - "No sending counterpart found for `%{name}` from %{realm} realm.", + format!( + "No sending counterpart found for `{name}` from {realm} realm.", name = receive_flow.message_name, realm = realm_label(expected_sender_realm), ) @@ -351,8 +351,8 @@ fn first_mismatch_diagnostic( return Some(( DiagnosticCode::GmodNetReadWriteOrderMismatch, receive_flow.receive_range, - t!( - "Read/write count mismatch for `%{name}`: writer has %{write_count} values, receiver reads %{read_count} values.", + format!( + "Read/write count mismatch for `{name}`: writer has {write_count} values, receiver reads {read_count} values.", name = receive_flow.message_name, write_count = send_flow.writes.len(), read_count = receive_flow.reads.len(), @@ -376,8 +376,8 @@ fn first_mismatch_diagnostic( return Some(( DiagnosticCode::GmodNetReadWriteOrderMismatch, receive_flow.receive_range, - t!( - "Read/write order mismatch for `%{name}` at position %{position}: expected `%{expected}`, got `%{actual}`.", + format!( + "Read/write order mismatch for `{name}` at position {position}: expected `{expected}`, got `{actual}`.", name = receive_flow.message_name, position = index + 1, expected = expected_read_kind.to_fn_name(), @@ -390,8 +390,8 @@ fn first_mismatch_diagnostic( return Some(( DiagnosticCode::GmodNetReadWriteTypeMismatch, receive_flow.receive_range, - t!( - "Read/write type mismatch for `%{name}` at position %{position}: expected `%{expected}`, got `%{actual}`.", + format!( + "Read/write type mismatch for `{name}` at position {position}: expected `{expected}`, got `{actual}`.", name = receive_flow.message_name, position = index + 1, expected = expected_read_kind.to_fn_name(), @@ -525,8 +525,8 @@ fn check_bits_mismatch( context.add_diagnostic( DiagnosticCode::GmodNetReadWriteBitsMismatch, receive_flow.receive_range, - t!( - "Bit-width mismatch for `%{name}` at position %{position}: writer uses `%{op}(%{expected})`, reader uses `%{rop}(%{actual})`.", + format!( + "Bit-width mismatch for `{name}` at position {position}: writer uses `{op}({expected})`, reader uses `{rop}({actual})`.", name = receive_flow.message_name, position = index + 1, op = write.kind.to_fn_name(), diff --git a/crates/glua_code_analysis/src/diagnostic/checker/gmod_realm_misuse.rs b/crates/glua_code_analysis/src/diagnostic/checker/gmod_realm_misuse.rs index 6fe20ebfd..7b33f9fd3 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/gmod_realm_misuse.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/gmod_realm_misuse.rs @@ -1177,27 +1177,27 @@ fn mismatch_message( let callee_realm = realm_label(callee_realm); match code { - DiagnosticCode::GmodRealmMismatch => t!( - "Realm mismatch: calling `%{name}` in %{call_realm} realm but declaration is %{decl_realm}.", + DiagnosticCode::GmodRealmMismatch => format!( + "Realm mismatch: calling `{name}` in {call_realm} realm but declaration is {decl_realm}.", name = call_name, call_realm = call_realm, decl_realm = callee_realm, ) .to_string(), - DiagnosticCode::GmodRealmMismatchHeuristic => t!( - "Potential realm mismatch (heuristic): `%{name}` is called in inferred %{call_realm} realm while declaration is inferred %{decl_realm}.", + DiagnosticCode::GmodRealmMismatchHeuristic => format!( + "Potential realm mismatch (heuristic): `{name}` is called in inferred {call_realm} realm while declaration is inferred {decl_realm}.", name = call_name, call_realm = call_realm, decl_realm = callee_realm, ) .to_string(), - DiagnosticCode::GmodUnknownRealm => t!( - "Unable to resolve call realm for `%{name}`; declaration appears to be %{decl_realm}.", + DiagnosticCode::GmodUnknownRealm => format!( + "Unable to resolve call realm for `{name}`; declaration appears to be {decl_realm}.", name = call_name, decl_realm = callee_realm, ) .to_string(), - _ => t!("Realm mismatch for `%{name}`.", name = call_name).to_string(), + _ => format!("Realm mismatch for `{name}`.", name = call_name).to_string(), } } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/gmod_systems.rs b/crates/glua_code_analysis/src/diagnostic/checker/gmod_systems.rs index 93ec1b771..060c1a922 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/gmod_systems.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/gmod_systems.rs @@ -35,8 +35,8 @@ impl Checker for GmodSystemsChecker { context.add_diagnostic( DiagnosticCode::GmodUnknownNetMessage, name_range, - t!( - "Unknown net message `%{name}` used by net.Start.", + format!( + "Unknown net message `{name}` used by net.Start.", name = name ) .to_string(), @@ -61,8 +61,8 @@ impl Checker for GmodSystemsChecker { context.add_diagnostic( DiagnosticCode::GmodDuplicateSystemRegistration, name_range, - t!( - "Duplicate %{kind} name `%{name}` is registered multiple times.", + format!( + "Duplicate {kind} name `{name}` is registered multiple times.", kind = "network string", name = name ) @@ -88,8 +88,8 @@ impl Checker for GmodSystemsChecker { context.add_diagnostic( DiagnosticCode::GmodDuplicateSystemRegistration, name_range, - t!( - "Duplicate %{kind} name `%{name}` is registered multiple times.", + format!( + "Duplicate {kind} name `{name}` is registered multiple times.", kind = "concommand", name = name ) @@ -115,8 +115,8 @@ impl Checker for GmodSystemsChecker { context.add_diagnostic( DiagnosticCode::GmodDuplicateSystemRegistration, name_range, - t!( - "Duplicate %{kind} name `%{name}` is registered multiple times.", + format!( + "Duplicate {kind} name `{name}` is registered multiple times.", kind = "convar", name = name ) diff --git a/crates/glua_code_analysis/src/diagnostic/checker/incomplete_signature_doc.rs b/crates/glua_code_analysis/src/diagnostic/checker/incomplete_signature_doc.rs index b136c11f0..dd84690bf 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/incomplete_signature_doc.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/incomplete_signature_doc.rs @@ -49,8 +49,8 @@ fn check_doc( context.add_diagnostic( DiagnosticCode::MissingGlobalDoc, stat.get_range(), - t!( - "Missing comment for global function `%{name}`.", + format!( + "Missing comment for global function `{name}`.", name = function_name ) .to_string(), @@ -131,14 +131,14 @@ fn check_params( let name = name_token.get_name_text(); if !doc_param_names.contains(name) && name != "_" { let message = if is_global { - t!( - "Missing @param annotation for parameter `%{name}` in global function `%{function_name}`.", + format!( + "Missing @param annotation for parameter `{name}` in global function `{function_name}`.", name = name, function_name = function_name ) } else { - t!( - "Incomplete signature. Missing @param annotation for parameter `%{name}`.", + format!( + "Incomplete signature. Missing @param annotation for parameter `{name}`.", name = name ) }; @@ -174,14 +174,14 @@ fn check_returns( if return_stat_len > doc_return_len { let message = if is_global { - t!( - "Missing @return annotation at index `%{index}` in global function `%{function_name}`.", + format!( + "Missing @return annotation at index `{index}` in global function `{function_name}`.", index = i + 1, function_name = function_name ) } else { - t!( - "Incomplete signature. Missing @return annotation at index `%{index}`.", + format!( + "Incomplete signature. Missing @return annotation at index `{index}`.", index = i + 1 ) }; diff --git a/crates/glua_code_analysis/src/diagnostic/checker/local_const_reassign.rs b/crates/glua_code_analysis/src/diagnostic/checker/local_const_reassign.rs index 9222b432c..653729396 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/local_const_reassign.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/local_const_reassign.rs @@ -48,7 +48,7 @@ fn check_local_const_reassign( context.add_diagnostic( DiagnosticCode::LocalConstReassign, decl_ref.range, - t!("Cannot reassign to a constant variable").to_string(), + "Cannot reassign to a constant variable".to_string(), None, ); } @@ -56,7 +56,7 @@ fn check_local_const_reassign( context.add_diagnostic( DiagnosticCode::IterVariableReassign, decl_ref.range, - t!("Should not reassign to iter variable").to_string(), + "Should not reassign to iter variable".to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/missing_fields.rs b/crates/glua_code_analysis/src/diagnostic/checker/missing_fields.rs index efecc6db3..fb7184b8c 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/missing_fields.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/missing_fields.rs @@ -171,8 +171,8 @@ fn check_table_expr( context.add_diagnostic( DiagnosticCode::MissingFields, expr.get_range(), - t!( - "Missing required fields in type `%{typ}`: %{fields}", + format!( + "Missing required fields in type `{typ}`: {fields}", typ = humanize_lint_type(db, &table_type), fields = missing_fields ) diff --git a/crates/glua_code_analysis/src/diagnostic/checker/need_check_nil.rs b/crates/glua_code_analysis/src/diagnostic/checker/need_check_nil.rs index bec56f005..c9094ce91 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/need_check_nil.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/need_check_nil.rs @@ -107,14 +107,14 @@ fn check_call_expr( context.add_diagnostic( DiagnosticCode::UncheckedNilAccess, prefix.get_range(), - t!("%{name} may be nil", name = prefix.syntax().text()).to_string(), + format!("{name} may be nil", name = prefix.syntax().text()).to_string(), None, ); } else if !should_skip_deferred_nullable_function_call(&call_expr, &prefix) { context.add_diagnostic( DiagnosticCode::NeedCheckNil, prefix.get_range(), - t!("function %{name} may be nil", name = prefix.syntax().text()).to_string(), + format!("function {name} may be nil", name = prefix.syntax().text()).to_string(), None, ); } @@ -158,7 +158,7 @@ fn report_unsafe_receiver( context.add_diagnostic( diagnostic_code, receiver.get_range(), - t!("%{name} may be nil", name = receiver.syntax().text()).to_string(), + format!("{name} may be nil", name = receiver.syntax().text()).to_string(), None, ); return true; @@ -175,8 +175,8 @@ fn report_unsafe_receiver( context.add_diagnostic( DiagnosticCode::NeedCheckNil, receiver.get_range(), - t!( - "%{name} may be NULL; check IsValid before calling Entity methods", + format!( + "{name} may be NULL; check IsValid before calling Entity methods", name = receiver.syntax().text() ) .to_string(), @@ -237,7 +237,7 @@ fn check_index_expr( context.add_diagnostic( diagnostic_code, prefix.get_range(), - t!("%{name} may be nil", name = prefix.syntax().text()).to_string(), + format!("{name} may be nil", name = prefix.syntax().text()).to_string(), None, ); } @@ -690,8 +690,8 @@ fn check_binary_expr( context.add_diagnostic( DiagnosticCode::GmodNullCheck, binary_expr.get_range(), - t!( - "%{name} may be NULL; comparing to nil does not prove entity validity, use IsValid(...) instead", + format!( + "{name} may be NULL; comparing to nil does not prove entity validity, use IsValid(...) instead", name = non_nil_side.syntax().text() ) .to_string(), @@ -715,7 +715,7 @@ fn check_binary_expr( context.add_diagnostic( DiagnosticCode::NeedCheckNil, left.get_range(), - t!("%{name} value may be nil", name = left.syntax().text()).to_string(), + format!("{name} value may be nil", name = left.syntax().text()).to_string(), None, ); } @@ -725,7 +725,7 @@ fn check_binary_expr( context.add_diagnostic( DiagnosticCode::NeedCheckNil, right.get_range(), - t!("%{name} value may be nil", name = right.syntax().text()).to_string(), + format!("{name} value may be nil", name = right.syntax().text()).to_string(), None, ); } @@ -769,8 +769,8 @@ fn check_condition_expr( context.add_diagnostic( DiagnosticCode::GmodNullCheck, expr.get_range(), - t!( - "%{name} may be NULL; NULL is truthy, use IsValid(...) to check entity validity", + format!( + "{name} may be NULL; NULL is truthy, use IsValid(...) to check entity validity", name = expr.syntax().text() ) .to_string(), diff --git a/crates/glua_code_analysis/src/diagnostic/checker/param_type_check.rs b/crates/glua_code_analysis/src/diagnostic/checker/param_type_check.rs index d16dde6ad..0c62e5da3 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/param_type_check.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/param_type_check.rs @@ -12,6 +12,7 @@ use crate::{ LuaType, LuaTypeOwner, LuaUnionType, RenderLevel, SemanticDeclLevel, SemanticModel, TypeCheckFailReason, TypeCheckResult, TypeVisitTrait, VariadicType, diagnostic::checker::assign_type_mismatch::check_table_expr, humanize_type, infer_index_expr, + resolve_alias_type, }; use super::{Checker, DiagnosticContext, should_suppress_inferred_value_mismatch}; @@ -248,14 +249,15 @@ fn type_is_callable_with_actionable_params( LuaType::DocFunction(func) => { is_candidate = function_has_actionable_params(func); } - LuaType::Ref(type_id) | LuaType::Def(type_id) => { - is_candidate = db - .get_type_index() - .get_type_decl(type_id) - .and_then(|type_decl| type_decl.get_alias_ref()) - .is_some_and(|origin| { - type_is_callable_with_actionable_params(db, origin, visited_signatures) - }); + LuaType::Ref(_) | LuaType::Def(_) => { + let resolved = resolve_alias_type(db, inner_type); + if resolved.typ != *inner_type { + is_candidate = type_is_callable_with_actionable_params( + db, + &resolved.typ, + visited_signatures, + ); + } } LuaType::Signature(signature_id) => { is_candidate = @@ -757,8 +759,8 @@ fn add_type_check_diagnostic( context.add_diagnostic( DiagnosticCode::ParamTypeMismatch, range, - t!( - "expected `%{source}` but found `%{found}`. %{reason}", + format!( + "expected `{source}` but found `{found}`. {reason}", source = humanize_type(db, param_type, RenderLevel::Simple), found = humanize_type(db, expr_type, RenderLevel::Simple), reason = reason_message diff --git a/crates/glua_code_analysis/src/diagnostic/checker/readonly_check.rs b/crates/glua_code_analysis/src/diagnostic/checker/readonly_check.rs index 34293ce54..0f6eedd68 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/readonly_check.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/readonly_check.rs @@ -158,7 +158,7 @@ fn check_and_report_semantic_id( context.add_diagnostic( DiagnosticCode::ReadOnly, range, - t!("The variable is marked as readonly and cannot be assigned to.").to_string(), + "The variable is marked as readonly and cannot be assigned to.".to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/redefined_local.rs b/crates/glua_code_analysis/src/diagnostic/checker/redefined_local.rs index 87723fe50..38020f0db 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/redefined_local.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/redefined_local.rs @@ -45,7 +45,8 @@ impl Checker for RedefinedLocalChecker { context.add_diagnostic( DiagnosticCode::RedefinedLocal, decl.get_range(), - t!("Redefined local variable `%{name}`", name = decl.get_name()).to_string(), + format!("Redefined local variable `{name}`", name = decl.get_name()) + .to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/require_module_visibility.rs b/crates/glua_code_analysis/src/diagnostic/checker/require_module_visibility.rs index f8c9b49ff..e838cb146 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/require_module_visibility.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/require_module_visibility.rs @@ -47,8 +47,8 @@ fn check_require_call_expr( context.add_diagnostic( DiagnosticCode::RequireModuleNotVisible, arg_expr.get_range(), - t!( - "Module '%{module}' is not visible. It has @export restrictions.", + format!( + "Module '{module}' is not visible. It has @export restrictions.", module = module_info.full_module_name ) .to_string(), diff --git a/crates/glua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs b/crates/glua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs index d6c2305be..7d5a0efea 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/return_type_mismatch.rs @@ -196,8 +196,8 @@ fn add_type_check_diagnostic( context.add_diagnostic( DiagnosticCode::ReturnTypeMismatch, range, - t!( - "Annotations specify that return value %{index} has a type of `%{source}`, returning value of type `%{found}` here instead. %{reason}", + format!( + "Annotations specify that return value {index} has a type of `{source}`, returning value of type `{found}` here instead. {reason}", index = index + 1, source = humanize_lint_type(db, param_type), found = humanize_lint_type(db, expr_type), diff --git a/crates/glua_code_analysis/src/diagnostic/checker/syntax_error.rs b/crates/glua_code_analysis/src/diagnostic/checker/syntax_error.rs index 53c9abc0e..4f901287e 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/syntax_error.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/syntax_error.rs @@ -93,15 +93,15 @@ fn check_normal_string_error(string_token: &LuaSyntaxToken) -> Result<(), String let hex = chars.by_ref().take(2).collect::(); if hex.len() == 2 && hex.chars().all(|c| c.is_ascii_hexdigit()) { if u8::from_str_radix(&hex, 16).is_err() { - return Err(t!( - "Invalid hex escape sequence '\\x%{hex}'", + return Err(format!( + "Invalid hex escape sequence '\\x{hex}'", hex = hex ) .to_string()); } } else { - return Err(t!( - "Invalid hex escape sequence '\\x%{hex}'", + return Err(format!( + "Invalid hex escape sequence '\\x{hex}'", hex = hex ) .to_string()); @@ -115,8 +115,8 @@ fn check_normal_string_error(string_token: &LuaSyntaxToken) -> Result<(), String if let Ok(code_point) = u32::from_str_radix(&unicode_hex, 16) && std::char::from_u32(code_point).is_none() { - return Err(t!( - "Invalid unicode escape sequence '\\u{{%{unicode_hex}}}'", + return Err(format!( + "Invalid unicode escape sequence '\\u{{{unicode_hex}}}'", unicode_hex = unicode_hex ) .to_string()); @@ -175,7 +175,7 @@ fn check_dots_literal_error( context.add_diagnostic( DiagnosticCode::SyntaxError, literal_expr.get_range(), - t!("Cannot use `...` outside a vararg function.").to_string(), + "Cannot use `...` outside a vararg function.".to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/unbalanced_assignments.rs b/crates/glua_code_analysis/src/diagnostic/checker/unbalanced_assignments.rs index 0b85bd539..2d180080b 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/unbalanced_assignments.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/unbalanced_assignments.rs @@ -73,7 +73,7 @@ fn check_unbalanced_assignment( context.add_diagnostic( DiagnosticCode::UnbalancedAssignments, var.get_range(), - t!("The value is assigned as `nil` because the number of values is not enough.") + "The value is assigned as `nil` because the number of values is not enough." .to_string(), None, ); diff --git a/crates/glua_code_analysis/src/diagnostic/checker/undefined_doc_param.rs b/crates/glua_code_analysis/src/diagnostic/checker/undefined_doc_param.rs index f952dcbfe..9d03b42ad 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/undefined_doc_param.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/undefined_doc_param.rs @@ -34,8 +34,8 @@ fn check_doc_param( context.add_diagnostic( DiagnosticCode::UndefinedDocParam, name_token.get_range(), - t!( - "Undefined doc param: `%{name}`", + format!( + "Undefined doc param: `{name}`", name = name_token.get_name_text() ) .to_string(), diff --git a/crates/glua_code_analysis/src/diagnostic/checker/undefined_global.rs b/crates/glua_code_analysis/src/diagnostic/checker/undefined_global.rs index 290ac85e7..8c40be59d 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/undefined_global.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/undefined_global.rs @@ -1,9 +1,9 @@ use std::collections::{HashMap, HashSet}; use glua_parser::{ - BinaryOperator, LuaAssignStat, LuaAstNode, LuaBlock, LuaCallExpr, LuaClosureExpr, LuaExpr, - LuaIfStat, LuaIndexKey, LuaLiteralToken, LuaLocalStat, LuaNameExpr, LuaStat, LuaTableField, - UnaryOperator, + BinaryOperator, LuaAssignStat, LuaAstNode, LuaBinaryExpr, LuaBlock, LuaCallExpr, + LuaClosureExpr, LuaExpr, LuaIfStat, LuaIndexKey, LuaLiteralToken, LuaLocalStat, LuaNameExpr, + LuaStat, LuaTableField, UnaryOperator, }; use rowan::{TextRange, TextSize}; @@ -83,6 +83,14 @@ fn calc_guarded_name_expr_ranges(semantic_model: &SemanticModel) -> HashSet() { + collect_short_circuit_guarded_name_expr_ranges( + semantic_model, + &binary_expr, + &mut guarded_ranges, + ); + } + guarded_ranges } @@ -106,17 +114,19 @@ fn calc_continuation_guarded_name_expr_ranges( continue; }; - let Some(guarded_name) = continuation_guard_name(semantic_model, &if_stat) else { + let Some(guarded_names) = continuation_guard_names(semantic_model, &if_stat) else { continue; }; - guard_rules_by_name - .entry(guarded_name) - .or_default() - .push(ContinuationGuardRule { - block_range, - guard_start: if_stat.get_range().end(), - }); + for guarded_name in guarded_names { + guard_rules_by_name + .entry(guarded_name) + .or_default() + .push(ContinuationGuardRule { + block_range, + guard_start: if_stat.get_range().end(), + }); + } } } @@ -146,13 +156,77 @@ fn calc_continuation_guarded_name_expr_ranges( guarded_ranges } -fn continuation_guard_name(semantic_model: &SemanticModel, if_stat: &LuaIfStat) -> Option { +fn continuation_guard_names( + semantic_model: &SemanticModel, + if_stat: &LuaIfStat, +) -> Option> { let block = if_stat.get_block()?; if !is_return_only_block(&block) { return None; } - extract_continuation_guarded_name(semantic_model, &if_stat.get_condition_expr()?) + let names = extract_continuation_guarded_names(semantic_model, &if_stat.get_condition_expr()?); + if names.is_empty() { None } else { Some(names) } +} + +fn collect_short_circuit_guarded_name_expr_ranges( + semantic_model: &SemanticModel, + binary_expr: &LuaBinaryExpr, + guarded_ranges: &mut HashSet, +) { + let op = binary_expr + .get_op_token() + .map(|op| op.get_op()) + .unwrap_or(BinaryOperator::OpNop); + + if op != BinaryOperator::OpAnd { + return; + } + + let Some((left_expr, right_expr)) = binary_expr.get_exprs() else { + return; + }; + + let mut lhs_guard_ranges = HashSet::new(); + let lhs_guarded_names = + collect_truthy_guarded_names(semantic_model, &left_expr, &mut lhs_guard_ranges); + guarded_ranges.extend(lhs_guard_ranges); + + if lhs_guarded_names.is_empty() { + return; + } + + for rhs_name_expr in right_expr.descendants::() { + let Some(name_text) = rhs_name_expr.get_name_text() else { + continue; + }; + + if lhs_guarded_names.contains(name_text.as_str()) { + guarded_ranges.insert(rhs_name_expr.get_range()); + } + } +} + +#[derive(Debug)] +enum GuardedTarget { + ArgName(LuaNameExpr), + GlobalName(String), +} + +impl GuardedTarget { + fn name(&self) -> Option { + match self { + Self::ArgName(name_expr) => name_expr.get_name_text().map(|name| name.to_string()), + Self::GlobalName(name_text) => Some(name_text.clone()), + } + } + + fn range(&self) -> Option { + match self { + Self::ArgName(name_expr) => Some(name_expr.get_range()), + Self::GlobalName(_) => None, + } + } } fn is_return_only_block(block: &LuaBlock) -> bool { @@ -173,50 +247,59 @@ fn is_return_only_block(block: &LuaBlock) -> bool { has_return_stat } -fn extract_continuation_guarded_name( +fn extract_continuation_guarded_names( semantic_model: &SemanticModel, expr: &LuaExpr, -) -> Option { +) -> HashSet { + let mut names = HashSet::new(); + match expr { LuaExpr::ParenExpr(paren_expr) => { - extract_continuation_guarded_name(semantic_model, &paren_expr.get_expr()?) + if let Some(inner_expr) = paren_expr.get_expr() { + names.extend(extract_continuation_guarded_names( + semantic_model, + &inner_expr, + )); + } } LuaExpr::UnaryExpr(unary_expr) => { let is_not = unary_expr .get_op_token() .is_some_and(|op| op.get_op() == UnaryOperator::OpNot); if !is_not { - return None; + return HashSet::new(); } - extract_truthy_guarded_name(semantic_model, &unary_expr.get_expr()?) + if let Some(inner_expr) = unary_expr.get_expr() { + let mut condition_guard_ranges = HashSet::new(); + names.extend(collect_truthy_guarded_names( + semantic_model, + &inner_expr, + &mut condition_guard_ranges, + )); + } } LuaExpr::BinaryExpr(binary_expr) => { let is_eq = binary_expr .get_op_token() .is_some_and(|op| op.get_op() == BinaryOperator::OpEq); if !is_eq { - return None; + return HashSet::new(); } - let (left_expr, right_expr) = binary_expr.get_exprs()?; - name_compared_with_nil(&left_expr, &right_expr) - .and_then(|name_expr| name_expr.get_name_text().map(|text| text.to_string())) + let Some((left_expr, right_expr)) = binary_expr.get_exprs() else { + return names; + }; + if let Some(name_expr) = name_compared_with_nil(&left_expr, &right_expr) + && let Some(name_text) = name_expr.get_name_text() + { + names.insert(name_text.to_string()); + } } - _ => None, + _ => {} } -} -fn extract_truthy_guarded_name(semantic_model: &SemanticModel, expr: &LuaExpr) -> Option { - match expr { - LuaExpr::ParenExpr(paren_expr) => { - extract_truthy_guarded_name(semantic_model, &paren_expr.get_expr()?) - } - LuaExpr::NameExpr(name_expr) => name_expr.get_name_text().map(|text| text.to_string()), - LuaExpr::CallExpr(call_expr) => guarded_call_target_name(semantic_model, call_expr) - .and_then(|name_expr| name_expr.get_name_text().map(|text| text.to_string())), - _ => None, - } + names } fn collect_clause_guarded_name_ranges( @@ -359,11 +442,13 @@ fn collect_truthy_guarded_names( } LuaExpr::CallExpr(call_expr) => { let mut names = HashSet::new(); - if let Some(name_expr) = guarded_call_target_name(semantic_model, call_expr) - && let Some(name_text) = name_expr.get_name_text() + if let Some(guarded_target) = guarded_call_target_name(semantic_model, call_expr) + && let Some(name_text) = guarded_target.name() { - condition_guard_ranges.insert(name_expr.get_range()); - names.insert(name_text.to_string()); + if let Some(guard_range) = guarded_target.range() { + condition_guard_ranges.insert(guard_range); + } + names.insert(name_text); } names } @@ -416,9 +501,13 @@ fn is_nil_literal(expr: &LuaExpr) -> bool { fn guarded_call_target_name( semantic_model: &SemanticModel, call_expr: &LuaCallExpr, -) -> Option { +) -> Option { let prefix_expr = call_expr.get_prefix_expr()?; + if let Some(name) = guarded_call_require_target_name(semantic_model, call_expr, &prefix_expr) { + return Some(name); + } + match prefix_expr { LuaExpr::NameExpr(name_expr) => { let helper_name = name_expr.get_name_text()?; @@ -429,7 +518,7 @@ fn guarded_call_target_name( } let first_arg = call_expr.get_args_list()?.get_args().next()?; - unwrap_paren_to_name_expr(&first_arg) + unwrap_paren_to_name_expr(&first_arg).map(GuardedTarget::ArgName) } LuaExpr::IndexExpr(index_expr) => { if !call_expr.is_colon_call() { @@ -444,12 +533,67 @@ fn guarded_call_target_name( return None; } - unwrap_paren_to_name_expr(&index_expr.get_prefix_expr()?) + unwrap_paren_to_name_expr(&index_expr.get_prefix_expr()?).map(GuardedTarget::ArgName) } _ => None, } } +fn guarded_call_require_target_name( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + prefix_expr: &LuaExpr, +) -> Option { + if call_expr.is_colon_call() { + return None; + } + + let Ok(LuaType::Signature(signature_id)) = semantic_model.infer_expr(prefix_expr.clone()) + else { + return None; + }; + + let signature = semantic_model + .get_db() + .get_signature_index() + .get(&signature_id)?; + + let guard_arg_idx = signature.require_guard_param()?; + + let arg_expr = call_expr.get_args_list()?.get_args().nth(guard_arg_idx)?; + + let module_name = literal_string_expr_value(&arg_expr)?; + + if is_lua_identifier(&module_name) { + Some(GuardedTarget::GlobalName(module_name)) + } else { + None + } +} + +fn literal_string_expr_value(expr: &LuaExpr) -> Option { + match expr { + LuaExpr::LiteralExpr(literal_expr) => match literal_expr.get_literal()? { + LuaLiteralToken::String(string_token) => Some(string_token.get_value()), + _ => None, + }, + LuaExpr::ParenExpr(paren_expr) => paren_expr + .get_expr() + .and_then(|expr| literal_string_expr_value(&expr)), + _ => None, + } +} + +fn is_lua_identifier(text: &str) -> bool { + let mut chars = text.chars(); + let Some(first_char) = chars.next() else { + return false; + }; + + (first_char == '_' || first_char.is_ascii_alphabetic()) + && chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) +} + fn collect_truthy_guarded_names_with_not_chain( semantic_model: &SemanticModel, expr: &LuaExpr, @@ -552,8 +696,10 @@ fn collect_condition_guard_side_effects( // `IsValid(x)` / `x:IsValid()` style helper calls already imply // their target exists — reuse the existing helper so we stay in // sync with the truthy-path detection. - if let Some(name_expr) = guarded_call_target_name(semantic_model, call_expr) { - condition_guard_ranges.insert(name_expr.get_range()); + if let Some(guarded_target) = guarded_call_target_name(semantic_model, call_expr) + && let Some(range) = guarded_target.range() + { + condition_guard_ranges.insert(range); } // Also walk argument expressions — `foo(x.y)` should still guard `x`. if let Some(args_list) = call_expr.get_args_list() { @@ -728,7 +874,7 @@ fn check_name_expr( context.add_diagnostic( undefined_global_diagnostic_code(name_range, silent_use_ranges), name_range, - t!("undefined global variable: %{name}", name = name_text).to_string(), + format!("undefined global variable: {name}", name = name_text).to_string(), None, ); return Some(()); @@ -808,7 +954,7 @@ fn check_name_expr( context.add_diagnostic( diag_code, name_range, - t!("undefined global variable: %{name}", name = name_text).to_string(), + format!("undefined global variable: {name}", name = name_text).to_string(), None, ); diff --git a/crates/glua_code_analysis/src/diagnostic/checker/unknown_doc_tag.rs b/crates/glua_code_analysis/src/diagnostic/checker/unknown_doc_tag.rs index faedbaceb..24b11316d 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/unknown_doc_tag.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/unknown_doc_tag.rs @@ -46,7 +46,7 @@ fn check_tag( context.add_diagnostic( DiagnosticCode::UnknownDocTag, token.get_range(), - t!("Unknown doc tag: `%{name}`", name = token.get_text()).to_string(), + format!("Unknown doc tag: `{name}`", name = token.get_text()).to_string(), Some(Value::String(token.get_text().to_string())), ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/unnecessary_assert.rs b/crates/glua_code_analysis/src/diagnostic/checker/unnecessary_assert.rs index d6b4b6911..371e1a469 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/unnecessary_assert.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/unnecessary_assert.rs @@ -37,15 +37,14 @@ fn check_assert_rule( context.add_diagnostic( DiagnosticCode::UnnecessaryAssert, call_expr.get_range(), - t!("Unnecessary assert: this expression is always truthy").to_string(), + "Unnecessary assert: this expression is always truthy".to_string(), None, ); } else if first_type.is_always_falsy() { context.add_diagnostic( DiagnosticCode::UnnecessaryAssert, call_expr.get_range(), - t!("Impossible assert: this expression is always falsy; prefer `error()`") - .to_string(), + "Impossible assert: this expression is always falsy; prefer `error()`".to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/unnecessary_if.rs b/crates/glua_code_analysis/src/diagnostic/checker/unnecessary_if.rs index b1ed4f286..bff78ef8b 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/unnecessary_if.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/unnecessary_if.rs @@ -35,14 +35,14 @@ fn check_condition( context.add_diagnostic( DiagnosticCode::UnnecessaryIf, condition.get_range(), - t!("Unnecessary `if` statement: this condition is always truthy").to_string(), + "Unnecessary `if` statement: this condition is always truthy".to_string(), None, ); } else if expr_type.is_always_falsy() { context.add_diagnostic( DiagnosticCode::UnnecessaryIf, condition.get_range(), - t!("Impossible `if` statement: this condition is always falsy").to_string(), + "Impossible `if` statement: this condition is always falsy".to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/checker/unused.rs b/crates/glua_code_analysis/src/diagnostic/checker/unused.rs index c2c5f4414..3e42f9033 100644 --- a/crates/glua_code_analysis/src/diagnostic/checker/unused.rs +++ b/crates/glua_code_analysis/src/diagnostic/checker/unused.rs @@ -43,8 +43,8 @@ impl Checker for UnusedChecker { context.add_diagnostic( DiagnosticCode::Unused, range, - t!( - "%{name} is never used, if this is intentional, prefix it with an underscore: _%{name}", + format!( + "{name} is never used, if this is intentional, prefix it with an underscore: _{name}", name = name ).to_string(), None) @@ -63,9 +63,7 @@ impl Checker for UnusedChecker { context.add_diagnostic( DiagnosticCode::UnusedSelf, range, - t!( - "Implicit self is never used, if this is intentional, please use '.' instead of ':' to define the method", - ).to_string(), + "Implicit self is never used, if this is intentional, please use '.' instead of ':' to define the method".to_string(), None, ); } diff --git a/crates/glua_code_analysis/src/diagnostic/test/gmod_hook_name_test.rs b/crates/glua_code_analysis/src/diagnostic/test/gmod_hook_name_test.rs index 3dcfaf47c..40ce6546a 100644 --- a/crates/glua_code_analysis/src/diagnostic/test/gmod_hook_name_test.rs +++ b/crates/glua_code_analysis/src/diagnostic/test/gmod_hook_name_test.rs @@ -7,6 +7,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); } #[gtest] diff --git a/crates/glua_code_analysis/src/diagnostic/test/gmod_systems_test.rs b/crates/glua_code_analysis/src/diagnostic/test/gmod_systems_test.rs index 19819d375..4c8c69424 100644 --- a/crates/glua_code_analysis/src/diagnostic/test/gmod_systems_test.rs +++ b/crates/glua_code_analysis/src/diagnostic/test/gmod_systems_test.rs @@ -9,6 +9,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); } #[gtest] diff --git a/crates/glua_code_analysis/src/diagnostic/test/legacy_module_test.rs b/crates/glua_code_analysis/src/diagnostic/test/legacy_module_test.rs index 401e7105a..adbc56d31 100644 --- a/crates/glua_code_analysis/src/diagnostic/test/legacy_module_test.rs +++ b/crates/glua_code_analysis/src/diagnostic/test/legacy_module_test.rs @@ -321,7 +321,7 @@ mod test { #[test] fn legacy_module_namespace_does_not_leak_across_main_workspaces() { let mut analysis = crate::EmmyLuaAnalysis::new(); - analysis.init_std_lib(None); + analysis.init_std_lib(); let mut emmyrc = Emmyrc::default(); emmyrc.workspace.enable_isolation = true; analysis.update_config(Arc::new(emmyrc)); diff --git a/crates/glua_code_analysis/src/diagnostic/test/param_type_check_test.rs b/crates/glua_code_analysis/src/diagnostic/test/param_type_check_test.rs index 55f29098c..c37bb89e7 100644 --- a/crates/glua_code_analysis/src/diagnostic/test/param_type_check_test.rs +++ b/crates/glua_code_analysis/src/diagnostic/test/param_type_check_test.rs @@ -25,6 +25,27 @@ mod test { )); } + #[test] + fn test_mutual_alias_type_check_does_not_overflow() { + let mut ws = VirtualWorkspace::new(); + + assert!(!ws.check_code_for( + DiagnosticCode::ParamTypeMismatch, + r#" + ---@alias AliasA AliasB + ---@alias AliasB AliasA + + ---@param value string + local function takesString(value) end + + ---@type AliasA + local value + + takesString(value) + "# + )); + } + #[test] fn test_issue_82() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/glua_code_analysis/src/diagnostic/test/undefined_field_test.rs b/crates/glua_code_analysis/src/diagnostic/test/undefined_field_test.rs index cd4196933..210eba67a 100644 --- a/crates/glua_code_analysis/src/diagnostic/test/undefined_field_test.rs +++ b/crates/glua_code_analysis/src/diagnostic/test/undefined_field_test.rs @@ -94,6 +94,30 @@ mod test { .expect("expected inferred index expression type") } + fn index_expr_type_displays( + ws: &VirtualWorkspace, + file_id: crate::FileId, + expr_text: &str, + ) -> Vec { + let semantic_model = ws + .analysis + .compilation + .get_semantic_model(file_id) + .expect("expected semantic model"); + + semantic_model + .get_root() + .descendants::() + .filter(|index_expr| index_expr.syntax().text() == expr_text) + .map(|index_expr| { + let typ = semantic_model + .infer_expr(LuaExpr::IndexExpr(index_expr)) + .expect("expected inferred index expression type"); + ws.humanize_type(typ) + }) + .collect() + } + fn contains_empty_table_bootstrap(db: &crate::DbIndex, typ: &LuaType) -> bool { match typ { LuaType::Table => true, @@ -254,6 +278,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.analysis .diagnostic .enable_only(DiagnosticCode::UndefinedField); @@ -364,6 +389,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.analysis .diagnostic .enable_only(DiagnosticCode::UndefinedField); @@ -428,6 +454,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.analysis .diagnostic .enable_only(DiagnosticCode::UndefinedField); @@ -768,6 +795,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "gamemodes/helix/gamemode/shared.lua", @@ -3686,6 +3714,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.analysis .diagnostic .enable_only(DiagnosticCode::UndefinedField); @@ -3696,12 +3725,15 @@ mod test { ix = ix or { gui = {} } "#, ); - ws.def_file( - "gamemodes/helix-hl2rp/schema/derma/cl_combinedisplay.lua", + ws.def( r#" ---@class DPanel ---@field Remove fun(self: DPanel) - + "#, + ); + ws.def_file( + "gamemodes/helix-hl2rp/schema/derma/cl_combinedisplay.lua", + r#" local PANEL = {} function PANEL:Init() @@ -3722,6 +3754,8 @@ mod test { end "#, ); + let gui_displays = index_expr_type_displays(&ws, hooks_file, "ix.gui"); + let combine_displays = index_expr_type_displays(&ws, hooks_file, "ix.gui.combine"); let diagnostics = ws .analysis @@ -3739,7 +3773,7 @@ mod test { assert!( undefined_fields.is_empty(), - "unexpected UndefinedField diagnostics for panel self-assignment: {undefined_fields:#?}" + "unexpected UndefinedField diagnostics for panel self-assignment on gui={gui_displays:?} combine={combine_displays:?}: {undefined_fields:#?}" ); } @@ -3749,6 +3783,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "gamemodes/helix/gamemode/cl_init.lua", @@ -3786,6 +3821,7 @@ mod test { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "gamemodes/helix/gamemode/cl_init.lua", diff --git a/crates/glua_code_analysis/src/diagnostic/test/undefined_global_test.rs b/crates/glua_code_analysis/src/diagnostic/test/undefined_global_test.rs index 9627345c1..7ea10e49c 100644 --- a/crates/glua_code_analysis/src/diagnostic/test/undefined_global_test.rs +++ b/crates/glua_code_analysis/src/diagnostic/test/undefined_global_test.rs @@ -79,6 +79,35 @@ mod test { }) } + /// Like [`has_undefined_global_name`] but matches either `UndefinedGlobal` or + /// `UndefinedGlobalAssignment` for the same name. + fn has_any_undefined_global_name( + ws: &mut VirtualWorkspace, + file_path: &str, + content: &str, + name: &str, + ) -> bool { + let file_id = ws.def_file(file_path, content); + let diagnostics = ws + .analysis + .diagnose_file(file_id, CancellationToken::new()) + .unwrap_or_default(); + let strict_code = Some(NumberOrString::String( + DiagnosticCode::UndefinedGlobal.get_name().to_string(), + )); + let assignment_code = Some(NumberOrString::String( + DiagnosticCode::UndefinedGlobalAssignment + .get_name() + .to_string(), + )); + let message_needled = format!("undefined global variable: {name}"); + + diagnostics.iter().any(|diagnostic| { + (diagnostic.code == strict_code || diagnostic.code == assignment_code) + && diagnostic.message.contains(&message_needled) + }) + } + #[test] fn test_issue_250() { let mut ws = VirtualWorkspace::new_with_init_std_lib(); @@ -171,6 +200,27 @@ mod test { )); } + #[test] + fn test_guard_clause_not_compound_and_suppresses_following_uses() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + assert!(!has_undefined_global_name( + &mut ws, + "lua/starfall/libs_sv/wire.lua", + r#" + module("sf_wire", package.seeall) + + return function(instance) + if not (WireLib and WireLib.CreateInputs) then return end + + instance:AddHook("initialize", function() + WireLib.CreateInputs() + end) + end + "#, + "WireLib", + )); + } + #[test] fn test_top_level_guard_clause_not_global_suppresses_later_direct_use() { let mut ws = VirtualWorkspace::new_with_init_std_lib(); @@ -515,6 +565,47 @@ mod test { )); } + #[test] + fn test_short_circuit_and_guards_rhs_global_use() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + assert!(!has_undefined_global_name( + &mut ws, + "lua/entities/starfall_processor/init.lua", + r#" + function ENT:PreEntityCopy() + local info = WireLib and WireLib.BuildDupeInfo(self) or {} + end + "#, + "WireLib", + )); + } + + #[test] + fn test_short_circuit_and_lhs_optional_global_probe_is_silent() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + assert!(!has_undefined_global_name( + &mut ws, + "test.lua", + r#" + local enabled = OptionalAddon and true + "#, + "OptionalAddon", + )); + } + + #[test] + fn test_short_circuit_and_does_not_guard_unrelated_rhs_global() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + assert!(has_undefined_global_name( + &mut ws, + "test.lua", + r#" + local info = WireLib and OtherLib.BuildDupeInfo(self) or {} + "#, + "OtherLib", + )); + } + #[test] fn test_derma_define_control_panel_not_undefined_global() { let mut ws = VirtualWorkspace::new(); @@ -962,6 +1053,316 @@ mod test { )); } + #[test] + fn test_sf_require_wrapper_suppresses_xinput_access_across_files() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def_file( + "lua/starfall/libs_cl/sf_require.lua", + r#" + SF = {} + + function SF.Require(name) + if util.IsBinaryModuleInstalled(name) then + local ok, err = pcall(require, name) + if ok then + return true + else + ErrorNoHalt(err .. "\n") + return false + end + end + return false + end + "#, + ); + + assert!(!has_any_undefined_global_name( + &mut ws, + "lua/starfall/libs_cl/xinput.lua", + r#" + if not SF.Require("xinput") then return function() end end + xinput.getState() + "#, + "xinput", + )); + } + + #[test] + fn test_sf_require_dot_index_continuation_guard_allows_late_access() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def_file( + "lua/starfall/libs_cl/sf_require.lua", + r#" + SF = {} + + function SF.Require(name) + if util.IsBinaryModuleInstalled(name) then + local ok, err = pcall(require, name) + if ok then + return true + else + ErrorNoHalt(err .. "\n") + return false + end + end + return false + end + "#, + ); + + assert!(!has_any_undefined_global_name( + &mut ws, + "lua/starfall/libs_cl/xinput_guard.lua", + r#" + function GetInputState() + if not SF.Require("xinput") then + return function() end + end + + return xinput.getState() + end + "#, + "xinput", + )); + } + + #[test] + fn test_sf_require_does_not_suppress_use_before_guard() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def_file( + "lua/starfall/libs_cl/sf_require.lua", + r#" + SF = {} + + function SF.Require(name) + if util.IsBinaryModuleInstalled(name) then + local ok, err = pcall(require, name) + if ok then + return true + else + ErrorNoHalt(err .. "\n") + return false + end + end + return false + end + "#, + ); + + assert!(has_undefined_global_name( + &mut ws, + "lua/starfall/libs_cl/xinput.lua", + r#" + xinput.getState() + if not SF.Require("xinput") then return function() end end + "#, + "xinput", + )); + } + + #[test] + fn test_sf_require_is_binary_module_installed_check_only_does_not_suppress() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def_file( + "lua/starfall/libs_cl/sf_require.lua", + r#" + SF = {} + + function SF.Require(name) + if util.IsBinaryModuleInstalled(name) then + return true + end + return false + end + "#, + ); + + assert!(has_undefined_global_name( + &mut ws, + "lua/starfall/libs_cl/xinput.lua", + r#" + if not SF.Require("xinput") then return function() end end + xinput.getState() + "#, + "xinput", + )); + } + + #[test] + fn test_sf_require_unconditional_true_after_pcall_does_not_suppress() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def_file( + "lua/starfall/libs_cl/sf_require.lua", + r#" + SF = {} + + function SF.Require(name) + local ok = pcall(require, name) + return true + end + "#, + ); + + assert!(has_any_undefined_global_name( + &mut ws, + "lua/starfall/libs_cl/xinput.lua", + r#" + if not SF.Require("xinput") then return function() end end + xinput.getState() + "#, + "xinput", + )); + } + + #[test] + fn test_sf_require_mutated_pcall_ok_does_not_suppress() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def_file( + "lua/starfall/libs_cl/sf_require.lua", + r#" + SF = {} + + function SF.Require(name) + local ok = pcall(require, name) + ok = true + if ok then + return true + end + return false + end + "#, + ); + + assert!(has_any_undefined_global_name( + &mut ws, + "lua/starfall/libs_cl/xinput.lua", + r#" + if not SF.Require("xinput") then return function() end end + xinput.getState() + "#, + "xinput", + )); + } + + #[test] + fn test_util_binary_module_installed_check_alone_does_not_suppress_xinput() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + + assert!(has_undefined_global_name( + &mut ws, + "lua/starfall/libs_cl/xinput.lua", + r#" + if util.IsBinaryModuleInstalled("xinput") then + xinput.getState() + end + "#, + "xinput", + )); + } + + #[test] + fn test_sf_require_non_literal_module_name_is_not_a_guard() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def_file( + "lua/starfall/libs_cl/sf_require.lua", + r#" + SF = {} + + function SF.Require(name) + if util.IsBinaryModuleInstalled(name) then + local ok, err = pcall(require, name) + if ok then + return true + else + ErrorNoHalt(err .. "\n") + return false + end + end + return false + end + "#, + ); + + assert!(has_any_undefined_global_name( + &mut ws, + "lua/starfall/libs_cl/xinput.lua", + r#" + local modname = "xinput" + if not SF.Require(modname) then return function() end end + xinput.getState() + "#, + "xinput", + )); + } + + #[test] + fn test_sf_require_truthy_clause_guard_suppresses_xinput() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def_file( + "lua/starfall/libs_cl/sf_require.lua", + r#" + SF = {} + + function SF.Require(name) + if util.IsBinaryModuleInstalled(name) then + local ok, err = pcall(require, name) + if ok then + return true + else + ErrorNoHalt(err .. "\n") + return false + end + end + return false + end + "#, + ); + + assert!(!has_any_undefined_global_name( + &mut ws, + "lua/starfall/libs_cl/xinput.lua", + r#" + if SF.Require("xinput") then + xinput.getState() + end + "#, + "xinput", + )); + } + + #[test] + fn test_sf_require_short_circuit_and_does_not_warn_xinput() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + ws.def_file( + "lua/starfall/libs_cl/sf_require.lua", + r#" + SF = {} + + function SF.Require(name) + if util.IsBinaryModuleInstalled(name) then + local ok, err = pcall(require, name) + if ok then + return true + else + ErrorNoHalt(err .. "\n") + return false + end + end + return false + end + "#, + ); + + assert!(!has_any_undefined_global_name( + &mut ws, + "lua/starfall/libs_cl/xinput.lua", + r#" + SF.Require("xinput") and xinput.getState() + "#, + "xinput", + )); + } + /// Short-circuit guard: in `if a and tmysql.Version then T else F end`, /// the *else* branch is reached when `a` is falsy, in which case /// `tmysql.Version` was never evaluated. We must NOT widen `tmysql` in diff --git a/crates/glua_code_analysis/src/lib.rs b/crates/glua_code_analysis/src/lib.rs index 190694c35..c7eda250a 100644 --- a/crates/glua_code_analysis/src/lib.rs +++ b/crates/glua_code_analysis/src/lib.rs @@ -13,7 +13,6 @@ mod config; mod db_index; mod diagnostic; mod gamemode_base; -mod locale; mod profile; mod resources; mod semantic; @@ -27,11 +26,8 @@ pub use diagnostic::*; pub use gamemode_base::detect_gamemode_base_libraries; pub use glua_codestyle::*; use glua_parser::{LineIndex, LuaParser, LuaSyntaxTree}; -pub use locale::get_locale_code; use lsp_types::Uri; pub use profile::Profile; -pub use resources::get_best_resources_dir; -pub use resources::load_resource_from_include_dir; use resources::load_resource_std; use schema_to_glua::SchemaConverter; pub use semantic::*; @@ -39,20 +35,11 @@ use std::collections::HashMap; use std::path::Path; use std::str::FromStr; use std::{collections::HashSet, path::PathBuf, sync::Arc}; -pub use test_lib::VirtualWorkspace; +pub use test_lib::{GMOD_CALL_ARG_BUILTINS_FIXTURE, VirtualWorkspace}; use tokio_util::sync::CancellationToken; use url::Url; pub use vfs::*; -#[macro_use] -extern crate rust_i18n; - -rust_i18n::i18n!("./locales", fallback = "en"); - -pub fn set_locale(locale: &str) { - rust_i18n::set_locale(locale); -} - pub async fn fetch_schema_urls(urls: Vec) -> HashMap { let mut url_contents = HashMap::new(); for url in urls { @@ -112,9 +99,20 @@ impl EmmyLuaAnalysis { } } - pub fn init_std_lib(&mut self, create_resources_dir: Option) { + pub fn init_std_lib(&mut self) { let is_jit = matches!(self.emmyrc.runtime.version, EmmyrcLuaVersion::LuaJIT); - let (std_root, files) = load_resource_std(create_resources_dir, is_jit); + let (std_root, files) = load_resource_std(is_jit); + // Normalize so the root's drive-letter casing matches VFS file paths + // (the URI round-trip uppercases the Windows drive letter). Without + // this, `extract_module_path` prefix matching would fail when the + // env-derived root has a lowercase drive letter. + let std_root = normalize_workspace_root(std_root); + self.init_std_lib_from_files(std_root, files); + } + + /// Register a pre-built set of embedded std files directly into the analysis + /// workspace without going through the resource-loading pipeline. + pub(crate) fn init_std_lib_from_files(&mut self, std_root: PathBuf, files: Vec) { self.compilation .get_db_mut() .get_module_index_mut() diff --git a/crates/glua_code_analysis/src/locale/mod.rs b/crates/glua_code_analysis/src/locale/mod.rs deleted file mode 100644 index 0c98443dd..000000000 --- a/crates/glua_code_analysis/src/locale/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub fn get_locale_code(locale: &str) -> String { - let mut locale = locale.to_string(); - // If the passed `locale` contains '-', convert '-' to '_' and convert the following letters to uppercase - if locale.contains("-") { - let parts = locale.split("-").collect::>(); - if parts.len() == 2 { - locale = format!("{}_{}", parts[0], parts[1].to_uppercase()); - } - } - match locale.as_str() { - "zh_TW" => "zh_HK".to_string(), - "en_US" => "en".to_string(), - "en_GB" => "en".to_string(), - _ => locale, - } -} diff --git a/crates/glua_code_analysis/src/resources/best_resource_path.rs b/crates/glua_code_analysis/src/resources/best_resource_path.rs deleted file mode 100644 index 31301f5ab..000000000 --- a/crates/glua_code_analysis/src/resources/best_resource_path.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::path::PathBuf; - -pub fn get_best_resources_dir() -> PathBuf { - if cfg!(debug_assertions) { - return exe_dir().join("resources"); - } - - if cfg!(target_os = "windows") { - // On Windows, try LOCALAPPDATA first - if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") { - PathBuf::from(local_app_data) - .join("glua_ls") - .join("resources") - } else { - // Fall back to the directory next to the executable - exe_dir().join("resources") - } - } else { - // On non-Windows platforms, try XDG_DATA_HOME first - if let Ok(xdg_data_home) = std::env::var("XDG_DATA_HOME") { - PathBuf::from(xdg_data_home) - .join("glua_ls") - .join("resources") - } else { - // If XDG_DATA_HOME is not set, use default XDG path - if let Ok(home) = std::env::var("HOME") { - PathBuf::from(home) - .join(".local") - .join("share") - .join("glua_ls") - .join("resources") - } else { - // Fall back to the directory next to the executable - exe_dir().join("resources") - } - } - } -} - -fn exe_dir() -> PathBuf { - let mut exe = std::env::current_exe().expect("executable available"); - exe.pop(); - exe -} diff --git a/crates/glua_code_analysis/src/resources/mod.rs b/crates/glua_code_analysis/src/resources/mod.rs index d30a9315e..a66e75f04 100644 --- a/crates/glua_code_analysis/src/resources/mod.rs +++ b/crates/glua_code_analysis/src/resources/mod.rs @@ -1,65 +1,76 @@ -mod best_resource_path; - use std::path::{Path, PathBuf}; -pub use best_resource_path::get_best_resources_dir; use include_dir::{Dir, DirEntry, include_dir}; -use crate::{LuaFileInfo, get_locale_code, load_workspace_files}; +use crate::LuaFileInfo; static RESOURCE_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/resources"); -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -pub fn load_resource_std( - create_resources_dir: Option, - is_jit: bool, -) -> (PathBuf, Vec) { - // 指定了输出的资源目录, 目前只有 lsp 会指定 - if let Some(create_resources_dir) = create_resources_dir { - let resource_path = if create_resources_dir.is_empty() { - get_best_resources_dir() - } else { - PathBuf::from(&create_resources_dir) - }; - // 此时会存在 i18n, 我们需要根据当前语言环境切换到对应语言的 std 目录 - let std_dir = get_std_dir(&resource_path); - let result = load_resource_from_file_system(&resource_path); - if let Some(mut files) = result { - if !is_jit { - remove_jit_resource(&mut files); - } - return (std_dir, files); - } + +/// Stable virtual path used to prefix embedded std-lib file paths for VFS +/// registration. The directory does **not** need to exist on disk; it only +/// needs to be an absolute path so that [`crate::file_path_to_uri`] produces +/// valid `file://` URIs. +fn virtual_resources_dir() -> PathBuf { + // Match the platform layout the old `get_best_resources_dir()` would + // return so downstream VFS registration is unchanged. + #[cfg(target_os = "windows")] + { + std::env::var("LOCALAPPDATA") + .map(PathBuf::from) + .unwrap_or_else(|_| { + // `C:` is drive-relative (not absolute); `C:\\` is absolute. + // Use current_exe parent as a more robust fallback when the + // env var is unset; fall back to an absolute drive root. + std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(Path::to_path_buf)) + .unwrap_or_else(|| PathBuf::from("C:\\")) + }) + .join("glua_ls") + .join("resources") + } + #[cfg(not(target_os = "windows"))] + { + std::env::var("XDG_DATA_HOME") + .map(PathBuf::from) + .or_else(|_| { + std::env::var("HOME").map(|h| PathBuf::from(h).join(".local").join("share")) + }) + .unwrap_or_else(|_| PathBuf::from("/tmp")) + .join("glua_ls") + .join("resources") } - // 没有指定资源目录, 那么直接使用默认的资源目录, 此时不会存在 i18n - let resoucres_dir = get_best_resources_dir(); - let std_dir = resoucres_dir.join("std"); - let files = load_resource_from_include_dir(); - let mut files = files +} + +pub fn load_resource_std(is_jit: bool) -> (PathBuf, Vec) { + let resources_dir = virtual_resources_dir(); + let std_root = resources_dir.join("std"); + + let raw_files = load_resource_from_include_dir(); + let mut files: Vec = raw_files .into_iter() - .filter_map(|file| { - if file.path.ends_with(".lua") { - let path = resoucres_dir - .join(&file.path) - .to_str() - .expect("UTF-8 paths") - .to_string(); - Some(LuaFileInfo { - path, - content: file.content, - }) - } else { - None + .filter(|file| file.path.ends_with(".lua")) + .map(|file| { + let path = resources_dir + .join(&file.path) + .to_str() + .expect("UTF-8 paths") + .to_string(); + LuaFileInfo { + path, + content: file.content, } }) - .collect::<_>(); + .collect(); + if !is_jit { remove_jit_resource(&mut files); } - (std_dir, files) + + (std_root, files) } -fn remove_jit_resource(files: &mut Vec) { +pub(crate) fn remove_jit_resource(files: &mut Vec) { const JIT_FILES_TO_REMOVE: &[&str] = &[ "jit.lua", "jit/profile.lua", @@ -77,82 +88,7 @@ fn remove_jit_resource(files: &mut Vec) { }); } -fn load_resource_from_file_system(resources_dir: &Path) -> Option> { - // lsp i18n 的资源在更早之前的 crates\glua_ls\src\handlers\initialized\std_i18n.rs 中写入到文件系统 - if check_need_dump_to_file_system() { - log::info!("Creating resources dir: {:?}", resources_dir); - let files = load_resource_from_include_dir(); - for file in &files { - let path = resources_dir.join(&file.path); - let parent = path.parent()?; - if !parent.exists() { - match std::fs::create_dir_all(parent) { - Ok(_) => {} - Err(e) => { - log::error!("Failed to create dir: {:?}, {:?}", parent, e); - return None; - } - } - } - - match std::fs::write(&path, &file.content) { - Ok(_) => {} - Err(e) => { - log::error!("Failed to write file: {:?}, {:?}", path, e); - return None; - } - } - } - - let version_path = resources_dir.join("version"); - let content = VERSION.to_string(); - match std::fs::write(&version_path, content) { - Ok(_) => {} - Err(e) => { - log::error!("Failed to write file: {:?}, {:?}", version_path, e); - return None; - } - } - } - - let std_dir = get_std_dir(&resources_dir); - let match_pattern = vec!["**/*.lua".to_string()]; - let files = match load_workspace_files(&std_dir, &match_pattern, &Vec::new(), &Vec::new(), None) - { - Ok(files) => files, - Err(e) => { - log::error!("Failed to load std lib: {:?}", e); - vec![] - } - }; - - Some(files) -} - -fn check_need_dump_to_file_system() -> bool { - if cfg!(debug_assertions) { - return true; - } - - let resoucres_dir = get_best_resources_dir(); - let version_path = resoucres_dir.join("version"); - - if !version_path.exists() { - return true; - } - - let Ok(content) = std::fs::read_to_string(&version_path) else { - return true; - }; - let version = content.trim(); - if version != VERSION { - return true; - } - - false -} - -pub fn load_resource_from_include_dir() -> Vec { +pub(crate) fn load_resource_from_include_dir() -> Vec { let mut files = Vec::new(); walk_resource_dir(&RESOURCE_DIR, &mut files); files @@ -177,14 +113,97 @@ fn walk_resource_dir(dir: &Dir, files: &mut Vec) { } } -// 优先使用当前语言环境的 std-{locale} 目录, 否则回退到默认的 std 目录 -fn get_std_dir(resources_dir: &Path) -> PathBuf { - let locale = get_locale_code(&rust_i18n::locale()); - if locale != "en" { - let locale_dir = resources_dir.join(format!("std-{locale}")); - if locale_dir.exists() { - return locale_dir; +#[cfg(test)] +mod tests { + use googletest::prelude::*; + + use super::*; + use crate::normalize_workspace_root; + + #[gtest] + fn embedded_std_contains_call_arg_attribute_defs() { + let files = load_resource_from_include_dir(); + let builtin = files.iter().find(|f| f.path.ends_with("builtin.lua")); + expect_that!(builtin, some(anything())); + let builtin = builtin.unwrap(); + expect_that!(builtin.content.contains("---@attribute call_arg"), eq(true)); + expect_that!( + builtin.content.contains("---@attribute overload_call_arg"), + eq(true) + ); + } + + #[gtest] + fn virtual_resources_dir_is_absolute() { + let dir = virtual_resources_dir(); + expect_that!(dir.is_absolute(), eq(true)); + } + + #[gtest] + fn virtual_resources_dir_std_subdir_is_absolute() { + let dir = virtual_resources_dir().join("std"); + expect_that!(dir.is_absolute(), eq(true)); + } + + #[gtest] + fn load_resource_std_returns_absolute_std_root() { + let (std_root, _files) = load_resource_std(true); + expect_that!(std_root.is_absolute(), eq(true)); + } + + #[gtest] + fn init_std_lib_classifies_builtin_as_std_workspace() { + let mut analysis = crate::EmmyLuaAnalysis::new(); + analysis.init_std_lib(); + + // Find the builtin.lua file in the VFS and verify it's classified as STD. + let vfs = analysis.compilation.get_db().get_vfs(); + let module_index = analysis.compilation.get_db().get_module_index(); + + let mut found_builtin = false; + for file_id in vfs.get_all_file_ids() { + if let Some(path) = vfs.get_file_path(&file_id) { + if path.to_string_lossy().ends_with("builtin.lua") { + found_builtin = true; + expect_that!(module_index.is_std(&file_id), eq(true)); + break; + } + } + } + expect_that!(found_builtin, eq(true)); + } + + #[gtest] + fn normalize_workspace_root_preserves_absolute_paths() { + // Use a platform-appropriate absolute path. + let root = if cfg!(windows) { + PathBuf::from("C:/some/absolute/path") + } else { + PathBuf::from("/some/absolute/path") + }; + let normalized = normalize_workspace_root(root); + expect_that!(normalized.is_absolute(), eq(true)); + } + + #[cfg(target_os = "windows")] + #[gtest] + fn normalize_workspace_root_uppercases_drive_letter() { + let lowercase = PathBuf::from("c:/Users/test/resources"); + let normalized = normalize_workspace_root(lowercase); + let normalized_str = normalized.to_string_lossy(); + // After URI round-trip, the drive letter should be uppercase. + expect_that!(normalized_str.starts_with("C:"), eq(true)); + } + + #[cfg(target_os = "windows")] + #[gtest] + fn virtual_resources_dir_has_uppercase_drive_on_windows() { + let dir = virtual_resources_dir(); + let dir_str = dir.to_string_lossy(); + // Absolute Windows paths should have an uppercase drive letter + // (e.g. C:\ not c:\). + if dir_str.len() >= 2 && dir_str.as_bytes()[1] == b':' { + expect_that!(dir_str.as_bytes()[0].is_ascii_uppercase(), eq(true)); } } - resources_dir.join("std") } diff --git a/crates/glua_code_analysis/src/semantic/generic/instantiate_type/instantiate_func_generic.rs b/crates/glua_code_analysis/src/semantic/generic/instantiate_type/instantiate_func_generic.rs index 60df9f61a..70509074f 100644 --- a/crates/glua_code_analysis/src/semantic/generic/instantiate_type/instantiate_func_generic.rs +++ b/crates/glua_code_analysis/src/semantic/generic/instantiate_type/instantiate_func_generic.rs @@ -1,12 +1,12 @@ use std::{collections::HashSet, ops::Deref, sync::Arc}; -use glua_parser::{LuaAstNode, LuaDocTypeList, LuaNameExpr}; -use glua_parser::{LuaCallExpr, LuaExpr}; +use glua_parser::{LuaAstNode, LuaCallExpr, LuaChunk, LuaDocTypeList, LuaExpr, LuaNameExpr}; use internment::ArcIntern; +use smol_str::SmolStr; use crate::{ - DocTypeInferContext, FileId, GenericTpl, GenericTplId, LuaFunctionType, LuaGenericType, - TypeVisitTrait, + DocTypeInferContext, FileId, GenericTpl, GenericTplId, LuaDocDefaultValue, LuaFunctionType, + LuaGenericType, LuaSemanticDeclId, TypeVisitTrait, db_index::{DbIndex, LuaType}, infer_doc_type, semantic::{ @@ -19,14 +19,174 @@ use crate::{ variadic_tpl_pattern_match, }, }, - infer::InferFailReason, + infer::{ + InferFailReason, + narrow::get_type_at_flow::{ + explicit_param_string_default_reaches_flow, inferred_string_default_reaches_flow, + }, + }, infer_enclosing_self_type, infer_expr, }, }; -use crate::{LuaMemberOwner, LuaSemanticDeclId, SemanticDeclLevel, infer_node_semantic_decl}; +use crate::{LuaMemberOwner, SemanticDeclLevel, infer_node_semantic_decl}; use super::TypeSubstitutor; +/// Resolve a flow-valid inferred string default for a call argument expression. +/// +/// When the arg is a local variable with an inferred string default (from +/// `x = x or "literal"`), and the self-coalescing assignment is the last +/// write to that variable that dominates the call site, returns +/// `Some(LuaType::StringConst(value))`. +/// +/// Only returns a value when: +/// - `arg_type` is exactly `LuaType::String` +/// - `param_type` actually contains a `StrTplRef` +/// - exactly one candidate default is flow-valid at the use site +/// - no explicit `---@param` default takes precedence +fn resolve_str_default_from_arg( + db: &DbIndex, + cache: &mut LuaInferCache, + param_type: &LuaType, + arg_expr: &LuaExpr, +) -> Option { + // Quick gate: only when param contains a StrTplRef. + if !param_type.contain_tpl() { + return None; + } + let mut has_str_tpl = false; + param_type.visit_type(&mut |t| { + if matches!(t, LuaType::StrTplRef(_)) { + has_str_tpl = true; + } + }); + if !has_str_tpl { + return None; + } + + // Resolve the argument's declaration. + let name_expr = LuaNameExpr::cast(arg_expr.syntax().clone())?; + let file_id = cache.get_file_id(); + let range = name_expr.get_range(); + let local_ref = db.get_reference_index().get_local_reference(&file_id)?; + let decl_id = local_ref.get_decl_id(&range)?; + + // Seed the cache with the use-site realm so that flow-reachability + // checks evaluate from the call argument's realm context (not the + // declaration position fallback). + let use_site_realm = db + .get_gmod_infer_index() + .get_realm_at_offset(&file_id, range.start()); + let previous_realm = cache.flow_query_realm.replace(use_site_realm); + + let result = resolve_str_default_from_arg_inner(db, cache, param_type, arg_expr, decl_id); + + cache.flow_query_realm = previous_realm; + result +} + +/// Inner helper for `resolve_str_default_from_arg` — separated so the +/// caller can seed/restore `flow_query_realm` around the call. +fn resolve_str_default_from_arg_inner( + db: &DbIndex, + cache: &mut LuaInferCache, + _param_type: &LuaType, + arg_expr: &LuaExpr, + decl_id: crate::LuaDeclId, +) -> Option { + let file_id = cache.get_file_id(); + + // ── Explicit default precedence ─────────────────────────────────── + // If the variable is a function parameter with an explicit `---@param` + // default (e.g. `---@param x string = "foo"`), prefer that over any + // inferred `x = x or "foo"` default — but ONLY when: + // 1. the default is a `String` (this resolver is for string-template binding), + // 2. the explicit default is still flow-valid at the use site. + if let Some(decl) = db.get_decl_index().get_decl(&decl_id) { + if let crate::LuaDeclExtra::Param { + idx: param_idx_in_sig, + signature_id, + .. + } = &decl.extra + { + if let Some(default_val) = db + .get_signature_index() + .get(signature_id) + .and_then(|sig| sig.get_param_info_by_id(*param_idx_in_sig)) + .and_then(|info| info.default_value.as_ref()) + { + // Only String defaults participate in string-template binding. + if let LuaDocDefaultValue::String(s) = default_val { + // Flow-validity: the explicit default must still reach + // the use site (not killed by a non-coalescing reassignment). + let flow_tree = db.get_flow_index().get_flow_tree(&file_id); + let root = LuaChunk::cast(arg_expr.get_root()); + let flow_valid = match (&flow_tree, &root) { + (Some(tree), Some(root)) => tree + .get_flow_id(arg_expr.get_syntax_id()) + .is_some_and(|use_flow_id| { + explicit_param_string_default_reaches_flow( + db, + tree, + cache, + root, + decl_id, + use_flow_id, + ) + }), + _ => false, + }; + + if flow_valid { + return Some(LuaType::StringConst(SmolStr::new(s.as_str()).into())); + } + } + // Non-String explicit defaults (Boolean, Number, Nil) are + // ignored by this resolver — they don't bind string templates. + } + } + } + + // Get inferred string default candidates from the side-map. + let candidates = db + .get_property_index() + .get_inferred_string_defaults(&decl_id)?; + if candidates.is_empty() { + return None; + } + + // Get the flow tree and the flow ID for the argument expression (use site). + // Using the arg expression's syntax ID (not the call expression's) is + // consistent with how existing narrow-flow querying works in + // `semantic/infer/narrow/mod.rs`. + let flow_tree = db.get_flow_index().get_flow_tree(&file_id)?; + let use_flow_id = flow_tree.get_flow_id(arg_expr.get_syntax_id())?; + let root = LuaChunk::cast(arg_expr.get_root())?; + + // Check each candidate for flow-validity. Exactly one must be valid. + let mut valid_value: Option = None; + for candidate in candidates { + let reaches = inferred_string_default_reaches_flow( + db, + flow_tree, + cache, + &root, + decl_id, + use_flow_id, + candidate.source_range, + ); + if reaches { + if valid_value.is_some() { + // Multiple valid candidates — ambiguous, don't bind. + return None; + } + valid_value = Some(candidate.value.clone()); + } + } + + valid_value.map(|v| LuaType::StringConst(v.into())) +} + pub fn instantiate_func_generic( db: &DbIndex, cache: &mut LuaInferCache, @@ -186,7 +346,18 @@ fn infer_generic_types_from_call( break; } _ => { - tpl_pattern_match(context, func_param_type, &arg_type)?; + // Try to bind from a registered string default when the + // inferred arg_type is plain `String` and the param has a + // StrTplRef. This covers `x = x or "literal"` patterns where + // the canonical type stays `string` but the declaration carries + // an auxiliary default-value metadata. + let effective_type = if matches!(arg_type, LuaType::String) { + resolve_str_default_from_arg(db, context.cache, func_param_type, call_arg_expr) + .unwrap_or_else(|| arg_type.clone()) + } else { + arg_type.clone() + }; + tpl_pattern_match(context, func_param_type, &effective_type)?; } } } diff --git a/crates/glua_code_analysis/src/semantic/generic/tpl_pattern/mod.rs b/crates/glua_code_analysis/src/semantic/generic/tpl_pattern/mod.rs index 15411ebc3..51b366db1 100644 --- a/crates/glua_code_analysis/src/semantic/generic/tpl_pattern/mod.rs +++ b/crates/glua_code_analysis/src/semantic/generic/tpl_pattern/mod.rs @@ -185,7 +185,14 @@ pub fn tpl_pattern_match( } } LuaType::StrTplRef(str_tpl) => { - if let LuaType::StringConst(s) = target { + // Bind from StringConst (existing), DocStringConst (adjacent + // soundness), or StringConst carried as an effective default. + let string_value = match target { + LuaType::StringConst(s) => Some(s), + LuaType::DocStringConst(s) => Some(s), + _ => None, + }; + if let Some(s) = string_value { let prefix = str_tpl.get_prefix(); let suffix = str_tpl.get_suffix(); let type_name = SmolStr::new(format!("{}{}{}", prefix, s, suffix)); diff --git a/crates/glua_code_analysis/src/semantic/infer/infer_doc_type.rs b/crates/glua_code_analysis/src/semantic/infer/infer_doc_type.rs index 29f4de190..31e95a413 100644 --- a/crates/glua_code_analysis/src/semantic/infer/infer_doc_type.rs +++ b/crates/glua_code_analysis/src/semantic/infer/infer_doc_type.rs @@ -428,6 +428,7 @@ fn infer_unary_type(ctx: DocTypeInferContext<'_>, unary_type: &LuaDocUnaryType) fn infer_func_type(ctx: DocTypeInferContext<'_>, func: &LuaDocFuncType) -> LuaType { let mut params_result = Vec::new(); + let mut optional_params = Vec::new(); let mut is_variadic = false; for param in func.get_params() { let name = if let Some(param) = param.get_name_token() { @@ -451,6 +452,7 @@ fn infer_func_type(ctx: DocTypeInferContext<'_>, func: &LuaDocFuncType) -> LuaTy None }; + optional_params.push(nullable && name != "..."); params_result.push((name, type_ref)); } @@ -495,6 +497,7 @@ fn infer_func_type(ctx: DocTypeInferContext<'_>, func: &LuaDocFuncType) -> LuaTy params_result, return_type, ) + .with_optional_params(optional_params) .into(), ) } diff --git a/crates/glua_code_analysis/src/semantic/infer/infer_index/mod.rs b/crates/glua_code_analysis/src/semantic/infer/infer_index/mod.rs index 13cdedcdc..81350de96 100644 --- a/crates/glua_code_analysis/src/semantic/infer/infer_index/mod.rs +++ b/crates/glua_code_analysis/src/semantic/infer/infer_index/mod.rs @@ -343,6 +343,20 @@ fn infer_table_member( } Err(err) => return Err(err), }; + + if matches!(index_key, LuaIndexKey::Expr(_)) + && member_key_is_unknown_expr(&key) + && let Some(member_type) = infer_gmod_same_file_expr_key_member_type( + db, + &owner, + &key, + cache.get_file_id(), + index_expr.get_position(), + ) + { + return Ok(member_type); + } + if let Some(member_item) = db.get_member_index().get_member_item(&owner, &key) { return member_item.resolve_type_with_realm_at_offset( db, @@ -363,6 +377,18 @@ fn infer_table_member( return Ok(base); } + if matches!(index_key, LuaIndexKey::Expr(_)) + && let Some(member_type) = infer_gmod_same_file_expr_key_member_type( + db, + &owner, + &key, + cache.get_file_id(), + index_expr.get_position(), + ) + { + return Ok(member_type); + } + if let Some(member_type) = infer_cross_file_matching_expr_key_member_type( db, &owner, @@ -436,6 +462,81 @@ fn is_literal_table_field_access(index_key: &LuaIndexKey) -> bool { matches!(index_key, LuaIndexKey::Name(_) | LuaIndexKey::String(_)) } +fn infer_gmod_same_file_expr_key_member_type( + db: &DbIndex, + owner: &LuaMemberOwner, + key: &LuaMemberKey, + access_file_id: FileId, + access_position: TextSize, +) -> Option { + if !db.get_emmyrc().gmod.enabled || !db.get_emmyrc().gmod.infer_dynamic_fields { + return None; + } + + let access_key_type = crate::semantic::member::member_key_as_type(key)?; + let members = db.get_member_index().get_members(owner)?; + let mut result = LuaType::Unknown; + + for member in members { + if member.get_file_id() != access_file_id { + continue; + } + + let key_match = if member_key_is_unknown_expr(member.get_key()) { + expr_dynamic_access_matches_unknown_member(&access_key_type) + } else { + member_key_matches_type(db, &access_key_type, member.get_key()) + }; + if !key_match { + continue; + } + + let member_item = crate::db_index::LuaMemberIndexItem::One(member.get_id()); + let Ok(member_type) = + member_item.resolve_type_with_realm_at_offset(db, &access_file_id, access_position) + else { + continue; + }; + + result = TypeOps::Union.apply(db, &result, &member_type); + } + + if result.is_unknown() { + None + } else { + Some(nullable_if_needed(db, result)) + } +} + +fn expr_dynamic_access_matches_unknown_member(access_key_type: &LuaType) -> bool { + match access_key_type { + LuaType::Any + | LuaType::Unknown + | LuaType::String + | LuaType::StringConst(_) + | LuaType::DocStringConst(_) + | LuaType::Number + | LuaType::Integer + | LuaType::IntegerConst(_) + | LuaType::DocIntegerConst(_) + | LuaType::FloatConst(_) => true, + LuaType::TypeGuard(inner) => expr_dynamic_access_matches_unknown_member(inner), + LuaType::Union(union) => union + .into_vec() + .iter() + .any(expr_dynamic_access_matches_unknown_member), + _ => false, + } +} + +fn nullable_if_needed(db: &DbIndex, typ: LuaType) -> LuaType { + if typ.is_nullable() { + typ + } else { + TypeOps::Union.apply(db, &typ, &LuaType::Nil) + } +} + fn infer_cross_file_matching_expr_key_member_type( db: &DbIndex, owner: &LuaMemberOwner, diff --git a/crates/glua_code_analysis/src/semantic/infer/mod.rs b/crates/glua_code_analysis/src/semantic/infer/mod.rs index aa2f09dc7..d45d0e382 100644 --- a/crates/glua_code_analysis/src/semantic/infer/mod.rs +++ b/crates/glua_code_analysis/src/semantic/infer/mod.rs @@ -6,7 +6,7 @@ mod infer_index; mod infer_name; mod infer_table; mod infer_unary; -mod narrow; +pub(crate) mod narrow; mod test; use std::{ops::Deref, sync::Arc}; diff --git a/crates/glua_code_analysis/src/semantic/infer/narrow/condition_flow/call_flow.rs b/crates/glua_code_analysis/src/semantic/infer/narrow/condition_flow/call_flow.rs index 40bf2a862..3a34bff50 100644 --- a/crates/glua_code_analysis/src/semantic/infer/narrow/condition_flow/call_flow.rs +++ b/crates/glua_code_analysis/src/semantic/infer/narrow/condition_flow/call_flow.rs @@ -9,6 +9,7 @@ use crate::{ semantic::infer::{ VarRefId, infer_index::infer_member_by_member_key, + infer_param_with_cache, narrow::{ ResultTypeOrContinue, condition_flow::InferConditionFlow, get_single_antecedent, get_type_at_cast_flow::cast_type, get_type_at_flow::get_type_at_flow, narrow_down_type, @@ -494,7 +495,7 @@ fn get_type_at_call_expr_by_type_guard( match condition_flow { InferConditionFlow::TrueCondition => Ok(ResultTypeOrContinue::Result( - narrow_type_guard_true_branch(db, antecedent_type, guard_type), + narrow_type_guard_true_branch(db, cache, var_ref_id, antecedent_type, guard_type), )), InferConditionFlow::FalseCondition => Ok(ResultTypeOrContinue::Result( TypeOps::Remove.apply(db, &antecedent_type, &guard_type), @@ -504,6 +505,8 @@ fn get_type_at_call_expr_by_type_guard( fn narrow_type_guard_true_branch( db: &DbIndex, + cache: &mut LuaInferCache, + var_ref_id: &VarRefId, antecedent_type: LuaType, guard_type: LuaType, ) -> LuaType { @@ -521,9 +524,44 @@ fn narrow_type_guard_true_branch( return guard_type; } + // The inferred type cache for mutable unannotated parameters is flow-insensitive: + // later writes can poison the origin type used by earlier guards (for example, + // Starfall's `sfmeshdata = meshToStream(...)` made `elseif istable(sfmeshdata)` + // start from `string`). In that case the runtime TypeGuard is the better authority. + if is_inferred_mutable_param_without_declared_type(db, cache, var_ref_id) { + return guard_type; + } + remove_false_or_nil(antecedent_type) } +fn is_inferred_mutable_param_without_declared_type( + db: &DbIndex, + cache: &mut LuaInferCache, + var_ref_id: &VarRefId, +) -> bool { + let Some(decl_id) = var_ref_id.get_decl_id_ref() else { + return false; + }; + let Some(decl) = db.get_decl_index().get_decl(&decl_id) else { + return false; + }; + if !decl.is_param() || infer_param_with_cache(db, cache, decl).is_ok() { + return false; + } + if !db + .get_type_index() + .get_type_cache(&decl_id.into()) + .is_some_and(|type_cache| type_cache.is_infer()) + { + return false; + } + + db.get_reference_index() + .get_decl_references(&decl_id.file_id, &decl_id) + .is_some_and(|decl_refs| decl_refs.mutable) +} + #[allow(clippy::too_many_arguments)] fn get_type_at_call_expr_by_signature_self( db: &DbIndex, diff --git a/crates/glua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs b/crates/glua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs index 860caaa81..7a0177e3b 100644 --- a/crates/glua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs +++ b/crates/glua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs @@ -8,9 +8,9 @@ use rowan::TextSize; use crate::{ AssignVarHint, CacheEntry, DbIndex, FlowAntecedent, FlowId, FlowNode, FlowNodeKind, FlowTree, - GmodRealm, InferFailReason, LuaArrayType, LuaDeclId, LuaInferCache, LuaMemberId, LuaMemberKey, - LuaMemberOwner, LuaSemanticDeclId, LuaSignatureId, LuaType, LuaTypeOwner, LuaUnionType, - TypeOps, infer_expr, + GlobalId, GmodRealm, InferFailReason, LuaArrayType, LuaDeclId, LuaInferCache, LuaMemberId, + LuaMemberKey, LuaMemberOwner, LuaSemanticDeclId, LuaSignatureId, LuaType, LuaTypeOwner, + LuaUnionType, TypeOps, infer_expr, semantic::infer::{ InferResult, VarRefId, infer_expr_list_value_type_at, infer_param_with_cache, narrow::{ @@ -275,10 +275,14 @@ fn get_type_at_flow_walk( } } FlowNodeKind::Call(call_ptr) => { - let call_expr_stat = call_ptr.to_node(root).ok_or(InferFailReason::None)?; + let call_expr = call_ptr.to_node(root).ok_or(InferFailReason::None)?; + if call_expr_returns_never(db, cache, call_expr.clone()) { + return Ok(LuaType::Never); + } + if let Some(effects) = db .get_flow_index() - .get_special_call_effects(&cache.get_file_id(), call_expr_stat.get_position()) + .get_special_call_effects(&cache.get_file_id(), call_expr.get_position()) { let mut effect_type = None; for effect in effects { @@ -643,7 +647,8 @@ fn merge_antecedent_types( if matches!( antecedent_node.kind, FlowNodeKind::Unreachable | FlowNodeKind::Return | FlowNodeKind::Break - ) { + ) || call_flow_node_returns_never(db, cache, root, antecedent_node) + { continue; } @@ -675,7 +680,8 @@ fn merge_antecedent_types( if matches!( antecedent_node.kind, FlowNodeKind::Unreachable | FlowNodeKind::Return | FlowNodeKind::Break - ) { + ) || call_flow_node_returns_never(db, cache, root, antecedent_node) + { continue; } @@ -691,6 +697,140 @@ fn merge_antecedent_types( Ok(merge_flow_branch_types(db, var_ref_id, branch_types)) } +fn call_flow_node_returns_never( + db: &DbIndex, + cache: &mut LuaInferCache, + root: &LuaChunk, + flow_node: &FlowNode, +) -> bool { + let FlowNodeKind::Call(call_ptr) = &flow_node.kind else { + return false; + }; + let Some(call_expr) = call_ptr.to_node(root) else { + return false; + }; + call_expr_returns_never(db, cache, call_expr) +} + +fn call_expr_returns_never( + db: &DbIndex, + cache: &mut LuaInferCache, + call_expr: glua_parser::LuaCallExpr, +) -> bool { + if call_expr.is_error() { + return true; + } + + match call_expr.get_prefix_expr() { + Some(LuaExpr::NameExpr(name_expr)) => db + .get_reference_index() + .get_local_reference(&cache.get_file_id()) + .and_then(|local_ref| local_ref.get_decl_id(&name_expr.get_range())) + .is_some_and(|decl_id| { + semantic_decl_returns_never(db, LuaSemanticDeclId::LuaDecl(decl_id)) + }), + Some(LuaExpr::IndexExpr(index_expr)) => { + index_call_prefix_returns_never(db, cache, index_expr) + } + _ => false, + } +} + +fn index_call_prefix_returns_never( + db: &DbIndex, + cache: &mut LuaInferCache, + index_expr: LuaIndexExpr, +) -> bool { + if semantic_decl_returns_never( + db, + LuaSemanticDeclId::Member(LuaMemberId::new( + index_expr.get_syntax_id(), + cache.get_file_id(), + )), + ) { + return true; + } + + let Some(LuaExpr::NameExpr(prefix_name)) = index_expr.get_prefix_expr() else { + return false; + }; + if !name_expr_is_global_root(db, cache, &prefix_name) { + return false; + } + + let Some(global_name) = prefix_name.get_name_text() else { + return false; + }; + let Some(member_key) = index_expr + .get_index_key() + .and_then(|key| LuaMemberKey::from_index_key(db, cache, &key).ok()) + else { + return false; + }; + + let owner = LuaMemberOwner::GlobalPath(GlobalId::new(&global_name)); + db.get_member_index() + .get_member_item(&owner, &member_key) + .and_then(|member_item| { + member_item.resolve_semantic_decl_with_realm_at_offset( + db, + &cache.get_file_id(), + index_expr.get_position(), + ) + }) + .is_some_and(|semantic_decl| semantic_decl_returns_never(db, semantic_decl)) +} + +fn name_expr_is_global_root( + db: &DbIndex, + cache: &LuaInferCache, + name_expr: &glua_parser::LuaNameExpr, +) -> bool { + let Some(decl_id) = db + .get_reference_index() + .get_var_reference_decl(&cache.get_file_id(), name_expr.get_range()) + else { + return true; + }; + + db.get_decl_index() + .get_decl(&decl_id) + .is_some_and(|decl| decl.is_global() || decl.is_module_scoped()) +} + +fn semantic_decl_returns_never(db: &DbIndex, semantic_decl: LuaSemanticDeclId) -> bool { + if let Some(signature_id) = db.get_property_index().get_signature_owner(&semantic_decl) { + return signature_returns_never(db, signature_id); + } + + match semantic_decl { + LuaSemanticDeclId::Signature(signature_id) => signature_returns_never(db, signature_id), + LuaSemanticDeclId::LuaDecl(decl_id) => db + .get_type_index() + .get_type_cache(&decl_id.into()) + .is_some_and(|type_cache| type_returns_never(db, type_cache.as_type())), + LuaSemanticDeclId::Member(member_id) => db + .get_type_index() + .get_type_cache(&member_id.into()) + .is_some_and(|type_cache| type_returns_never(db, type_cache.as_type())), + LuaSemanticDeclId::TypeDecl(_) => false, + } +} + +fn signature_returns_never(db: &DbIndex, signature_id: LuaSignatureId) -> bool { + db.get_signature_index() + .get(&signature_id) + .is_some_and(|signature| signature.get_return_type().is_never()) +} + +fn type_returns_never(db: &DbIndex, typ: &LuaType) -> bool { + match typ { + LuaType::Signature(signature_id) => signature_returns_never(db, *signature_id), + LuaType::DocFunction(func) => func.get_ret().is_never(), + _ => false, + } +} + fn merge_flow_branch_types( db: &DbIndex, var_ref_id: &VarRefId, @@ -1413,3 +1553,436 @@ fn prefer_table_of_over_bare_table(_db: &DbIndex, ty: LuaType) -> LuaType { _ => ty, } } + +/// Check whether an explicit `---@param x string = "literal"` default is +/// still live at a given use site by walking the flow graph backward. +/// +/// Returns `true` only when the declaration origin for `decl_id` is reachable +/// from `use_flow_id` without passing through a killing assignment. A +/// self-coalescing `x = x or ...` assignment is NOT a kill — it is a fallback +/// that the explicit default takes precedence over. Any other assignment to +/// the same variable kills the explicit default. +pub fn explicit_param_string_default_reaches_flow( + db: &DbIndex, + tree: &FlowTree, + cache: &mut LuaInferCache, + root: &LuaChunk, + decl_id: LuaDeclId, + use_flow_id: FlowId, +) -> bool { + let var_ref_id = VarRefId::VarRef(decl_id); + let mut visited = HashSet::new(); + explicit_default_reaches_inner( + db, + tree, + cache, + root, + &var_ref_id, + use_flow_id, + &mut visited, + ) +} + +fn explicit_default_reaches_inner( + db: &DbIndex, + tree: &FlowTree, + cache: &mut LuaInferCache, + root: &LuaChunk, + var_ref_id: &VarRefId, + flow_id: FlowId, + visited: &mut HashSet, +) -> bool { + // Guard against infinite loops in cyclic flow graphs. + if !visited.insert(flow_id) { + return false; + } + + let Some(flow_node) = tree.get_flow_node(flow_id) else { + return false; + }; + + match &flow_node.kind { + // Reached the declaration origin for our target variable — + // explicit default is proven valid. + FlowNodeKind::DeclPosition(position) => { + if matches!(var_ref_id.get_decl_id_ref(), Some(decl_id) if decl_id.position == *position) + { + true + } else { + // DeclPosition for another variable — walk past it. + walk_antecedents_for_explicit_default( + db, tree, cache, root, var_ref_id, flow_node, visited, + ) + } + } + // For parameters, the flow tree may not have a DeclPosition node; + // reaching Start without encountering a killing assignment proves + // the explicit default is still live. + FlowNodeKind::Start => true, + // Dead paths. + FlowNodeKind::Unreachable | FlowNodeKind::Return | FlowNodeKind::Break => false, + FlowNodeKind::Assignment(assign_ptr, assign_hint) => { + let can_match = matches!( + (assign_hint, var_ref_id), + (AssignVarHint::Mixed, _) + | (AssignVarHint::NameOnly, VarRefId::VarRef(_)) + | (AssignVarHint::NameOnly, VarRefId::GlobalName(_, _)) + | (AssignVarHint::NameOnly, VarRefId::SelfRef(_)) + | (AssignVarHint::IndexOnly, VarRefId::IndexRef(_, _)) + ); + + if can_match { + if let Some(assign_stat) = assign_ptr.to_node(root) { + let (vars, _) = assign_stat.get_var_and_expr_list(); + for var in vars.iter() { + if let Some(ref_id) = get_var_expr_var_ref_id(db, cache, var.to_expr()) { + if ref_id == *var_ref_id { + // Any assignment to the variable kills the + // explicit default — including self-coalescing + // assignments like `x = x or "literal"`. + // After such an assignment, the inferred-default + // path takes over for downstream use sites. + return false; + } + } + } + } + } + + walk_antecedents_for_explicit_default( + db, tree, cache, root, var_ref_id, flow_node, visited, + ) + } + _ => walk_antecedents_for_explicit_default( + db, tree, cache, root, var_ref_id, flow_node, visited, + ), + } +} + +/// Walk backward through antecedents of a flow node, requiring the explicit +/// default to reach on ALL live paths (conjunction). +/// +/// Mirrors the realm-filtering approach from `merge_antecedent_types`: +/// wrong-realm antecedents are skipped on the first pass. If filtering +/// removes ALL live antecedents, a second pass without realm checks +/// preserves conservative behaviour. +fn walk_antecedents_for_explicit_default( + db: &DbIndex, + tree: &FlowTree, + cache: &mut LuaInferCache, + root: &LuaChunk, + var_ref_id: &VarRefId, + flow_node: &FlowNode, + visited: &mut HashSet, +) -> bool { + match &flow_node.antecedent { + Some(FlowAntecedent::Single(antecedent_id)) => explicit_default_reaches_inner( + db, + tree, + cache, + root, + var_ref_id, + *antecedent_id, + visited, + ), + Some(FlowAntecedent::Multiple(idx)) => { + if let Some(antecedents) = tree.get_multi_antecedents(*idx) { + let target_realm = cache.flow_query_realm.unwrap_or_else(|| { + db.get_gmod_infer_index() + .get_realm_at_offset(&cache.get_file_id(), var_ref_id.get_position()) + }); + + // First pass: realm-filtered. + let base_visited = visited.clone(); + let mut any_live = false; + for &antecedent_id in antecedents { + let Some(ante_node) = tree.get_flow_node(antecedent_id) else { + continue; + }; + if ante_node.kind.is_unreachable() || ante_node.kind.is_change_flow() { + continue; + } + + let ante_realm = + get_or_compute_flow_node_realm(db, cache, root, antecedent_id, ante_node); + if !realms_can_reach(target_realm, ante_realm) { + continue; + } + + any_live = true; + let mut path_visited = base_visited.clone(); + if !explicit_default_reaches_inner( + db, + tree, + cache, + root, + var_ref_id, + antecedent_id, + &mut path_visited, + ) { + return false; + } + visited.extend(path_visited); + } + + if any_live { + return true; + } + + // Fallback: no live antecedents after realm filtering — + // retry without realm checks for conservative behaviour. + let mut any_live = false; + for &antecedent_id in antecedents { + let Some(ante_node) = tree.get_flow_node(antecedent_id) else { + continue; + }; + if ante_node.kind.is_unreachable() || ante_node.kind.is_change_flow() { + continue; + } + any_live = true; + let mut path_visited = base_visited.clone(); + if !explicit_default_reaches_inner( + db, + tree, + cache, + root, + var_ref_id, + antecedent_id, + &mut path_visited, + ) { + return false; + } + visited.extend(path_visited); + } + any_live + } else { + false + } + } + None => false, + } +} + +/// Check whether an inferred string default (from `x = x or "literal"`) is +/// still live at a given use site by walking the flow graph backward. +/// +/// Returns `true` only when the self-coalescing assignment at +/// `default_source_range` is the **last** assignment to `decl_id` that +/// dominates the use — any later write to the same variable kills it. +pub fn inferred_string_default_reaches_flow( + db: &DbIndex, + tree: &FlowTree, + cache: &mut LuaInferCache, + root: &LuaChunk, + decl_id: LuaDeclId, + use_flow_id: FlowId, + default_source_range: rowan::TextRange, +) -> bool { + let var_ref_id = VarRefId::VarRef(decl_id); + let mut visited = HashSet::new(); + inferred_string_default_reaches_inner( + db, + tree, + cache, + root, + &var_ref_id, + use_flow_id, + default_source_range, + &mut visited, + ) +} + +fn inferred_string_default_reaches_inner( + db: &DbIndex, + tree: &FlowTree, + cache: &mut LuaInferCache, + root: &LuaChunk, + var_ref_id: &VarRefId, + flow_id: FlowId, + default_source_range: rowan::TextRange, + visited: &mut HashSet, +) -> bool { + // Guard against infinite loops in cyclic flow graphs. + if !visited.insert(flow_id) { + return false; + } + + let Some(flow_node) = tree.get_flow_node(flow_id) else { + return false; + }; + + match &flow_node.kind { + FlowNodeKind::Start => false, + FlowNodeKind::Unreachable | FlowNodeKind::Return | FlowNodeKind::Break => false, + FlowNodeKind::DeclPosition(_) => walk_antecedents_for_default( + db, + tree, + cache, + root, + var_ref_id, + flow_node, + default_source_range, + visited, + ), + FlowNodeKind::Assignment(assign_ptr, assign_hint) => { + let can_match = matches!( + (assign_hint, var_ref_id), + (AssignVarHint::Mixed, _) + | (AssignVarHint::NameOnly, VarRefId::VarRef(_)) + | (AssignVarHint::NameOnly, VarRefId::GlobalName(_, _)) + | (AssignVarHint::NameOnly, VarRefId::SelfRef(_)) + | (AssignVarHint::IndexOnly, VarRefId::IndexRef(_, _)) + ); + + if can_match { + if let Some(assign_stat) = assign_ptr.to_node(root) { + let (vars, _exprs) = assign_stat.get_var_and_expr_list(); + for var in vars { + if let Some(ref_id) = get_var_expr_var_ref_id(db, cache, var.to_expr()) { + if ref_id == *var_ref_id { + let assign_range = assign_stat.get_range(); + // Same range → matching assignment (default proven). + // Different range → later write kills the default. + return assign_range == default_source_range; + } + } + } + } + } + + walk_antecedents_for_default( + db, + tree, + cache, + root, + var_ref_id, + flow_node, + default_source_range, + visited, + ) + } + _ => walk_antecedents_for_default( + db, + tree, + cache, + root, + var_ref_id, + flow_node, + default_source_range, + visited, + ), + } +} + +/// Walk backward through antecedents of a flow node, requiring the default +/// to reach on ALL live paths (conjunction). +/// +/// Mirrors the realm-filtering approach from `merge_antecedent_types`: +/// wrong-realm antecedents are skipped on the first pass. If filtering +/// removes ALL live antecedents, a second pass without realm checks +/// preserves conservative behaviour. +fn walk_antecedents_for_default( + db: &DbIndex, + tree: &FlowTree, + cache: &mut LuaInferCache, + root: &LuaChunk, + var_ref_id: &VarRefId, + flow_node: &FlowNode, + default_source_range: rowan::TextRange, + visited: &mut HashSet, +) -> bool { + match &flow_node.antecedent { + Some(FlowAntecedent::Single(antecedent_id)) => inferred_string_default_reaches_inner( + db, + tree, + cache, + root, + var_ref_id, + *antecedent_id, + default_source_range, + visited, + ), + Some(FlowAntecedent::Multiple(idx)) => { + if let Some(antecedents) = tree.get_multi_antecedents(*idx) { + let target_realm = cache.flow_query_realm.unwrap_or_else(|| { + db.get_gmod_infer_index() + .get_realm_at_offset(&cache.get_file_id(), var_ref_id.get_position()) + }); + + // First pass: realm-filtered. + let base_visited = visited.clone(); + let mut any_live = false; + for &antecedent_id in antecedents { + let Some(ante_node) = tree.get_flow_node(antecedent_id) else { + continue; + }; + if ante_node.kind.is_unreachable() || ante_node.kind.is_change_flow() { + continue; + } + + let ante_realm = + get_or_compute_flow_node_realm(db, cache, root, antecedent_id, ante_node); + if !realms_can_reach(target_realm, ante_realm) { + continue; + } + + any_live = true; + let mut path_visited = base_visited.clone(); + if !inferred_string_default_reaches_inner( + db, + tree, + cache, + root, + var_ref_id, + antecedent_id, + default_source_range, + &mut path_visited, + ) { + return false; + } + visited.extend(path_visited); + } + + if any_live { + return true; + } + + // Fallback: no live antecedents after realm filtering — + // retry without realm checks for conservative behaviour. + let mut any_live = false; + for &antecedent_id in antecedents { + let Some(ante_node) = tree.get_flow_node(antecedent_id) else { + continue; + }; + if ante_node.kind.is_unreachable() || ante_node.kind.is_change_flow() { + continue; + } + any_live = true; + let mut path_visited = base_visited.clone(); + if !inferred_string_default_reaches_inner( + db, + tree, + cache, + root, + var_ref_id, + antecedent_id, + default_source_range, + &mut path_visited, + ) { + return false; + } + visited.extend(path_visited); + } + any_live + } else { + false + } + } + None => { + // No antecedents — reached the implicit start without ever + // encountering the recorded assignment. The default was NOT + // proven on this path. + false + } + } +} diff --git a/crates/glua_code_analysis/src/semantic/infer/narrow/mod.rs b/crates/glua_code_analysis/src/semantic/infer/narrow/mod.rs index ea5a2b834..dd3260a1b 100644 --- a/crates/glua_code_analysis/src/semantic/infer/narrow/mod.rs +++ b/crates/glua_code_analysis/src/semantic/infer/narrow/mod.rs @@ -1,6 +1,6 @@ mod condition_flow; mod get_type_at_cast_flow; -mod get_type_at_flow; +pub mod get_type_at_flow; mod narrow_type; mod var_ref_id; @@ -15,6 +15,9 @@ use crate::{ }, }; pub use get_type_at_cast_flow::get_type_at_call_expr_inline_cast; +pub use get_type_at_flow::{ + explicit_param_string_default_reaches_flow, inferred_string_default_reaches_flow, +}; use glua_parser::{LuaAstNode, LuaChunk, LuaExpr}; pub use narrow_type::{narrow_down_type, narrow_false_or_nil, remove_false_or_nil}; pub use var_ref_id::{SelfRefId, VarRefId, VarRefRootId, get_var_expr_var_ref_id}; diff --git a/crates/glua_code_analysis/src/semantic/mod.rs b/crates/glua_code_analysis/src/semantic/mod.rs index e47d1dea0..d6d4cd185 100644 --- a/crates/glua_code_analysis/src/semantic/mod.rs +++ b/crates/glua_code_analysis/src/semantic/mod.rs @@ -22,6 +22,9 @@ use glua_parser::{ }; pub(crate) use infer::check_iter_var_range; pub use infer::infer_index_expr; +pub use infer::narrow::{ + explicit_param_string_default_reaches_flow, inferred_string_default_reaches_flow, +}; pub(crate) use infer::resolve_decl_backed_global_path_member_type; use infer::{infer_bind_value_type, infer_call_arg_expr_list_types, infer_expr_list_types}; pub use infer::{infer_table_field_value_should_be, infer_table_should_be}; diff --git a/crates/glua_code_analysis/src/semantic/type_check/complex_type/object_type_check.rs b/crates/glua_code_analysis/src/semantic/type_check/complex_type/object_type_check.rs index 33a2001ea..55bee9e28 100644 --- a/crates/glua_code_analysis/src/semantic/type_check/complex_type/object_type_check.rs +++ b/crates/glua_code_analysis/src/semantic/type_check/complex_type/object_type_check.rs @@ -254,8 +254,8 @@ fn check_member_value( } Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "member %{key} not match, expect %{typ}, but got %{got}", + format!( + "member {key} not match, expect {typ}, but got {got}", key = key_display, typ = humanize_type(context.db, source_type, RenderLevel::Simple), got = humanize_type(context.db, member_type, RenderLevel::Simple) @@ -296,7 +296,7 @@ fn check_object_type_compact_table_const( continue; } else { return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing member %{key}", key = key.to_path().to_string()).to_string(), + format!("missing member {key}", key = key.to_path()).to_string(), )); } } @@ -391,7 +391,7 @@ fn check_object_type_compact_type_ref( } return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing member %{key}", key = key.to_path().to_string()).to_string(), + format!("missing member {key}", key = key.to_path()).to_string(), )); }; diff --git a/crates/glua_code_analysis/src/semantic/type_check/complex_type/tuple_type_check.rs b/crates/glua_code_analysis/src/semantic/type_check/complex_type/tuple_type_check.rs index 4cc92a31f..74b0ac184 100644 --- a/crates/glua_code_analysis/src/semantic/type_check/complex_type/tuple_type_check.rs +++ b/crates/glua_code_analysis/src/semantic/type_check/complex_type/tuple_type_check.rs @@ -96,7 +96,7 @@ fn check_tuple_types_compact_tuple_types( continue; } else { return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing tuple member %{idx}", idx = i + source_start + 1).to_string(), + format!("missing tuple member {idx}", idx = i + source_start + 1).to_string(), )); } } @@ -150,8 +150,8 @@ fn check_tuple_types_compact_tuple_types( Ok(_) => {} Err(TypeCheckFailReason::TypeNotMatch) => { return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "tuple member %{idx} not match, expect %{typ}, but got %{got}", + format!( + "tuple member {idx} not match, expect {typ}, but got {got}", idx = i + source_start + 1, typ = humanize_type( context.db, @@ -201,8 +201,8 @@ fn check_tuple_type_compact_table( Ok(_) => {} Err(TypeCheckFailReason::TypeNotMatch) => { return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "tuple member %{idx} not match, expect %{typ}, but got %{got}", + format!( + "tuple member {idx} not match, expect {typ}, but got {got}", idx = i + 1, typ = humanize_type( context.db, @@ -222,7 +222,7 @@ fn check_tuple_type_compact_table( continue; } else { return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing tuple member %{idx}", idx = i + 1).to_string(), + format!("missing tuple member {idx}", idx = i + 1).to_string(), )); } } @@ -252,8 +252,8 @@ fn check_tuple_type_compact_object_type( Ok(_) => {} Err(TypeCheckFailReason::TypeNotMatch) => { return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "tuple member %{idx} not match, expect %{typ}, but got %{got}", + format!( + "tuple member {idx} not match, expect {typ}, but got {got}", idx = i + 1, typ = humanize_type( context.db, @@ -274,7 +274,7 @@ fn check_tuple_type_compact_object_type( continue; } else { return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing tuple member %{idx}", idx = i + 1).to_string(), + format!("missing tuple member {idx}", idx = i + 1).to_string(), )); } } diff --git a/crates/glua_code_analysis/src/semantic/type_check/generic_type.rs b/crates/glua_code_analysis/src/semantic/type_check/generic_type.rs index 8dc2e88e9..f1a7eaebe 100644 --- a/crates/glua_code_analysis/src/semantic/type_check/generic_type.rs +++ b/crates/glua_code_analysis/src/semantic/type_check/generic_type.rs @@ -191,8 +191,8 @@ fn check_generic_type_compact_table( return Err(TypeCheckFailReason::TypeNotMatch); } return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "member %{name} type not match, expect %{expect}, got %{got}", + format!( + "member {name} type not match, expect {expect}, got {got}", name = key.to_path(), expect = humanize_type(context.db, &source_member_type, RenderLevel::Simple), @@ -208,7 +208,7 @@ fn check_generic_type_compact_table( } return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing member %{name}, in table", name = key.to_path()).to_string(), + format!("missing member {name}, in table", name = key.to_path()).to_string(), )); } _ => {} // 可选成员未找到,继续检查 diff --git a/crates/glua_code_analysis/src/semantic/type_check/ref_type.rs b/crates/glua_code_analysis/src/semantic/type_check/ref_type.rs index 150248769..260507da7 100644 --- a/crates/glua_code_analysis/src/semantic/type_check/ref_type.rs +++ b/crates/glua_code_analysis/src/semantic/type_check/ref_type.rs @@ -68,7 +68,7 @@ pub fn check_ref_type_compact( // unreachable! .ok_or(if context.detail { TypeCheckFailReason::TypeNotMatchWithReason( - t!("type `%{name}` not found.", name = source_id.get_name()).to_string(), + format!("type `{name}` not found.", name = source_id.get_name()).to_string(), ) } else { TypeCheckFailReason::TypeNotMatch @@ -389,8 +389,8 @@ fn check_ref_type_compact_table( } return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "member %{name} type not match, expect %{expect}, got %{got}", + format!( + "member {name} type not match, expect {expect}, got {got}", name = key.to_path(), expect = humanize_type(context.db, source_member_type, RenderLevel::Simple), @@ -406,7 +406,7 @@ fn check_ref_type_compact_table( } return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing member %{name}, in table", name = key.to_path()).to_string(), + format!("missing member {name}, in table", name = key.to_path()).to_string(), )); } _ => {} // Optional member not found, continue @@ -477,8 +477,8 @@ fn check_ref_type_compact_object( } return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!( - "member %{name} type not match, expect %{expect}, got %{got}", + format!( + "member {name} type not match, expect {expect}, got {got}", name = key.to_path(), expect = humanize_type(context.db, &source_member_type, RenderLevel::Simple), @@ -493,7 +493,7 @@ fn check_ref_type_compact_object( return Err(TypeCheckFailReason::TypeNotMatch); } return Err(TypeCheckFailReason::TypeNotMatchWithReason( - t!("missing member %{name}, in table", name = key.to_path()).to_string(), + format!("missing member {name}, in table", name = key.to_path()).to_string(), )); } _ => {} // Optional member not found, continue diff --git a/crates/glua_code_analysis/src/test_lib/mod.rs b/crates/glua_code_analysis/src/test_lib/mod.rs index 89c02ab97..cbf20b55b 100644 --- a/crates/glua_code_analysis/src/test_lib/mod.rs +++ b/crates/glua_code_analysis/src/test_lib/mod.rs @@ -35,6 +35,160 @@ pub struct DiagnosticSnapshot { pub message: String, } +pub const GMOD_CALL_ARG_BUILTINS_FIXTURE: &str = r#" +---@meta +---@attribute call_arg(domain: string, role: string, priority: integer?) +---@attribute overload_call_arg(param: integer, domain: string, role: string, priority: integer?) + +util = util or {} +net = net or {} +hook = hook or {} +timer = timer or {} +concommand = concommand or {} +vgui = vgui or {} +derma = derma or {} + +Entity = Entity or {} + +---@[call_arg("gmod.net_message", "define")] +---@param str string +function util.AddNetworkString(str) end + +---@[call_arg("gmod.net_message", "start")] +---@param messageName string +function net.Start(messageName, unreliable) end + +---@[call_arg("gmod.net_message", "receive")] +---@param messageName string +---@[call_arg("gmod.net_message", "callback")] +---@param callback function +function net.Receive(messageName, callback) end + +---@[call_arg("gmod.hook", "add")] +---@param eventName string +---@param identifier any +---@[call_arg("gmod.hook", "callback")] +---@param func function +function hook.Add(eventName, identifier, func) end + +---@[call_arg("gmod.hook", "emit")] +---@param eventName string +function hook.Run(eventName, ...) end + +---@[call_arg("gmod.hook", "emit")] +---@param eventName string +---@[call_arg("gmod.hook", "gamemode_table")] +---@param gamemodeTable table +function hook.Call(eventName, gamemodeTable, ...) end + +---@[call_arg("gmod.hook", "remove")] +---@param eventName string +---@param identifier any +function hook.Remove(eventName, identifier) end + +---@[call_arg("gmod.concommand", "define")] +---@param name string +---@[call_arg("gmod.concommand", "callback")] +---@param callback function +function concommand.Add(name, callback, autoComplete, helpText, flags) end + +---@[call_arg("gmod.convar", "define_server")] +---@param name string +function _G.CreateConVar(name, value, flags, helptext, min, max) end + +---@[call_arg("gmod.convar", "define_client")] +---@param name string +function _G.CreateClientConVar(name, default, shouldsave, userinfo, helptext, min, max) end + +---@[call_arg("gmod.class_base", "reference")] +---@param value string +function _G.DEFINE_BASECLASS(value) end + +---@[call_arg("gmod.gamemode", "reference")] +---@param base string +function _G.DeriveGamemode(base) end + +---@[call_arg("gmod.color", "r")] +---@param r number +---@[call_arg("gmod.color", "g")] +---@param g number +---@[call_arg("gmod.color", "b")] +---@param b number +---@[call_arg("gmod.color", "a")] +---@param a? number +---@return Color +function _G.Color(r, g, b, a) end + +---@[call_arg("gmod.timer", "define")] +---@param identifier string +---@[call_arg("gmod.timer", "callback")] +---@param func function +function timer.Create(identifier, delay, repetitions, func) end + +---@param delay number +---@[call_arg("gmod.timer", "simple")] +---@param func function +function timer.Simple(delay, func) end + +---@[call_arg("gmod.vgui_panel", "reference")] +---@param className string +function vgui.Create(className, parent, name) end + +---@[call_arg("gmod.vgui_panel", "define")] +---@param name string +---@[call_arg("gmod.vgui_panel", "table")] +---@param panel table +---@[call_arg("gmod.vgui_panel", "base")] +---@param base string +function vgui.Register(name, panel, base) end + +---@[call_arg("gmod.vgui_panel", "define_control")] +---@param class string +---@param description string +---@[call_arg("gmod.vgui_panel", "table")] +---@param panel table +---@[call_arg("gmod.vgui_panel", "base")] +---@param base string +function derma.DefineControl(class, description, panel, base) end + +---@[call_arg("gmod.derma_skin", "define")] +---@param name string +function derma.DefineSkin(name, description, skin) end + +---@[call_arg("gmod.derma_skin", "reference")] +---@param name string +function derma.GetNamedSkin(name) end + +function derma.GetSkinTable() end + +---@[call_arg("gmod.derma_skin", "reference")] +---@param skinName string +function Panel:SetSkin(skinName) end + +---@[overload_call_arg(0, "gmod.network_var", "type")] +---@[overload_call_arg(1, "gmod.network_var", "define")] +---@overload fun(type: string, name: string, extended?: table) +---@[call_arg("gmod.network_var", "type")] +---@param type string +---@param slot number +---@[call_arg("gmod.network_var", "define")] +---@param name string +---@param extended? table +function Entity:NetworkVar(type, slot, name, extended) end + +---@[overload_call_arg(0, "gmod.network_var", "type")] +---@[overload_call_arg(2, "gmod.network_var", "define_element")] +---@overload fun(type: string, element: string, name: string, extended?: table) +---@[call_arg("gmod.network_var", "type")] +---@param type string +---@param slot number +---@param element string +---@[call_arg("gmod.network_var", "define_element")] +---@param name string +---@param extended? table +function Entity:NetworkVarElement(type, slot, element, name, extended) end +"#; + #[cfg(test)] pub fn diagnostics_to_snapshot_set( file: impl Into, @@ -112,7 +266,7 @@ impl VirtualWorkspace { pub fn new_with_init_std_lib() -> Self { let generator = VirtualUrlGenerator::new(); let mut analysis = EmmyLuaAnalysis::new(); - analysis.init_std_lib(None); + analysis.init_std_lib(); let base = &generator.base; analysis.add_main_workspace(base.clone()); VirtualWorkspace { @@ -196,6 +350,13 @@ impl VirtualWorkspace { ) } + pub fn def_gmod_call_arg_builtins(&mut self) -> FileId { + self.def_file( + "lua/includes/glua_ls_gmod_call_arg_builtins.lua", + GMOD_CALL_ARG_BUILTINS_FIXTURE, + ) + } + pub fn def_file(&mut self, file_name: &str, content: &str) -> FileId { let uri = self.virtual_url_generator.new_uri(file_name); diff --git a/crates/glua_code_analysis/src/vfs/loader.rs b/crates/glua_code_analysis/src/vfs/loader.rs index 62fe39875..35db3ad43 100644 --- a/crates/glua_code_analysis/src/vfs/loader.rs +++ b/crates/glua_code_analysis/src/vfs/loader.rs @@ -57,6 +57,18 @@ pub fn load_workspace_files( return Ok(files); } + // Guard: if the root doesn't exist or isn't a directory, skip the walk + // entirely. This prevents walking nonexistent paths or non-directory roots + // (e.g. an empty path that resolved to the workspace root, or a library + // path that was removed/renamed). + if !root.exists() || !root.is_dir() { + log::warn!( + "Workspace root does not exist or is not a directory, skipping: {:?}", + root + ); + return Ok(Vec::new()); + } + let include_pattern: Vec<&str> = include_pattern.iter().map(String::as_str).collect(); let include_set = match wax::any(include_pattern) { Ok(glob) => glob, @@ -156,6 +168,8 @@ pub fn read_file_with_encoding(path: &Path, encoding: &str) -> Option { #[cfg(test)] mod tests { use super::{LuaFileInfo, normalize_path_for_ordering, sort_lua_files_by_normalized_path}; + use googletest::prelude::*; + use std::path::PathBuf; #[test] fn vfs_loader_normalizes_slashes_and_trailing_separator() { @@ -182,4 +196,27 @@ mod tests { assert_eq!(files[0].path, r"C:/workspace/A.lua"); assert_eq!(files[1].path, r"C:\workspace\z.lua"); } + + #[gtest] + fn load_workspace_files_returns_empty_for_nonexistent_root() { + let nonexistent = PathBuf::from("/some/path/that/does/not/exist/__glua_ls_test__"); + let result = + super::load_workspace_files(&nonexistent, &["*.lua".to_string()], &[], &[], None); + + expect_that!(result, ok(anything())); + let files = result.unwrap(); + expect_that!(files.len(), eq(0)); + } + + #[gtest] + fn load_workspace_files_returns_empty_for_nonexistent_root_windows() { + // Use a Windows-style path that is extremely unlikely to exist. + let nonexistent = PathBuf::from(r"C:\__glua_ls_nonexistent_test_dir__\workspace"); + let result = + super::load_workspace_files(&nonexistent, &["*.lua".to_string()], &[], &[], None); + + expect_that!(result, ok(anything())); + let files = result.unwrap(); + expect_that!(files.len(), eq(0)); + } } diff --git a/crates/glua_doc_cli/src/init.rs b/crates/glua_doc_cli/src/init.rs index 86c6acaba..6afadfd29 100644 --- a/crates/glua_doc_cli/src/init.rs +++ b/crates/glua_doc_cli/src/init.rs @@ -108,7 +108,7 @@ pub fn load_workspace( } analysis.update_config(Arc::new(emmyrc)); - analysis.init_std_lib(None); + analysis.init_std_lib(); let file_infos = collect_workspace_files( &workspace_folders, diff --git a/crates/glua_ls/Cargo.toml b/crates/glua_ls/Cargo.toml index b9334a003..23290cef2 100644 --- a/crates/glua_ls/Cargo.toml +++ b/crates/glua_ls/Cargo.toml @@ -33,15 +33,12 @@ notify.workspace = true tokio-util.workspace = true rowan.workspace = true walkdir.workspace = true -rust-i18n.workspace = true glob.workspace = true -serde_yml.workspace = true itertools.workspace = true dirs.workspace = true wax.workspace = true internment.workspace = true smol_str.workspace = true -include_dir.workspace = true [dependencies.clap] workspace = true diff --git a/crates/glua_ls/locales/action/zh_CN.yaml b/crates/glua_ls/locales/action/zh_CN.yaml deleted file mode 100644 index 3c3a90de2..000000000 --- a/crates/glua_ls/locales/action/zh_CN.yaml +++ /dev/null @@ -1,24 +0,0 @@ -Disable current line diagnostic (%{name}): | - 在此行禁用诊断 (%{name}) - -Disable all diagnostics in current file (%{name}): | - 在此文件禁用诊断 (%{name}) - -Disable all diagnostics in current project (%{name}): | - 在此项目禁用诊断 (%{name}) - -# TODO: translate -Add @%{name} to the list of known tags: | - Add @%{name} to the list of known tags - -use cast to remove nil: | - 使用 cast 移除 nil - -Do you want to modify the require path?: | - 你想要修改 `require` 的路径吗? - -Modify: | - 修改 - -Replace with local alias '%{name}': | - 替换为本地变量别名 '%{name}' diff --git a/crates/glua_ls/locales/keywords/en.yaml b/crates/glua_ls/locales/keywords/en.yaml deleted file mode 100644 index 71eb21cda..000000000 --- a/crates/glua_ls/locales/keywords/en.yaml +++ /dev/null @@ -1,205 +0,0 @@ -keywords.for: | - The `for` keyword is used to create a loop that can iterate over a range, collection, or iterator. - - ### Example Usage - - ```lua - -- Iterate over a range - for i = 1, 10 do - print(i) - end - - -- Iterate over a collection - local fruits = {"apple", "banana", "cherry"} - for index, fruit in ipairs(fruits) do - print(index, fruit) - end - ``` - -keywords.if: | - The `if` keyword is used for conditional statements, executing different code blocks based on the truthiness of the condition. - - ### Example Usage - - ```lua - local x = 10 - if x > 5 then - print("x is greater than 5") - elseif x == 5 then - print("x is equal to 5") - else - print("x is less than 5") - end - ``` - -keywords.while: | - The `while` keyword is used to create a loop that repeats as long as the condition is true. - - ### Example Usage - - ```lua - local i = 1 - while i <= 10 do - print(i) - i = i + 1 - end - ``` - -keywords.function: | - The `function` keyword is used to define a function, which can contain a set of instructions and can be called. - - ### Example Usage - - ```lua - function greet(name) - print("Hello, " .. name) - end - - greet("world") - ``` - -keywords.local: | - The `local` keyword is used to declare local variables or functions, which are limited to the scope of the block. - - ### Example Usage - - ```lua - local x = 10 - local function add(a, b) - return a + b - end - - print(add(x, 5)) - ``` - -keywords.return: | - The `return` keyword is used to return values from a function and terminate the function's execution. - - ### Example Usage - - ```lua - function add(a, b) - return a + b - end - - local sum = add(5, 3) - print(sum) -- Output 8 - ``` - -keywords.break: | - The `break` keyword is used to exit the current loop. - - ### Example Usage - - ```lua - local i = 1 - while i <= 10 do - if i == 5 then - break - end - print(i) - i = i + 1 - end - -- Output 1 to 4 - ``` - -keywords.do: | - The `do` keyword is used to create a block, where the variables inside the block are local. - - ### Example Usage - - ```lua - local x = 10 - do - local x = 5 - print(x) -- Output 5 - end - print(x) -- Output 10 - ``` - -keywords.end: | - The `end` keyword is used to end a block, function, or control structure. - - ### Example Usage - - ```lua - if true then - print("This is true") - end - ``` - -keywords.repeat: | - The `repeat` keyword is used to create a loop that ends when the condition is true. - - ### Example Usage - - ```lua - local i = 1 - repeat - print(i) - i = i + 1 - until i > 5 - -- Output 1 to 5 - ``` - -keywords.until: | - The `until` keyword is used in a `repeat` loop to indicate the end condition of the loop. - - ### Example Usage - - ```lua - local i = 1 - repeat - print(i) - i = i + 1 - until i > 5 - -- Output 1 to 5 - ``` - -keywords.then: | - The `then` keyword is used in an `if` statement to indicate the code block to execute when the condition is true. - - ### Example Usage - - ```lua - local x = 10 - if x > 5 then - print("x is greater than 5") - end - ``` - -keywords.elseif: | - The `elseif` keyword is used in an `if` statement to indicate another condition to check. - - ### Example Usage - - ```lua - local x = 10 - if x > 5 then - print("x is greater than 5") - elseif x == 5 then - print("x is equal to 5") - end - ``` - -keywords.in: | - The `in` keyword is used in a generic `for` loop to indicate the collection or iterator to iterate over. - - ### Example Usage - - ```lua - local fruits = {"apple", "banana", "cherry"} - for index, fruit in ipairs(fruits) do - print(index, fruit) - end - -keywords.goto: | - The `goto` keyword is used to jump to a label in the code. - - ### Example Usage - - ```lua - ::label:: - print("Hello") - goto label - ``` diff --git a/crates/glua_ls/locales/keywords/zh_CN.yaml b/crates/glua_ls/locales/keywords/zh_CN.yaml deleted file mode 100644 index 0f2a73295..000000000 --- a/crates/glua_ls/locales/keywords/zh_CN.yaml +++ /dev/null @@ -1,205 +0,0 @@ -keywords.for: | - `for` 关键字用于创建一个循环,可以遍历一个范围、集合或迭代器。 - - ### 使用示例 - - ```lua - -- 遍历一个范围 - for i = 1, 10 do - print(i) - end - - -- 遍历一个集合 - local fruits = {"apple", "banana", "cherry"} - for index, fruit in ipairs(fruits) do - print(index, fruit) - end - ``` - -keywords.if: | - `if` 关键字用于条件判断,根据条件的真假执行不同的代码块。 - - ### 使用示例 - - ```lua - local x = 10 - if x > 5 then - print("x 大于 5") - elseif x == 5 then - print("x 等于 5") - else - print("x 小于 5") - end - ``` - -keywords.while: | - `while` 关键字用于创建一个循环,只要条件为真就会重复执行代码块。 - - ### 使用示例 - - ```lua - local i = 1 - while i <= 10 do - print(i) - i = i + 1 - end - ``` - -keywords.function: | - `function` 关键字用于定义一个函数,可以包含一组指令,并且可以被调用。 - - ### 使用示例 - - ```lua - function greet(name) - print("Hello, " .. name) - end - - greet("world") - ``` - -keywords.local: | - `local` 关键字用于声明局部变量或局部函数,作用范围仅限于所在的代码块。 - - ### 使用示例 - - ```lua - local x = 10 - local function add(a, b) - return a + b - end - - print(add(x, 5)) - ``` - -keywords.return: | - `return` 关键字用于从函数中返回值,并结束函数的执行。 - - ### 使用示例 - - ```lua - function add(a, b) - return a + b - end - - local sum = add(5, 3) - print(sum) -- 输出 8 - ``` - -keywords.break: | - `break` 关键字用于退出当前循环。 - - ### 使用示例 - - ```lua - local i = 1 - while i <= 10 do - if i == 5 then - break - end - print(i) - i = i + 1 - end - -- 输出 1 到 4 - ``` - -keywords.do: | - `do` 关键字用于创建一个块,块中的变量是局部的。 - - ### 使用示例 - - ```lua - local x = 10 - do - local x = 5 - print(x) -- 输出 5 - end - print(x) -- 输出 10 - ``` - -keywords.end: | - `end` 关键字用于结束一个块、函数或控制结构。 - - ### 使用示例 - - ```lua - if true then - print("This is true") - end - ``` - -keywords.repeat: | - `repeat` 关键字用于创建一个循环,直到条件为真时结束。 - - ### 使用示例 - - ```lua - local i = 1 - repeat - print(i) - i = i + 1 - until i > 5 - -- 输出 1 到 5 - ``` - -keywords.until: | - `until` 关键字用于在 `repeat` 循环中,表示循环的结束条件。 - - ### 使用示例 - - ```lua - local i = 1 - repeat - print(i) - i = i + 1 - until i > 5 - -- 输出 1 到 5 - ``` - -keywords.then: | - `then` 关键字用于 `if` 语句中,表示条件为真时执行的代码块。 - - ### 使用示例 - - ```lua - local x = 10 - if x > 5 then - print("x 大于 5") - end - ``` - -keywords.elseif: | - `elseif` 关键字用于 `if` 语句中,表示另一个条件判断。 - - ### 使用示例 - - ```lua - local x = 10 - if x > 5 then - print("x 大于 5") - elseif x == 5 then - print("x 等于 5") - end - ``` - -keywords.in: | - `in` 关键字用于泛型 `for` 循环中,表示要遍历的集合或迭代器。 - - ### 使用示例 - - ```lua - local fruits = {"apple", "banana", "cherry"} - for index, fruit in ipairs(fruits) do - print(index, fruit) - end - ``` -keywords.goto: | - `goto` 关键字用于跳转到代码中的某个标签。 - - ### 使用示例 - - ```lua - ::label:: - print("Hello") - goto label - ``` diff --git a/crates/glua_ls/locales/keywords/zh_HK.yaml b/crates/glua_ls/locales/keywords/zh_HK.yaml deleted file mode 100644 index 27aae0e8f..000000000 --- a/crates/glua_ls/locales/keywords/zh_HK.yaml +++ /dev/null @@ -1,231 +0,0 @@ -keywords.for: | - `for` 關鍵字用於創建一個循環,可以遍歷一個範圍、集合或迭代器。 - - ### 使用示例 - - ```lua - -- 遍歷一個範圍 - for i = 1, 10 do - print(i) - end - - -- 遍歷一個集合 - local fruits = {"apple", "banana", "cherry"} - for index, fruit in ipairs(fruits) do - print(index, fruit) - end - ``` - -# ...existing code... - -keywords.if: | - `if` 關鍵字用於條件判斷,根據條件的真假執行不同的代碼塊。 - - ### 使用示例 - - ```lua - local x = 10 - if x > 5 then - print("x 大於 5") - elseif x == 5 then - print("x 等於 5") - else - print("x 小於 5") - end - ``` - -# ...existing code... - -keywords.while: | - `while` 關鍵字用於創建一個循環,只要條件為真就會重複執行代碼塊。 - - ### 使用示例 - - ```lua - local i = 1 - while i <= 10 do - print(i) - i = i + 1 - end - ``` - -# ...existing code... - -keywords.function: | - `function` 關鍵字用於定義一個函數,可以包含一組指令,並且可以被調用。 - - ### 使用示例 - - ```lua - function greet(name) - print("Hello, " .. name) - end - - greet("world") - ``` - -# ...existing code... - -keywords.local: | - `local` 關鍵字用於聲明局部變量或局部函數,作用範圍僅限於所在的代碼塊。 - - ### 使用示例 - - ```lua - local x = 10 - local function add(a, b) - return a + b - end - - print(add(x, 5)) - ``` - -# ...existing code... - -keywords.return: | - `return` 關鍵字用於從函數中返回值,並結束函數的執行。 - - ### 使用示例 - - ```lua - function add(a, b) - return a + b - end - - local sum = add(5, 3) - print(sum) -- 輸出 8 - ``` - -# ...existing code... - -keywords.break: | - `break` 關鍵字用於退出當前循環。 - - ### 使用示例 - - ```lua - local i = 1 - while i <= 10 do - if i == 5 then - break - end - print(i) - i = i + 1 - end - -- 輸出 1 到 4 - ``` - -# ...existing code... - -keywords.do: | - `do` 關鍵字用於創建一個塊,塊中的變量是局部的。 - - ### 使用示例 - - ```lua - local x = 10 - do - local x = 5 - print(x) -- 輸出 5 - end - print(x) -- 輸出 10 - ``` - -# ...existing code... - -keywords.end: | - `end` 關鍵字用於結束一個塊、函數或控制結構。 - - ### 使用示例 - - ```lua - if true then - print("This is true") - end - ``` - -# ...existing code... - -keywords.repeat: | - `repeat` 關鍵字用於創建一個循環,直到條件為真時結束。 - - ### 使用示例 - - ```lua - local i = 1 - repeat - print(i) - i = i + 1 - until i > 5 - -- 輸出 1 到 5 - ``` - -# ...existing code... - -keywords.until: | - `until` 關鍵字用於在 `repeat` 循環中,表示循環的結束條件。 - - ### 使用示例 - - ```lua - local i = 1 - repeat - print(i) - i = i + 1 - until i > 5 - -- 輸出 1 到 5 - ``` - -# ...existing code... - -keywords.then: | - `then` 關鍵字用於 `if` 語句中,表示條件為真時執行的代碼塊。 - - ### 使用示例 - - ```lua - local x = 10 - if x > 5 then - print("x 大於 5") - end - ``` - -# ...existing code... - -keywords.elseif: | - `elseif` 關鍵字用於 `if` 語句中,表示另一個條件判斷。 - - ### 使用示例 - - ```lua - local x = 10 - if x > 5 then - print("x 大於 5") - elseif x == 5 then - print("x 等於 5") - end - ``` - -# ...existing code... - -keywords.in: | - `in` 關鍵字用於泛型 `for` 循環中,表示要遍歷的集合或迭代器。 - - ### 使用示例 - - ```lua - local fruits = {"apple", "banana", "cherry"} - for index, fruit in ipairs(fruits) do - print(index, fruit) - end - ``` -keywords.goto: | - `goto` 關鍵字用於跳轉到程式中的某個標籤。 - - ### 使用示例 - - ```lua - ::label:: - print("Hello") - goto label - ``` diff --git a/crates/glua_ls/locales/misc.yaml b/crates/glua_ls/locales/misc.yaml deleted file mode 100644 index 4c5ee1134..000000000 --- a/crates/glua_ls/locales/misc.yaml +++ /dev/null @@ -1,5 +0,0 @@ -_version: 2 -completion.index %{label}: - en: index %{label} - zh_CN: 索引 %{label} - zh_HK: 索引 %{label} diff --git a/crates/glua_ls/locales/tags/en.yaml b/crates/glua_ls/locales/tags/en.yaml deleted file mode 100644 index ca60c2dc0..000000000 --- a/crates/glua_ls/locales/tags/en.yaml +++ /dev/null @@ -1,277 +0,0 @@ -tags.class: | - The `class` tag is used to document a class or a struct. - Example: - ```lua - ---@class MyClass - local MyClass = {} - ``` -tags.enum: | - The `enum` tag is used to document an enumeration. - Example: - ```lua - ---@enum MyEnum - local MyEnum = { - Value1 = 1, - Value2 = 2 - } - ``` -tags.interface: | - The `interface` is deprecated, use `class` instead. - Example: - ```lua - ---@interface MyInterface - local MyInterface = {} - ``` -tags.alias: | - The `alias` tag is used to document a type alias. - Example: - ```lua - ---@alias MyTypeAlias string|number - ``` -tags.field: | - The `field` tag is used to document a field of a class or a struct. - Example: - ```lua - ---@class MyClass - ---@field publicField string - MyClass = {} - ``` -tags.type: | - The `type` tag is used to document a type. - Example: - ```lua - ---@type string - local myString = "Hello" - ``` -tags.param: | - The `param` tag is used to document a function parameter. - Example: - ```lua - ---@param paramName string - function myFunction(paramName) - end - ``` -tags.return: | - The `return` tag is used to document the return value of a function. - Example: - ```lua - ---@return string - function myFunction() - return "Hello" - end - ``` -tags.generic: | - The `generic` tag is used to document generic types. - Example: - ```lua - ---@generic T - ---@param param T - ---@return T - function identity(param) - return param - end - ``` -tags.see: | - The `see` tag is used to reference another documentation entry. - Example: - ```lua - ---@see otherFunction - function myFunction() - end - ``` -tags.deprecated: | - The `deprecated` tag is used to mark a function or a field as deprecated. - Example: - ```lua - ---@deprecated - function oldFunction() - end - ``` -tags.cast: | - The `cast` tag is used to document a type cast. - Example: - ```lua - ---@cast varName string - local varName = someValue - ``` -tags.overload: | - The `overload` tag is used to document an overloaded function. - Example: - ```lua - ---@overload fun(param: string):void - function myFunction(param) - end - ``` -tags.async: | - The `async` tag is used to document an asynchronous function. - Example: - ```lua - ---@async - function asyncFunction() - end - ``` -tags.public: | - The `public` tag is used to mark a field or a function as public. - Example: - ```lua - ---@public - MyClass.publicField = "" - ``` -tags.protected: | - The `protected` tag is used to mark a field or a function as protected. - Example: - ```lua - ---@protected - MyClass.protectedField = "" - ``` -tags.private: | - The `private` tag is used to mark a field or a function as private. - Example: - ```lua - ---@private - local privateField = "" - ``` -tags.package: | - The `package` tag is used to document a package. - Example: - ```lua - ---@package - local myPackage = {} - ``` -tags.meta: | - The `meta` tag is used to document meta information. - Example: - ```lua - ---@meta - local metaInfo = {} - ``` -tags.diagnostic: | - The `diagnostic` tag is used to document diagnostic information. - Example: - ```lua - ---@diagnostic disable-next-line: unused-global - local unusedVar = 1 - ``` -tags.version: | - The `version` tag is used to document the version of a module or a function. - Example: - ```lua - ---@version 1.0 - function myFunction() - end - ``` -tags.as: | - The `as` tag is used to document type assertions. - Example: - ```lua - ---@as string - local varName = someValue - ``` -tags.nodiscard: | - The `nodiscard` tag is used to indicate that the return value should not be discarded. - Example: - ```lua - ---@nodiscard - function importantFunction() - return "Important" - end - ``` -tags.operator: | - The `operator` tag is used to document operator overloads. - Example: - ```lua - ---@class - ---@operator add(MyClass):MyClass - ``` -tags.module: | - The `module` tag is used to document a module. - Example: - ```lua - ---@module MyModule - local MyModule = {} - ``` -tags.namespace: | - The `namespace` tag is used to document a namespace. - Example: - ```lua - ---@namespace MyNamespace - ``` -tags.using: | - The `using` tag is used to document using declarations. - Example: - ```lua - ---@using MyNamespace - ``` -tags.source: | - The `source` tag is used to document the source of a function or a module. - Example: - ```lua - ---@source https://example.com/source - function myFunction() - end - ``` -tags.readonly: | - The `readonly` tag is used to mark a field as read-only. - but it is not supported in current - Example: - ```lua - ---@readonly - MyClass.readonlyField = "constant" - ``` -tags.export: | - The `export` tag is used to indicate that a variable is exported, supporting quick import. - It accepts `namespace` or `global` as parameters. If no parameter is provided, it defaults to `global`. - Example: - ```lua - ---@export namespace -- When set to `namespace`, only allows import within the same namespace - local export = {} - - export.func = function() - -- When typing `func` in other files, import suggestions will be shown - end - - return export - ``` -tags.language: | - The `language` tag is used to specify language injection for code blocks. - Example: - ```lua - ---@language sql - local t = [[ - SELECT * FROM users WHERE id = 1; - SELECT name, email FROM users WHERE active = 1; - UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = 1; - DELETE FROM users WHERE id = 2; - INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'); - ]] - ``` -tags.attribute: | - `attribute` tag defines an attribute. Attribute is used to attach extra information to a definition. - Example: - ```lua - ---@attribute deprecated(message: string?) - - ---@class A - ---@[deprecated("delete")] # `b` field is marked as deprecated - ---@field b string - ---@[deprecated] # If `attribute` allows no parameters, the parentheses can be omitted - ---@field c string - ``` -tags.hook: | - The `hook` tag registers a function as a GMod hook. - Example: - ```lua - ---@hook PlayerSpawn - ``` -tags.realm: | - The `realm` tag indicates which realm(s) this field/function is available in (client, server, or shared). - Example: - ```lua - ---@realm client - ``` -tags.fileparam: | - The `fileparam` tag provides file-scoped default types for unannotated function parameters holding a specific name. - Example: - ```lua - ---@fileparam ply Player - ``` diff --git a/crates/glua_ls/locales/tags/zh_CN.yaml b/crates/glua_ls/locales/tags/zh_CN.yaml deleted file mode 100644 index df8075a92..000000000 --- a/crates/glua_ls/locales/tags/zh_CN.yaml +++ /dev/null @@ -1,259 +0,0 @@ -tags.class: | - `class` 标签用于表示一个类或结构体。 - 示例: - ```lua - ---@class MyClass - local MyClass = {} - ``` -tags.enum: | - `enum` 标签用于表示一个枚举。 - 示例: - ```lua - ---@enum MyEnum - local MyEnum = { - Value1 = 1, - Value2 = 2 - } - ``` -tags.interface: | - `interface` 标签已弃用,请使用 `class`。 - 示例: - ```lua - ---@interface MyInterface - local MyInterface = {} - ``` -tags.alias: | - `alias` 标签用于表示一个类型别名。 - 示例: - ```lua - ---@alias MyTypeAlias string|number - ``` -tags.field: | - `field` 标签用于表示一个类或结构体的字段。 - 示例: - ```lua - ---@class MyClass - ---@field publicField string - MyClass = {} - ``` -tags.type: | - `type` 标签用于表示一个类型。 - 示例: - ```lua - ---@type string - local myString = "Hello" - ``` -tags.param: | - `param` 标签用于表示一个函数参数。 - 示例: - ```lua - ---@param paramName string - function myFunction(paramName) - end - ``` -tags.return: | - `return` 标签用于表示一个函数的返回值。 - 示例: - ```lua - ---@return string - function myFunction() - return "Hello" - end - ``` -tags.generic: | - `generic` 标签用于表示泛型类型。 - 示例: - ```lua - ---@generic T - ---@param param T - ---@return T - function identity(param) - return param - end - ``` -tags.see: | - `see` 标签用于引用另一个文档条目。 - 示例: - ```lua - ---@see otherFunction - function myFunction() - end - ``` -tags.deprecated: | - `deprecated` 标签用于标记一个函数或字段为已弃用。 - 示例: - ```lua - ---@deprecated - function oldFunction() - end - ``` -tags.cast: | - `cast` 标签用于表示一个类型转换。 - 示例: - ```lua - ---@cast varName string - local varName = someValue - ``` -tags.overload: | - `overload` 标签用于表示一个重载函数。 - 示例: - ```lua - ---@overload fun(param: string):void - function myFunction(param) - end - ``` -tags.async: | - `async` 标签用于表示一个异步函数。 - 示例: - ```lua - ---@async - function asyncFunction() - end - ``` -tags.public: | - `public` 标签用于标记一个字段或函数为公共的。 - 示例: - ```lua - ---@public - MyClass.publicField = "" - ``` -tags.protected: | - `protected` 标签用于标记一个字段或函数为受保护的。 - 示例: - ```lua - ---@protected - MyClass.protectedField = "" - ``` -tags.private: | - `private` 标签用于标记一个字段或函数为私有的。 - 示例: - ```lua - ---@private - local privateField = "" - ``` -tags.package: | - `package` 标签用于表示一个包。 - 示例: - ```lua - ---@package - local myPackage = {} - ``` -tags.meta: | - `meta` 标签用于表示元信息。 - 示例: - ```lua - ---@meta - local metaInfo = {} - ``` -tags.diagnostic: | - `diagnostic` 标签用于表示诊断信息。 - 示例: - ```lua - ---@diagnostic disable-next-line: unused-global - local unusedVar = 1 - ``` -tags.version: | - `version` 标签用于表示一个模块或函数的版本。 - 示例: - ```lua - ---@version 1.0 - function myFunction() - end - ``` -tags.as: | - `as` 标签用于表示类型断言。 - 示例: - ```lua - ---@as string - local varName = someValue - ``` -tags.nodiscard: | - `nodiscard` 标签用于指示返回值不应被丢弃。 - 示例: - ```lua - ---@nodiscard - function importantFunction() - return "Important" - end - ``` -tags.operator: | - `operator` 标签用于表示运算符重载。 - 示例: - ```lua - ---@class - ---@operator add(MyClass):MyClass - ``` -tags.module: | - `module` 标签用于表示一个模块。 - 示例: - ```lua - ---@module MyModule - local MyModule = {} - ``` -tags.namespace: | - `namespace` 标签用于表示一个命名空间。 - 示例: - ```lua - ---@namespace MyNamespace - ``` -tags.using: | - `using` 标签用于表示使用声明。 - 示例: - ```lua - ---@using MyNamespace - ``` -tags.source: | - `source` 标签用于表示一个函数或模块的来源。 - 示例: - ```lua - ---@source https://example.com/source - function myFunction() - end - ``` -tags.readonly: | - `readonly` 标签用于标记一个字段为只读。 - 但目前不支持 - 示例: - ```lua - ---@readonly - MyClass.readonlyField = "constant" - ``` -tags.export: | - `export` 标签用于表示一个变量为导出的,用于支持快速导入。 - 接收的参数为 `namespace` 或 `global`,不输入参数默认为 `global`。 - 示例: - ```lua - ---@export namespace -- 当为`namespace`时仅允许同命名空间引入 - local export = {} - - export.func = function() - -- 在其他文件输入`func`时会提示导入 - end - - return export - ``` -tags.language: | - `language` 标签用于为代码块指定语言注入。 - 示例: - ```lua - ---@language sql - local t = [[ - SELECT * FROM users WHERE id = 1; - SELECT name, email FROM users WHERE active = 1; - UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = 1; - DELETE FROM users WHERE id = 2; - INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'); - ]] - ``` -tags.attribute: | - `attribute` 标签定义一个特性。特性用于附加额外信息到定义。 - 示例: - ```lua - ---@attribute deprecated(message: string?) - - ---@class A - ---@[deprecated("delete")] - ---@field b string # `b` 字段被标记为已弃用 - ---@[deprecated] # 如果`attribute`允许无参数,则可以省略括号 - ---@field c string - ``` diff --git a/crates/glua_ls/locales/tags/zh_HK.yaml b/crates/glua_ls/locales/tags/zh_HK.yaml deleted file mode 100644 index 4c4eb8ef8..000000000 --- a/crates/glua_ls/locales/tags/zh_HK.yaml +++ /dev/null @@ -1,257 +0,0 @@ -tags.class: | - `class` 標籤用於表示一個類或結構體。 - 示例: - ```lua - ---@class MyClass - local MyClass = {} - ``` -tags.enum: | - `enum` 標籤用於表示一個枚舉。 - 示例: - ```lua - ---@enum MyEnum - local MyEnum = { - Value1 = 1, - Value2 = 2 - } - ``` -tags.interface: | - `interface` 標籤已棄用,請使用 `class`。 - 示例: - ```lua - ---@interface MyInterface - local MyInterface = {} - ``` -tags.alias: | - `alias` 標籤用於表示一個類型別名。 - 示例: - ```lua - ---@alias MyTypeAlias string|number - ``` -tags.field: | - `field` 標籤用於表示一個類或結構體的字段。 - 示例: - ```lua - ---@class MyClass - ---@field publicField string - MyClass = {} - ``` -tags.type: | - `type` 標籤用於表示一個類型。 - 示例: - ```lua - ---@type string - local myString = "Hello" - ``` -tags.param: | - `param` 標籤用於表示一個函數參數。 - 示例: - ```lua - ---@param paramName string - function myFunction(paramName) - end - ``` -tags.return: | - `return` 標籤用於表示一個函數的返回值。 - 示例: - ```lua - ---@return string - function myFunction() - return "Hello" - end - ``` -tags.generic: | - `generic` 標籤用於表示泛型類型。 - 示例: - ```lua - ---@generic T - ---@param param T - ---@return T - function identity(param) - return param - end - ``` -tags.see: | - `see` 標籤用於引用另一個文檔條目。 - 示例: - ```lua - ---@see otherFunction - function myFunction() - end - ``` -tags.deprecated: | - `deprecated` 標籤用於標記一個函數或字段為已棄用。 - 示例: - ```lua - ---@deprecated - function oldFunction() - end - ``` -tags.cast: | - `cast` 標籤用於表示一個類型轉換。 - 示例: - ```lua - ---@cast varName string - local varName = someValue - ``` -tags.overload: | - `overload` 標籤用於表示一個重載函數。 - 示例: - ```lua - ---@overload fun(param: string):void - function myFunction(param) - end - ``` -tags.async: | - `async` 標籤用於表示一個異步函數。 - 示例: - ```lua - ---@async - function asyncFunction() - end - ``` -tags.public: | - `public` 標籤用於標記一個字段或函數為公共的。 - 示例: - ```lua - ---@public - MyClass.publicField = "" - ``` -tags.protected: | - `protected` 標籤用於標記一個字段或函數為受保護的。 - 示例: - ```lua - ---@protected - MyClass.protectedField = "" - ``` -tags.private: | - `private` 標籤用於標記一個字段或函數為私有的。 - 示例: - ```lua - ---@private - local privateField = "" - ``` -tags.package: | - `package` 標籤用於表示一個包。 - 示例: - ```lua - ---@package - local myPackage = {} - ``` -tags.meta: | - `meta` 標籤用於表示元信息。 - 示例: - ```lua - ---@meta - local metaInfo = {} - ``` -tags.diagnostic: | - `diagnostic` 標籤用於表示診斷信息。 - 示例: - ```lua - ---@diagnostic disable-next-line: unused-global - local unusedVar = 1 - ``` -tags.version: | - `version` 標籤用於表示一個模塊或函數的版本。 - 示例: - ```lua - ---@version 1.0 - function myFunction() - end - ``` -tags.as: | - `as` 標籤用於表示類型斷言。 - 示例: - ```lua - ---@as string - local varName = someValue - ``` -tags.nodiscard: | - `nodiscard` 標籤用於指示返回值不應被丟棄。 - 示例: - ```lua - ---@nodiscard - function importantFunction() - return "Important" - end - ``` -tags.operator: | - `operator` 標籤用於表示運算符重載。 - 示例: - ```lua - ---@class - ---@operator add(MyClass):MyClass - ``` -tags.module: | - `module` 標籤用於表示一個模塊。 - 示例: - ```lua - ---@module MyModule - local MyModule = {} - ``` -tags.namespace: | - `namespace` 標籤用於表示一個命名空間。 - 示例: - ```lua - ---@namespace MyNamespace - ``` -tags.using: | - `using` 標籤用於表示使用聲明。 - 示例: - ```lua - ---@using MyNamespace - ``` -tags.source: | - `source` 標籤用於表示一個函數或模塊的來源。 - 示例: - ```lua - ---@source https://example.com/source - function myFunction() - end - ``` -tags.readonly: | - `readonly` 標籤用於標記一個字段為只讀。 - 但目前不支持 - 示例: - ```lua - ---@readonly - MyClass.readonlyField = "constant" - ``` -tags.export: | - `export` 標籤用於表示一個變量為導出的,用於支持快速導入。 - 接收的參數為 `namespace` 或 `global`,不輸入參數默認為 `global`。 - 示例: - ```lua - ---@export namespace -- 當為`namespace`時僅允許同命名空間引入 - local export = {} - - export.func = function() - -- 在其他文件輸入`func`時會提示導入 - end - ``` -tags.language: | - `language` 標籤用於為程式碼塊指定語言注入。 - 示例: - ```lua - ---@language sql - local t = [[ - SELECT * FROM users WHERE id = 1; - SELECT name, email FROM users WHERE active = 1; - UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = 1; - DELETE FROM users WHERE id = 2; - INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'); - ]] - ``` -tags.attribute: | - `attribute` 標籤定義一個特性。特性用於附加額外信息到定義。 - 示例: - ```lua - ---@attribute deprecated(message: string?) - - ---@class A - ---@[deprecated("delete")] # `b` 字段被標記為已棄用 - ---@field b string - ---@[deprecated] # 如果`attribute`允許無參數,則可以省略括號 - ---@field c string - ``` diff --git a/crates/glua_ls/src/cmd_args.rs b/crates/glua_ls/src/cmd_args.rs index 6bef35f6b..2aec452bd 100644 --- a/crates/glua_ls/src/cmd_args.rs +++ b/crates/glua_ls/src/cmd_args.rs @@ -28,10 +28,6 @@ pub struct CmdArgs { #[cfg_attr(feature = "cli", structopt(long, default_value = "none"))] pub log_path: NoneableString, - /// Path to the resources and logs directory. Use 'none' to indicate that assets should not be output to the file system. - #[cfg_attr(feature = "cli", structopt(long, default_value = ""))] - pub resources_path: NoneableString, - /// Whether to load the standard library. #[cfg_attr(feature = "cli", structopt(long, default_value = "true"))] pub load_stdlib: CmdBool, @@ -172,3 +168,30 @@ impl From for ClientId { } } } + +#[cfg(test)] +mod tests { + use super::*; + use googletest::prelude::*; + use std::str::FromStr; + + #[gtest] + fn noneable_string_none_value_parses_to_none() { + let parsed = NoneableString::from_str("none").unwrap(); + expect_that!(parsed.0, none()); + } + + #[gtest] + fn noneable_string_none_case_insensitive() { + let parsed = NoneableString::from_str("None").unwrap(); + expect_that!(parsed.0, none()); + let parsed2 = NoneableString::from_str("NONE").unwrap(); + expect_that!(parsed2.0, none()); + } + + #[gtest] + fn noneable_string_non_none_value_parses_to_some() { + let parsed = NoneableString::from_str("/some/path").unwrap(); + expect_that!(parsed.0, some(eq("/some/path"))); + } +} diff --git a/crates/glua_ls/src/context/mod.rs b/crates/glua_ls/src/context/mod.rs index 083a1118e..cf3bce66b 100644 --- a/crates/glua_ls/src/context/mod.rs +++ b/crates/glua_ls/src/context/mod.rs @@ -132,7 +132,10 @@ fn keep_stale_editor_data_on_cancel(method: &str) -> bool { // causes brief visual flickering as the client clears its display. matches!( method, - "textDocument/inlayHint" | "textDocument/semanticTokens/full" | "gluals/annotator" + "textDocument/codeLens" + | "textDocument/inlayHint" + | "textDocument/semanticTokens/full" + | "gluals/annotator" ) } @@ -145,10 +148,10 @@ fn should_send_stale_response_on_cancel(method: &str, response: &Response) -> bo return false; } - if method == "textDocument/inlayHint" { - // Returning stale-but-empty inlay hints clears currently rendered hints - // while the user is typing. Let RequestCanceled keep the previous hints - // visible until we have a fresh, complete inlay result. + if matches!(method, "textDocument/codeLens" | "textDocument/inlayHint") { + // Returning stale-but-empty results for inlay hints/code lens can clear + // currently rendered UI while typing. Let RequestCanceled keep the + // previous output visible until fresh results are ready. return result.as_array().is_some_and(|hints| !hints.is_empty()); } @@ -361,7 +364,7 @@ mod tests { use std::time::Duration; #[gtest] - fn stale_inlay_hint_response_requires_non_empty_array() -> Result<()> { + fn stale_inlay_and_code_lens_response_requires_non_empty_array() -> Result<()> { let empty = Response::new_ok(1.into(), json!([])); let non_empty = Response::new_ok(2.into(), json!([{"label": ": number"}])); @@ -373,6 +376,14 @@ mod tests { should_send_stale_response_on_cancel("textDocument/inlayHint", &non_empty), eq(true) )?; + verify_that!( + should_send_stale_response_on_cancel("textDocument/codeLens", &empty), + eq(false) + )?; + verify_that!( + should_send_stale_response_on_cancel("textDocument/codeLens", &non_empty), + eq(true) + )?; Ok(()) } @@ -399,7 +410,7 @@ mod tests { } #[gtest] - fn cancel_all_requests_except_preserves_inlay_requests() -> Result<()> { + fn cancel_all_requests_except_preserves_inlay_and_code_lens_requests() -> Result<()> { let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build"); runtime.block_on(async { let (conn, _peer) = lsp_server::Connection::memory(); @@ -429,26 +440,44 @@ mod tests { ) .await; - let (inlay_token, hover_token) = { + let code_lens_id: RequestId = 3.into(); + context + .task( + code_lens_id.clone(), + RequestTaskMetadata::new("textDocument/codeLens", None), + |_cancel_token| async move { + tokio::time::sleep(Duration::from_millis(250)).await; + Some(Response::new_ok(code_lens_id, json!([{"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}}, "command": {"title": "test", "command": "test"}}]))) + }, + ) + .await; + + let (inlay_token, code_lens_token, hover_token) = { let requests = context.requests.lock().await; let inlay = requests .get(&RequestId::from(1)) .expect("inlay request should exist") .cancel_token .clone(); + let code_lens = requests + .get(&RequestId::from(3)) + .expect("code lens request should exist") + .cancel_token + .clone(); let hover = requests .get(&RequestId::from(2)) .expect("hover request should exist") .cancel_token .clone(); - (inlay, hover) + (inlay, code_lens, hover) }; context - .cancel_all_requests_except(&["textDocument/inlayHint"]) + .cancel_all_requests_except(&["textDocument/inlayHint", "textDocument/codeLens"]) .await; verify_that!(inlay_token.is_cancelled(), eq(false))?; + verify_that!(code_lens_token.is_cancelled(), eq(false))?; verify_that!(hover_token.is_cancelled(), eq(true))?; Ok(()) }) diff --git a/crates/glua_ls/src/handlers/code_actions/actions/build_fix_code.rs b/crates/glua_ls/src/handlers/code_actions/actions/build_fix_code.rs index 91baccd50..215db4882 100644 --- a/crates/glua_ls/src/handlers/code_actions/actions/build_fix_code.rs +++ b/crates/glua_ls/src/handlers/code_actions/actions/build_fix_code.rs @@ -38,7 +38,7 @@ pub fn build_need_check_nil( }; actions.push(CodeActionOrCommand::CodeAction(CodeAction { - title: t!("use cast to remove nil").to_string(), + title: "use cast to remove nil".to_string(), kind: Some(CodeActionKind::QUICKFIX), edit: Some(WorkspaceEdit { changes: Some(HashMap::from([(document.get_uri(), vec![text_edit])])), @@ -63,10 +63,10 @@ pub fn build_add_doc_tag( let tag_name = data.as_str()?; actions.push(CodeActionOrCommand::CodeAction(CodeAction { - title: t!("Add @%{name} to the list of known tags", name = tag_name).to_string(), + title: format!("Add @{tag_name} to the list of known tags"), kind: Some(CodeActionKind::QUICKFIX), command: Some(make_auto_doc_tag_command( - t!("Add @%{name} to the list of known tags", name = tag_name).as_ref(), + format!("Add @{tag_name} to the list of known tags").as_ref(), tag_name, )), @@ -173,7 +173,7 @@ fn push_gmod_null_check_action( let text_edit = TextEdit { range, new_text }; actions.push(CodeActionOrCommand::CodeAction(CodeAction { - title: t!("Use IsValid(...) for GMod NULL check").to_string(), + title: "Use IsValid(...) for GMod NULL check".to_string(), kind: Some(CodeActionKind::QUICKFIX), edit: Some(WorkspaceEdit { changes: Some(HashMap::from([(document.get_uri(), vec![text_edit])])), @@ -197,7 +197,7 @@ pub fn build_preferred_local_alias_fix( }; actions.push(CodeActionOrCommand::CodeAction(CodeAction { - title: t!("Replace with local alias '%{name}'", name = alias_name).to_string(), + title: format!("Replace with local alias '{alias_name}'"), kind: Some(CodeActionKind::QUICKFIX), edit: Some(WorkspaceEdit { changes: Some(HashMap::from([(document.get_uri(), vec![text_edit])])), diff --git a/crates/glua_ls/src/handlers/code_actions/build_actions.rs b/crates/glua_ls/src/handlers/code_actions/build_actions.rs index a9b90addd..326e1bade 100644 --- a/crates/glua_ls/src/handlers/code_actions/build_actions.rs +++ b/crates/glua_ls/src/handlers/code_actions/build_actions.rs @@ -92,11 +92,10 @@ fn add_disable_code_action( } actions.push(CodeActionOrCommand::CodeAction(CodeAction { - title: t!( - "Disable current line diagnostic (%{name})", + title: format!( + "Disable current line diagnostic ({name})", name = diagnostic_code.get_name() - ) - .to_string(), + ), kind: Some(CodeActionKind::QUICKFIX), edit: Some(WorkspaceEdit { changes: build_disable_next_line_changes(semantic_model, range.start, diagnostic_code), @@ -106,11 +105,10 @@ fn add_disable_code_action( })); actions.push(CodeActionOrCommand::CodeAction(CodeAction { - title: t!( - "Disable all diagnostics in current file (%{name})", + title: format!( + "Disable all diagnostics in current file ({name})", name = diagnostic_code.get_name() - ) - .to_string(), + ), kind: Some(CodeActionKind::QUICKFIX), edit: Some(WorkspaceEdit { changes: build_disable_file_changes(semantic_model, diagnostic_code), @@ -120,15 +118,14 @@ fn add_disable_code_action( })); actions.push(CodeActionOrCommand::CodeAction(CodeAction { - title: t!( - "Disable all diagnostics in current project (%{name})", + title: format!( + "Disable all diagnostics in current project ({name})", name = diagnostic_code.get_name() - ) - .to_string(), + ), kind: Some(CodeActionKind::QUICKFIX), command: Some(make_disable_code_command( - t!( - "Disable all diagnostics in current project (%{name})", + format!( + "Disable all diagnostics in current project ({name})", name = diagnostic_code.get_name() ) .as_ref(), diff --git a/crates/glua_ls/src/handlers/code_lens/build_code_lens.rs b/crates/glua_ls/src/handlers/code_lens/build_code_lens.rs index c5915e669..f51ef0a5d 100644 --- a/crates/glua_ls/src/handlers/code_lens/build_code_lens.rs +++ b/crates/glua_ls/src/handlers/code_lens/build_code_lens.rs @@ -1,5 +1,6 @@ use glua_code_analysis::{ LuaDeclId, LuaMemberId, LuaMemberOwner, LuaType, LuaTypeDeclId, SemanticModel, + resolve_alias_type, }; use glua_parser::{ LuaAssignStat, LuaAst, LuaAstNode, LuaAstToken, LuaCallExpr, LuaExpr, LuaFuncStat, @@ -9,6 +10,7 @@ use lsp_types::{CodeLens, Command, Range}; use rowan::NodeOrToken; use super::{CodeLensData, CodeLensResolveData}; +use crate::handlers::gmod_string_context::find_call_arg_roles; fn is_top_level_stat(syntax: &glua_parser::LuaSyntaxNode) -> bool { syntax @@ -247,8 +249,11 @@ fn find_gmod_class_from_type( semantic_model: &SemanticModel, typ: &LuaType, ) -> Option { - match typ { - LuaType::Def(type_id) => find_gmod_class_from_type_id(semantic_model, type_id), + let resolved = resolve_alias_type(semantic_model.get_db(), typ); + match &resolved.typ { + LuaType::Def(type_id) | LuaType::Ref(type_id) => { + find_gmod_class_from_type_id(semantic_model, type_id) + } _ => None, } } @@ -278,7 +283,8 @@ fn find_gmod_class_from_type_id( .get_super_types(type_id)?; for super_type in supers { - let super_name = match &super_type { + let resolved_super = resolve_alias_type(semantic_model.get_db(), &super_type); + let super_name = match &resolved_super.typ { LuaType::Def(id) | LuaType::Ref(id) => id.get_simple_name(), _ => continue, }; @@ -314,39 +320,40 @@ fn push_gmod_class_code_lens(result: &mut Vec, range: Range, info: &Gm }); } -/// Adds CodeLenses above net.Start / net.Receive / util.AddNetworkString -/// call sites that mirror the VGUI/entity lens style: a lazy-resolved -/// "N usage(s)" lens (clicking it opens the references panel for every -/// indexed send/receive of this message) plus a `Name : Kind` label lens -/// where `Kind` is `Send`, `Receive`, or `Register` so the role of the -/// call site is obvious at a glance. Detailed payload information lives -/// in the hover, not the lens. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum NetCodeLensCallKind { + Define, + Start, + Receive, +} + +/// Adds CodeLenses above net message call sites that mirror the VGUI/entity +/// lens style: a lazy-resolved "N usage(s)" lens plus a `Name : Kind` label. +/// The annotated message argument decides whether a call defines, starts, or +/// receives a net message. Builtins keep their familiar labels through their +/// annotation metadata; wrappers fall back to the wrapper's call path label. fn add_net_call_code_lens( semantic_model: &SemanticModel, result: &mut Vec, call_expr: LuaCallExpr, ) -> Option<()> { let call_path = call_expr.get_access_path()?; - let kind_label: String = match call_path.as_str() { - "net.Start" => resolve_start_kind_label(semantic_model, &call_expr), - "net.Receive" => resolve_receive_kind_label(semantic_model, &call_expr), - "util.AddNetworkString" => "util.AddNetworkString".to_string(), - _ => return Some(()), - }; - - let args_list = call_expr.get_args_list()?; - let first_arg = args_list.get_args().next()?; - let LuaExpr::LiteralExpr(literal_expr) = first_arg else { - return Some(()); - }; - let LuaLiteralToken::String(string_token) = literal_expr.get_literal()? else { - return Some(()); - }; + let (kind, message_arg_idx) = net_code_lens_call_kind(semantic_model, &call_expr)?; + let string_token = string_arg_at(&call_expr, message_arg_idx)?; let raw_name = string_token.get_value(); let message_name = raw_name.trim(); if message_name.is_empty() { return Some(()); } + let kind_label = match kind { + NetCodeLensCallKind::Define => call_path.clone(), + NetCodeLensCallKind::Start => { + resolve_start_kind_label(semantic_model, &call_expr, &call_path, message_arg_idx) + } + NetCodeLensCallKind::Receive => { + resolve_receive_kind_label(semantic_model, &call_expr, &call_path) + } + }; let document = semantic_model.get_document(); let range = document.to_lsp_range(call_expr.get_range())?; @@ -374,26 +381,64 @@ fn add_net_call_code_lens( Some(()) } +fn net_code_lens_call_kind( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, +) -> Option<(NetCodeLensCallKind, usize)> { + let args_list = call_expr.get_args_list()?; + let args: Vec = args_list.get_args().collect(); + if !args + .iter() + .any(|arg| matches!(arg, LuaExpr::LiteralExpr(literal_expr) if matches!(literal_expr.get_literal(), Some(LuaLiteralToken::String(_))))) + { + return None; + } + + let arg_count = args.len(); + for (arg_idx, role) in find_call_arg_roles( + semantic_model, + call_expr, + arg_count, + "gmod.net_message", + &["define", "start", "receive"], + ) { + let kind = match role.role.as_str() { + "define" => NetCodeLensCallKind::Define, + "start" => NetCodeLensCallKind::Start, + "receive" => NetCodeLensCallKind::Receive, + _ => continue, + }; + return Some((kind, arg_idx)); + } + None +} + +fn string_arg_at(call_expr: &LuaCallExpr, arg_idx: usize) -> Option { + let args_list = call_expr.get_args_list()?; + let arg = args_list.get_args().nth(arg_idx)?; + let LuaExpr::LiteralExpr(literal_expr) = arg else { + return None; + }; + let LuaLiteralToken::String(string_token) = literal_expr.get_literal()? else { + return None; + }; + Some(string_token) +} + /// Picks the label for a `net.Start` lens. Looks up the indexed send flow /// originating at this call site and uses the actual transport call name /// (e.g. `net.SendToServer`) so the lens reflects the realm/recipients /// instead of the generic `net.Start`. Falls back to `net.Start` when the /// flow could not be resolved (wrapped helpers, conservative stubs, message /// not yet indexed, etc). -fn resolve_start_kind_label(semantic_model: &SemanticModel, call_expr: &LuaCallExpr) -> String { - let fallback = "net.Start".to_string(); - let args_list = match call_expr.get_args_list() { - Some(args) => args, - None => return fallback, - }; - let first_arg = match args_list.get_args().next() { - Some(arg) => arg, - None => return fallback, - }; - let LuaExpr::LiteralExpr(literal_expr) = first_arg else { - return fallback; - }; - let Some(LuaLiteralToken::String(string_token)) = literal_expr.get_literal() else { +fn resolve_start_kind_label( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + fallback: &str, + message_arg_idx: usize, +) -> String { + let fallback = fallback.to_string(); + let Some(string_token) = string_arg_at(call_expr, message_arg_idx) else { return fallback; }; let message_name = string_token.get_value(); @@ -432,9 +477,11 @@ fn resolve_start_kind_label(semantic_model: &SemanticModel, call_expr: &LuaCallE /// flows; pattern-based pairing keeps the label faithful to *this* receive's /// actual senders. Falls back to `net.Receive` when no candidate matches /// (e.g. counterpart not yet indexed, opaque callback, ambiguous realm). -fn resolve_receive_kind_label(semantic_model: &SemanticModel, call_expr: &LuaCallExpr) -> String { - let fallback = "net.Receive".to_string(); - +fn resolve_receive_kind_label( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + fallback: &str, +) -> String { let file_id = semantic_model.get_file_id(); let call_range = call_expr.get_range(); let db = semantic_model.get_db(); @@ -442,14 +489,14 @@ fn resolve_receive_kind_label(semantic_model: &SemanticModel, call_expr: &LuaCal let infer_index = db.get_gmod_infer_index(); let Some(file_data) = network_index.get_file_data(file_id) else { - return fallback; + return fallback.to_string(); }; let Some(receive_flow) = file_data .receive_flows .iter() .find(|flow| flow.receive_range == call_range) else { - return fallback; + return fallback.to_string(); }; let paired = glua_code_analysis::pair_senders_for_receive( @@ -468,7 +515,7 @@ fn resolve_receive_kind_label(semantic_model: &SemanticModel, call_expr: &LuaCal } if kinds.is_empty() { - fallback + fallback.to_string() } else { kinds.join(", ") } @@ -520,6 +567,7 @@ mod tests { #[gtest] fn vgui_reassigned_panel_code_lens_labels_resolve_per_region() { let mut ws = VirtualWorkspace::new(); + ws.def_gmod_call_arg_builtins(); let mut emmyrc = glua_code_analysis::Emmyrc::default(); emmyrc.gmod.enabled = true; emmyrc.gmod.vgui.code_lens_enabled = true; @@ -560,6 +608,7 @@ mod tests { #[gtest] fn vgui_reassigned_panel_assignment_code_lens_resolves_per_region() { let mut ws = VirtualWorkspace::new(); + ws.def_gmod_call_arg_builtins(); let mut emmyrc = glua_code_analysis::Emmyrc::default(); emmyrc.gmod.enabled = true; emmyrc.gmod.vgui.code_lens_enabled = true; @@ -621,4 +670,62 @@ mod tests { assert_that!(assignment_titles[0].as_str(), eq("ReButton : DButton")); assert_that!(assignment_titles[1].as_str(), eq("ReTree : DTree")); } + + #[gtest] + fn net_code_lens_uses_annotated_message_argument_roles() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = glua_code_analysis::Emmyrc::default(); + emmyrc.gmod.enabled = true; + emmyrc.gmod.network.enabled = true; + emmyrc.gmod.network.code_lens_enabled = true; + ws.update_emmyrc(emmyrc); + + let file_id = ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@param realm string + ---@[call_arg("gmod.net_message", "define")] + ---@param name string + function RegisterScopedNet(realm, name) end + + ---@param realm string + ---@[call_arg("gmod.net_message", "start")] + ---@param name string + function StartScopedNet(realm, name) end + + ---@param realm string + ---@[call_arg("gmod.net_message", "receive")] + ---@param name string + ---@param callback fun() + function ReceiveScopedNet(realm, name, callback) end + + RegisterScopedNet("shared", "WrappedMessage") + StartScopedNet("shared", "WrappedMessage") + ReceiveScopedNet("shared", "WrappedMessage", function() end) + "#, + ); + let semantic_model = ws + .analysis + .compilation + .get_semantic_model(file_id) + .expect("expected semantic model"); + let lenses = build_code_lens(&semantic_model).expect("expected code lenses"); + + let titles: Vec = lenses + .iter() + .filter_map(|lens| lens.command.as_ref().map(|command| command.title.clone())) + .collect(); + assert_that!(titles, contains(eq("WrappedMessage : RegisterScopedNet"))); + assert_that!(titles, contains(eq("WrappedMessage : StartScopedNet"))); + assert_that!(titles, contains(eq("WrappedMessage : ReceiveScopedNet"))); + + let net_message_lens_count = lenses + .iter() + .filter_map(|lens| lens.data.as_ref()) + .filter_map(|value| serde_json::from_value::(value.clone()).ok()) + .filter(|data| matches!(data.payload, CodeLensData::NetMessage(_))) + .count(); + assert_that!(net_message_lens_count, eq(3usize)); + } } diff --git a/crates/glua_ls/src/handlers/code_lens/mod.rs b/crates/glua_ls/src/handlers/code_lens/mod.rs index 927a08c68..4769cbcb3 100644 --- a/crates/glua_ls/src/handlers/code_lens/mod.rs +++ b/crates/glua_ls/src/handlers/code_lens/mod.rs @@ -23,6 +23,15 @@ pub async fn on_code_lens_handler( return None; } + let uri = params.text_document.uri; + + if !context + .wait_until_latest_document_version_applied(&uri, &cancel_token) + .await + { + return None; + } + // Wait for pending reindex work so VS Code keeps the current lenses visible // instead of clearing them during the dirty window, which causes layout flicker. if !context @@ -33,7 +42,6 @@ pub async fn on_code_lens_handler( return None; } - let uri = params.text_document.uri; let analysis = context.read_analysis(&cancel_token).await?; let file_id = analysis.get_file_id(&uri)?; let semantic_model = analysis.compilation.get_semantic_model(file_id)?; diff --git a/crates/glua_ls/src/handlers/completion/add_completions/add_member_completion.rs b/crates/glua_ls/src/handlers/completion/add_completions/add_member_completion.rs index 7a4febb1b..71e69b4bd 100644 --- a/crates/glua_ls/src/handlers/completion/add_completions/add_member_completion.rs +++ b/crates/glua_ls/src/handlers/completion/add_completions/add_member_completion.rs @@ -223,7 +223,14 @@ pub fn add_member_completion_with_description_hint( } if can_add_snippet { - if apply_staged_call_snippet(builder, &label, status, &mut completion_item).is_none() + if apply_staged_call_snippet( + builder, + &label, + status, + &remove_nil_type, + &mut completion_item, + ) + .is_none() && builder.support_snippets(typ) && let Some(snippet) = get_function_snippet(builder, &label, typ, call_display) { @@ -588,7 +595,7 @@ fn try_add_alias_completion_item_new( )); // 更新 label_details 添加别名提示 - let index_hint = t!("completion.index %{label}", label = label).to_string(); + let index_hint = format!("index {label}"); let label_details = alias_completion_item .label_details .get_or_insert_with(Default::default); diff --git a/crates/glua_ls/src/handlers/completion/providers/gmod_system_provider.rs b/crates/glua_ls/src/handlers/completion/providers/gmod_system_provider.rs index 0bcff4459..4e220b024 100644 --- a/crates/glua_ls/src/handlers/completion/providers/gmod_system_provider.rs +++ b/crates/glua_ls/src/handlers/completion/providers/gmod_system_provider.rs @@ -1,12 +1,13 @@ use std::collections::HashMap; use glua_code_analysis::{ - FileId, GmodHookSiteMetadata, GmodRealm, NetSendFlow, NetSendKind, SemanticModel, + FileId, GmodHookSiteMetadata, GmodRealm, LuaType, NetSendFlow, NetSendKind, SemanticModel, + find_call_arg_role_from_type, }; use glua_parser::{ LuaAstNode, LuaAstToken, LuaCallArgList, LuaCallExpr, LuaComment, LuaCommentOwner, LuaDocTag, LuaDocTagRealm, LuaExpr, LuaFuncStat, LuaIndexExpr, LuaIndexKey, LuaLiteralExpr, - LuaLocalFuncStat, LuaStringToken, PathTrait, + LuaLocalFuncStat, LuaStringToken, }; use lsp_types::{ Command, CompletionItem, CompletionTextEdit, InsertTextFormat, InsertTextMode, TextEdit, @@ -16,6 +17,10 @@ use rowan::TextSize; use crate::handlers::completion::add_completions::CompletionTriggerStatus; use crate::handlers::completion::completion_builder::CompletionBuilder; use crate::handlers::completion::completion_data::CompletionData; +use crate::handlers::gmod_string_context::{ + find_call_arg_roles, find_string_call_arg_role, is_hook_name_string_context, + is_net_message_string_context, +}; use crate::handlers::hover::resolve_hook_property_owner; use super::get_text_edit_range_in_string; @@ -30,6 +35,13 @@ struct HookStats { callback_params: Option<(u8, Vec)>, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StagedStringCallKind { + NetReceive, + HookAdd, + HookEmit { include_gamemode_arg: bool }, +} + pub fn add_completion(builder: &mut CompletionBuilder) -> Option<()> { if builder.is_cancelled() { return None; @@ -55,33 +67,40 @@ pub fn add_completion(builder: &mut CompletionBuilder) -> Option<()> { && let Some(call_expr) = literal_expr .get_parent::() .and_then(|args| args.get_parent::()) - && let Some(call_path) = call_expr.get_access_path() + && let Some(arg_index) = literal_arg_index(&call_expr, &literal_expr) && let Some(text_edit_range) = get_text_edit_range_in_string(builder, string_token) { - let string_added = if is_net_message_string_context(&call_expr, literal_expr.clone()) { - if staged_call_snippets_enabled(builder) && matches_call_path(&call_path, "net.Receive") - { - add_staged_net_receive_completion_items(builder, &call_expr) - } else { - add_net_message_completion_items(builder, Some(text_edit_range)) - } - } else if is_hook_name_string_context(builder, &call_expr, literal_expr) { - if staged_call_snippets_enabled(builder) && matches_call_path(&call_path, "hook.Add") { - add_staged_hook_add_completion_items(builder, &call_expr) - } else if staged_call_snippets_enabled(builder) - && matches_call_path(&call_path, "hook.Run") - { - add_staged_hook_emit_completion_items(builder, &call_expr, false) - } else if staged_call_snippets_enabled(builder) - && matches_call_path(&call_path, "hook.Call") - { - add_staged_hook_emit_completion_items(builder, &call_expr, true) + let string_added = + if is_net_message_string_context(&builder.semantic_model, &call_expr, arg_index) { + match staged_string_call_kind(&builder.semantic_model, &call_expr, arg_index) { + Some(StagedStringCallKind::NetReceive) + if staged_call_snippets_enabled(builder) => + { + add_staged_net_receive_completion_items(builder, &call_expr) + } + _ => add_net_message_completion_items(builder, Some(text_edit_range)), + } + } else if is_hook_name_string_context(&builder.semantic_model, &call_expr, arg_index) { + match staged_string_call_kind(&builder.semantic_model, &call_expr, arg_index) { + Some(StagedStringCallKind::HookAdd) + if staged_call_snippets_enabled(builder) => + { + add_staged_hook_add_completion_items(builder, &call_expr) + } + Some(StagedStringCallKind::HookEmit { + include_gamemode_arg, + }) if staged_call_snippets_enabled(builder) => { + add_staged_hook_emit_completion_items( + builder, + &call_expr, + include_gamemode_arg, + ) + } + _ => add_hook_completion_items(builder, Some(text_edit_range)), + } } else { - add_hook_completion_items(builder, Some(text_edit_range)) - } - } else { - false - }; + false + }; if string_added { builder.stop_here(); } @@ -96,30 +115,20 @@ pub fn apply_staged_call_snippet( builder: &CompletionBuilder, label: &str, status: CompletionTriggerStatus, + typ: &LuaType, completion_item: &mut CompletionItem, ) -> Option<()> { if status != CompletionTriggerStatus::Dot || !staged_call_snippets_enabled(builder) { return None; } - let index_expr = builder - .trigger_token - .parent_ancestors() - .find_map(LuaIndexExpr::cast)?; - let prefix_path = expr_access_path(&index_expr.get_prefix_expr()?)?; - let call_path = format!("{prefix_path}.{label}"); - if !matches_call_path(&call_path, "hook.Add") - && !matches_call_path(&call_path, "hook.Run") - && !matches_call_path(&call_path, "hook.Call") - && !matches_call_path(&call_path, "net.Receive") - { - return None; - } + let kind = staged_string_call_kind_from_type(builder.semantic_model.get_db(), typ)?; - completion_item.insert_text = Some(if matches_call_path(&call_path, "hook.Call") { - format!(r#"{}("${{1}}", ${{2:GAMEMODE}})"#, label) - } else { - format!(r#"{}("${{1}}")"#, label) + completion_item.insert_text = Some(match kind { + StagedStringCallKind::HookEmit { + include_gamemode_arg: true, + } => format!(r#"{}("${{1}}", ${{2:GAMEMODE}})"#, label), + _ => format!(r#"{}("${{1}}")"#, label), }); completion_item.insert_text_format = Some(InsertTextFormat::SNIPPET); completion_item.sort_text = Some(format!("000_gmod_staged_call_{}", label.to_lowercase())); @@ -723,72 +732,105 @@ fn collect_hook_completion_entries( .collect() } -fn is_net_message_string_context(call_expr: &LuaCallExpr, literal_expr: LuaLiteralExpr) -> bool { - let Some(call_path) = call_expr.get_access_path() else { - return false; - }; - if !matches_call_path(&call_path, "util.AddNetworkString") - && !matches_call_path(&call_path, "net.Start") - && !matches_call_path(&call_path, "net.Receive") +fn literal_arg_index(call_expr: &LuaCallExpr, literal_expr: &LuaLiteralExpr) -> Option { + let args_list = call_expr.get_args_list()?; + args_list + .get_args() + .position(|arg| arg.get_position() == literal_expr.get_position()) +} + +fn staged_string_call_kind( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + arg_index: usize, +) -> Option { + if find_string_call_arg_role( + semantic_model, + call_expr, + arg_index, + "gmod.net_message", + &["receive"], + ) + .is_some() { - return false; + return Some(StagedStringCallKind::NetReceive); } - let Some(args_list) = call_expr.get_args_list() else { - return false; - }; - let arg_idx = args_list - .get_args() - .position(|arg| arg.get_position() == literal_expr.get_position()); + let hook_role = find_string_call_arg_role( + semantic_model, + call_expr, + arg_index, + "gmod.hook", + &["add", "emit"], + )?; + match hook_role.role.as_str() { + "add" => Some(StagedStringCallKind::HookAdd), + "emit" => Some(StagedStringCallKind::HookEmit { + include_gamemode_arg: call_has_hook_gamemode_table_role(semantic_model, call_expr), + }), + _ => None, + } +} + +fn staged_string_call_kind_from_type( + db: &glua_code_analysis::DbIndex, + typ: &LuaType, +) -> Option { + if find_call_arg_role_from_type(db, typ, 0, "gmod.net_message", &["receive"]).is_some() { + return Some(StagedStringCallKind::NetReceive); + } - arg_idx == Some(0) + let hook_role = find_call_arg_role_from_type(db, typ, 0, "gmod.hook", &["add", "emit"])?; + match hook_role.role.as_str() { + "add" => Some(StagedStringCallKind::HookAdd), + "emit" => Some(StagedStringCallKind::HookEmit { + include_gamemode_arg: find_call_arg_role_from_type( + db, + typ, + 1, + "gmod.hook", + &["gamemode_table"], + ) + .is_some(), + }), + _ => None, + } } -fn is_hook_name_string_context( - builder: &CompletionBuilder, +fn call_has_hook_gamemode_table_role( + semantic_model: &SemanticModel, call_expr: &LuaCallExpr, - literal_expr: LuaLiteralExpr, ) -> bool { - let Some(call_path) = call_expr.get_access_path() else { - return false; - }; - let is_builtin = matches_call_path(&call_path, "hook.Add") - || matches_call_path(&call_path, "hook.Run") - || matches_call_path(&call_path, "hook.Call"); - let is_custom_emitter = builder - .semantic_model - .get_emmyrc() - .gmod - .hook_mappings - .emitter_to_hook - .iter() - .any(|(emitter_path, mapped_hook)| { - mapped_hook == "*" && matches_call_path(&call_path, emitter_path) - }); - if !is_builtin && !is_custom_emitter { - return false; + if let Some(args_list) = call_expr.get_args_list() { + let arg_count = args_list.get_args().count(); + if find_call_arg_roles( + semantic_model, + call_expr, + arg_count, + "gmod.hook", + &["gamemode_table"], + ) + .into_iter() + .any(|(_, role)| role.role == "gamemode_table") + { + return true; + } } - let Some(args_list) = call_expr.get_args_list() else { + let Some(prefix_expr) = call_expr.get_prefix_expr() else { return false; }; - let arg_idx = args_list - .get_args() - .position(|arg| arg.get_position() == literal_expr.get_position()); - arg_idx == Some(0) -} - -fn matches_call_path(path: &str, target: &str) -> bool { - path == target || path.ends_with(&format!(".{target}")) || path.ends_with(&format!(":{target}")) -} - -fn expr_access_path(expr: &LuaExpr) -> Option { - match expr { - LuaExpr::NameExpr(name_expr) => Some(name_expr.get_name_text()?.to_string()), - LuaExpr::IndexExpr(index_expr) => index_expr.get_access_path(), - LuaExpr::CallExpr(call_expr) => call_expr.get_access_path(), - _ => None, - } + let Some(callable_type) = semantic_model.infer_expr(prefix_expr).ok() else { + return false; + }; + find_call_arg_role_from_type( + semantic_model.get_db(), + &callable_type, + if call_expr.is_colon_call() { 2 } else { 1 }, + "gmod.hook", + &["gamemode_table"], + ) + .is_some() } fn completion_string_token(builder: &CompletionBuilder) -> Option { diff --git a/crates/glua_ls/src/handlers/definition/mod.rs b/crates/glua_ls/src/handlers/definition/mod.rs index aa8400a5e..4e651519a 100644 --- a/crates/glua_ls/src/handlers/definition/mod.rs +++ b/crates/glua_ls/src/handlers/definition/mod.rs @@ -4,13 +4,16 @@ mod goto_function; mod goto_module_file; mod goto_path; +use std::{collections::HashMap, rc::Rc}; + use glua_code_analysis::{ - EmmyLuaAnalysis, FileId, LuaCompilation, LuaType, SemanticDeclLevel, SemanticModel, WorkspaceId, + EmmyLuaAnalysis, FileId, GmodScriptedClassCallKind, LuaCompilation, LuaType, SemanticDeclLevel, + SemanticModel, WorkspaceId, }; use glua_parser::{ LuaAssignStat, LuaAstNode, LuaAstToken, LuaCallArgList, LuaCallExpr, LuaDocDescription, - LuaDocTagSee, LuaGeneralToken, LuaIndexExpr, LuaLiteralExpr, LuaStringToken, LuaTokenKind, - PathTrait, + LuaDocTagSee, LuaExpr, LuaGeneralToken, LuaIndexExpr, LuaLiteralExpr, LuaLiteralToken, + LuaStringToken, LuaTokenKind, }; pub use goto_def_definition::goto_def_definition; use goto_def_definition::goto_str_tpl_ref_definition; @@ -29,8 +32,10 @@ use crate::context::ServerContextSnapshot; use crate::handlers::definition::goto_function::goto_overload_function; use crate::handlers::definition::goto_path::goto_path; use crate::handlers::gmod_string_context::{ - NetMessageCallKind, extract_string_call_context, is_vgui_panel_string_context, - matches_call_path, net_message_call_kind, + NetMessageCallKind, annotated_net_message_flow_call_kind, extract_string_call_context, + find_string_call_arg_role, is_annotated_derma_skin_string_context, + is_annotated_vgui_panel_string_context, is_hook_name_string_context, + is_net_message_string_context, }; use crate::util::find_ref_at; @@ -50,13 +55,23 @@ pub async fn on_goto_definition_handler( let file_id = analysis.get_file_id(&uri)?; let position = params.text_document_position_params.position; - definition(&analysis, file_id, position) + definition_with_cancel(&analysis, file_id, position, Some(&cancel_token)) } +#[cfg(test)] pub fn definition( analysis: &EmmyLuaAnalysis, file_id: FileId, position: Position, +) -> Option { + definition_with_cancel(analysis, file_id, position, None) +} + +fn definition_with_cancel( + analysis: &EmmyLuaAnalysis, + file_id: FileId, + position: Position, + cancel_token: Option<&CancellationToken>, ) -> Option { let semantic_model = analysis.compilation.get_semantic_model(file_id)?; let root = semantic_model.get_root(); @@ -179,14 +194,28 @@ pub fn definition( ) { return Some(hook_response); } - if let Some(vgui_panel_response) = - goto_vgui_panel_definition(&semantic_model, string_token.clone()) - { + if let Some(vgui_panel_response) = goto_vgui_panel_definition( + &semantic_model, + &analysis.compilation, + string_token.clone(), + cancel_token, + ) { return Some(vgui_panel_response); } - if let Some(net_message_response) = - goto_net_message_definition(&semantic_model, string_token.clone()) - { + if let Some(derma_skin_response) = goto_derma_skin_definition( + &semantic_model, + &analysis.compilation, + string_token.clone(), + cancel_token, + ) { + return Some(derma_skin_response); + } + if let Some(net_message_response) = goto_net_message_definition( + &semantic_model, + &analysis.compilation, + string_token.clone(), + cancel_token, + ) { return Some(net_message_response); } if let Some(str_tpl_ref_response) = @@ -421,15 +450,12 @@ fn goto_hook_definition( .get_parent::()? .get_parent::()?; - let call_path = call_expr.get_access_path()?; - if !matches_call_path(&call_path, "hook.Add") - && !matches_call_path(&call_path, "hook.Run") - && !matches_call_path(&call_path, "hook.Call") - { - return None; - } let args_list = call_expr.get_args_list()?; - if args_list.get_args().next()?.get_position() != literal_expr.get_position() { + let arg_index = args_list + .get_args() + .position(|arg| arg.get_position() == literal_expr.get_position())?; + + if !is_hook_name_string_context(semantic_model, &call_expr, arg_index) { return None; } @@ -452,14 +478,23 @@ fn goto_hook_definition( fn goto_vgui_panel_definition( semantic_model: &SemanticModel, + compilation: &LuaCompilation, string_token: LuaStringToken, + cancel_token: Option<&CancellationToken>, ) -> Option { if !semantic_model.get_emmyrc().gmod.enabled { return None; } let context = extract_string_call_context(&string_token)?; - if !is_vgui_panel_string_context(&context.call_path, context.arg_index) { + let annotated_context = string_token + .get_parent::() + .and_then(|literal| literal.get_parent::()) + .and_then(|args| args.get_parent::()) + .is_some_and(|call_expr| { + is_annotated_vgui_panel_string_context(semantic_model, &call_expr, context.arg_index) + }); + if !annotated_context { return None; } @@ -471,15 +506,26 @@ fn goto_vgui_panel_definition( let mut locations = Vec::new(); for (file_id, call) in definitions { + let definition_range = call.define_arg_range(GmodScriptedClassCallKind::VguiRegister); let Some(document) = semantic_model.get_document_by_file_id(file_id) else { continue; }; - let Some(location) = document.to_lsp_location(call.syntax_id.get_range()) else { + let Some(location) = document.to_lsp_location(definition_range) else { continue; }; - locations.push(location); + push_unique_location(&mut locations, location); } + collect_annotated_string_definitions( + semantic_model, + compilation, + &panel_name, + "gmod.vgui_panel", + &["define", "define_control"], + &mut locations, + cancel_token, + ); + if locations.is_empty() { return None; } @@ -487,23 +533,281 @@ fn goto_vgui_panel_definition( Some(GotoDefinitionResponse::Array(locations)) } +fn goto_derma_skin_definition( + semantic_model: &SemanticModel, + compilation: &LuaCompilation, + string_token: LuaStringToken, + cancel_token: Option<&CancellationToken>, +) -> Option { + if !semantic_model.get_emmyrc().gmod.enabled { + return None; + } + + let context = extract_string_call_context(&string_token)?; + let annotated_context = string_token + .get_parent::() + .and_then(|literal| literal.get_parent::()) + .and_then(|args| args.get_parent::()) + .is_some_and(|call_expr| { + is_annotated_derma_skin_string_context(semantic_model, &call_expr, context.arg_index) + }); + if !annotated_context { + return None; + } + + let definitions = semantic_model + .get_db() + .get_gmod_class_metadata_index() + .find_derma_skin_definitions(&context.name); + + let mut locations = Vec::new(); + for (file_id, call) in definitions { + let definition_range = call.define_arg_range(GmodScriptedClassCallKind::DermaDefineSkin); + let Some(document) = semantic_model.get_document_by_file_id(file_id) else { + continue; + }; + let Some(location) = document.to_lsp_location(definition_range) else { + continue; + }; + push_unique_location(&mut locations, location); + } + + collect_annotated_string_definitions( + semantic_model, + compilation, + &context.name, + "gmod.derma_skin", + &["define"], + &mut locations, + cancel_token, + ); + + if locations.is_empty() { + return None; + } + + Some(GotoDefinitionResponse::Array(locations)) +} + +fn collect_annotated_string_definitions( + semantic_model: &SemanticModel, + compilation: &LuaCompilation, + name: &str, + domain: &str, + roles: &[&str], + locations: &mut Vec, + cancel_token: Option<&CancellationToken>, +) { + let before_indexed = locations.len(); + let mut semantic_cache = HashMap::new(); + for reference in semantic_model + .get_db() + .get_reference_index() + .get_string_references(name) + { + if cancel_token.is_some_and(CancellationToken::is_cancelled) { + return; + } + + let Some(reference_semantic_model) = + get_semantic_model_cached(compilation, &mut semantic_cache, reference.file_id) + else { + continue; + }; + let root = reference_semantic_model.get_root(); + let Some(reference_token) = root + .syntax() + .token_at_offset(reference.value.start()) + .right_biased() + else { + continue; + }; + let Some(reference_string_token) = LuaStringToken::cast(reference_token) else { + continue; + }; + let Some(reference_context) = extract_string_call_context(&reference_string_token) else { + continue; + }; + if reference_context.name != name { + continue; + } + + let Some(call_expr) = reference_string_token + .get_parent::() + .and_then(|literal| literal.get_parent::()) + .and_then(|args| args.get_parent::()) + else { + continue; + }; + if find_string_call_arg_role( + &reference_semantic_model, + &call_expr, + reference_context.arg_index, + domain, + roles, + ) + .is_none() + { + continue; + } + + let Some(location) = reference_semantic_model + .get_document() + .to_lsp_location(reference_string_token.get_range()) + else { + continue; + }; + push_unique_location(locations, location); + } + + if locations.len() == before_indexed { + collect_annotated_string_definitions_from_ast( + semantic_model, + compilation, + name, + domain, + roles, + locations, + cancel_token, + &mut semantic_cache, + ); + } +} + +fn collect_annotated_string_definitions_from_ast<'a>( + semantic_model: &SemanticModel, + compilation: &'a LuaCompilation, + name: &str, + domain: &str, + roles: &[&str], + locations: &mut Vec, + cancel_token: Option<&CancellationToken>, + semantic_cache: &mut HashMap>>, +) { + for file_id in semantic_model.get_db().get_vfs().get_all_file_ids() { + if cancel_token.is_some_and(CancellationToken::is_cancelled) { + return; + } + + let Some(reference_semantic_model) = + get_semantic_model_cached(compilation, semantic_cache, file_id) + else { + continue; + }; + let root = reference_semantic_model.get_root(); + for call_expr in root.descendants::() { + if cancel_token.is_some_and(CancellationToken::is_cancelled) { + return; + } + + let Some(args_list) = call_expr.get_args_list() else { + continue; + }; + for (arg_index, arg) in args_list.get_args().enumerate() { + let LuaExpr::LiteralExpr(literal_expr) = arg else { + continue; + }; + let Some(LuaLiteralToken::String(string_token)) = literal_expr.get_literal() else { + continue; + }; + let Some(candidate_name) = + crate::handlers::gmod_string_context::normalize_string_name( + string_token.get_value(), + ) + else { + continue; + }; + if candidate_name != name { + continue; + } + if find_string_call_arg_role( + &reference_semantic_model, + &call_expr, + arg_index, + domain, + roles, + ) + .is_none() + { + continue; + } + + let Some(location) = reference_semantic_model + .get_document() + .to_lsp_location(string_token.get_range()) + else { + continue; + }; + push_unique_location(locations, location); + } + } + } +} + +fn get_semantic_model_cached<'a>( + compilation: &'a LuaCompilation, + semantic_cache: &mut HashMap>>, + file_id: FileId, +) -> Option>> { + if let Some(cached) = semantic_cache.get(&file_id) { + return Some(Rc::clone(cached)); + } + + let semantic_model = Rc::new(compilation.get_semantic_model(file_id)?); + semantic_cache.insert(file_id, Rc::clone(&semantic_model)); + Some(semantic_model) +} + +fn push_unique_location(locations: &mut Vec, location: Location) { + if !locations.contains(&location) { + locations.push(location); + } +} + fn goto_net_message_definition( semantic_model: &SemanticModel, + compilation: &LuaCompilation, string_token: LuaStringToken, + cancel_token: Option<&CancellationToken>, ) -> Option { if !semantic_model.get_emmyrc().gmod.enabled { return None; } let context = extract_string_call_context(&string_token)?; - let call_kind = net_message_call_kind(&context.call_path, context.arg_index)?; + let call_expr = string_token + .get_parent::() + .and_then(|literal| literal.get_parent::()) + .and_then(|args| args.get_parent::()); + let is_context = call_expr.as_ref().is_some_and(|call_expr| { + is_net_message_string_context(semantic_model, call_expr, context.arg_index) + }); + if !is_context { + return None; + } + let call_kind = call_expr.as_ref().and_then(|call_expr| { + annotated_net_message_flow_call_kind(semantic_model, call_expr, context.arg_index) + }); + let annotated_reference_context = call_expr.as_ref().is_some_and(|call_expr| { + find_string_call_arg_role( + semantic_model, + call_expr, + context.arg_index, + "gmod.net_message", + &["define", "reference"], + ) + .is_some() + }); + if call_kind.is_none() && !annotated_reference_context { + return None; + } let message_name = context.name; let network_index = semantic_model.get_db().get_gmod_network_index(); let mut locations = Vec::new(); match call_kind { - NetMessageCallKind::Start => { + Some(NetMessageCallKind::Start) => { for (file_id, flow) in network_index.get_receive_flows_for_message(&message_name) { let Some(document) = semantic_model.get_document_by_file_id(file_id) else { continue; @@ -511,10 +815,10 @@ fn goto_net_message_definition( let Some(location) = document.to_lsp_location(flow.receive_range) else { continue; }; - locations.push(location); + push_unique_location(&mut locations, location); } } - NetMessageCallKind::Receive => { + Some(NetMessageCallKind::Receive) => { for (file_id, flow) in network_index.get_send_flows_for_message(&message_name) { let Some(document) = semantic_model.get_document_by_file_id(file_id) else { continue; @@ -522,11 +826,22 @@ fn goto_net_message_definition( let Some(location) = document.to_lsp_location(flow.start_range) else { continue; }; - locations.push(location); + push_unique_location(&mut locations, location); } } + None => {} } + collect_annotated_string_definitions( + semantic_model, + compilation, + &message_name, + "gmod.net_message", + &["define"], + &mut locations, + cancel_token, + ); + if locations.is_empty() { return None; } diff --git a/crates/glua_ls/src/handlers/document_color/build_color.rs b/crates/glua_ls/src/handlers/document_color/build_color.rs index 1d3882759..94860d172 100644 --- a/crates/glua_ls/src/handlers/document_color/build_color.rs +++ b/crates/glua_ls/src/handlers/document_color/build_color.rs @@ -6,6 +6,8 @@ use glua_parser::{ use lsp_types::{Color, ColorInformation}; use rowan::{TextRange, TextSize}; +use crate::handlers::gmod_string_context::find_call_arg_roles; + #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct GmodColor { pub(crate) red: f32, @@ -84,6 +86,12 @@ fn try_build_semantic_color_tuple( return None; } + if try_build_annotated_color_tuple(call_expr.clone(), document, semantic_model, &args, result) + .is_some() + { + return Some(()); + } + let func = semantic_model.infer_call_expr_func(call_expr.clone(), Some(args.len()))?; let params = func.get_params(); @@ -182,6 +190,75 @@ fn try_build_semantic_color_tuple( Some(()) } +fn try_build_annotated_color_tuple( + call_expr: LuaCallExpr, + document: &LuaDocument, + semantic_model: &SemanticModel, + args: &[LuaExpr], + result: &mut Vec, +) -> Option<()> { + let mut red_idx = None; + let mut green_idx = None; + let mut blue_idx = None; + let mut alpha_idx = None; + + for (arg_idx, role) in find_call_arg_roles( + semantic_model, + &call_expr, + args.len(), + "gmod.color", + &["r", "red", "g", "green", "b", "blue", "a", "alpha"], + ) { + match role.role.as_str() { + "r" | "red" => red_idx = Some(arg_idx), + "g" | "green" => green_idx = Some(arg_idx), + "b" | "blue" => blue_idx = Some(arg_idx), + "a" | "alpha" => alpha_idx = Some(arg_idx), + _ => {} + } + } + + let red_idx = red_idx?; + let green_idx = green_idx?; + let blue_idx = blue_idx?; + if !(red_idx < green_idx && green_idx < blue_idx) { + return None; + } + if alpha_idx.is_some_and(|alpha_idx| alpha_idx <= blue_idx) { + return None; + } + + let mut components = [0.0f32; 4]; + components[0] = numeric_color_component(args.get(red_idx)?)?; + components[1] = numeric_color_component(args.get(green_idx)?)?; + components[2] = numeric_color_component(args.get(blue_idx)?)?; + components[3] = if let Some(alpha_idx) = alpha_idx { + numeric_color_component(args.get(alpha_idx)?)? + } else { + 1.0 + }; + + let first_arg = args.get(red_idx)?; + let last_arg = args.get(alpha_idx.unwrap_or(blue_idx))?; + let text_range = TextRange::new( + first_arg.syntax().text_range().start(), + last_arg.syntax().text_range().end(), + ); + let range = document.to_lsp_range(text_range)?; + + result.push(ColorInformation { + range, + color: Color { + red: components[0], + green: components[1], + blue: components[2], + alpha: components[3], + }, + }); + + Some(()) +} + /// Detects `Color(r, g, b)` or `Color(r, g, b, a)` calls where every argument is a /// numeric integer literal in the 0–255 range and registers a color swatch for them. fn try_build_gmod_color_call( @@ -262,6 +339,23 @@ fn gmod_color_call_info(call_expr: &LuaCallExpr) -> Option<(GmodColor, Vec Option { + let LuaExpr::LiteralExpr(lit_expr) = arg else { + return None; + }; + let LuaLiteralToken::Number(num_token) = lit_expr.get_literal()? else { + return None; + }; + let value: f64 = match num_token.get_number_value() { + NumberResult::Int(n) => n as f64, + NumberResult::Uint(n) => n as f64, + NumberResult::Float(n) => n, + }; + (0.0..=255.0) + .contains(&value) + .then_some((value / 255.0) as f32) +} + fn try_build_color_information( token: LuaSyntaxToken, document: &LuaDocument, @@ -449,6 +543,86 @@ mod tests { Ok(()) } + #[gtest] + fn detects_annotated_gmod_color_tuple() -> Result<()> { + let colors = collect_colors_semantic( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@param first number + ---@[call_arg("gmod.color", "r")] + ---@param second number + ---@[call_arg("gmod.color", "g")] + ---@param third number + ---@[call_arg("gmod.color", "b")] + ---@param fourth number + function Paint(first, second, third, fourth) end + + Paint(12, 34, 56, 78) + "#, + ); + + verify_that!(colors.len(), eq(1))?; + verify_that!(colors[0].color.red, eq(34.0 / 255.0))?; + verify_that!(colors[0].color.green, eq(56.0 / 255.0))?; + verify_that!(colors[0].color.blue, eq(78.0 / 255.0))?; + verify_that!(colors[0].color.alpha, eq(1.0))?; + Ok(()) + } + + #[gtest] + fn detects_annotated_gmod_color_tuple_with_alpha_on_colon_call() -> Result<()> { + let colors = collect_colors_semantic( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + Painter = {} + + ---@param self table + ---@[call_arg("gmod.color", "r")] + ---@param redish number + ---@[call_arg("gmod.color", "g")] + ---@param greenish number + ---@[call_arg("gmod.color", "b")] + ---@param blueish number + ---@[call_arg("gmod.color", "a")] + ---@param alphaish number + function Painter.SetTint(self, redish, greenish, blueish, alphaish) end + + Painter:SetTint(10, 20, 30, 40) + "#, + ); + + verify_that!(colors.len(), eq(1))?; + verify_that!(colors[0].color.red, eq(10.0 / 255.0))?; + verify_that!(colors[0].color.green, eq(20.0 / 255.0))?; + verify_that!(colors[0].color.blue, eq(30.0 / 255.0))?; + verify_that!(colors[0].color.alpha, eq(40.0 / 255.0))?; + Ok(()) + } + + #[gtest] + fn annotated_color_tuple_does_not_duplicate_param_name_tuple() -> Result<()> { + let colors = collect_colors_semantic( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.color", "r")] + ---@param r number + ---@[call_arg("gmod.color", "g")] + ---@param g number + ---@[call_arg("gmod.color", "b")] + ---@param b number + function Paint(r, g, b) end + + Paint(1, 2, 3) + "#, + ); + + verify_that!(colors.len(), eq(1))?; + Ok(()) + } + #[gtest] fn does_not_duplicate_color_swatches() -> Result<()> { let colors = collect_colors_semantic( diff --git a/crates/glua_ls/src/handlers/document_symbol/builder.rs b/crates/glua_ls/src/handlers/document_symbol/builder.rs index f8a40013d..b801fd66d 100644 --- a/crates/glua_ls/src/handlers/document_symbol/builder.rs +++ b/crates/glua_ls/src/handlers/document_symbol/builder.rs @@ -140,10 +140,13 @@ impl<'a> DocumentSymbolBuilder<'a> { fn collect_vgui_panel_call( decl_tree: &LuaDeclarationTree, call: &GmodScriptedClassCallMetadata, - table_var_arg_index: usize, + default_table_var_arg_index: usize, panel_names: &mut HashMap, ) { - let panel_name = match call.literal_args.first() { + let panel_arg_index = call.vgui_panel_define_arg_idx(); + let table_var_arg_index = call.vgui_panel_table_arg_idx(default_table_var_arg_index); + + let panel_name = match call.literal_args.get(panel_arg_index) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => name, _ => return, }; diff --git a/crates/glua_ls/src/handlers/document_symbol/mod.rs b/crates/glua_ls/src/handlers/document_symbol/mod.rs index 566bdb534..0099516bf 100644 --- a/crates/glua_ls/src/handlers/document_symbol/mod.rs +++ b/crates/glua_ls/src/handlers/document_symbol/mod.rs @@ -713,6 +713,7 @@ mod tests { #[gtest] fn vgui_panel_symbols_are_class_named_and_methods_are_nested() -> Result<()> { let mut ws = VirtualWorkspace::new(); + ws.def_gmod_call_arg_builtins(); let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); @@ -783,6 +784,7 @@ mod tests { fn vgui_panel_assignment_symbols_keep_selection_range_and_expand_container_range() -> Result<()> { let mut ws = VirtualWorkspace::new(); + ws.def_gmod_call_arg_builtins(); let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); @@ -931,6 +933,7 @@ mod tests { #[gtest] fn hook_add_has_named_outline_symbol() -> Result<()> { let mut ws = VirtualWorkspace::new(); + ws.def_gmod_call_arg_builtins(); let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); @@ -959,6 +962,7 @@ mod tests { #[gtest] fn hook_symbol_range_contains_nested_children() -> Result<()> { let mut ws = VirtualWorkspace::new(); + ws.def_gmod_call_arg_builtins(); let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; emmyrc.gmod.outline.verbosity = EmmyrcGmodOutlineVerbosity::Verbose; @@ -1063,6 +1067,7 @@ mod tests { #[gtest] fn net_receive_callback_params_are_inlined_to_call_symbol() -> Result<()> { let mut ws = VirtualWorkspace::new(); + ws.def_gmod_call_arg_builtins(); let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); diff --git a/crates/glua_ls/src/handlers/gmod_scripted_classes/build_gmod_scripted_classes.rs b/crates/glua_ls/src/handlers/gmod_scripted_classes/build_gmod_scripted_classes.rs index 00a3f653b..c1b0b9b7c 100644 --- a/crates/glua_ls/src/handlers/gmod_scripted_classes/build_gmod_scripted_classes.rs +++ b/crates/glua_ls/src/handlers/gmod_scripted_classes/build_gmod_scripted_classes.rs @@ -125,7 +125,8 @@ fn push_vgui_panel_entries( } fn extract_vgui_panel_name(call: &GmodScriptedClassCallMetadata) -> Option<&str> { - match call.literal_args.first() { + let panel_arg_index = call.vgui_panel_define_arg_idx(); + match call.literal_args.get(panel_arg_index) { Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => Some(name.as_str()), _ => None, } @@ -212,6 +213,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "lua/autorun/client/cl_panel_defs.lua", diff --git a/crates/glua_ls/src/handlers/gmod_string_context.rs b/crates/glua_ls/src/handlers/gmod_string_context.rs index d6082a091..74a615aec 100644 --- a/crates/glua_ls/src/handlers/gmod_string_context.rs +++ b/crates/glua_ls/src/handlers/gmod_string_context.rs @@ -1,5 +1,10 @@ +use glua_code_analysis::{ + LuaCallArgRole, LuaDeclId, LuaMemberId, LuaSemanticDeclId, LuaSignatureId, LuaTypeOwner, + SemanticDeclLevel, SemanticModel, find_call_arg_role_from_type, +}; use glua_parser::{ - LuaAstNode, LuaAstToken, LuaCallArgList, LuaCallExpr, LuaLiteralExpr, LuaStringToken, PathTrait, + LuaAstNode, LuaAstToken, LuaCallArgList, LuaCallExpr, LuaClosureExpr, LuaLiteralExpr, + LuaStringToken, PathTrait, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -10,7 +15,6 @@ pub(crate) enum NetMessageCallKind { #[derive(Clone, Debug)] pub(crate) struct StringCallContext { - pub(crate) call_path: String, pub(crate) arg_index: usize, pub(crate) name: String, } @@ -23,50 +27,310 @@ pub(crate) fn extract_string_call_context( let arg_index = call_arg_list .get_args() .position(|arg| arg.get_position() == literal_expr.get_position())?; - let call_expr = call_arg_list.get_parent::()?; + call_arg_list.get_parent::()?; Some(StringCallContext { - call_path: call_expr.get_access_path()?, arg_index, name: normalize_string_name(string_token.get_value())?, }) } -pub(crate) fn is_vgui_panel_string_context(call_path: &str, arg_index: usize) -> bool { - if matches_call_path(call_path, "vgui.Create") { - return arg_index == 0; - } +pub(crate) fn is_annotated_vgui_panel_string_context( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + arg_index: usize, +) -> bool { + has_string_call_arg_role( + semantic_model, + call_expr, + arg_index, + "gmod.vgui_panel", + &["define", "define_control", "base", "reference"], + ) +} - if matches_call_path(call_path, "vgui.Register") { - return arg_index == 0 || arg_index == 2; +pub(crate) fn is_annotated_derma_skin_string_context( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + arg_index: usize, +) -> bool { + has_string_call_arg_role( + semantic_model, + call_expr, + arg_index, + "gmod.derma_skin", + &["define", "reference"], + ) +} + +pub(crate) fn find_string_call_arg_role( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + arg_index: usize, + domain: &str, + roles: &[&str], +) -> Option { + let prefix_expr = call_expr.get_prefix_expr()?; + let adjusted_arg_index = adjusted_arg_index(call_expr, arg_index); + if let Some(semantic_decl) = semantic_model.find_decl( + prefix_expr.syntax().clone().into(), + SemanticDeclLevel::NoTrace, + ) && let Some(role) = find_call_arg_role_from_semantic_decl( + semantic_model, + semantic_decl, + adjusted_arg_index, + domain, + roles, + ) { + return Some(role); } - if matches_call_path(call_path, "derma.DefineControl") { - return arg_index == 0 || arg_index == 3; + let callable_type = semantic_model.infer_expr(prefix_expr).ok()?; + find_call_arg_role_from_type( + semantic_model.get_db(), + &callable_type, + adjusted_arg_index, + domain, + roles, + ) +} + +pub(crate) fn find_call_arg_roles( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + arg_count: usize, + domain: &str, + roles: &[&str], +) -> Vec<(usize, LuaCallArgRole)> { + let Some(prefix_expr) = call_expr.get_prefix_expr() else { + return Vec::new(); + }; + + if let Some(semantic_decl) = semantic_model.find_decl( + prefix_expr.syntax().clone().into(), + SemanticDeclLevel::NoTrace, + ) { + return (0..arg_count) + .filter_map(|arg_index| { + find_call_arg_role_from_semantic_decl( + semantic_model, + semantic_decl.clone(), + adjusted_arg_index(call_expr, arg_index), + domain, + roles, + ) + .map(|role| (arg_index, role)) + }) + .collect(); } - // `:Add` is broadly matched by method name only (no receiver type check). - // False positives are mitigated by the subsequent VGUI index lookup finding no match. - matches_call_path(call_path, "Add") && arg_index == 0 + let Some(callable_type) = semantic_model.infer_expr(prefix_expr).ok() else { + return Vec::new(); + }; + + (0..arg_count) + .filter_map(|arg_index| { + find_call_arg_role_from_type( + semantic_model.get_db(), + &callable_type, + adjusted_arg_index(call_expr, arg_index), + domain, + roles, + ) + .map(|role| (arg_index, role)) + }) + .collect() +} + +pub(crate) fn has_string_call_arg_role( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + arg_index: usize, + domain: &str, + roles: &[&str], +) -> bool { + find_string_call_arg_role(semantic_model, call_expr, arg_index, domain, roles).is_some() +} + +fn adjusted_arg_index(call_expr: &LuaCallExpr, arg_index: usize) -> usize { + if call_expr.is_colon_call() { + arg_index + 1 + } else { + arg_index + } } -pub(crate) fn net_message_call_kind( - call_path: &str, +pub(crate) fn annotated_net_message_flow_call_kind( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, arg_index: usize, ) -> Option { - if arg_index != 0 { - return None; + annotated_net_message_call_kind(semantic_model, call_expr, arg_index) +} + +pub(crate) fn is_net_message_string_context( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + arg_index: usize, +) -> bool { + has_string_call_arg_role( + semantic_model, + call_expr, + arg_index, + "gmod.net_message", + &["define", "start", "receive", "reference"], + ) +} + +pub(crate) fn is_hook_name_string_context( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + arg_index: usize, +) -> bool { + if has_string_call_arg_role( + semantic_model, + call_expr, + arg_index, + "gmod.hook", + &["add", "emit", "remove", "reference"], + ) { + return true; } - if matches_call_path(call_path, "net.Start") { - return Some(NetMessageCallKind::Start); + let Some(call_path) = call_expr.get_access_path() else { + return false; + }; + arg_index == 0 + && semantic_model + .get_emmyrc() + .gmod + .hook_mappings + .emitter_to_hook + .iter() + .any(|(emitter_path, mapped_hook)| mapped_hook == "*" && call_path == *emitter_path) +} + +fn find_call_arg_role_from_semantic_decl( + semantic_model: &SemanticModel, + semantic_decl: LuaSemanticDeclId, + arg_index: usize, + domain: &str, + roles: &[&str], +) -> Option { + match semantic_decl { + LuaSemanticDeclId::Signature(signature_id) => find_call_arg_role_from_signature_id( + semantic_model, + signature_id, + arg_index, + domain, + roles, + ), + LuaSemanticDeclId::LuaDecl(decl_id) => { + find_call_arg_role_from_decl_id(semantic_model, decl_id, arg_index, domain, roles) + } + LuaSemanticDeclId::Member(member_id) => { + find_call_arg_role_from_member_id(semantic_model, member_id, arg_index, domain, roles) + } + LuaSemanticDeclId::TypeDecl(_) => None, } +} - if matches_call_path(call_path, "net.Receive") { - return Some(NetMessageCallKind::Receive); +fn find_call_arg_role_from_signature_id( + semantic_model: &SemanticModel, + signature_id: LuaSignatureId, + arg_index: usize, + domain: &str, + roles: &[&str], +) -> Option { + let signature = semantic_model + .get_db() + .get_signature_index() + .get(&signature_id)?; + let mut best = None; + signature.visit_call_arg_roles_for_param(arg_index, &mut |role| { + if role.domain != domain || !roles.iter().any(|candidate| *candidate == role.role) { + return; + } + + if best.as_ref().is_none_or(|current: &LuaCallArgRole| { + role.priority.unwrap_or(0) > current.priority.unwrap_or(0) + }) { + best = Some(role.clone()); + } + }); + best +} + +fn find_call_arg_role_from_decl_id( + semantic_model: &SemanticModel, + decl_id: LuaDeclId, + arg_index: usize, + domain: &str, + roles: &[&str], +) -> Option { + if let Some(signature_id) = signature_id_from_decl_value(semantic_model, decl_id) + && let Some(role) = find_call_arg_role_from_signature_id( + semantic_model, + signature_id, + arg_index, + domain, + roles, + ) + { + return Some(role); } - None + let typ = semantic_model.get_type(decl_id.into()); + find_call_arg_role_from_type(semantic_model.get_db(), &typ, arg_index, domain, roles) +} + +fn find_call_arg_role_from_member_id( + semantic_model: &SemanticModel, + member_id: LuaMemberId, + arg_index: usize, + domain: &str, + roles: &[&str], +) -> Option { + let typ = semantic_model.get_type(LuaTypeOwner::Member(member_id)); + find_call_arg_role_from_type(semantic_model.get_db(), &typ, arg_index, domain, roles) +} + +fn signature_id_from_decl_value( + semantic_model: &SemanticModel, + decl_id: LuaDeclId, +) -> Option { + let decl = semantic_model + .get_db() + .get_decl_index() + .get_decl(&decl_id)?; + let value_syntax_id = decl.get_value_syntax_id()?; + let root = semantic_model + .get_db() + .get_vfs() + .get_syntax_tree(&decl_id.file_id)? + .get_red_root(); + let value_node = value_syntax_id.to_node_from_root(&root)?; + let closure = LuaClosureExpr::cast(value_node)?; + Some(LuaSignatureId::from_closure(decl_id.file_id, &closure)) +} + +pub(crate) fn annotated_net_message_call_kind( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + arg_index: usize, +) -> Option { + let role = find_string_call_arg_role( + semantic_model, + call_expr, + arg_index, + "gmod.net_message", + &["start", "receive"], + )?; + match role.role.as_str() { + "start" => Some(NetMessageCallKind::Start), + "receive" => Some(NetMessageCallKind::Receive), + _ => None, + } } pub(crate) fn normalize_string_name(name: String) -> Option { @@ -77,17 +341,3 @@ pub(crate) fn normalize_string_name(name: String) -> Option { Some(trimmed.to_string()) } } - -pub(crate) fn matches_call_path(path: &str, target: &str) -> bool { - if path == target { - return true; - } - if path.len() > target.len() { - let sep_idx = path.len() - target.len() - 1; - let sep = path.as_bytes()[sep_idx]; - if (sep == b'.' || sep == b':') && path[sep_idx + 1..] == *target { - return true; - } - } - false -} diff --git a/crates/glua_ls/src/handlers/hover/build_hover.rs b/crates/glua_ls/src/handlers/hover/build_hover.rs index 4378a82ad..a73bf67cb 100644 --- a/crates/glua_ls/src/handlers/hover/build_hover.rs +++ b/crates/glua_ls/src/handlers/hover/build_hover.rs @@ -1,20 +1,23 @@ use std::collections::HashSet; use glua_code_analysis::{ - DbIndex, LuaCompilation, LuaDeclExtra, LuaDeclId, LuaDocument, LuaMemberId, LuaMemberKey, - LuaMemberOwner, LuaSemanticDeclId, LuaSignatureId, LuaType, LuaTypeDeclId, RenderLevel, - SemanticDeclLevel, SemanticInfo, SemanticModel, + DbIndex, LuaCompilation, LuaDeclExtra, LuaDeclId, LuaDocument, LuaInferCache, LuaMemberId, + LuaMemberKey, LuaMemberOwner, LuaSemanticDeclId, LuaSignatureId, LuaType, LuaTypeDeclId, + RenderLevel, SemanticDeclLevel, SemanticInfo, SemanticModel, + explicit_param_string_default_reaches_flow, inferred_string_default_reaches_flow, }; use glua_code_analysis::{humanize_member_key_name, humanize_type}; use glua_parser::{ - LuaAssignStat, LuaAstNode, LuaCallArgList, LuaExpr, LuaFuncStat, LuaIndexExpr, LuaSyntaxKind, - LuaSyntaxToken, LuaTableExpr, LuaTableField, LuaVarExpr, PathTrait, + LuaAssignStat, LuaAstNode, LuaCallArgList, LuaChunk, LuaExpr, LuaFuncStat, LuaIndexExpr, + LuaSyntaxId, LuaSyntaxKind, LuaSyntaxToken, LuaTableExpr, LuaTableField, LuaVarExpr, PathTrait, }; use lsp_types::{Hover, HoverContents, MarkedString, MarkupContent}; use rowan::{TextRange, TextSize}; use crate::handlers::completion::{color_info_from_expr, color_info_from_type, is_color_type}; -use crate::handlers::hover::function::{build_function_hover, is_function}; +use crate::handlers::hover::function::{ + build_function_hover, format_doc_default_value, is_function, +}; use crate::handlers::hover::humanize_type_decl::build_type_decl_hover; use crate::handlers::hover::humanize_types::hover_humanize_type; @@ -310,11 +313,17 @@ fn build_decl_hover( } else { "(global) " }; + + // Append default value when available (explicit param default or + // inferred `x = x or "literal"` default), using the same syntax + // as function-hover default params/returns. + let default_suffix = resolve_decl_default_display(builder, decl_id); builder.set_type_description(format!( - "{}{}: {}", + "{}{}: {}{}", prefix, decl.get_name(), - type_humanize_text + type_humanize_text, + default_suffix, )); } @@ -352,6 +361,154 @@ fn build_decl_hover( Some(()) } +/// Resolve a displayable default suffix for a declaration at the hover site. +/// +/// Checks for an explicit param default first (from `---@param x type="default"`), +/// then for an inferred default (from `x = x or "literal"`). +/// Returns a string like ` = "value"` or ` = 3` (empty string if no default). +/// +/// Uses flow-validity checking to ensure the default is still live at the +/// hover site (not killed by a later reassignment). +fn resolve_decl_default_display(builder: &HoverBuilder, decl_id: LuaDeclId) -> String { + let Some(trigger_token) = builder.get_trigger_token() else { + return String::new(); + }; + + let file_id = builder.semantic_model.get_file_id(); + let db = builder.semantic_model.get_db(); + let flow_tree = db.get_flow_index().get_flow_tree(&file_id); + let root = flow_tree.and_then(|_| { + db.get_vfs() + .get_syntax_tree(&file_id) + .and_then(|tree| LuaChunk::cast(tree.get_red_root())) + }); + + let Some((flow_tree, root)) = flow_tree.zip(root) else { + return String::new(); + }; + + // Walk up ancestors to find a node with a flow binding. + // This handles declaration sites (LocalName), assignment LHS, + // and reference sites (NameExpr) uniformly. + let mut use_flow_id = None; + + // First check if the trigger token itself has a flow binding. + let token_syntax_id = LuaSyntaxId::from_token(&trigger_token); + if let Some(flow_id) = flow_tree.get_flow_id(token_syntax_id) { + use_flow_id = Some(flow_id); + } + + // Then walk up ancestors. + if use_flow_id.is_none() { + let mut current = trigger_token.parent(); + while let Some(node) = current { + let syntax_id = LuaSyntaxId::from_node(&node); + if let Some(flow_id) = flow_tree.get_flow_id(syntax_id) { + use_flow_id = Some(flow_id); + break; + } + current = node.parent(); + } + } + + // Create a temporary cache for flow-validity checking. + let mut cache = LuaInferCache::new(file_id, Default::default()); + + // Seed the cache with the use-site realm so that flow-reachability + // checks evaluate from the correct realm context (not the declaration + // position fallback). + let use_site_realm = db + .get_gmod_infer_index() + .get_realm_at_offset(&file_id, trigger_token.text_range().start()); + cache.flow_query_realm = Some(use_site_realm); + + // 1. Explicit param default from signature metadata. + if let Some(decl) = db.get_decl_index().get_decl(&decl_id) { + if let LuaDeclExtra::Param { + idx, signature_id, .. + } = &decl.extra + { + if let Some(default_val) = db + .get_signature_index() + .get(signature_id) + .and_then(|sig| sig.get_param_info_by_id(*idx)) + .and_then(|info| info.default_value.as_ref()) + { + // At the declaration site itself (hovering the param name in + // the function signature), the explicit default is always valid. + // For use sites, check flow-validity. + let flow_valid = match use_flow_id { + Some(use_flow_id) => explicit_param_string_default_reaches_flow( + db, + flow_tree, + &mut cache, + &root, + decl_id, + use_flow_id, + ), + // No flow binding found — this is likely the declaration + // site. Show the explicit default. + None => true, + }; + + if flow_valid { + return format!(" = {}", format_doc_default_value(default_val)); + } + } + } + } + + // 2. Inferred default from `x = x or "literal"` pattern. + // Only show if we have a flow ID to validate against. + let Some(use_flow_id) = use_flow_id else { + return String::new(); + }; + + if let Some(candidates) = db + .get_property_index() + .get_inferred_string_defaults(&decl_id) + { + let trigger_range = trigger_token.text_range(); + for candidate in candidates { + // If the trigger token is the LHS variable of the self-coalescing + // assignment (e.g. hovering the LHS `x` of `x = x or "literal"`), + // the default is being established here, so it's always valid. + // Narrow check: only the matching LHS variable range — NOT the + // entire assignment statement — so that hovering the RHS read + // (e.g. the `x` in `x = x or "literal"`) does not inherit + // the default. + let at_assignment_site = candidate.source_range.contains_range(trigger_range) + && trigger_token.parent().is_some_and(|parent| { + parent + .ancestors() + .find_map(LuaAssignStat::cast) + .is_some_and(|assign| { + assign.get_range() == candidate.source_range + && assign.get_var_and_expr_list().0.iter().any(|var| { + var.syntax().text_range().contains_range(trigger_range) + }) + }) + }); + + let reaches = at_assignment_site + || inferred_string_default_reaches_flow( + db, + flow_tree, + &mut cache, + &root, + decl_id, + use_flow_id, + candidate.source_range, + ); + if reaches { + return format!(" = {:?}", candidate.value); + } + } + } + + String::new() +} + fn build_member_hover( builder: &mut HoverBuilder, db: &DbIndex, diff --git a/crates/glua_ls/src/handlers/hover/function/mod.rs b/crates/glua_ls/src/handlers/hover/function/mod.rs index 468a09dad..b6686eef6 100644 --- a/crates/glua_ls/src/handlers/hover/function/mod.rs +++ b/crates/glua_ls/src/handlers/hover/function/mod.rs @@ -764,7 +764,7 @@ fn build_function_param( rendered } -fn format_doc_default_value(default_value: &LuaDocDefaultValue) -> String { +pub(crate) fn format_doc_default_value(default_value: &LuaDocDefaultValue) -> String { match default_value { LuaDocDefaultValue::Nil => "nil".to_string(), LuaDocDefaultValue::Boolean(value) => value.to_string(), diff --git a/crates/glua_ls/src/handlers/hover/mod.rs b/crates/glua_ls/src/handlers/hover/mod.rs index 912556dcd..d4d6f6fb8 100644 --- a/crates/glua_ls/src/handlers/hover/mod.rs +++ b/crates/glua_ls/src/handlers/hover/mod.rs @@ -13,6 +13,7 @@ mod realm_badge; use super::RegisterCapabilities; use crate::context::ServerContextSnapshot; +use crate::handlers::gmod_string_context::is_hook_name_string_context; use crate::util::{find_ref_at, resolve_ref_single}; pub use build_hover::build_hover_content_for_completion; use build_hover::{build_assignment_target_hover, build_semantic_info_hover}; @@ -22,11 +23,11 @@ pub use find_origin::{ }; use glua_code_analysis::{ EmmyLuaAnalysis, FileId, GmodRealm, LuaMemberKey, LuaSemanticDeclId, LuaType, LuaTypeDeclId, - RenderLevel, WorkspaceId, humanize_type, resolve_gmod_hook_add_callback_doc_function, + RenderLevel, WorkspaceId, humanize_type, resolve_gmod_hook_callback_doc_function, }; use glua_parser::{ LuaAstNode, LuaAstToken, LuaCallArgList, LuaCallExpr, LuaDocDescription, LuaLiteralExpr, - LuaStringToken, LuaTokenKind, PathTrait, + LuaStringToken, LuaTokenKind, }; use glua_parser_desc::parse_ref_target; pub use hover_builder::HoverBuilder; @@ -252,7 +253,11 @@ fn hover_gmod_hook_name_string( let call_expr = literal_expr .get_parent::()? .get_parent::()?; - if !is_hook_name_string_context(&call_expr, literal_expr) { + let args_list = call_expr.get_args_list()?; + let arg_index = args_list + .get_args() + .position(|arg| arg.get_position() == literal_expr.get_position())?; + if !is_hook_name_string_context(semantic_model, &call_expr, arg_index) { return None; } @@ -293,11 +298,6 @@ fn hover_gmod_hook_callback_function( let call_arg_list = closure_expr.get_parent::()?; let call_expr = call_arg_list.get_parent::()?; - let call_path = call_expr.get_access_path()?; - if !matches_call_path(&call_path, "hook.Add") { - return None; - } - // Use text range comparison instead of syntax node identity to robustly // locate the closure's position in the argument list across traversal paths. let closure_range = closure_expr.syntax().text_range(); @@ -307,14 +307,17 @@ fn hover_gmod_hook_callback_function( .find(|(_, arg)| arg.syntax().text_range() == closure_range) .map(|(idx, _)| idx); - if param_idx != Some(2) { - return None; - } + let param_idx = param_idx?; - let hook_name = glua_code_analysis::extract_hook_name(&call_expr)?; - let property_owner = - resolve_hook_property_owner(semantic_model, file_id, position_offset, &hook_name)?; let db = semantic_model.get_db(); + let resolved_callback = + resolve_gmod_hook_callback_doc_function(db, &call_expr, param_idx, None, file_id)?; + let property_owner = resolve_hook_property_owner( + semantic_model, + file_id, + position_offset, + &resolved_callback.hook_name, + )?; let document = semantic_model.get_document(); // Build the base hover from the hook property owner (gives description, realm, param docs) @@ -328,10 +331,8 @@ fn hover_gmod_hook_callback_function( // Now override the primary type description with an anonymous callback signature, // e.g. `function(ply: Player, seat: Vehicle) -> boolean` // using the resolved callback doc function for this hook. - // param_idx == Some(2) is guaranteed by the guard above. - if let Some(callback_func) = - resolve_gmod_hook_add_callback_doc_function(db, &call_expr, 2, None, file_id) { + let callback_func = resolved_callback.function; let params_str = callback_func .get_params() .iter() @@ -422,34 +423,6 @@ pub(crate) fn resolve_hook_property_owner( fallback } -fn is_hook_name_string_context(call_expr: &LuaCallExpr, literal_expr: LuaLiteralExpr) -> bool { - let Some(call_path) = call_expr.get_access_path() else { - return false; - }; - if !matches_call_path(&call_path, "hook.Add") - && !matches_call_path(&call_path, "hook.Run") - && !matches_call_path(&call_path, "hook.Call") - { - return false; - } - - let Some(args_list) = call_expr.get_args_list() else { - return false; - }; - let arg_idx = args_list - .get_args() - .position(|arg| arg.get_position() == literal_expr.get_position()); - arg_idx == Some(0) -} - -fn matches_call_path(path: &str, target: &str) -> bool { - // All call sites pass a fully-qualified target (e.g. "hook.Add"). - // get_access_path() also returns the full qualified path, so an exact equality check - // is both necessary and sufficient. A suffix check would produce false positives for - // paths like "mylib.hook.Add" when the target is "hook.Add". - path == target -} - fn is_realm_compatible(call_realm: GmodRealm, item_realm: GmodRealm) -> bool { !matches!( (call_realm, item_realm), diff --git a/crates/glua_ls/src/handlers/hover/net_hover.rs b/crates/glua_ls/src/handlers/hover/net_hover.rs index b30ad0639..4b49473ba 100644 --- a/crates/glua_ls/src/handlers/hover/net_hover.rs +++ b/crates/glua_ls/src/handlers/hover/net_hover.rs @@ -1,18 +1,17 @@ use std::{cmp::Reverse, collections::HashMap}; +use crate::handlers::gmod_string_context::is_net_message_string_context; use glua_code_analysis::{ EmmyLuaAnalysis, FileId, NetFlowKind, NetOpEntry, NetOpKind, NetReceiveFlow, NetSendFlow, SemanticModel, }; use glua_parser::{ LuaAstNode, LuaAstToken, LuaCallArgList, LuaCallExpr, LuaLiteralExpr, LuaStringToken, - LuaSyntaxToken, PathTrait, + LuaSyntaxToken, }; use lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind}; use rowan::TextRange; -const NET_TRIGGER_CALL_PATHS: &[&str] = &["net.Start", "net.Receive", "util.AddNetworkString"]; - pub fn hover_gmod_net_message_string( analysis: &EmmyLuaAnalysis, semantic_model: &SemanticModel, @@ -30,7 +29,7 @@ pub fn hover_gmod_net_message_string( .get_parent::()? .get_parent::()?; - if !is_net_message_name_context(&call_expr, &literal_expr) { + if !is_net_message_name_context(semantic_model, &call_expr, &literal_expr) { return None; } @@ -57,20 +56,22 @@ pub fn hover_gmod_net_message_string( }) } -fn is_net_message_name_context(call_expr: &LuaCallExpr, literal_expr: &LuaLiteralExpr) -> bool { - let Some(call_path) = call_expr.get_access_path() else { - return false; - }; - if !NET_TRIGGER_CALL_PATHS.iter().any(|p| *p == call_path) { - return false; - } +fn is_net_message_name_context( + semantic_model: &SemanticModel, + call_expr: &LuaCallExpr, + literal_expr: &LuaLiteralExpr, +) -> bool { let Some(args_list) = call_expr.get_args_list() else { return false; }; let arg_idx = args_list .get_args() .position(|arg| arg.get_position() == literal_expr.get_position()); - arg_idx == Some(0) + let Some(arg_idx) = arg_idx else { + return false; + }; + + is_net_message_string_context(semantic_model, call_expr, arg_idx) } fn render_net_message_hover( diff --git a/crates/glua_ls/src/handlers/initialized/locale.rs b/crates/glua_ls/src/handlers/initialized/locale.rs deleted file mode 100644 index 9a1b37cd4..000000000 --- a/crates/glua_ls/src/handlers/initialized/locale.rs +++ /dev/null @@ -1,13 +0,0 @@ -use glua_code_analysis::get_locale_code; -use log::info; -use lsp_types::InitializeParams; - -pub fn set_ls_locale(params: &InitializeParams) -> Option<()> { - let locale = params.locale.as_ref()?; - let locale = get_locale_code(locale); - info!("set locale: {}", locale); - glua_parser::set_locale(&locale); - glua_code_analysis::set_locale(&locale); - rust_i18n::set_locale(&locale); - Some(()) -} diff --git a/crates/glua_ls/src/handlers/initialized/mod.rs b/crates/glua_ls/src/handlers/initialized/mod.rs index 36e05f4a0..2c69c4de3 100644 --- a/crates/glua_ls/src/handlers/initialized/mod.rs +++ b/crates/glua_ls/src/handlers/initialized/mod.rs @@ -1,7 +1,5 @@ mod client_config; mod codestyle; -mod locale; -mod std_i18n; use std::{ collections::{HashMap, HashSet}, @@ -15,9 +13,7 @@ use crate::{ FileDiagnostic, LspFeatures, ProgressTask, ServerContextSnapshot, StatusBar, WorkspaceFileMatcher, get_client_id, load_emmy_config, }, - handlers::{ - initialized::std_i18n::try_generate_translated_std, text_document::register_files_watch, - }, + handlers::text_document::register_files_watch, logger::init_logger, util::{LongRunningWatchdogStatus, spawn_long_running_watchdog}, }; @@ -36,8 +32,6 @@ pub async fn initialized_handler( cmd_args: CmdArgs, ) -> Option<()> { log::info!("initialized handler started"); - // init locale - locale::set_ls_locale(¶ms); let workspace_folders = get_workspace_folders(¶ms); let main_root: Option<&str> = match workspace_folders.first() { Some(path) => path.root.to_str(), @@ -140,10 +134,12 @@ pub async fn initialized_handler( log::info!("configuration loaded"); // init std lib - watchdog_status.set_phase("Loading standard libraries"); - log::info!("loading standard libraries"); - init_std_lib(context.analysis(), &cmd_args, emmyrc.clone()).await; - log::info!("standard libraries loaded"); + if cmd_args.load_stdlib.0 { + watchdog_status.set_phase("Loading standard libraries"); + log::info!("loading standard libraries"); + init_std_lib(context.analysis(), emmyrc.clone()).await; + log::info!("standard libraries loaded"); + } { watchdog_status.set_phase("Preparing workspace manager"); @@ -395,8 +391,44 @@ fn build_workspace_collection_folders( workspaces.push(WorkspaceFolder::new(PathBuf::from(extra_root), false)); } + // Canonicalize the main workspace root for self-overlap detection. + let main_root_canon = workspaces + .first() + .and_then(|ws| ws.root.canonicalize().ok()); + for lib in &emmyrc.workspace.library { - workspaces.push(WorkspaceFolder::new(PathBuf::from(lib.get_path()), true)); + let configured = lib.get_path(); + let path = PathBuf::from(configured); + + // Filter invalid library paths: empty, nonexistent, not a directory, + // or resolves to the same path as the main workspace root (self-overlap + // would cause a redundant full re-scan of the workspace). + if configured.trim().is_empty() { + log::warn!( + "Skipping empty library path from config entry: {:?}", + configured + ); + continue; + } + if !path.exists() { + log::warn!("Skipping library path that does not exist: {:?}", path); + continue; + } + if !path.is_dir() { + log::warn!("Skipping library path that is not a directory: {:?}", path); + continue; + } + if let (Ok(lib_canon), Some(ws_canon)) = (path.canonicalize(), &main_root_canon) + && lib_canon == *ws_canon + { + log::warn!( + "Skipping library path that resolves to the workspace root (self-overlap): {:?}", + path + ); + continue; + } + + workspaces.push(WorkspaceFolder::new(path, true)); } for package_dir in &emmyrc.workspace.package_dirs { @@ -443,21 +475,13 @@ pub fn get_workspace_folders(params: &InitializeParams) -> Vec workspace_folders } -pub async fn init_std_lib( - analysis: &RwLock, - cmd_args: &CmdArgs, - emmyrc: Arc, -) { - log::info!( - "initializing std lib with resources path: {:?}", - cmd_args.resources_path - ); +pub async fn init_std_lib(analysis: &RwLock, emmyrc: Arc) { + log::info!("initializing std lib"); let mut analysis = analysis.write().await; - if cmd_args.load_stdlib.0 { + { // double update config analysis.update_config(emmyrc); - try_generate_translated_std(); - analysis.init_std_lib(cmd_args.resources_path.0.clone()); + analysis.init_std_lib(); } log::info!("initialized std lib complete"); diff --git a/crates/glua_ls/src/handlers/initialized/std_i18n.rs b/crates/glua_ls/src/handlers/initialized/std_i18n.rs deleted file mode 100644 index 4083ab3b5..000000000 --- a/crates/glua_ls/src/handlers/initialized/std_i18n.rs +++ /dev/null @@ -1,394 +0,0 @@ -use std::collections::HashMap; -use std::path::{Path, PathBuf}; - -use glua_code_analysis::{LuaFileInfo, get_best_resources_dir, get_locale_code}; -use include_dir::{Dir, include_dir}; - -static STD_I18N_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/std_i18n"); - -#[derive(Debug, Clone, serde::Deserialize)] -struct MetaFile { - version: u32, - line_base: u32, - col_base: u32, - #[allow(dead_code)] - file: String, - entries: Vec, -} - -#[derive(Debug, Clone, serde::Deserialize)] -struct MetaEntry { - key: String, - kind: MetaKind, - range: MetaRange, - hash: String, - context_hash: String, -} - -#[derive(Debug, Clone, serde::Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum MetaKind { - DocBlock { indent: String }, - LineTail { prefix: String }, -} - -#[derive(Debug, Clone, serde::Deserialize)] -struct MetaRange { - start: MetaPos, - end: MetaPos, -} - -#[derive(Debug, Clone, Copy, serde::Deserialize)] -struct MetaPos { - line: u32, - col: u32, -} - -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -/// 尝试生成翻译后的 std -pub fn try_generate_translated_std() -> Option<()> { - let locale = get_locale_code(&rust_i18n::locale()); - if locale == "en" { - return Some(()); - } - - // 确定是否存在对应语言的翻译文件 - let first_sub_dir = STD_I18N_DIR - .entries() - .iter() - .filter_map(|e| e.as_dir()) - .next()?; - - let locale_yaml = format!("{}.yaml", locale); - let has_locale_file = first_sub_dir - .entries() - .iter() - .filter_map(|e| e.as_file()) - .any(|f| { - f.path() - .file_name() - .is_some_and(|n| n == locale_yaml.as_str()) - }); - if !has_locale_file { - return Some(()); - } - - let resources_dir = get_best_resources_dir(); - if !check_need_dump_std(&resources_dir, &locale) { - return None; - } - // 获取最佳资源目录作为输出目录的父目录 - generate(&locale, &resources_dir); - Some(()) -} - -/// 检查是否需要重新生成翻译后的 std 文件 -fn check_need_dump_std(resources_dir: &Path, locale: &str) -> bool { - // debug 模式下总是重新生成 - if cfg!(debug_assertions) { - return true; - } - // 不存在对应语言的翻译文件, 需要生成 - let translated_std_dir = resources_dir.join(format!("std-{locale}")); - if !translated_std_dir.exists() { - return true; - } - - let version_path = resources_dir.join("version"); - - // 版本文件不存在, 需要重新生成 - if !version_path.exists() { - return true; - } - - // 读取版本文件失败, 需要重新生成 - let Ok(content) = std::fs::read_to_string(&version_path) else { - return true; - }; - - // 版本不匹配, 需要重新生成 - let version = content.trim(); - if version != VERSION { - return true; - } - false -} - -/// Params: -/// - `locale` - 语言 -/// - `out_parent_dir` - 输出目录的父目录 -fn generate(locale: &str, out_parent_dir: &Path) -> Vec { - let origin_std_files = glua_code_analysis::load_resource_from_include_dir(); - let translate_std_root = out_parent_dir.join(format!("std-{locale}")); - log::info!("Creating std-{locale} dir: {:?}", translate_std_root); - - let mut out_files: Vec = Vec::with_capacity(origin_std_files.len()); - - for file in origin_std_files { - let rel = match std_rel_path(&file.path) { - Some(r) => r, - None => continue, - }; - - let translated = - translate_one_std_file(locale, &rel, &file.content).unwrap_or(file.content); - let out_path = translate_std_root.join(&rel); - if let Some(parent) = out_path.parent() { - let _ = std::fs::create_dir_all(parent); - } - let _ = std::fs::write(&out_path, &translated); - - out_files.push(LuaFileInfo { - path: out_path.to_string_lossy().to_string(), - content: translated, - }); - } - - out_files -} - -fn translate_one_std_file(locale: &str, rel_lua_path: &Path, content: &str) -> Option { - let stem = rel_lua_path.with_extension(""); - let stem_str = stem.to_string_lossy().replace('\\', "/"); - - let meta_path = format!("{stem_str}/meta.yaml"); - let tr_path = format!("{stem_str}/{locale}.yaml"); - - let meta = read_meta(&meta_path)?; - let translations = read_translations(&tr_path)?; - - Some(apply_meta_translations(content, &meta, &translations)) -} - -fn read_meta(path_in_dir: &str) -> Option { - let file = STD_I18N_DIR.get_file(path_in_dir)?; - let raw = file.contents_utf8()?; - serde_yml::from_str(raw).ok() -} - -fn read_translations(path_in_dir: &str) -> Option> { - let file = STD_I18N_DIR.get_file(path_in_dir)?; - let raw = file.contents_utf8()?; - serde_yml::from_str(raw).ok() -} - -fn apply_meta_translations( - content: &str, - meta: &MetaFile, - translations: &HashMap, -) -> String { - if meta.version != 1 || meta.line_base != 0 || meta.col_base != 0 { - return content.to_string(); - } - - let newline = if content.contains("\r\n") { - "\r\n" - } else { - "\n" - }; - let line_starts = build_line_start_offsets(content); - - let mut replacements: Vec<(usize, usize, String)> = Vec::new(); - for entry in &meta.entries { - let Some(translated) = translations - .get(&entry.key) - .map(|s| s.to_string()) - .filter(|t| !t.trim().is_empty()) - else { - continue; - }; - - let start = match pos_to_offset(&line_starts, entry.range.start) { - Some(o) => o, - None => continue, - }; - let end = match pos_to_offset(&line_starts, entry.range.end) { - Some(o) => o, - None => continue, - }; - if start > end || end > content.len() { - continue; - } - - let slice = content.get(start..end).unwrap_or(""); - if fnv1a64_hex(slice) != entry.hash { - continue; - } - - let context_line = line_slice_at_offset(content, &line_starts, start); - if fnv1a64_hex(context_line) != entry.context_hash { - continue; - } - - let rep = match &entry.kind { - MetaKind::DocBlock { indent } => { - let mut rep = build_doc_block_string(indent, &translated, newline); - if line_break_len_at(content, end) > 0 && rep.ends_with(newline) { - rep.truncate(rep.len().saturating_sub(newline.len())); - } - rep - } - MetaKind::LineTail { prefix } => { - let one_line = to_one_line(&translated); - format!("{prefix}{one_line}") - } - }; - - replacements.push((start, end, rep)); - } - - if replacements.is_empty() { - return content.to_string(); - } - - replacements.sort_by_key(|(s, _, _)| *s); - let mut out = String::with_capacity(content.len() + 256); - let mut cursor = 0usize; - for (start, end, rep) in replacements { - if start < cursor || end < start || end > content.len() { - continue; - } - out.push_str(&content[cursor..start]); - out.push_str(&rep); - cursor = end; - } - out.push_str(&content[cursor..]); - out -} - -fn std_rel_path(path: &str) -> Option { - // `glua_code_analysis` 嵌入资源的路径形如 `std/builtin.lua`. - let p = Path::new(path); - let mut it = p.components(); - let first = it.next()?.as_os_str().to_string_lossy(); - if first != "std" { - return None; - } - let rest = it.as_path(); - Some(rest.to_path_buf()) -} - -fn build_line_start_offsets(s: &str) -> Vec { - let mut out = Vec::new(); - out.push(0); - for (i, b) in s.as_bytes().iter().enumerate() { - if *b == b'\n' { - out.push(i + 1); - } - } - out -} - -fn pos_to_offset(line_starts: &[usize], pos: MetaPos) -> Option { - let line = pos.line as usize; - let col = pos.col as usize; - let line_start = *line_starts.get(line)?; - Some(line_start.saturating_add(col)) -} - -fn line_slice_at_offset<'a>(s: &'a str, line_starts: &[usize], offset: usize) -> &'a str { - if s.is_empty() { - return ""; - } - let offset = offset.min(s.len()); - - // upper_bound(line_starts, offset) - 1 - let idx = match line_starts.binary_search(&offset) { - Ok(i) => i, - Err(i) => i.saturating_sub(1), - }; - let line = idx.min(line_starts.len().saturating_sub(1)); - - let line_start = *line_starts.get(line).unwrap_or(&0); - let next_start = line_starts.get(line + 1).copied().unwrap_or(s.len()); - let mut line_end = next_start; - if line_end > line_start && s.as_bytes().get(line_end - 1) == Some(&b'\n') { - line_end -= 1; - if line_end > line_start && s.as_bytes().get(line_end - 1) == Some(&b'\r') { - line_end -= 1; - } - } - s.get(line_start..line_end).unwrap_or("") -} - -fn line_break_len_at(content: &str, offset: usize) -> usize { - let bytes = content.as_bytes(); - if offset >= bytes.len() { - return 0; - } - match bytes[offset] { - b'\r' => { - if offset + 1 < bytes.len() && bytes[offset + 1] == b'\n' { - 2 - } else { - 1 - } - } - b'\n' => 1, - _ => 0, - } -} - -fn build_doc_block_string(indent: &str, translated: &str, newline: &str) -> String { - let translated_norm = translated.replace("\r\n", "\n"); - let translated_trim = translated_norm.trim_end_matches('\n'); - - let mut out = String::new(); - if translated_trim.is_empty() { - out.push_str(indent); - out.push_str("---"); - out.push_str(newline); - return out; - } - - for line in translated_trim.split('\n') { - out.push_str(indent); - if line.is_empty() { - out.push_str("---"); - } else { - out.push_str("--- "); - out.push_str(line); - } - out.push_str(newline); - } - - out -} - -fn to_one_line(s: &str) -> String { - s.replace("\r\n", "\n") - .lines() - .map(|l| l.trim()) - .filter(|l| !l.is_empty()) - .collect::>() - .join(" ") -} - -fn fnv1a64_hex(s: &str) -> String { - let mut hash: u64 = 0xcbf29ce484222325; - for b in s.as_bytes() { - hash ^= *b as u64; - hash = hash.wrapping_mul(0x00000100000001B3); - } - format!("{hash:016x}") -} - -#[cfg(test)] -mod tests { - use std::path::Path; - - use super::generate; - - #[test] - #[ignore] - fn test_generate_translated() { - let test_output_dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .join("glua_code_analysis") - .join("resources"); - let files = generate("zh_CN", &test_output_dir); - assert!(!files.is_empty()); - } -} diff --git a/crates/glua_ls/src/handlers/inlay_hint/build_function_hint.rs b/crates/glua_ls/src/handlers/inlay_hint/build_function_hint.rs index ed8e77a07..e09c97aef 100644 --- a/crates/glua_ls/src/handlers/inlay_hint/build_function_hint.rs +++ b/crates/glua_ls/src/handlers/inlay_hint/build_function_hint.rs @@ -1,6 +1,6 @@ use glua_code_analysis::{ LuaDeclId, LuaType, LuaUnionType, RenderLevel, SemanticModel, format_union_type, humanize_type, - infer_param_with_cache, + infer_param_with_cache, resolve_alias_type, }; use glua_parser::{LuaAstNode, LuaAstToken, LuaClosureExpr, LuaParamName}; use itertools::Itertools; @@ -193,7 +193,20 @@ fn get_base_type_location(semantic_model: &SemanticModel, name: &str) -> Option< fn hint_humanize_type(semantic_model: &SemanticModel, typ: &LuaType, level: RenderLevel) -> String { match typ { - LuaType::Ref(id) | LuaType::Def(id) => id.get_simple_name().to_string(), + LuaType::Ref(id) | LuaType::Def(id) => { + let resolved = resolve_alias_type(semantic_model.get_db(), typ); + if let Some(alias_id) = resolved.alias_id + && resolved.typ != *typ + { + return format!( + "{} = {}", + alias_id.get_simple_name(), + hint_humanize_type(semantic_model, &resolved.typ, level) + ); + } + + id.get_simple_name().to_string() + } LuaType::Generic(generic) => { let base_type_id = generic.get_base_type_id(); let base_type_name = diff --git a/crates/glua_ls/src/handlers/inlay_hint/build_inlay_hint.rs b/crates/glua_ls/src/handlers/inlay_hint/build_inlay_hint.rs index 6e0b461cc..676f1c111 100644 --- a/crates/glua_ls/src/handlers/inlay_hint/build_inlay_hint.rs +++ b/crates/glua_ls/src/handlers/inlay_hint/build_inlay_hint.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use glua_code_analysis::{ AsyncState, FileId, GmodRealm, InferGuard, LuaFunctionType, LuaMember, LuaMemberId, LuaMemberKey, LuaMemberOwner, LuaOperatorId, LuaOperatorMetaMethod, LuaOperatorOwner, - LuaSemanticDeclId, LuaType, LuaTypeDecl, SemanticModel, WorkspaceId, + LuaSemanticDeclId, LuaType, LuaTypeDecl, SemanticModel, WorkspaceId, resolve_alias_type, }; use glua_parser::{ LuaAssignStat, LuaAst, LuaAstNode, LuaCallExpr, LuaExpr, LuaFuncStat, LuaIndexExpr, @@ -529,7 +529,8 @@ fn get_vgui_panel_name( semantic_model: &SemanticModel, typ: &LuaType, ) -> Option<(String, Option)> { - match typ { + let resolved = resolve_alias_type(semantic_model.get_db(), typ); + match &resolved.typ { LuaType::Ref(type_decl_id) | LuaType::Def(type_decl_id) => { let type_name = type_decl_id.get_simple_name(); let base_name = semantic_model @@ -1165,6 +1166,12 @@ fn get_scripted_entity_suffix( }; // Check VGUI panels first + let member_type = LuaType::Ref(type_id.clone()); + let resolved = resolve_alias_type(semantic_model.get_db(), &member_type); + let type_id = match &resolved.typ { + LuaType::Ref(id) | LuaType::Def(id) => id, + _ => return None, + }; let type_name = type_id.get_simple_name(); if let Some(base_name) = semantic_model .get_db() @@ -1184,11 +1191,11 @@ fn get_scripted_entity_suffix( .get_super_types(type_id)?; for super_type in supers { - let super_id = match &super_type { - LuaType::Ref(id) => id, + let resolved_super = resolve_alias_type(semantic_model.get_db(), &super_type); + let super_name = match &resolved_super.typ { + LuaType::Ref(id) | LuaType::Def(id) => id.get_simple_name(), _ => continue, }; - let super_name = super_id.get_simple_name(); match super_name { "ENT" | "Entity" | "SWEP" | "Weapon" | "EFFECT" | "TOOL" | "Tool" | "PLUGIN" | "GM" => { return Some(format!(" ({type_name} : {super_name})")); diff --git a/crates/glua_ls/src/handlers/notification_handler.rs b/crates/glua_ls/src/handlers/notification_handler.rs index bf56d8070..850feabd0 100644 --- a/crates/glua_ls/src/handlers/notification_handler.rs +++ b/crates/glua_ls/src/handlers/notification_handler.rs @@ -78,10 +78,10 @@ pub async fn on_notification_handler( let workspace = snapshot.workspace_manager().read().await; workspace.update_workspace_version(WorkspaceDiagnosticLevel::Fast, true); } - // Keep inlay hint requests alive so they can wait for fresh data - // instead of being cancelled and causing visible flicker. + // Keep stale-aware UI requests alive so they can wait for fresh + // data instead of flickering while typing. server_context - .cancel_all_requests_except(&["textDocument/inlayHint"]) + .cancel_all_requests_except(&["textDocument/codeLens", "textDocument/inlayHint"]) .await; // Mark analysis dirty BEFORE handing the update to the coalescer so // follow-up requests see the stale state immediately. diff --git a/crates/glua_ls/src/handlers/references/reference_searcher.rs b/crates/glua_ls/src/handlers/references/reference_searcher.rs index c2acb8c90..3cd4e672f 100644 --- a/crates/glua_ls/src/handlers/references/reference_searcher.rs +++ b/crates/glua_ls/src/handlers/references/reference_searcher.rs @@ -4,19 +4,20 @@ use std::{ }; use glua_code_analysis::{ - DeclReferenceCell, FileId, LuaCompilation, LuaDeclId, LuaMemberId, LuaMemberKey, - LuaSemanticDeclId, LuaType, LuaTypeDeclId, SemanticDeclLevel, SemanticModel, + DeclReferenceCell, FileId, GmodScriptedClassCallKind, LuaCompilation, LuaDeclId, LuaMemberId, + LuaMemberKey, LuaSemanticDeclId, LuaType, LuaTypeDeclId, SemanticDeclLevel, SemanticModel, }; use glua_parser::{ - LuaAssignStat, LuaAst, LuaAstNode, LuaAstToken, LuaCallExpr, LuaExpr, LuaLiteralToken, - LuaNameToken, LuaStringToken, LuaSyntaxNode, LuaSyntaxToken, LuaTableField, PathTrait, + LuaAssignStat, LuaAst, LuaAstNode, LuaAstToken, LuaCallArgList, LuaCallExpr, LuaExpr, + LuaLiteralExpr, LuaLiteralToken, LuaNameToken, LuaStringToken, LuaSyntaxNode, LuaSyntaxToken, + LuaTableField, }; use lsp_types::Location; use tokio_util::sync::CancellationToken; use crate::handlers::gmod_string_context::{ - extract_string_call_context, is_vgui_panel_string_context, net_message_call_kind, - normalize_string_name, + extract_string_call_context, is_annotated_derma_skin_string_context, + is_annotated_vgui_panel_string_context, is_net_message_string_context, normalize_string_name, }; use crate::handlers::hover::find_member_origin_owner; @@ -86,6 +87,18 @@ pub fn search_references( return Some(result); } + if search_derma_skin_string_references( + semantic_model, + compilation, + string_token.clone(), + &mut result, + cancel_token, + ) + .is_some() + { + return Some(result); + } + if search_net_message_references(semantic_model, string_token.clone(), &mut result) .is_some() { @@ -484,7 +497,14 @@ fn search_vgui_panel_string_references( cancel_token: &CancellationToken, ) -> Option<()> { let context = extract_string_call_context(&token)?; - if !is_vgui_panel_string_context(&context.call_path, context.arg_index) { + let annotated_context = token + .get_parent::() + .and_then(|literal| literal.get_parent::()) + .and_then(|args| args.get_parent::()) + .is_some_and(|call_expr| { + is_annotated_vgui_panel_string_context(semantic_model, &call_expr, context.arg_index) + }); + if !annotated_context { return None; } @@ -493,11 +513,7 @@ fn search_vgui_panel_string_references( .get_gmod_class_metadata_index() .find_vgui_panel_definitions(&context.name) { - let definition_range = call - .args - .first() - .map(|arg| arg.syntax_id.get_range()) - .unwrap_or(call.syntax_id.get_range()); + let definition_range = call.define_arg_range(GmodScriptedClassCallKind::VguiRegister); let Some(document) = semantic_model.get_document_by_file_id(file_id) else { continue; }; @@ -536,9 +552,18 @@ fn search_vgui_panel_string_references( let Some(reference_context) = extract_string_call_context(&reference_string_token) else { continue; }; - if !is_vgui_panel_string_context(&reference_context.call_path, reference_context.arg_index) - || reference_context.name != context.name - { + let annotated_reference_context = reference_string_token + .get_parent::() + .and_then(|literal| literal.get_parent::()) + .and_then(|args| args.get_parent::()) + .is_some_and(|call_expr| { + is_annotated_vgui_panel_string_context( + reference_semantic_model.as_ref(), + &call_expr, + reference_context.arg_index, + ) + }); + if !annotated_reference_context || reference_context.name != context.name { continue; } @@ -585,13 +610,168 @@ fn collect_vgui_context_string_references_from_ast( let root = semantic_model.get_root(); for call_expr in root.descendants::() { - let Some(call_path) = call_expr.get_access_path() else { + let Some(args_list) = call_expr.get_args_list() else { continue; }; + for (arg_index, arg) in args_list.get_args().enumerate() { + let LuaExpr::LiteralExpr(literal_expr) = arg else { + continue; + }; + let Some(LuaLiteralToken::String(string_token)) = literal_expr.get_literal() else { + continue; + }; + let Some(name) = normalize_string_name(string_token.get_value()) else { + continue; + }; + + let annotated_context = + is_annotated_vgui_panel_string_context(&semantic_model, &call_expr, arg_index); + if name != panel_name || !annotated_context { + continue; + } + + let Some(location) = semantic_model + .get_document() + .to_lsp_location(string_token.get_range()) + else { + continue; + }; + push_unique_location(result, location); + } + } + } +} + +fn search_derma_skin_string_references( + semantic_model: &SemanticModel, + compilation: &LuaCompilation, + token: LuaStringToken, + result: &mut Vec, + cancel_token: &CancellationToken, +) -> Option<()> { + let context = extract_string_call_context(&token)?; + let annotated_context = token + .get_parent::() + .and_then(|literal| literal.get_parent::()) + .and_then(|args| args.get_parent::()) + .is_some_and(|call_expr| { + is_annotated_derma_skin_string_context(semantic_model, &call_expr, context.arg_index) + }); + if !annotated_context { + return None; + } + if let Some(location) = semantic_model + .get_document() + .to_lsp_location(token.get_range()) + { + push_unique_location(result, location); + } + + for (file_id, call) in semantic_model + .get_db() + .get_gmod_class_metadata_index() + .find_derma_skin_definitions(&context.name) + { + let definition_range = call.define_arg_range(GmodScriptedClassCallKind::DermaDefineSkin); + let Some(document) = semantic_model.get_document_by_file_id(file_id) else { + continue; + }; + let Some(location) = document.to_lsp_location(definition_range) else { + continue; + }; + push_unique_location(result, location); + } + + let string_refs = semantic_model + .get_db() + .get_reference_index() + .get_string_references(&context.name); + let before_usage_refs = result.len(); + let mut semantic_cache = HashMap::new(); + for in_filed_reference_range in string_refs { + if cancel_token.is_cancelled() { + return None; + } + + let Some(reference_semantic_model) = get_semantic_model_cached( + compilation, + &mut semantic_cache, + in_filed_reference_range.file_id, + ) else { + continue; + }; + + let root = reference_semantic_model.get_root(); + let Some(reference_token) = root + .syntax() + .token_at_offset(in_filed_reference_range.value.start()) + .right_biased() + else { + continue; + }; + let Some(reference_string_token) = LuaStringToken::cast(reference_token) else { + continue; + }; + let Some(reference_context) = extract_string_call_context(&reference_string_token) else { + continue; + }; + let annotated_reference_context = reference_string_token + .get_parent::() + .and_then(|literal| literal.get_parent::()) + .and_then(|args| args.get_parent::()) + .is_some_and(|call_expr| { + is_annotated_derma_skin_string_context( + reference_semantic_model.as_ref(), + &call_expr, + reference_context.arg_index, + ) + }); + if !annotated_reference_context || reference_context.name != context.name { + continue; + } + + let document = reference_semantic_model.get_document(); + let Some(location) = document.to_lsp_location(in_filed_reference_range.value) else { + continue; + }; + push_unique_location(result, location); + } + + if result.len() == before_usage_refs { + collect_derma_skin_context_string_references_from_ast( + compilation, + &context.name, + result, + cancel_token, + ); + } + + Some(()) +} + +fn collect_derma_skin_context_string_references_from_ast( + compilation: &LuaCompilation, + skin_name: &str, + result: &mut Vec, + cancel_token: &CancellationToken, +) { + let mut semantic_cache = HashMap::new(); + let file_ids = compilation.get_db().get_vfs().get_all_file_ids(); + for file_id in file_ids { + if cancel_token.is_cancelled() { + return; + } + let Some(semantic_model) = + get_semantic_model_cached(compilation, &mut semantic_cache, file_id) + else { + continue; + }; + + let root = semantic_model.get_root(); + for call_expr in root.descendants::() { let Some(args_list) = call_expr.get_args_list() else { continue; }; - for (arg_index, arg) in args_list.get_args().enumerate() { let LuaExpr::LiteralExpr(literal_expr) = arg else { continue; @@ -603,7 +783,9 @@ fn collect_vgui_context_string_references_from_ast( continue; }; - if name != panel_name || !is_vgui_panel_string_context(&call_path, arg_index) { + let annotated_context = + is_annotated_derma_skin_string_context(&semantic_model, &call_expr, arg_index); + if name != skin_name || !annotated_context { continue; } @@ -625,11 +807,42 @@ fn search_net_message_references( result: &mut Vec, ) -> Option<()> { let context = extract_string_call_context(&token)?; - let _ = net_message_call_kind(&context.call_path, context.arg_index)?; - + let call_expr = token + .get_parent::() + .and_then(|literal| literal.get_parent::()) + .and_then(|args| args.get_parent::())?; + if !is_net_message_string_context(semantic_model, &call_expr, context.arg_index) { + return None; + } let network_index = semantic_model.get_db().get_gmod_network_index(); + let send_flows = network_index.get_send_flows_for_message(&context.name); + let receive_flows = network_index.get_receive_flows_for_message(&context.name); + let file_id = semantic_model.get_file_id(); + let call_range = call_expr.get_range(); + let indexed_flow_context = send_flows + .iter() + .any(|(flow_file_id, flow)| *flow_file_id == file_id && flow.start_range == call_range) + || receive_flows.iter().any(|(flow_file_id, flow)| { + *flow_file_id == file_id && flow.receive_range == call_range + }); + let annotated_literal_context = !indexed_flow_context + && crate::handlers::gmod_string_context::find_string_call_arg_role( + semantic_model, + &call_expr, + context.arg_index, + "gmod.net_message", + &["define", "start", "receive", "reference"], + ) + .is_some(); + if annotated_literal_context + && let Some(location) = semantic_model + .get_document() + .to_lsp_location(token.get_range()) + { + push_unique_location(result, location); + } - for (file_id, flow) in network_index.get_send_flows_for_message(&context.name) { + for (file_id, flow) in send_flows { let Some(document) = semantic_model.get_document_by_file_id(file_id) else { continue; }; @@ -639,7 +852,7 @@ fn search_net_message_references( push_unique_location(result, location); } - for (file_id, flow) in network_index.get_receive_flows_for_message(&context.name) { + for (file_id, flow) in receive_flows { let Some(document) = semantic_model.get_document_by_file_id(file_id) else { continue; }; diff --git a/crates/glua_ls/src/handlers/semantic_token/build_semantic_tokens.rs b/crates/glua_ls/src/handlers/semantic_token/build_semantic_tokens.rs index d70cfc4ef..4527dc50a 100644 --- a/crates/glua_ls/src/handlers/semantic_token/build_semantic_tokens.rs +++ b/crates/glua_ls/src/handlers/semantic_token/build_semantic_tokens.rs @@ -11,7 +11,7 @@ use crate::{context::ClientId, handlers::semantic_token::language_injector::inje use glua_code_analysis::{ DbIndex, Emmyrc, GlobalId, LocalAttribute, LuaDecl, LuaDeclExtra, LuaMemberFeature, LuaMemberId, LuaMemberOwner, LuaSemanticDeclId, LuaType, LuaTypeDeclId, LuaTypeOwner, - SemanticDeclLevel, SemanticModel, WorkspaceId, parse_require_module_info, + SemanticDeclLevel, SemanticModel, WorkspaceId, parse_require_module_info, resolve_alias_type, }; use glua_parser::{ LuaAssignStat, LuaAst, LuaAstNode, LuaAstToken, LuaCallArgList, LuaCallExpr, LuaComment, @@ -1576,7 +1576,25 @@ fn semantic_token_type_for_type_decl( } else if type_decl.is_class() { Some(SemanticTokenType::CLASS) } else if type_decl.is_alias() { - Some(SemanticTokenType::TYPE) + match resolve_alias_type(semantic_model.get_db(), &LuaType::Ref(type_id.clone())).typ { + LuaType::Ref(origin_id) | LuaType::Def(origin_id) => { + let Some(origin_decl) = semantic_model + .get_db() + .get_type_index() + .get_type_decl(&origin_id) + else { + return Some(SemanticTokenType::TYPE); + }; + if origin_decl.is_enum() { + Some(SemanticTokenType::ENUM) + } else if origin_decl.is_class() { + Some(SemanticTokenType::CLASS) + } else { + Some(SemanticTokenType::TYPE) + } + } + _ => Some(SemanticTokenType::TYPE), + } } else { None } @@ -1595,8 +1613,9 @@ fn is_object_like_decl_type( } fn is_object_like_value_type(semantic_model: &SemanticModel, decl_type: &LuaType) -> bool { - match decl_type { - LuaType::Ref(type_id) => semantic_model + let resolved = resolve_alias_type(semantic_model.get_db(), decl_type); + match &resolved.typ { + LuaType::Ref(type_id) | LuaType::Def(type_id) => semantic_model .get_db() .get_type_index() .get_type_decl(type_id) diff --git a/crates/glua_ls/src/handlers/test/completion_test.rs b/crates/glua_ls/src/handlers/test/completion_test.rs index c0e5eb145..cbfae0d41 100644 --- a/crates/glua_ls/src/handlers/test/completion_test.rs +++ b/crates/glua_ls/src/handlers/test/completion_test.rs @@ -2740,6 +2740,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def( r#" util.AddNetworkString("known_message") @@ -2788,6 +2789,38 @@ mod tests { Ok(()) } + #[gtest] + fn test_gmod_net_message_completion_uses_annotated_call_arg_role() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + util.AddNetworkString("known_message") + + ---@[call_arg("gmod.net_message", "start")] + ---@param name string + function startNet(name) end + "#, + ); + + check!(ws.check_completion( + r#" + startNet("") + "#, + vec![VirtualCompletionItem { + label: "known_message".to_string(), + kind: CompletionItemKind::EVENT, + label_detail: Some("(1 registration, 0 receivers)".to_string()), + }], + )); + Ok(()) + } + #[gtest] fn test_gmod_net_read_completion_in_receive_callback() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); @@ -2795,6 +2828,7 @@ mod tests { emmyrc.gmod.enabled = true; emmyrc.gmod.network.completion.smart_read_suggestions = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def_file( "addons/test/lua/autorun/server/send.lua", @@ -2861,6 +2895,7 @@ mod tests { emmyrc.gmod.enabled = true; emmyrc.gmod.network.completion.smart_read_suggestions = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def_file( "addons/test/lua/autorun/server/send.lua", @@ -3024,6 +3059,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def( r#" hook.Add("Think", "id", function() end) @@ -3077,6 +3113,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def( r#" hook.Add("PlayerSpawn", "id", function(a, b) end) @@ -3130,6 +3167,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def( r#" function GM:PlayerSpawn(ply, transition) end @@ -3150,6 +3188,74 @@ mod tests { Ok(()) } + #[gtest] + fn test_gmod_hook_completion_requires_annotated_call_arg_role() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def( + r#" + function GM:PlayerSpawn(ply, transition) end + "#, + ); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + hook.Run("") + "#, + )?; + let file_id = ws.def(content.as_str()); + let result = completion( + &ws.analysis, + file_id, + position, + CompletionTriggerKind::INVOKED, + CancellationToken::new(), + ) + .ok_or("failed to get completion") + .or_fail()?; + let items = match result { + CompletionResponse::Array(items) => items, + CompletionResponse::List(list) => list.items, + }; + + verify_that!( + items.iter().any(|item| item.label == "PlayerSpawn"), + eq(false) + )?; + + Ok(()) + } + + #[gtest] + fn test_gmod_hook_completion_uses_annotated_call_arg_role() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + check!(ws.check_completion( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + function GM:PlayerSpawn(ply, transition) end + + ---@[call_arg("gmod.hook", "emit")] + ---@param name string + local function runHook(name) end + + runHook("") + "#, + vec![VirtualCompletionItem { + label: "PlayerSpawn".to_string(), + kind: CompletionItemKind::EVENT, + label_detail: Some("(1 method; args: ply, transition)".to_string()), + }], + )); + + Ok(()) + } + #[gtest] fn test_gmod_hook_completion_prefers_method_callback_params_over_hook_add_callback() -> Result<()> { @@ -3157,6 +3263,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def( r#" hook.Add("PlayerSpawn", "id", function(a, b) end) @@ -3261,12 +3368,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); - ws.def( - r#" - hook = hook or {} - function hook.Add(eventName, identifier, func) end - "#, - ); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -3312,6 +3414,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def( r#" function GM:PlayerSpawn(ply, transition) end @@ -3371,12 +3474,75 @@ mod tests { Ok(()) } + #[gtest] + fn test_gmod_annotated_hook_wrapper_uses_call_arg_staged_snippet() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + function GM:PlayerSpawn(ply, transition) end + + mylib = { hook = {} } + ---@[call_arg("gmod.hook", "add")] + ---@param eventName string + function mylib.hook.Add(eventName, identifier, func) end + "#, + ); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + mylib.hook.Add("") + "#, + )?; + let file_id = ws.def(content.as_str()); + let result = completion( + &ws.analysis, + file_id, + position, + CompletionTriggerKind::INVOKED, + CancellationToken::new(), + ) + .ok_or("failed to get completion") + .or_fail()?; + let items = match result { + CompletionResponse::Array(items) => items, + CompletionResponse::List(list) => list.items, + }; + + let item = items + .iter() + .find(|item| item.label == "PlayerSpawn") + .ok_or("missing PlayerSpawn completion") + .or_fail()?; + let text_edit = item + .text_edit + .as_ref() + .ok_or("missing hook completion text edit") + .or_fail()?; + let lsp_types::CompletionTextEdit::Edit(text_edit) = text_edit else { + return fail!("expected text edit for annotated hook wrapper completion"); + }; + + verify_eq!(item.kind, Some(CompletionItemKind::EVENT))?; + verify_that!( + text_edit.new_text.as_str(), + eq("PlayerSpawn\", \"${1:identifier}\", function(ply, transition)\n\t$0\nend)") + )?; + + Ok(()) + } + #[gtest] fn test_gmod_hook_run_string_completion_expands_emit_snippet() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def( r#" function GM:PlayerSpawn(ply, transition) end @@ -3442,6 +3608,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def( r#" function GM:PlayerSpawn(ply, transition) end @@ -3505,6 +3672,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def( r#" function GM:PlayerSpawn(ply, transition) end @@ -3546,12 +3714,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); - ws.def( - r#" - net = net or {} - function net.Receive(name, func) end - "#, - ); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -3597,6 +3760,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def_file( "addons/test/lua/autorun/server/send.lua", r#" @@ -3659,6 +3823,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def_file( "sv_hook.lua", r#" @@ -3693,6 +3858,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def_file( "sh_hook_annotated.lua", r#" diff --git a/crates/glua_ls/src/handlers/test/definition_test.rs b/crates/glua_ls/src/handlers/test/definition_test.rs index 7558d8125..546766595 100644 --- a/crates/glua_ls/src/handlers/test/definition_test.rs +++ b/crates/glua_ls/src/handlers/test/definition_test.rs @@ -1259,6 +1259,65 @@ mod tests { Ok(()) } + #[gtest] + fn test_goto_hook_definition_from_annotated_call_arg_role() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + + check!(ws.check_definition( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@class GM + ---@type GM + GM = GM or {} + + function GM:SomeHook(ply) end + + ---@[call_arg("gmod.hook", "emit")] + ---@param eventName string + local function emit_hook(eventName) end + + emit_hook("SomeHook") + "#, + vec![Expected { + file: "".to_string(), + line: 7, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_hook_definition_from_hook_remove() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + check!(ws.check_definition( + r#" + ---@class GM + ---@type GM + GM = GM or {} + + function GM:SomeHook(ply) end + + hook.Remove("SomeHook", "id") + "#, + vec![Expected { + file: "".to_string(), + line: 5, + }], + )); + + Ok(()) + } + #[gtest] fn test_goto_source_file_uri_redirects_before_stub_definition() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); @@ -1292,6 +1351,7 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def_file( "panels.lua", @@ -1314,12 +1374,301 @@ mod tests { Ok(()) } + #[gtest] + fn test_goto_vgui_panel_definition_from_annotated_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "panels.lua", + r#" + local PANEL = {} + vgui.Register("MyPanel", PANEL, "DPanel") + "#, + ); + ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.vgui_panel", "reference")] + ---@param name string + function createPanel(name) end + "#, + ); + + check!(ws.check_definition( + r#" + local pnl = createPanel("MyPanel") + "#, + vec![Expected { + file: "panels.lua".to_string(), + line: 2, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_vgui_panel_definition_from_overload_call_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "panels.lua", + r#" + local PANEL = {} + vgui.Register("MyPanel", PANEL, "DPanel") + "#, + ); + ws.def( + r#" + ---@attribute overload_call_arg(param: integer, domain: string, role: string, priority: integer?) + + ---@[overload_call_arg(0, "gmod.vgui_panel", "reference")] + ---@overload fun(className: string): Panel + ---@param panel Panel + function addPanel(panel) end + "#, + ); + + check!(ws.check_definition( + r#" + local pnl = addPanel("MyPanel") + "#, + vec![Expected { + file: "panels.lua".to_string(), + line: 2, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_vgui_panel_definition_from_annotated_define_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "panels.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.vgui_panel", "define")] + ---@param name string + local function registerPanel(name) end + + registerPanel("MyPanel") + "#, + ); + + check!(ws.check_definition( + r#" + local pnl = vgui.Create("MyPanel") + "#, + vec![Expected { + file: "panels.lua".to_string(), + line: 7, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_vgui_panel_definition_from_non_first_annotated_define_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "panels.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@param description string + ---@[call_arg("gmod.vgui_panel", "define")] + ---@param name string + local function registerPanel(description, name) end + + registerPanel( + "desc", + "MyPanel" + ) + "#, + ); + + check!(ws.check_definition( + r#" + local pnl = vgui.Create("MyPanel") + "#, + vec![Expected { + file: "panels.lua".to_string(), + line: 10, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_derma_skin_definition_from_string() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "skins.lua", + r#" + local SKIN = {} + derma.DefineSkin("MySkin", "Nice skin", SKIN) + "#, + ); + + check!(ws.check_definition( + r#" + derma.GetNamedSkin("MySkin") + "#, + vec![Expected { + file: "skins.lua".to_string(), + line: 2, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_derma_skin_definition_from_annotated_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "skins.lua", + r#" + local SKIN = {} + derma.DefineSkin("MySkin", "Nice skin", SKIN) + "#, + ); + ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.derma_skin", "reference")] + ---@param name string + function useSkin(name) end + "#, + ); + + check!(ws.check_definition( + r#" + useSkin("MySkin") + "#, + vec![Expected { + file: "skins.lua".to_string(), + line: 2, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_derma_skin_definition_from_annotated_define_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "skins.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.derma_skin", "define")] + ---@param name string + local function defineSkin(name) end + + defineSkin("MySkin") + "#, + ); + + check!(ws.check_definition( + r#" + derma.GetNamedSkin("MySkin") + "#, + vec![Expected { + file: "skins.lua".to_string(), + line: 7, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_derma_skin_definition_from_non_first_annotated_define_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "skins.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@param description string + ---@[call_arg("gmod.derma_skin", "define")] + ---@param name string + local function defineSkin(description, name) end + + defineSkin( + "desc", + "MySkin" + ) + "#, + ); + + check!(ws.check_definition( + r#" + derma.GetNamedSkin("MySkin") + "#, + vec![Expected { + file: "skins.lua".to_string(), + line: 10, + }], + )); + + Ok(()) + } + #[gtest] fn test_goto_net_message_definition_from_start_string() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def_file( "receive.lua", @@ -1341,12 +1690,270 @@ mod tests { Ok(()) } + #[gtest] + fn test_goto_net_message_definition_from_annotated_start_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "receive.lua", + r#" + net.Receive("MyMessage", function() end) + "#, + ); + ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.net_message", "start")] + ---@param name string + function startNet(name) end + "#, + ); + + check!(ws.check_definition( + r#" + startNet("MyMessage") + "#, + vec![Expected { + file: "receive.lua".to_string(), + line: 1, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_net_message_definition_from_annotated_define_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "net_defs.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.net_message", "define")] + ---@param name string + local function registerMessage(name) end + + registerMessage("MyMessage") + "#, + ); + ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.net_message", "reference")] + ---@param name string + function describeMessage(name) end + "#, + ); + + check!(ws.check_definition( + r#" + describeMessage("MyMessage") + "#, + vec![Expected { + file: "net_defs.lua".to_string(), + line: 7, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_net_message_definition_from_annotated_define_site() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + check!(ws.check_definition( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.net_message", "define")] + ---@param name string + local function registerMessage(name) end + + registerMessage("MyMessage") + "#, + vec![Expected { + file: "".to_string(), + line: 7, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_vgui_panel_definition_ignores_wrong_annotated_domain() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.derma_skin", "define")] + ---@param name string + local function defineSkin(name) end + + defineSkin("MyPanel") + "#, + ); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + vgui.Create("MyPanel") + "#, + )?; + let file_id = ws.def(&content); + let result = crate::handlers::definition::definition(&ws.analysis, file_id, position); + verify_that!(result, none())?; + + Ok(()) + } + + #[gtest] + fn test_goto_vgui_panel_definition_does_not_match_lookalike_builtin_path() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "panels.lua", + r#" + local PANEL = {} + vgui.Register("MyPanel", PANEL, "DPanel") + "#, + ); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + mylib = { vgui = {} } + function mylib.vgui.Create(name) end + mylib.vgui.Create("MyPanel") + "#, + )?; + let file_id = ws.def(&content); + let result = crate::handlers::definition::definition(&ws.analysis, file_id, position); + verify_that!(result, none())?; + + Ok(()) + } + + #[gtest] + fn test_goto_vgui_panel_definition_accepts_explicit_global_builtin_path() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "panels.lua", + r#" + local PANEL = {} + vgui.Register("MyPanel", PANEL, "DPanel") + "#, + ); + + check!(ws.check_definition( + r#" + local pnl = _G.vgui.Create("MyPanel") + "#, + vec![Expected { + file: "panels.lua".to_string(), + line: 2, + }], + )); + + Ok(()) + } + + #[gtest] + fn test_goto_derma_skin_definition_does_not_match_lookalike_builtin_path() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "skins.lua", + r#" + local SKIN = {} + derma.DefineSkin("MySkin", "Nice skin", SKIN) + "#, + ); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + mylib = { derma = {} } + function mylib.derma.GetNamedSkin(name) end + mylib.derma.GetNamedSkin("MySkin") + "#, + )?; + let file_id = ws.def(&content); + let result = crate::handlers::definition::definition(&ws.analysis, file_id, position); + verify_that!(result, none())?; + + Ok(()) + } + + #[gtest] + fn test_goto_derma_skin_definition_accepts_explicit_global_builtin_path() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "skins.lua", + r#" + local SKIN = {} + derma.DefineSkin("MySkin", "Nice skin", SKIN) + "#, + ); + + check!(ws.check_definition( + r#" + _G.derma.GetNamedSkin("MySkin") + "#, + vec![Expected { + file: "skins.lua".to_string(), + line: 2, + }], + )); + + Ok(()) + } + #[gtest] fn test_goto_net_message_definition_from_receive_string() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); ws.def_file( "send.lua", diff --git a/crates/glua_ls/src/handlers/test/hover_function_test.rs b/crates/glua_ls/src/handlers/test/hover_function_test.rs index f6a9c9561..d1869088d 100644 --- a/crates/glua_ls/src/handlers/test/hover_function_test.rs +++ b/crates/glua_ls/src/handlers/test/hover_function_test.rs @@ -1388,4 +1388,147 @@ mod tests { Ok(()) } + + /// Hover on a local variable with an explicit param default should display + /// the default value using the same syntax as function-hover default params. + #[gtest] + fn test_local_explicit_param_default_hover() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!(ws.check_hover( + r#" + ---@param panelClass string="DPanel" + local function create(panelClass) + end + "#, + VirtualHoverResult { + value: "```lua\nlocal panelClass: string = \"DPanel\"\n```".to_string(), + }, + )); + Ok(()) + } + + /// Hover on a local variable with an inferred default from + /// `x = x or "literal"` should display the default value. + #[gtest] + fn test_local_inferred_default_hover() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!(ws.check_hover( + r#" + ---@param panelClass string|nil + local function create(panelClass) + panelClass = panelClass or "DScrollPanel" + end + "#, + VirtualHoverResult { + value: "```lua\nlocal panelClass: string = \"DScrollPanel\"\n```".to_string(), + }, + )); + Ok(()) + } + + /// Explicit default must NOT show after a later reassignment kills it. + #[gtest] + fn test_local_explicit_param_default_hover_killed_by_reassignment() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!(ws.check_hover( + r#" + ---@param panelClass string="DPanel" + ---@param otherClass string + local function foo(panelClass, otherClass) + panelClass = otherClass + local p = panelClass + end + "#, + VirtualHoverResult { + value: "```lua\nlocal panelClass: string\n```".to_string(), + }, + )); + Ok(()) + } + + /// Hovering the RHS read of `x = x or "literal"` must NOT show the + /// inferred default, because the RHS is read *before* the assignment + /// establishes the default. + #[gtest] + fn test_hover_rhs_of_inferred_default_assignment_shows_no_default() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!(ws.check_hover( + r#" + ---@param panelClass string|nil + local function create(panelClass) + panelClass = panelClass or "DScrollPanel" + end + "#, + VirtualHoverResult { + value: "```lua\nlocal panelClass: string?\n```".to_string(), + }, + )); + Ok(()) + } + + /// Inferred default must NOT show after a later reassignment kills it. + #[gtest] + fn test_local_inferred_default_hover_killed_by_reassignment() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!(ws.check_hover( + r#" + ---@param panelClass string|nil + ---@param otherClass string + local function foo(panelClass, otherClass) + panelClass = panelClass or "DScrollPanel" + panelClass = otherClass + local p = panelClass + end + "#, + VirtualHoverResult { + value: "```lua\nlocal panelClass: string\n```".to_string(), + }, + )); + Ok(()) + } + + /// In a shared file, a wrong-realm reassignment (e.g. `if SERVER then`) + /// must NOT kill an inferred default when the hover use-site is in a + /// different realm (e.g. `if CLIENT then`). + #[gtest] + fn test_inferred_default_hover_shared_file_wrong_realm_reassignment_preserves_default() + -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = ws.get_emmyrc(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + ---@param panelClass string|nil + local function create(panelClass) + panelClass = panelClass or "DScrollPanel" + if SERVER then + panelClass = "ServerPanel" + end + if CLIENT then + local p = panelClass + end + end + "#, + )?; + let file_id = ws.def_file("lua/autorun/shared/sh_test.lua", &content); + let hover = crate::handlers::hover::hover(&ws.analysis, file_id, position, None) + .ok_or("expected hover") + .or_fail()?; + + let HoverContents::Markup(markup) = hover.contents else { + return fail!("expected HoverContents::Markup"); + }; + + // The default "DScrollPanel" must still be shown because the + // SERVER-only reassignment should not kill the default at a + // CLIENT use-site. + assert!( + markup.value.contains("DScrollPanel"), + "expected inferred default 'DScrollPanel' to survive SERVER-only reassignment at CLIENT use-site, got: {}", + markup.value + ); + Ok(()) + } } diff --git a/crates/glua_ls/src/handlers/test/hover_test.rs b/crates/glua_ls/src/handlers/test/hover_test.rs index 22cd221f7..600a8f5d0 100644 --- a/crates/glua_ls/src/handlers/test/hover_test.rs +++ b/crates/glua_ls/src/handlers/test/hover_test.rs @@ -717,6 +717,26 @@ mod tests { Ok(()) } + #[gtest] + fn test_alias_to_class_value_hover_shows_alias_expansion() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!(ws.check_hover( + r#" + ---@class Test2 + ---@field testField Entity + + ---@alias TestAlias Test2 + + ---@type TestAlias + local var + "#, + VirtualHoverResult { + value: "```lua\nlocal var: (alias) TestAlias = Test2 {\n testField: Entity,\n}\n```".to_string(), + }, + )); + Ok(()) + } + #[gtest] fn test_type_desc() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); @@ -1136,6 +1156,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "library/lua/includes/extensions/sandbox_hooks.lua", @@ -1219,6 +1240,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws.def_file( "library/lua/includes/extensions/sandbox_hooks.lua", @@ -1266,12 +1288,58 @@ local EscapeStringMap: { Ok(()) } + #[gtest] + fn test_hover_hook_name_does_not_match_lookalike_builtin_path() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = ws.get_emmyrc(); + emmyrc.gmod.enabled = true; + ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); + + ws.def_file( + "library/lua/includes/extensions/gm_hooks.lua", + r#" + ---@class GM + ---@type GM + GM = GM or {} + + function GM:PlayerSpawn(ply) end + "#, + ); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + mylib = { hook = {} } + function mylib.hook.Run(name) end + + mylib.hook.Run("PlayerSpawn") + "#, + )?; + let file_id = ws.def_file("gamemode/init.lua", &content); + + if let Some(hover) = crate::handlers::hover::hover(&ws.analysis, file_id, position, None) { + let HoverContents::Markup(markup) = hover.contents else { + return fail!("expected HoverContents::Markup"); + }; + assert!( + !markup.value.contains("GM:PlayerSpawn") + && !markup.value.contains("(method) PlayerSpawn") + && !markup.value.contains("(method) GM"), + "lookalike mylib.hook.Run should not use builtin hook hover without call_arg annotation, got: {}", + markup.value + ); + } + + Ok(()) + } + #[gtest] fn test_hover_hook_add_callback_parameter_usage_shows_inferred_type() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -1289,12 +1357,6 @@ local EscapeStringMap: { ---@return boolean function GM:AcceptInput(ent, input, activator, caller, value) end - hook = {} - ---@param eventName string - ---@param identifier any - ---@param func function - function hook.Add(eventName, identifier, func) end - hook.Add("AcceptInput", "test", function(ent, input, activator, caller, value) print(input) end) @@ -1379,6 +1441,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -1489,6 +1552,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -1543,6 +1607,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -1593,6 +1658,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -2613,6 +2679,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -2629,12 +2696,6 @@ local EscapeStringMap: { ---@return boolean function GM:CanPlayerEnterVehicle(ply, veh) end - hook = {} - ---@param eventName string - ---@param identifier any - ---@param func function - function hook.Add(eventName, identifier, func) end - hook.Add("CanPlayerEnterVehicle", "test", function(ply, veh) end) "#, )?; @@ -2698,6 +2759,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -2711,12 +2773,6 @@ local EscapeStringMap: { ---@return boolean function GM:PlayerConnect(ply) end - hook = {} - ---@param eventName string - ---@param identifier any - ---@param func function - function hook.Add(eventName, identifier, func) end - hook.Add("PlayerConnect", "test", function(ply) end) "#, )?; @@ -2745,6 +2801,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -2756,12 +2813,6 @@ local EscapeStringMap: { ---@return boolean function GM:SomeHook(ply) end - hook = {} - ---@param eventName string - ---@param identifier any - ---@param func function - function hook.Add(eventName, identifier, func) end - hook.Add("SomeHook", "test", function(ply) end) "#, )?; @@ -2783,6 +2834,83 @@ local EscapeStringMap: { Ok(()) } + #[gtest] + fn test_hover_hook_name_from_annotated_call_arg_role() -> Result<()> { + let mut ws = enable_gmod_workspace(); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@class GM + ---@type GM + GM = GM or {} + + ---@param ply Player + ---@return boolean + function GM:SomeHook(ply) end + + ---@[call_arg("gmod.hook", "emit")] + ---@param eventName string + local function emit_hook(eventName) end + + emit_hook("SomeHook") + "#, + )?; + let file_id = ws.def_file("gamemode/init.lua", &content); + let value = extract_hover_markdown(&ws, file_id, position); + + assert!( + value.contains("SomeHook"), + "annotated hook-name hover should show hook function signature, got: {value}" + ); + + Ok(()) + } + + #[gtest] + fn test_hover_hook_callback_from_annotated_call_arg_role() -> Result<()> { + let mut ws = enable_gmod_workspace(); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + ---@class Player + + ---@class GM + ---@type GM + GM = GM or {} + + ---@param ply Player + ---@return boolean + function GM:PlayerSpawn(ply) end + + ---@[call_arg("gmod.hook", "add")] + ---@param eventName string + ---@[call_arg("gmod.hook", "callback")] + ---@param callback function + local function add_hook(eventName, callback, identifier) end + + add_hook("PlayerSpawn", function(ply) end, "test") + "#, + )?; + let file_id = ws.def_file("gamemode/init.lua", &content); + let hover = crate::handlers::hover::hover(&ws.analysis, file_id, position, None) + .ok_or("expected annotated hook callback hover") + .or_fail()?; + + let HoverContents::Markup(markup) = hover.contents else { + return fail!("expected HoverContents::Markup"); + }; + + assert!( + markup.value.contains("function(ply: Player) -> boolean"), + "annotated hook callback hover should show hook callback signature, got: {}", + markup.value + ); + + Ok(()) + } + /// When `gmod.enabled` is false, hovering the `function` keyword in a hook.Add call /// should fall back to the generic keyword documentation. #[gtest] @@ -2802,12 +2930,6 @@ local EscapeStringMap: { ---@return boolean function GM:SomeHook(ply) end - hook = {} - ---@param eventName string - ---@param identifier any - ---@param func function - function hook.Add(eventName, identifier, func) end - hook.Add("SomeHook", "test", function(ply) end) "#, )?; @@ -2847,6 +2969,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -2883,12 +3006,10 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" - hook = {} - function hook.Add(eventName, identifier, func) end - -- "NonExistentHook" is not defined on GM/GAMEMODE, so no hook doc is available. hook.Add("NonExistentHook", "test", function(ply) end) "#, @@ -2925,6 +3046,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); let (content, position) = ProviderVirtualWorkspace::handle_file_content( r#" @@ -2935,12 +3057,6 @@ local EscapeStringMap: { ---@return boolean function GM:ReturnOnlyHook() end - hook = {} - ---@param eventName string - ---@param identifier any - ---@param func function - function hook.Add(eventName, identifier, func) end - hook.Add("ReturnOnlyHook", "test", function() end) "#, )?; @@ -2972,6 +3088,7 @@ local EscapeStringMap: { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); ws } @@ -3223,6 +3340,81 @@ local EscapeStringMap: { Ok(()) } + #[gtest] + fn test_hover_net_message_from_annotated_call_arg_role() -> Result<()> { + let mut ws = enable_gmod_workspace(); + + ws.def_file( + "lua/autorun/client/recv.lua", + r#" + net.Receive("WrappedMessage", function() + net.ReadString() + end) + "#, + ); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.net_message", "reference")] + ---@param name string + local function describe_message(name) end + + util.AddNetworkString("WrappedMessage") + describe_message("WrappedMessage") + "#, + )?; + let file_id = ws.def_file("lua/autorun/server/init.lua", &content); + let value = extract_hover_markdown(&ws, file_id, position); + + assert!( + value.contains("(net) \"WrappedMessage\""), + "expected annotated net hover header, got: {value}" + ); + assert!(value.contains("**Receivers**"), "got: {value}"); + assert!(value.contains("net.ReadString"), "got: {value}"); + + Ok(()) + } + + #[gtest] + fn test_hover_net_message_does_not_match_lookalike_builtin_path() -> Result<()> { + let mut ws = enable_gmod_workspace(); + + ws.def_file( + "lua/autorun/server/send.lua", + r#" + util.AddNetworkString("Lookalike") + net.Start("Lookalike") + net.Send(Entity(1)) + "#, + ); + + let (content, position) = ProviderVirtualWorkspace::handle_file_content( + r#" + mylib = { net = {} } + function mylib.net.Start(name) end + + mylib.net.Start("Lookalike") + "#, + )?; + let file_id = ws.def_file("lua/autorun/client/init.lua", &content); + + if let Some(hover) = crate::handlers::hover::hover(&ws.analysis, file_id, position, None) { + let HoverContents::Markup(markup) = hover.contents else { + return fail!("expected HoverContents::Markup"); + }; + assert!( + !markup.value.contains("(net) \"Lookalike\""), + "lookalike mylib.net.Start should not use builtin net hover without call_arg annotation, got: {}", + markup.value + ); + } + + Ok(()) + } + #[gtest] fn test_hover_net_message_groups_distinct_send_patterns() -> Result<()> { let mut ws = enable_gmod_workspace(); diff --git a/crates/glua_ls/src/handlers/test/inlay_hint_test.rs b/crates/glua_ls/src/handlers/test/inlay_hint_test.rs index 1321d1542..26421a35d 100644 --- a/crates/glua_ls/src/handlers/test/inlay_hint_test.rs +++ b/crates/glua_ls/src/handlers/test/inlay_hint_test.rs @@ -83,6 +83,29 @@ mod tests { Ok(()) } + #[gtest] + fn test_alias_to_class_local_hint_shows_target_class() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!(ws.check_inlay_hint( + r#" + ---@class Test2 + ---@field testField Entity + + ---@alias TestAlias Test2 + + ---@type TestAlias + local var + "#, + vec![VirtualInlayHint { + label: ": TestAlias = Test2".to_string(), + line: 7, + pos: 25, + ref_file: Some("".to_string()), + }] + )); + Ok(()) + } + #[gtest] fn test_local_hint_1() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); diff --git a/crates/glua_ls/src/handlers/test/references_test.rs b/crates/glua_ls/src/handlers/test/references_test.rs index 5b186bf6c..f9735183e 100644 --- a/crates/glua_ls/src/handlers/test/references_test.rs +++ b/crates/glua_ls/src/handlers/test/references_test.rs @@ -307,13 +307,20 @@ mod tests { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); check!(ws.check_references( r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.vgui_panel", "reference")] + ---@param name string + local function addPanel(name) end + local parent = vgui.Create("MyPanel") - parent:Add("MyPanel") + addPanel("MyPanel") "#, vec![ ( @@ -328,11 +335,11 @@ mod tests { vec![ VirtualLocation { file: "virtual_0.lua".to_string(), - line: 1, + line: 7, }, VirtualLocation { file: "virtual_0.lua".to_string(), - line: 4, + line: 10, }, VirtualLocation { file: "defs.lua".to_string(), @@ -348,12 +355,214 @@ mod tests { Ok(()) } + #[gtest] + fn test_gmod_vgui_panel_string_references_from_annotated_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + check!(ws.check_references( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.vgui_panel", "reference")] + ---@param name string + local function createPanel(name) end + + local parent = createPanel("MyPanel") + "#, + vec![ + ( + "panels.lua", + "\n\n\n\n\n\n\n\nvgui.Register(\"MyPanel\", PANEL, \"DPanel\")\n", + ), + ( + "usage.lua", + "\n\n\n\n\n\n\n\n\n\n\n\nlocal created = vgui.Create(\"MyPanel\")\n", + ), + ], + vec![ + VirtualLocation { + file: "panels.lua".to_string(), + line: 8, + }, + VirtualLocation { + file: "virtual_0.lua".to_string(), + line: 7, + }, + VirtualLocation { + file: "usage.lua".to_string(), + line: 12, + }, + ], + )); + + Ok(()) + } + + #[gtest] + fn test_gmod_vgui_panel_references_use_non_first_annotated_define_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + check!(ws.check_references( + r#" + vgui.Create("MyPanel") + "#, + vec![( + "panels.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@param description string + ---@[call_arg("gmod.vgui_panel", "define")] + ---@param name string + local function registerPanel(description, name) end + + registerPanel( + "desc", + "MyPanel" + ) + "#, + )], + vec![ + VirtualLocation { + file: "panels.lua".to_string(), + line: 10, + }, + VirtualLocation { + file: "virtual_0.lua".to_string(), + line: 1, + }, + ], + )); + + Ok(()) + } + + #[gtest] + fn test_gmod_derma_skin_string_references() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + check!(ws.check_references( + r#" + derma.GetNamedSkin("MySkin") + "#, + vec![( + "skins.lua", + "\n\n\n\n\n\n\nderma.DefineSkin(\"MySkin\", \"Nice skin\", {})\n", + ),], + vec![ + VirtualLocation { + file: "skins.lua".to_string(), + line: 7, + }, + VirtualLocation { + file: "virtual_0.lua".to_string(), + line: 1, + }, + ], + )); + + Ok(()) + } + + #[gtest] + fn test_gmod_derma_skin_string_references_from_annotated_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + check!(ws.check_references( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.derma_skin", "reference")] + ---@param name string + local function useSkin(name) end + + useSkin("MySkin") + "#, + vec![( + "skins.lua", + "\n\n\n\n\n\n\nderma.DefineSkin(\"MySkin\", \"Nice skin\", {})\n", + ),], + vec![ + VirtualLocation { + file: "virtual_0.lua".to_string(), + line: 7, + }, + VirtualLocation { + file: "skins.lua".to_string(), + line: 7, + }, + ], + )); + + Ok(()) + } + + #[gtest] + fn test_gmod_derma_skin_references_use_non_first_annotated_define_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); + + check!(ws.check_references( + r#" + derma.GetNamedSkin("MySkin") + "#, + vec![( + "skins.lua", + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@param description string + ---@[call_arg("gmod.derma_skin", "define")] + ---@param name string + local function defineSkin(description, name) end + + defineSkin( + "desc", + "MySkin" + ) + "#, + )], + vec![ + VirtualLocation { + file: "skins.lua".to_string(), + line: 10, + }, + VirtualLocation { + file: "virtual_0.lua".to_string(), + line: 1, + }, + ], + )); + + Ok(()) + } + #[gtest] fn test_gmod_net_message_string_references() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = true; ws.analysis.update_config(emmyrc.into()); + ws.def_gmod_call_arg_builtins(); check!(ws.check_references( r#" @@ -378,6 +587,42 @@ mod tests { Ok(()) } + #[gtest] + fn test_gmod_net_message_string_references_from_annotated_arg() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.gmod.enabled = true; + ws.analysis.update_config(emmyrc.into()); + + check!(ws.check_references( + r#" + ---@attribute call_arg(domain: string, role: string, priority: integer?) + + ---@[call_arg("gmod.net_message", "start")] + ---@param name string + local function startNet(name) end + + startNet("MyMessage") + "#, + vec![( + "receive.lua", + "\n\n\nnet.Receive(\"MyMessage\", function() end)\n" + )], + vec![ + VirtualLocation { + file: "virtual_0.lua".to_string(), + line: 7, + }, + VirtualLocation { + file: "receive.lua".to_string(), + line: 3, + }, + ], + )); + + Ok(()) + } + #[gtest] fn test_member_variable_references_include_usages() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); diff --git a/crates/glua_ls/src/handlers/test/semantic_token_test.rs b/crates/glua_ls/src/handlers/test/semantic_token_test.rs index 01cc1548c..f3c4b5578 100644 --- a/crates/glua_ls/src/handlers/test/semantic_token_test.rs +++ b/crates/glua_ls/src/handlers/test/semantic_token_test.rs @@ -983,6 +983,42 @@ local pnl = create() Ok(()) } + #[gtest] + fn test_alias_to_class_local_keeps_object_modifier() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let main = ws.def_file( + "main.lua", + r#"---@class Test2 + +---@alias TestAlias Test2 + +---@type TestAlias +local var +"#, + ); + + let data = ws.get_semantic_token_data_for_file(main)?; + let tokens = decode(&data); + + verify_that!( + has_token( + &tokens, + 5, + 6, + 3, + SemanticTokenType::VARIABLE, + &[ + SemanticTokenModifier::DECLARATION, + CustomSemanticTokenModifier::LOCAL, + CustomSemanticTokenModifier::OBJECT, + ], + ), + eq(true) + )?; + + Ok(()) + } + #[gtest] fn test_local_class_alias_keeps_class_and_local_signal() -> Result<()> { let mut ws = ProviderVirtualWorkspace::new(); diff --git a/crates/glua_ls/src/handlers/test/signature_helper_test.rs b/crates/glua_ls/src/handlers/test/signature_helper_test.rs index f5c663325..e1842fa34 100644 --- a/crates/glua_ls/src/handlers/test/signature_helper_test.rs +++ b/crates/glua_ls/src/handlers/test/signature_helper_test.rs @@ -143,17 +143,10 @@ mod tests { let mut emmyrc = ws.get_emmyrc(); emmyrc.gmod.enabled = true; ws.update_emmyrc(emmyrc); + ws.def_gmod_call_arg_builtins(); check!(ws.check_signature_helper( r#" - ---@class hook - hook = {} - - ---@param eventName string - ---@param identifier any - ---@param func function - function hook.Add(eventName, identifier, func) end - ---@class GM GM = {} diff --git a/crates/glua_ls/src/handlers/test_lib/mod.rs b/crates/glua_ls/src/handlers/test_lib/mod.rs index 1aa191c19..6b319dca4 100644 --- a/crates/glua_ls/src/handlers/test_lib/mod.rs +++ b/crates/glua_ls/src/handlers/test_lib/mod.rs @@ -1,4 +1,7 @@ -use glua_code_analysis::{EmmyLuaAnalysis, Emmyrc, FileId, RenderLevel, VirtualUrlGenerator}; +use glua_code_analysis::{ + EmmyLuaAnalysis, Emmyrc, FileId, GMOD_CALL_ARG_BUILTINS_FIXTURE, RenderLevel, + VirtualUrlGenerator, +}; use googletest::prelude::*; use itertools::Itertools; use lsp_types::{ @@ -125,7 +128,7 @@ impl ProviderVirtualWorkspace { let mut emmyrc = Emmyrc::default(); emmyrc.gmod.enabled = false; analysis.update_config(Arc::new(emmyrc)); - analysis.init_std_lib(None); + analysis.init_std_lib(); let base = &generator.base; analysis.add_main_workspace(base.clone()); ProviderVirtualWorkspace { @@ -149,6 +152,13 @@ impl ProviderVirtualWorkspace { .unwrap() } + pub fn def_gmod_call_arg_builtins(&mut self) -> FileId { + self.def_file( + "lua/includes/glua_ls_gmod_call_arg_builtins.lua", + GMOD_CALL_ARG_BUILTINS_FIXTURE, + ) + } + pub fn def_files(&mut self, files: Vec<(&str, &str)>) -> Vec { let mut removed_files = HashSet::new(); let mut updated_files = HashSet::new(); diff --git a/crates/glua_ls/src/handlers/workspace/did_rename_files.rs b/crates/glua_ls/src/handlers/workspace/did_rename_files.rs index d37d88bc9..38776d19e 100644 --- a/crates/glua_ls/src/handlers/workspace/did_rename_files.rs +++ b/crates/glua_ls/src/handlers/workspace/did_rename_files.rs @@ -76,9 +76,9 @@ pub async fn on_did_rename_files_handler( let show_message_params = ShowMessageRequestParams { typ: MessageType::INFO, - message: t!("Do you want to modify the require path?").to_string(), + message: "Do you want to modify the require path?".to_string(), actions: Some(vec![MessageActionItem { - title: t!("Modify").to_string(), + title: "Modify".to_string(), properties: HashMap::new(), }]), }; @@ -90,7 +90,7 @@ pub async fn on_did_rename_files_handler( .await { let cancel_token = CancellationToken::new(); - if selected_action.title == t!("Modify") { + if selected_action.title == "Modify" { client .apply_edit( ApplyWorkspaceEditParams { diff --git a/crates/glua_ls/src/lib.rs b/crates/glua_ls/src/lib.rs index 45ce34862..66944571d 100644 --- a/crates/glua_ls/src/lib.rs +++ b/crates/glua_ls/src/lib.rs @@ -10,7 +10,3 @@ mod util; pub use clap::Parser; pub use cmd_args::*; pub use server::{AsyncConnection, ExitError, run_ls}; - -#[macro_use] -extern crate rust_i18n; -rust_i18n::i18n!("./locales", fallback = "en"); diff --git a/crates/glua_ls/src/meta_text/mod.rs b/crates/glua_ls/src/meta_text/mod.rs index f083170f1..12da22823 100644 --- a/crates/glua_ls/src/meta_text/mod.rs +++ b/crates/glua_ls/src/meta_text/mod.rs @@ -1,7 +1,590 @@ +const KEYWORD_DOCS: &[(&str, &str)] = &[ + ( + "for", + r#"The `for` keyword is used to create a loop that can iterate over a range, collection, or iterator. + +### Example Usage + +```lua +-- Iterate over a range +for i = 1, 10 do + print(i) +end + +-- Iterate over a collection +local fruits = {"apple", "banana", "cherry"} +for index, fruit in ipairs(fruits) do + print(index, fruit) +end +```"#, + ), + ( + "if", + r#"The `if` keyword is used for conditional statements, executing different code blocks based on the truthiness of the condition. + +### Example Usage + +```lua +local x = 10 +if x > 5 then + print("x is greater than 5") +elseif x == 5 then + print("x is equal to 5") +else + print("x is less than 5") +end +```"#, + ), + ( + "while", + r#"The `while` keyword is used to create a loop that repeats as long as the condition is true. + +### Example Usage + +```lua +local i = 1 +while i <= 10 do + print(i) + i = i + 1 +end +```"#, + ), + ( + "function", + r#"The `function` keyword is used to define a function, which can contain a set of instructions and can be called. + +### Example Usage + +```lua +function greet(name) + print("Hello, " .. name) +end + +greet("world") +```"#, + ), + ( + "local", + r#"The `local` keyword is used to declare local variables or functions, which are limited to the scope of the block. + +### Example Usage + +```lua +local x = 10 +local function add(a, b) + return a + b +end + +print(add(x, 5)) +```"#, + ), + ( + "return", + r#"The `return` keyword is used to return values from a function and terminate the function's execution. + +### Example Usage + +```lua +function add(a, b) + return a + b +end + +local sum = add(5, 3) +print(sum) -- Output 8 +```"#, + ), + ( + "break", + r#"The `break` keyword is used to exit the current loop. + +### Example Usage + +```lua +local i = 1 +while i <= 10 do + if i == 5 then + break + end + print(i) + i = i + 1 +end +-- Output 1 to 4 +```"#, + ), + ( + "do", + r#"The `do` keyword is used to create a block, where the variables inside the block are local. + +### Example Usage + +```lua +local x = 10 +do + local x = 5 + print(x) -- Output 5 +end +print(x) -- Output 10 +```"#, + ), + ( + "end", + r#"The `end` keyword is used to end a block, function, or control structure. + +### Example Usage + +```lua +if true then + print("This is true") +end +```"#, + ), + ( + "repeat", + r#"The `repeat` keyword is used to create a loop that ends when the condition is true. + +### Example Usage + +```lua +local i = 1 +repeat + print(i) + i = i + 1 +until i > 5 +-- Output 1 to 5 +```"#, + ), + ( + "until", + r#"The `until` keyword is used in a `repeat` loop to indicate the end condition of the loop. + +### Example Usage + +```lua +local i = 1 +repeat + print(i) + i = i + 1 +until i > 5 +-- Output 1 to 5 +```"#, + ), + ( + "then", + r#"The `then` keyword is used in an `if` statement to indicate the code block to execute when the condition is true. + +### Example Usage + +```lua +local x = 10 +if x > 5 then + print("x is greater than 5") +end +```"#, + ), + ( + "elseif", + r#"The `elseif` keyword is used in an `if` statement to indicate another condition to check. + +### Example Usage + +```lua +local x = 10 +if x > 5 then + print("x is greater than 5") +elseif x == 5 then + print("x is equal to 5") +end +```"#, + ), + ( + "in", + r#"The `in` keyword is used in a generic `for` loop to indicate the collection or iterator to iterate over. + +### Example Usage + +```lua +local fruits = {"apple", "banana", "cherry"} +for index, fruit in ipairs(fruits) do + print(index, fruit) +end +```"#, + ), + ( + "goto", + r#"The `goto` keyword is used to jump to a label in the code. + +### Example Usage + +```lua +::label:: +print("Hello") +goto label +```"#, + ), +]; + +const TAG_DOCS: &[(&str, &str)] = &[ + ( + "class", + r#"The `class` tag is used to document a class or a struct. +Example: +```lua +---@class MyClass +local MyClass = {} +```"#, + ), + ( + "enum", + r#"The `enum` tag is used to document an enumeration. +Example: +```lua +---@enum MyEnum +local MyEnum = { + Value1 = 1, + Value2 = 2 +} +```"#, + ), + ( + "interface", + r#"The `interface` is deprecated, use `class` instead. +Example: +```lua +---@interface MyInterface +local MyInterface = {} +```"#, + ), + ( + "alias", + r#"The `alias` tag is used to document a type alias. +Example: +```lua +---@alias MyTypeAlias string|number +```"#, + ), + ( + "field", + r#"The `field` tag is used to document a field of a class or a struct. +Example: +```lua +---@class MyClass +---@field publicField string +MyClass = {} +```"#, + ), + ( + "type", + r#"The `type` tag is used to document a type. +Example: +```lua +---@type string +local myString = "Hello" +```"#, + ), + ( + "param", + r#"The `param` tag is used to document a function parameter. +Example: +```lua +---@param paramName string +function myFunction(paramName) +end +```"#, + ), + ( + "return", + r#"The `return` tag is used to document the return value of a function. +Example: +```lua +---@return string +function myFunction() + return "Hello" +end +```"#, + ), + ( + "generic", + r#"The `generic` tag is used to document generic types. +Example: +```lua +---@generic T +---@param param T +---@return T +function identity(param) + return param +end +```"#, + ), + ( + "see", + r#"The `see` tag is used to reference another documentation entry. +Example: +```lua +---@see otherFunction +function myFunction() +end +```"#, + ), + ( + "deprecated", + r#"The `deprecated` tag is used to mark a function or a field as deprecated. +Example: +```lua +---@deprecated +function oldFunction() +end +```"#, + ), + ( + "cast", + r#"The `cast` tag is used to document a type cast. +Example: +```lua +---@cast varName string +local varName = someValue +```"#, + ), + ( + "overload", + r#"The `overload` tag is used to document an overloaded function. +Example: +```lua +---@overload fun(param: string):void +function myFunction(param) +end +```"#, + ), + ( + "async", + r#"The `async` tag is used to document an asynchronous function. +Example: +```lua +---@async +function asyncFunction() +end +```"#, + ), + ( + "public", + r#"The `public` tag is used to mark a field or a function as public. +Example: +```lua +---@public +MyClass.publicField = "" +```"#, + ), + ( + "protected", + r#"The `protected` tag is used to mark a field or a function as protected. +Example: +```lua +---@protected +MyClass.protectedField = "" +```"#, + ), + ( + "private", + r#"The `private` tag is used to mark a field or a function as private. +Example: +```lua +---@private +local privateField = "" +```"#, + ), + ( + "package", + r#"The `package` tag is used to document a package. +Example: +```lua +---@package +local myPackage = {} +```"#, + ), + ( + "meta", + r#"The `meta` tag is used to document meta information. +Example: +```lua +---@meta +local metaInfo = {} +```"#, + ), + ( + "diagnostic", + r#"The `diagnostic` tag is used to document diagnostic information. +Example: +```lua +---@diagnostic disable-next-line: unused-global +local unusedVar = 1 +```"#, + ), + ( + "version", + r#"The `version` tag is used to document the version of a module or a function. +Example: +```lua +---@version 1.0 +function myFunction() +end +```"#, + ), + ( + "as", + r#"The `as` tag is used to document type assertions. +Example: +```lua +---@as string +local varName = someValue +```"#, + ), + ( + "nodiscard", + r#"The `nodiscard` tag is used to indicate that the return value should not be discarded. +Example: +```lua +---@nodiscard +function importantFunction() + return "Important" +end +```"#, + ), + ( + "operator", + r#"The `operator` tag is used to document operator overloads. +Example: +```lua +---@class +---@operator add(MyClass):MyClass +```"#, + ), + ( + "module", + r#"The `module` tag is used to document a module. +Example: +```lua +---@module MyModule +local MyModule = {} +```"#, + ), + ( + "namespace", + r#"The `namespace` tag is used to document a namespace. +Example: +```lua +---@namespace MyNamespace +```"#, + ), + ( + "using", + r#"The `using` tag is used to document using declarations. +Example: +```lua +---@using MyNamespace +```"#, + ), + ( + "source", + r#"The `source` tag is used to document the source of a function or a module. +Example: +```lua +---@source https://example.com/source +function myFunction() +end +```"#, + ), + ( + "readonly", + r#"The `readonly` tag is used to mark a field as read-only. +but it is not supported in current +Example: +```lua +---@readonly +MyClass.readonlyField = "constant" +```"#, + ), + ( + "export", + r#"The `export` tag is used to indicate that a variable is exported, supporting quick import. +It accepts `namespace` or `global` as parameters. If no parameter is provided, it defaults to `global`. +Example: +```lua +---@export namespace -- When set to `namespace`, only allows import within the same namespace +local export = {} + +export.func = function() + -- When typing `func` in other files, import suggestions will be shown +end + +return export +```"#, + ), + ( + "language", + r#"The `language` tag is used to specify language injection for code blocks. +Example: +```lua +---@language sql +local t = [[ + SELECT * FROM users WHERE id = 1; + SELECT name, email FROM users WHERE active = 1; + UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = 1; + DELETE FROM users WHERE id = 2; + INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'); +]] +```"#, + ), + ( + "attribute", + r#"`attribute` tag defines an attribute. Attribute is used to attach extra information to a definition. +Example: +```lua +---@attribute deprecated(message: string?) + +---@class A +---@[deprecated("delete")] # `b` field is marked as deprecated +---@field b string +---@[deprecated] # If `attribute` allows no parameters, the parentheses can be omitted +---@field c string +```"#, + ), + ( + "hook", + r#"The `hook` tag registers a function as a GMod hook. +Example: +```lua +---@hook PlayerSpawn +```"#, + ), + ( + "realm", + r#"The `realm` tag indicates which realm(s) this field/function is available in (client, server, or shared). +Example: +```lua +---@realm client +```"#, + ), + ( + "fileparam", + r#"The `fileparam` tag provides file-scoped default types for unannotated function parameters holding a specific name. +Example: +```lua +---@fileparam ply Player +```"#, + ), +]; + pub fn meta_keyword(key: &str) -> String { - t!(format!("keywords.{}", key)).to_string() + KEYWORD_DOCS + .iter() + .find(|(k, _)| *k == key) + .map(|(_, v)| v.to_string()) + .unwrap_or_default() } pub fn meta_doc_tag(key: &str) -> String { - t!(format!("tags.{}", key)).to_string() + TAG_DOCS + .iter() + .find(|(k, _)| *k == key) + .map(|(_, v)| v.to_string()) + .unwrap_or_default() } diff --git a/crates/glua_ls/std_i18n/bit/meta.yaml b/crates/glua_ls/std_i18n/bit/meta.yaml deleted file mode 100644 index b765eac46..000000000 --- a/crates/glua_ls/std_i18n/bit/meta.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: bit.lua -entries: [] diff --git a/crates/glua_ls/std_i18n/bit/zh_CN.yaml b/crates/glua_ls/std_i18n/bit/zh_CN.yaml deleted file mode 100644 index 8b1378917..000000000 --- a/crates/glua_ls/std_i18n/bit/zh_CN.yaml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/glua_ls/std_i18n/bit32/meta.yaml b/crates/glua_ls/std_i18n/bit32/meta.yaml deleted file mode 100644 index 15d5d982b..000000000 --- a/crates/glua_ls/std_i18n/bit32/meta.yaml +++ /dev/null @@ -1,174 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: bit32.lua -entries: -- key: bit32lib - kind: - type: doc_block - indent: '' - range: - start: - line: 6 - col: 0 - end: - line: 11 - col: 0 - hash: b18b7281aaaec493 - context_hash: de7cc417de1b3246 -- key: bit32lib.arshift - kind: - type: doc_block - indent: '' - range: - start: - line: 14 - col: 0 - end: - line: 22 - col: 0 - hash: ed2a88c4920f6d8c - context_hash: de7cc417de1b3246 -- key: bit32lib.band - kind: - type: doc_block - indent: '' - range: - start: - line: 28 - col: 0 - end: - line: 33 - col: 0 - hash: eda2ba432394c515 - context_hash: de7cc417de1b3246 -- key: bit32lib.bnot - kind: - type: doc_block - indent: '' - range: - start: - line: 37 - col: 0 - end: - line: 48 - col: 0 - hash: c7a8cb7786df2b1c - context_hash: de7cc417de1b3246 -- key: bit32lib.bor - kind: - type: doc_block - indent: '' - range: - start: - line: 53 - col: 0 - end: - line: 58 - col: 0 - hash: '1d8f054ce3d49bb3' - context_hash: de7cc417de1b3246 -- key: bit32lib.btest - kind: - type: doc_block - indent: '' - range: - start: - line: 62 - col: 0 - end: - line: 67 - col: 0 - hash: '2278c16008c2f0db' - context_hash: de7cc417de1b3246 -- key: bit32lib.bxor - kind: - type: doc_block - indent: '' - range: - start: - line: 71 - col: 0 - end: - line: 76 - col: 0 - hash: '67aa98324af03d6d' - context_hash: de7cc417de1b3246 -- key: bit32lib.extract - kind: - type: doc_block - indent: '' - range: - start: - line: 80 - col: 0 - end: - line: 85 - col: 0 - hash: '124c816100c12d49' - context_hash: de7cc417de1b3246 -- key: bit32lib.replace - kind: - type: doc_block - indent: '' - range: - start: - line: 92 - col: 0 - end: - line: 97 - col: 0 - hash: '08d8a168ba912d23' - context_hash: de7cc417de1b3246 -- key: bit32lib.lrotate - kind: - type: doc_block - indent: '' - range: - start: - line: 104 - col: 0 - end: - line: 109 - col: 0 - hash: '72c73581e540b235' - context_hash: de7cc417de1b3246 -- key: bit32lib.lshift - kind: - type: doc_block - indent: '' - range: - start: - line: 115 - col: 0 - end: - line: 126 - col: 0 - hash: '8d2fb4426479558e' - context_hash: de7cc417de1b3246 -- key: bit32lib.rrotate - kind: - type: doc_block - indent: '' - range: - start: - line: 132 - col: 0 - end: - line: 137 - col: 0 - hash: aacf9ee540de85b3 - context_hash: de7cc417de1b3246 -- key: bit32lib.rshift - kind: - type: doc_block - indent: '' - range: - start: - line: 143 - col: 0 - end: - line: 154 - col: 0 - hash: abecff6d17e2eaf9 - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/bit32/zh_CN.yaml b/crates/glua_ls/std_i18n/bit32/zh_CN.yaml deleted file mode 100644 index 0540024c9..000000000 --- a/crates/glua_ls/std_i18n/bit32/zh_CN.yaml +++ /dev/null @@ -1,80 +0,0 @@ -bit32lib: | - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32"]) - -bit32lib.arshift: | - 返回 `x` 向右算术位移 `disp` 位的结果。`disp` 为负时向左位移。 - - 这是算术位移操作:左侧空位用 `x` 的最高位填充,右侧空位用 `0` 填充。 - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.arshift"]) - -bit32lib.band: | - 返回其操作数的按位与(AND)结果。 - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.band"]) - -bit32lib.bnot: | - 返回 `x` 的按位取反结果。 - - ```lua - assert(bit32.bnot(x) == - (-1 - x) % 2^32) - ``` - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.bnot"]) - -bit32lib.bor: | - 返回其操作数的按位或(OR)结果。 - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.bor"]) - -bit32lib.btest: | - 返回一个布尔值,表示其操作数的按位与(AND)结果是否不为零。 - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.btest"]) - -bit32lib.bxor: | - 返回其操作数的按位异或(XOR)结果。 - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.bxor"]) - -bit32lib.extract: | - 返回由 `n` 的第 `field` 位到第 `field + width - 1` 位构成的无符号数。 - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.extract"]) - -bit32lib.replace: | - 返回 `n` 的一个副本,并将第 `field` 位到第 `field + width - 1` 位替换为 `v` 的值。 - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.replace"]) - -bit32lib.lrotate: | - 返回 `x` 向左旋转 `disp` 位的结果。`disp` 为负时向右旋转。 - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.lrotate"]) - -bit32lib.lshift: | - 返回 `x` 向左位移 `disp` 位的结果。`disp` 为负时向右位移。无论方向如何,空位都用 `0` 填充。 - - ```lua - assert(bit32.lshift(b, disp) == - (b * 2^disp) % 2^32) - ``` - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.lshift"]) - -bit32lib.rrotate: | - 返回 `x` 向右旋转 `disp` 位的结果。`disp` 为负时向左旋转。 - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.rrotate"]) - -bit32lib.rshift: | - 返回 `x` 向右位移 `disp` 位的结果。`disp` 为负时向左位移。无论方向如何,空位都用 `0` 填充。 - - ```lua - assert(bit32.rshift(b, disp) == - math.floor(b % 2^32 / 2^disp)) - ``` - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-bit32.rshift"]) - diff --git a/crates/glua_ls/std_i18n/builtin/meta.yaml b/crates/glua_ls/std_i18n/builtin/meta.yaml deleted file mode 100644 index af1a77364..000000000 --- a/crates/glua_ls/std_i18n/builtin/meta.yaml +++ /dev/null @@ -1,174 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: builtin.lua -entries: -- key: 'nil' - kind: - type: doc_block - indent: '' - range: - start: - line: 18 - col: 0 - end: - line: 22 - col: 0 - hash: e0061e5c045df64a - context_hash: de7cc417de1b3246 -- key: boolean - kind: - type: doc_block - indent: '' - range: - start: - line: 24 - col: 0 - end: - line: 27 - col: 0 - hash: '31cc8159cccf97f7' - context_hash: de7cc417de1b3246 -- key: number - kind: - type: doc_block - indent: '' - range: - start: - line: 29 - col: 0 - end: - line: 41 - col: 0 - hash: fa4ea32a81d5aed9 - context_hash: de7cc417de1b3246 -- key: userdata - kind: - type: doc_block - indent: '' - range: - start: - line: 45 - col: 0 - end: - line: 55 - col: 0 - hash: e3cc299bfe56ad03 - context_hash: de7cc417de1b3246 -- key: thread - kind: - type: doc_block - indent: '' - range: - start: - line: 59 - col: 0 - end: - line: 64 - col: 0 - hash: a0216db4b0f8ba68 - context_hash: de7cc417de1b3246 -- key: table - kind: - type: doc_block - indent: '' - range: - start: - line: 66 - col: 0 - end: - line: 96 - col: 0 - hash: '8c15b89f8cf799f8' - context_hash: de7cc417de1b3246 -- key: std.Select - kind: - type: doc_block - indent: '' - range: - start: - line: 118 - col: 0 - end: - line: 120 - col: 0 - hash: b508ab78f8d1bc46 - context_hash: de7cc417de1b3246 -- key: std.Unpack - kind: - type: doc_block - indent: '' - range: - start: - line: 122 - col: 0 - end: - line: 124 - col: 0 - hash: c7ad7bb74235dc8c - context_hash: de7cc417de1b3246 -- key: std.RawGet - kind: - type: doc_block - indent: '' - range: - start: - line: 126 - col: 0 - end: - line: 128 - col: 0 - hash: '7327558f3445dea6' - context_hash: de7cc417de1b3246 -- key: std.ConstTpl - kind: - type: doc_block - indent: '' - range: - start: - line: 130 - col: 0 - end: - line: 132 - col: 0 - hash: '0e263d1e0caa6b3b' - context_hash: de7cc417de1b3246 -- key: Parameters - kind: - type: doc_block - indent: '' - range: - start: - line: 146 - col: 0 - end: - line: 148 - col: 0 - hash: '71b070c2fa27cbf9' - context_hash: de7cc417de1b3246 -- key: ConstructorParameters - kind: - type: doc_block - indent: '' - range: - start: - line: 150 - col: 0 - end: - line: 152 - col: 0 - hash: '2f4bebd14d7824cb' - context_hash: de7cc417de1b3246 -- key: Partial - kind: - type: doc_block - indent: '' - range: - start: - line: 157 - col: 0 - end: - line: 159 - col: 0 - hash: '5e4213a7c3937182' - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/builtin/zh_CN.yaml b/crates/glua_ls/std_i18n/builtin/zh_CN.yaml deleted file mode 100644 index 15c6015ad..000000000 --- a/crates/glua_ls/std_i18n/builtin/zh_CN.yaml +++ /dev/null @@ -1,71 +0,0 @@ -nil: | - *nil* 类型只有一个值 **nil**,其主要特性是与任何其他值都不同; - 它通常表示缺少有用的值。 - -boolean: | - *boolean* 类型有两个值:**false** 和 **true**。**nil** 和 **false** - 都会使条件为假;任何其他值都会使条件为真。 - -number: | - **number** 类型在内部使用两种表示形式,或称为两种子类型,一种叫做 - *integer*,另一种叫做 *float*。Lua 对于何时使用哪种表示形式有明确的规则, - 但也会在需要时自动进行转换。因此,程序员可以选择忽略整数和浮点数之间的差异, - 也可以完全控制每个数字的表示形式。标准 Lua 使用 64 位整数和双精度(64 位) - 浮点数,但你也可以将 Lua 编译为使用 32 位整数和/或单精度(32 位)浮点数。 - 对于小型机器和嵌入式系统来说,同时使用 32 位整数和 32 位浮点数的选项特别有吸引力。 - (参见 `luaconf.h` 文件中的宏 `LUA_32BITS`。) - -userdata: | - *userdata* 类型用于允许将任意 C 数据存储在 Lua 变量中。userdata 值 - 表示一块原始内存。有两种 userdata:*full userdata*,是一个由 Lua - 管理内存块的对象;*light userdata*,它仅是一个 C 指针值。userdata 在 - Lua 中没有预定义的操作,除了赋值和相等性测试。通过使用 *metatables*,程序员 - 可以为 full userdata 值定义操作。userdata 值不能在 Lua 中创建或修改, - 只能通过 C API 进行。这保证了宿主程序所拥有数据的完整性。 - -thread: | - *thread* 类型表示独立的执行线程,用于实现协程。Lua 线程与操作系统 - 线程无关。Lua 在所有系统上都支持协程,即使是那些原生不支持线程的系统。 - -table: | - *table* 类型实现关联数组,即索引不仅可以是数字,还可以是除 **nil** - 和 NaN 之外的任何 Lua 值的数组。(*NaN* 是 IEEE 754 标准用于表示 - 未定义或不可表示的数值结果的特殊浮点值,例如 `0/0`。)表可以是异构的; - 也就是说,它们可以包含所有类型的值(除了 **nil**)。值为 **nil** 的 - 任何键都不被视为表的一部分。相反,任何不属于表的键其关联值都为 **nil**。 - - 表是 Lua 中唯一的数据结构;它们可以用来表示普通数组、列表、符号表、 - 集合、记录、图、树等。为了表示记录,Lua 使用字段名作为索引。语言通过 - 提供 `a.name` 作为 `a["name"]` 的语法糖来支持这种表示。 - - 与索引一样,表字段的值可以是任何类型。特别地,由于函数是一等公民,表 - 字段可以包含函数。因此表也可以携带 *methods*。 - - 表的索引遵循语言中原始相等的定义。表达式 `a[i]` 和 `a[j]` 当且仅当 - `i` 和 `j` 原始相等(即不使用元方法的相等)时表示相同的表元素。特别地, - 具有整数值的浮点数等于其对应的整数。为避免歧义,任何用作键的具有整数值 - 的浮点数都会转换为其对应的整数。例如,如果你写 `a[2.0] = true`, - 插入表中的实际键将是整数 `2`。(另一方面,2 和 "`2`" 是不同的 Lua 值, - 因此表示不同的表条目。) - -std.Select: | - Select 函数的内置类型 - -std.Unpack: | - Unpack 函数的内置类型 - -std.RawGet: | - Rawget 的内置类型 - -std.ConstTpl: | - 泛型模板的内置类型,用于匹配整数常量和 true/false - -Parameters: | - 以元组形式获取函数的参数 - -ConstructorParameters: | - 以元组形式获取构造函数的参数 - -Partial: | - 使 T 中的所有属性变为可选 - diff --git a/crates/glua_ls/std_i18n/coroutine/meta.yaml b/crates/glua_ls/std_i18n/coroutine/meta.yaml deleted file mode 100644 index a9c8be88b..000000000 --- a/crates/glua_ls/std_i18n/coroutine/meta.yaml +++ /dev/null @@ -1,161 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: coroutine.lua -entries: -- key: coroutinelib.create - kind: - type: doc_block - indent: '' - range: - start: - line: 18 - col: 0 - end: - line: 21 - col: 0 - hash: f783b853bcc6436b - context_hash: de7cc417de1b3246 -- key: coroutinelib.isyieldable - kind: - type: doc_block - indent: '' - range: - start: - line: 26 - col: 0 - end: - line: 31 - col: 0 - hash: '20f67ae45ee960e5' - context_hash: de7cc417de1b3246 -- key: coroutinelib.close - kind: - type: doc_block - indent: '' - range: - start: - line: 37 - col: 0 - end: - line: 40 - col: 0 - hash: ed170c5fa338cd96 - context_hash: de7cc417de1b3246 -- key: coroutinelib.resume - kind: - type: doc_block - indent: '' - range: - start: - line: 45 - col: 0 - end: - line: 56 - col: 0 - hash: c68b5b72b7f9a791 - context_hash: de7cc417de1b3246 -- key: coroutinelib.running - kind: - type: doc_block - indent: '' - range: - start: - line: 63 - col: 0 - end: - line: 66 - col: 0 - hash: '3b9508453c78ef79' - context_hash: de7cc417de1b3246 -- key: coroutinelib.status - kind: - type: doc_block - indent: '' - range: - start: - line: 70 - col: 0 - end: - line: 77 - col: 0 - hash: '2c49c377233084fa' - context_hash: de7cc417de1b3246 -- key: coroutinelib.status.return.1.running - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 79 - col: 18 - end: - line: 79 - col: 30 - hash: c82a37900965429a - context_hash: '6e48d348e70f84c9' -- key: coroutinelib.status.return.1.suspended - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 80 - col: 18 - end: - line: 80 - col: 47 - hash: fa936af8644ed8c3 - context_hash: '71a22165c82844bc' -- key: coroutinelib.status.return.1.normal - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 81 - col: 18 - end: - line: 81 - col: 45 - hash: a13ab0138fac6604 - context_hash: e8d5ba7a0e9a3e13 -- key: coroutinelib.status.return.1.dead - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 82 - col: 18 - end: - line: 82 - col: 57 - hash: '72fe2d58f456cb94' - context_hash: '768b9a36fcce3a9c' -- key: coroutinelib.wrap - kind: - type: doc_block - indent: '' - range: - start: - line: 86 - col: 0 - end: - line: 92 - col: 0 - hash: '980a35b924ee089e' - context_hash: de7cc417de1b3246 -- key: coroutinelib.yield - kind: - type: doc_block - indent: '' - range: - start: - line: 97 - col: 0 - end: - line: 100 - col: 0 - hash: '25ba6e8d8cf7cc0d' - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/coroutine/zh_CN.yaml b/crates/glua_ls/std_i18n/coroutine/zh_CN.yaml deleted file mode 100644 index a02293fe5..000000000 --- a/crates/glua_ls/std_i18n/coroutine/zh_CN.yaml +++ /dev/null @@ -1,54 +0,0 @@ -coroutinelib.create: | - 创建一个新的协程,主体函数为 `f`。`f` 必须是一个 Lua 函数。 - 返回这个新协程,一个类型为 `"thread"` 的对象。 - -coroutinelib.isyieldable: | - 当正在运行的协程可以让出时返回 true。 - - 如果正在运行的协程不是主线程且不在不可让出的 C 函数中, - 则该协程是可让出的。 - -coroutinelib.close: | - 关闭协程 `co`,关闭其所有待关闭的变量,并将协程置于死亡状态。 - -coroutinelib.resume: | - 开始或继续执行协程 `co`。第一次恢复协程时,它开始运行其主体函数。 - 值 `val1`, ... 作为参数传递给主体函数。如果协程已让出,`resume` - 会重新启动它;值 `val1`, ... 作为 yield 的返回结果传递。 - - 如果协程运行时没有任何错误,`resume` 返回 **true** 加上传递给 - `yield` 的任何值(当协程让出时)或主体函数返回的任何值(当协程 - 终止时)。如果有任何错误,`resume` 返回 **false** 加上错误消息。 - -coroutinelib.running: | - 返回正在运行的协程加上一个布尔值,当正在运行的协程是主协程时为 true。 - -coroutinelib.status: | - 以字符串形式返回协程 `co` 的状态:"`running`",如果协程正在运行 - (即它调用了 `status`);"`suspended`",如果协程在调用 `yield` - 时挂起,或者尚未开始运行;"`normal`",如果协程处于活动状态但未 - 运行(即它已恢复另一个协程);"`dead`",如果协程已完成其主体函数, - 或者因错误而停止。 - -coroutinelib.status.return.1.running: | - 正在运行 - -coroutinelib.status.return.1.suspended: | - 已暂停或未开始 - -coroutinelib.status.return.1.normal: | - 活动但未运行 - -coroutinelib.status.return.1.dead: | - 完成或错误停止 - -coroutinelib.wrap: | - 创建一个新的协程,主体函数为 `f`。`f` 必须是一个 Lua 函数。 - 返回一个函数,每次调用该函数时都会恢复协程。传递给该函数的任何 - 参数都作为 `resume` 的额外参数。返回与 `resume` 相同的值, - 但不包括第一个布尔值。如果发生错误,则传播该错误。 - -coroutinelib.yield: | - 挂起调用协程的执行。传递给 `yield` 的任何参数都作为额外结果 - 传递给 `resume`。 - diff --git a/crates/glua_ls/std_i18n/debug/meta.yaml b/crates/glua_ls/std_i18n/debug/meta.yaml deleted file mode 100644 index ef32208e7..000000000 --- a/crates/glua_ls/std_i18n/debug/meta.yaml +++ /dev/null @@ -1,447 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: debug.lua -entries: -- key: debuglib.debug - kind: - type: doc_block - indent: '' - range: - start: - line: 18 - col: 0 - end: - line: 26 - col: 62 - hash: '5728b52db9e5f2ff' - context_hash: de7cc417de1b3246 -- key: debuglib.getfenv - kind: - type: doc_block - indent: '' - range: - start: - line: 30 - col: 0 - end: - line: 33 - col: 0 - hash: '91e912d2bf6683e5' - context_hash: de7cc417de1b3246 -- key: debuglib.gethook - kind: - type: doc_block - indent: '' - range: - start: - line: 38 - col: 0 - end: - line: 42 - col: 0 - hash: df523859e51a83d8 - context_hash: de7cc417de1b3246 -- key: debuglib.InfoWhat.item.n - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 66 - col: 14 - end: - line: 66 - col: 33 - hash: f607d8295ab10b09 - context_hash: c7d6e0f1e9125070 -- key: debuglib.InfoWhat.item.S - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 67 - col: 14 - end: - line: 67 - col: 76 - hash: '4d8757c3d65e5316' - context_hash: '85c11923def6a850' -- key: debuglib.InfoWhat.item.l - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 68 - col: 14 - end: - line: 68 - col: 28 - hash: '3b4abcf26c63da90' - context_hash: '15b102f5e1274acd' -- key: debuglib.InfoWhat.item.t - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 69 - col: 14 - end: - line: 69 - col: 27 - hash: '0d74d35018965521' - context_hash: '105f4cd2d7e8614a' -- key: debuglib.InfoWhat.item.u - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 70 - col: 14 - end: - line: 70 - col: 44 - hash: '234b4c0d79270024' - context_hash: ccbf703eca4a4f14 -- key: debuglib.InfoWhat.item.f - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 71 - col: 14 - end: - line: 71 - col: 21 - hash: '075b011e6d60a10d' - context_hash: '88bf6438b8c0adf8' -- key: debuglib.InfoWhat.item.r - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 72 - col: 14 - end: - line: 72 - col: 39 - hash: '661489bb7eb64c19' - context_hash: '5a95e7316013239c' -- key: debuglib.InfoWhat.item.L - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 73 - col: 14 - end: - line: 73 - col: 28 - hash: a1993e3f27753996 - context_hash: '82805f0c17b3057b' -- key: debuglib.getinfo - kind: - type: doc_block - indent: '' - range: - start: - line: 76 - col: 0 - end: - line: 96 - col: 0 - hash: a340fa432951e50a - context_hash: de7cc417de1b3246 -- key: debuglib.getlocal@>5.2,JIT - kind: - type: doc_block - indent: '' - range: - start: - line: 105 - col: 0 - end: - line: 125 - col: 0 - hash: defd75cacb5f645b - context_hash: de7cc417de1b3246 -- key: debuglib.getlocal@5.1 - kind: - type: doc_block - indent: '' - range: - start: - line: 137 - col: 0 - end: - line: 147 - col: 0 - hash: '07f5462f1e8865d3' - context_hash: de7cc417de1b3246 -- key: debuglib.getmetatable - kind: - type: doc_block - indent: '' - range: - start: - line: 156 - col: 0 - end: - line: 159 - col: 0 - hash: '410e3c69b1af1d28' - context_hash: de7cc417de1b3246 -- key: debuglib.getregistry - kind: - type: doc_block - indent: '' - range: - start: - line: 164 - col: 0 - end: - line: 166 - col: 0 - hash: '8976bf1110d1f1ea' - context_hash: de7cc417de1b3246 -- key: debuglib.getupvalue - kind: - type: doc_block - indent: '' - range: - start: - line: 170 - col: 0 - end: - line: 177 - col: 0 - hash: '982c779504f348a1' - context_hash: de7cc417de1b3246 -- key: debuglib.getuservalue - kind: - type: doc_block - indent: '' - range: - start: - line: 184 - col: 0 - end: - line: 187 - col: 0 - hash: '139b3595baa85765' - context_hash: de7cc417de1b3246 -- key: debuglib.setcstacklimit - kind: - type: doc_block - indent: '' - range: - start: - line: 193 - col: 0 - end: - line: 201 - col: 0 - hash: '026fa4d8ce9ac37a' - context_hash: de7cc417de1b3246 -- key: debuglib.setfenv - kind: - type: doc_block - indent: '' - range: - start: - line: 206 - col: 0 - end: - line: 209 - col: 0 - hash: '0cd7bfb647fe6ac9' - context_hash: de7cc417de1b3246 -- key: debuglib.Hookmask.item.c - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 217 - col: 10 - end: - line: 217 - col: 48 - hash: '2a465f3164fdfca7' - context_hash: '4f1a28807fd9c99d' -- key: debuglib.Hookmask.item.r - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 218 - col: 10 - end: - line: 218 - col: 55 - hash: d2998a5af84ab995 - context_hash: '8f1344163909f994' -- key: debuglib.Hookmask.item.l - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 219 - col: 10 - end: - line: 219 - col: 57 - hash: cc3f0aa2749ba739 - context_hash: '4a1e231682598e4a' -- key: debuglib.sethook - kind: - type: doc_block - indent: '' - range: - start: - line: 221 - col: 0 - end: - line: 242 - col: 0 - hash: baa9120640597b69 - context_hash: de7cc417de1b3246 -- key: debuglib.setlocal - kind: - type: doc_block - indent: '' - range: - start: - line: 249 - col: 0 - end: - line: 256 - col: 0 - hash: '6b7612b243606250' - context_hash: de7cc417de1b3246 -- key: debuglib.setmetatable - kind: - type: doc_block - indent: '' - range: - start: - line: 264 - col: 0 - end: - line: 267 - col: 0 - hash: '441c3f569ab9675b' - context_hash: de7cc417de1b3246 -- key: debuglib.setupvalue - kind: - type: doc_block - indent: '' - range: - start: - line: 274 - col: 0 - end: - line: 278 - col: 0 - hash: ad4178b7dc4b78b6 - context_hash: de7cc417de1b3246 -- key: debuglib.setuservalue - kind: - type: doc_block - indent: '' - range: - start: - line: 284 - col: 0 - end: - line: 288 - col: 0 - hash: '23156e0556f56b1a' - context_hash: db5e2c86b41bac5c -- key: debuglib.traceback - kind: - type: doc_block - indent: '' - range: - start: - line: 294 - col: 0 - end: - line: 302 - col: 0 - hash: '7295f50d331bbd1f' - context_hash: '75684757332beac5' -- key: debuglib.traceback.param.thread - kind: - type: line_tail - prefix: '' - range: - start: - line: 304 - col: 34 - end: - line: 304 - col: 108 - hash: d8583bff49e75935 - context_hash: b4d473f240caabc8 -- key: debuglib.traceback.param.message - kind: - type: line_tail - prefix: '' - range: - start: - line: 305 - col: 26 - end: - line: 305 - col: 119 - hash: e64a956566b2b220 - context_hash: a234ab6651e02e11 -- key: debuglib.traceback.param.level - kind: - type: line_tail - prefix: '' - range: - start: - line: 306 - col: 26 - end: - line: 306 - col: 90 - hash: '533f667ee00e6b33' - context_hash: '76d3857147ea882a' -- key: debuglib.upvalueid - kind: - type: doc_block - indent: '' - range: - start: - line: 310 - col: 0 - end: - line: 317 - col: 0 - hash: b28a436780522b2e - context_hash: a9890306a2dd729d -- key: debuglib.upvaluejoin - kind: - type: doc_block - indent: '' - range: - start: - line: 324 - col: 0 - end: - line: 327 - col: 0 - hash: '8bcd6fd7914cb62e' - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/debug/zh_CN.yaml b/crates/glua_ls/std_i18n/debug/zh_CN.yaml deleted file mode 100644 index 3e6e6b16d..000000000 --- a/crates/glua_ls/std_i18n/debug/zh_CN.yaml +++ /dev/null @@ -1,176 +0,0 @@ -debuglib.debug: | - 进入与用户的交互模式,运行用户输入的每个字符串。使用简单的命令和 - 其他调试工具,用户可以检查全局和局部变量、更改它们的值、计算表达式 - 等等。仅包含单词 `cont` 的行会结束此函数,使调用者继续执行。 - - 注意,`debug.debug` 的命令在词法上不嵌套在任何函数内,因此无法 - 直接访问局部变量。 - -debuglib.getfenv: | - 返回对象 `o` 的环境。 - -debuglib.gethook: | - 以三个值返回线程的当前钩子设置:当前钩子函数、当前钩子掩码和当前 - 钩子计数(由 `debug.sethook` 函数设置)。 - -debuglib.InfoWhat.item.n: | - `name`、`namewhat` - -debuglib.InfoWhat.item.S: | - `source`、`short_src`、`linedefined`、`lastlinedefined`、`what` - -debuglib.InfoWhat.item.l: | - `currentline` - -debuglib.InfoWhat.item.t: | - `istailcall` - -debuglib.InfoWhat.item.u: | - `nups`、`nparams`、`isvararg` - -debuglib.InfoWhat.item.f: | - `func` - -debuglib.InfoWhat.item.r: | - `ftransfer`、`ntransfer` - -debuglib.InfoWhat.item.L: | - `activelines` - -debuglib.getinfo: | - 返回一个包含函数信息的表。你可以直接给出函数,或者给 `f` 一个数字值, - 表示在给定线程的调用栈中第 `f` 层运行的函数:第0层是当前函数 - (`getinfo` 本身);第1层是调用 `getinfo` 的函数(尾调用除外, - 它们不计入栈);依此类推。如果 `f` 是一个大于活动函数数量的数字, - 则 `getinfo` 返回 **nil**。 - - 返回的表可以包含 `lua_getinfo` 返回的所有字段,字符串 `what` 描述 - 要填充哪些字段。`what` 的默认值是获取所有可用信息,但不包括有效行的表。 - 如果存在选项 '`f`',则添加一个名为 `func` 的字段,包含函数本身。 - 如果存在选项 '`L`',则添加一个名为 `activelines` 的字段,包含有效行的表。 - - 例如,表达式 `debug.getinfo(1,"n").name` 返回一个包含当前函数名称的表 - (如果能找到合理的名称),表达式 `debug.getinfo(print)` 返回一个包含 - 关于 `print` 函数所有可用信息的表。 - -debuglib.getlocal@>5.2,JIT: | - 此函数返回栈中第 `level` 层函数的索引为 `local` 的局部变量的名称和值。 - 此函数不仅访问显式的局部变量,还包括参数、临时变量等。 - - 第一个参数或局部变量的索引为1,依此类推,按照它们在代码中声明的顺序, - 仅计算函数当前作用域中活动的变量。负索引指向可变参数;-1 是第一个 - 可变参数。如果给定索引没有变量,函数返回 **nil**,当调用时层级超出 - 范围则引发错误。(你可以调用 `debug.getinfo` 来检查层级是否有效。) - - 以 '(' (左括号)开头的变量名表示没有已知名称的变量(内部变量,如循环 - 控制变量和保存时没有调试信息的代码块中的变量)。 - - 参数 `f` 也可以是一个函数。在这种情况下,`getlocal` 仅返回函数参数的名称。 - -debuglib.getlocal@5.1: | - 此函数返回栈中第 `level` 层函数的索引为 `index` 的局部变量的名称和值。 - 第一个参数或局部变量的索引为 1,依此类推,直到最后一个活跃的局部变量。 - 如果不存在给定索引的局部变量,函数将返回 **nil**;如果调用的 `level` 超出范围,则会引发错误。 - (你可以调用 `debug.getinfo` 来检查 level 是否有效。) - - 以左括号 '(' 开头的变量名表示内部变量(循环控制变量、临时变量以及 C 函数的局部变量)。 - -debuglib.getmetatable: | - 返回给定 `value` 的元表,如果它没有元表则返回 **nil**。 - -debuglib.getregistry: | - 返回注册表。 - -debuglib.getupvalue: | - 此函数返回函数 `f` 的索引为 `up` 的上值的名称和值。如果给定索引没有 - 上值,函数返回 **nil**。 - - 以 '(' (左括号)开头的变量名表示没有已知名称的变量(来自保存时没有 - 调试信息的代码块的变量)。 - -debuglib.getuservalue: | - 返回与 userdata `u` 关联的第 `n` 个用户值以及一个布尔值, - 如果 userdata 没有该值则为 **false**。 - -debuglib.setcstacklimit: | - ### **在 `Lua 5.4.2` 中已弃用** - - 为 C 栈设置新的限制。此限制控制 Lua 中嵌套调用的深度,目的是避免栈溢出。 - - 成功时,此函数返回旧的限制。出错时,返回 `false`。 - -debuglib.setfenv: | - 将给定 `object` 的环境设置为给定的 `table`。 - -debuglib.Hookmask.item.c: | - 当 Lua 调用函数时调用钩子。 - -debuglib.Hookmask.item.r: | - 当 Lua 从函数返回时调用钩子。 - -debuglib.Hookmask.item.l: | - 当 Lua 进入新的代码行时调用钩子。 - -debuglib.sethook: | - 将给定函数设置为钩子。字符串 `mask` 和数字 `count` 描述何时调用钩子。 - 字符串掩码可以是以下字符的任意组合,具有以下含义: - - * `"c"`:每次 Lua 调用函数时调用钩子; - * `"r"`:每次 Lua 从函数返回时调用钩子; - * `"l"`:每次 Lua 进入新的代码行时调用钩子。 - - 此外,当 `count` 不为零时,钩子在每 `count` 条指令后被调用。 - - 不带参数调用时,`debug.sethook` 关闭钩子。 - - 调用钩子时,其第一个参数是描述触发调用的事件的字符串:`"call"` - (或 `"tail call"`)、`"return"`、`"line"` 和 `"count"`。对于行事件, - 钩子还会获得新行号作为其第二个参数。在钩子内部,你可以用层级2调用 - `getinfo` 来获取关于正在运行的函数的更多信息(层级0是 `getinfo` 函数, - 层级1是钩子函数)。 - -debuglib.setlocal: | - 此函数将值 `value` 赋给栈中第 `level` 层函数的索引为 `local` 的局部变量。 - 如果给定索引没有局部变量,函数返回 **nil**,当调用时 `level` 超出范围 - 则引发错误。(你可以调用 `getinfo` 来检查层级是否有效。)否则,返回 - 局部变量的名称。 - -debuglib.setmetatable: | - 将给定 `object` 的元表设置为给定的 `table`(可以是 **nil**)。返回值。 - -debuglib.setupvalue: | - 此函数将值 `value` 赋给函数 `f` 的索引为 `up` 的上值。如果给定索引 - 没有上值,函数返回 **nil**。否则,返回上值的名称。 - -debuglib.setuservalue: | - 将给定的 `value` 设置为与给定 `udata` 关联的第 `n` 个值。 - `udata` 必须是完全 userdata。 - - 返回 `udata`,如果 userdata 没有该值则返回 **nil**。 - -debuglib.traceback: | - 生成调用栈的回溯信息。 - 不带参数调用时,返回当前线程的回溯。 - 当第一个参数是线程时,为该线程生成回溯;可选的第二个参数(如果是字符串) - 会添加到回溯前面,可选的第三个参数设置回溯开始的层级(默认为1)。 - 当第一个参数不是线程且不是 nil 时,它被视为可选消息。在这种情况下, - 为当前线程生成回溯,如果提供了第二个参数,则指定起始层级。 - -debuglib.traceback.param.thread: | - 可选的线程或 nil。如果不是线程,则被解释为消息。 - -debuglib.traceback.param.message: | - 添加到回溯前面的可选消息。如果不是字符串(或 nil),则原样返回。 - -debuglib.traceback.param.level: | - 开始回溯的可选层级(默认为1)。 - -debuglib.upvalueid: | - 返回给定函数中编号为 `n` 的上值的唯一标识符(作为轻量 userdata)。 - - 这些唯一标识符允许程序检查不同的闭包是否共享上值。共享上值的 Lua 闭包 - (即访问同一外部局部变量的闭包)将为那些上值索引返回相同的 id。 - -debuglib.upvaluejoin: | - 使 Lua 闭包 f1 的第 `n1` 个上值引用 Lua 闭包 f2 的第 `n2` 个上值。 - diff --git a/crates/glua_ls/std_i18n/ffi/meta.yaml b/crates/glua_ls/std_i18n/ffi/meta.yaml deleted file mode 100644 index a7c2ec19b..000000000 --- a/crates/glua_ls/std_i18n/ffi/meta.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: ffi.lua -entries: [] diff --git a/crates/glua_ls/std_i18n/ffi/zh_CN.yaml b/crates/glua_ls/std_i18n/ffi/zh_CN.yaml deleted file mode 100644 index 8b1378917..000000000 --- a/crates/glua_ls/std_i18n/ffi/zh_CN.yaml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/glua_ls/std_i18n/global/meta.yaml b/crates/glua_ls/std_i18n/global/meta.yaml deleted file mode 100644 index 5764f537a..000000000 --- a/crates/glua_ls/std_i18n/global/meta.yaml +++ /dev/null @@ -1,694 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: global.lua -entries: -- key: assert - kind: - type: doc_block - indent: '' - range: - start: - line: 15 - col: 0 - end: - line: 20 - col: 0 - hash: f03a861ce68b284a - context_hash: de7cc417de1b3246 -- key: std.collectgarbage_opt.item.collect - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 27 - col: 16 - end: - line: 27 - col: 86 - hash: '229e8488ba07e8ea' - context_hash: '2a8e36ec8d36c53c' -- key: std.collectgarbage_opt.item.stop - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 28 - col: 13 - end: - line: 28 - col: 146 - hash: '96adf3b32ca6e135' - context_hash: e123261b824452c7 -- key: std.collectgarbage_opt.item.restart - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 29 - col: 16 - end: - line: 29 - col: 71 - hash: b48fe98ac44cdf12 - context_hash: baca979775fae0df -- key: std.collectgarbage_opt.item.count - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 30 - col: 14 - end: - line: 30 - col: 197 - hash: '177a9a4fd31e4486' - context_hash: d8e5af7a88d3b8ff -- key: std.collectgarbage_opt.item.step - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 31 - col: 13 - end: - line: 31 - col: 326 - hash: b4f96b76a038d88b - context_hash: '15f7c492d32e7ef7' -- key: std.collectgarbage_opt.item.setpause - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 32 - col: 17 - end: - line: 32 - col: 131 - hash: '3727b48717039660' - context_hash: '3531792cf8357d54' -- key: std.collectgarbage_opt.item.setstepmul - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 33 - col: 19 - end: - line: 33 - col: 142 - hash: '8a91801d3f89e5cb' - context_hash: b97374f2edd288c9 -- key: std.collectgarbage_opt.item.incremental - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 34 - col: 20 - end: - line: 34 - col: 177 - hash: b74ab58b9c276d37 - context_hash: f40f73f844e18fe1 -- key: std.collectgarbage_opt.item.generational - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 35 - col: 21 - end: - line: 35 - col: 173 - hash: '6f28b79368adc435' - context_hash: '3c71032e0436c962' -- key: std.collectgarbage_opt.item.isrunning - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 36 - col: 18 - end: - line: 36 - col: 101 - hash: ffcec93fe4c3260d - context_hash: e728581e6d3b89a0 -- key: std.collectgarbage.param.item.minormul - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 39 - col: 17 - end: - line: 39 - col: 34 - hash: '17c912903d2b33e7' - context_hash: '347dbdc05e9d61fc' -- key: std.collectgarbage.param.item.majorminor - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 40 - col: 19 - end: - line: 40 - col: 37 - hash: ca29b001e432351d - context_hash: aae259d24bc4e5cd -- key: std.collectgarbage.param.item.minormajor - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 41 - col: 19 - end: - line: 41 - col: 37 - hash: '45e16901f0e1c945' - context_hash: '8d26b6ec034acd6d' -- key: std.collectgarbage.param.item.pause - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 42 - col: 14 - end: - line: 42 - col: 30 - hash: '035f4092820a2d28' - context_hash: eaf41c3acfd833e4 -- key: std.collectgarbage.param.item.stepmul - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 43 - col: 16 - end: - line: 43 - col: 32 - hash: f0701f65388feb8c - context_hash: f1f84e457e031946 -- key: std.collectgarbage.param.item.stepsize - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 44 - col: 17 - end: - line: 44 - col: 27 - hash: '92c7a59e2065582e' - context_hash: '24078253f4b35579' -- key: collectgarbage - kind: - type: doc_block - indent: '' - range: - start: - line: 46 - col: 0 - end: - line: 73 - col: 0 - hash: bd476771d729520d - context_hash: de7cc417de1b3246 -- key: dofile - kind: - type: doc_block - indent: '' - range: - start: - line: 79 - col: 0 - end: - line: 85 - col: 0 - hash: '17eaac532d0d8b7f' - context_hash: de7cc417de1b3246 -- key: error - kind: - type: doc_block - indent: '' - range: - start: - line: 89 - col: 0 - end: - line: 98 - col: 0 - hash: '2ed38bdf5647d7e6' - context_hash: de7cc417de1b3246 -- key: _G - kind: - type: doc_block - indent: '' - range: - start: - line: 102 - col: 0 - end: - line: 106 - col: 0 - hash: ed6170e60c7d1e6c - context_hash: de7cc417de1b3246 -- key: getmetatable - kind: - type: doc_block - indent: '' - range: - start: - line: 109 - col: 0 - end: - line: 113 - col: 0 - hash: fb34ddfee855c143 - context_hash: de7cc417de1b3246 -- key: ipairs - kind: - type: doc_block - indent: '' - range: - start: - line: 117 - col: 0 - end: - line: 123 - col: 0 - hash: '81cc1b7ba3316736' - context_hash: de7cc417de1b3246 -- key: std.loadmode.item.b - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 129 - col: 10 - end: - line: 129 - col: 29 - hash: a7b23db765875ab4 - context_hash: d05225bac089de1c -- key: std.loadmode.item.t - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 130 - col: 10 - end: - line: 130 - col: 27 - hash: '3f888b84d5ff9cb6' - context_hash: '4c16fff1406dca0c' -- key: std.loadmode.item.bt - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 131 - col: 11 - end: - line: 131 - col: 32 - hash: '6281255a2e74d843' - context_hash: '36a85bed624ee77b' -- key: load - kind: - type: doc_block - indent: '' - range: - start: - line: 133 - col: 0 - end: - line: 162 - col: 0 - hash: '4ab0788de1108e49' - context_hash: de7cc417de1b3246 -- key: loadstring - kind: - type: doc_block - indent: '' - range: - start: - line: 172 - col: 0 - end: - line: 175 - col: 0 - hash: '284dd88d29965057' - context_hash: de7cc417de1b3246 -- key: loadfile - kind: - type: doc_block - indent: '' - range: - start: - line: 183 - col: 0 - end: - line: 186 - col: 0 - hash: '2509b32c0559be2b' - context_hash: de7cc417de1b3246 -- key: module - kind: - type: doc_block - indent: '' - range: - start: - line: 200 - col: 0 - end: - line: 204 - col: 0 - hash: b87ca1c9dd074609 - context_hash: de7cc417de1b3246 -- key: next - kind: - type: doc_block - indent: '' - range: - start: - line: 208 - col: 0 - end: - line: 225 - col: 0 - hash: '0c06915c244a9fc7' - context_hash: de7cc417de1b3246 -- key: pairs - kind: - type: doc_block - indent: '' - range: - start: - line: 232 - col: 0 - end: - line: 243 - col: 0 - hash: '05ca1b5e9206cb2b' - context_hash: de7cc417de1b3246 -- key: pcall - kind: - type: doc_block - indent: '' - range: - start: - line: 248 - col: 0 - end: - line: 255 - col: 0 - hash: fc6ad4dc0c8dd9eb - context_hash: de7cc417de1b3246 -- key: print - kind: - type: doc_block - indent: '' - range: - start: - line: 261 - col: 0 - end: - line: 266 - col: 35 - hash: bb19bb9c28d20e8b - context_hash: de7cc417de1b3246 -- key: rawequal - kind: - type: doc_block - indent: '' - range: - start: - line: 269 - col: 0 - end: - line: 272 - col: 0 - hash: '40261afe4c5e2460' - context_hash: de7cc417de1b3246 -- key: rawget - kind: - type: doc_block - indent: '' - range: - start: - line: 277 - col: 0 - end: - line: 280 - col: 0 - hash: '3d0e3f29adb567bf' - context_hash: de7cc417de1b3246 -- key: rawlen - kind: - type: doc_block - indent: '' - range: - start: - line: 287 - col: 0 - end: - line: 290 - col: 0 - hash: '3bb95a6d6f8d5477' - context_hash: de7cc417de1b3246 -- key: rawset - kind: - type: doc_block - indent: '' - range: - start: - line: 294 - col: 0 - end: - line: 298 - col: 0 - hash: c726cd5a8ed490a8 - context_hash: de7cc417de1b3246 -- key: require - kind: - type: doc_block - indent: '' - range: - start: - line: 303 - col: 0 - end: - line: 333 - col: 0 - hash: '1dd74d532228044d' - context_hash: de7cc417de1b3246 -- key: select - kind: - type: doc_block - indent: '' - range: - start: - line: 337 - col: 0 - end: - line: 342 - col: 0 - hash: '70e32f7a9c40be28' - context_hash: de7cc417de1b3246 -- key: setmetatable - kind: - type: doc_block - indent: '' - range: - start: - line: 380 - col: 0 - end: - line: 387 - col: 0 - hash: '6cb51df47e53ad08' - context_hash: de7cc417de1b3246 -- key: tonumber - kind: - type: doc_block - indent: '' - range: - start: - line: 392 - col: 0 - end: - line: 407 - col: 0 - hash: '23e0d900a782af87' - context_hash: de7cc417de1b3246 -- key: tostring - kind: - type: doc_block - indent: '' - range: - start: - line: 413 - col: 0 - end: - line: 421 - col: 0 - hash: adc137af28e53d37 - context_hash: de7cc417de1b3246 -- key: type - kind: - type: doc_block - indent: '' - range: - start: - line: 435 - col: 0 - end: - line: 440 - col: 0 - hash: '0800556f7051218b' - context_hash: de7cc417de1b3246 -- key: _VERSION - kind: - type: doc_block - indent: '' - range: - start: - line: 444 - col: 0 - end: - line: 446 - col: 75 - hash: '97152d055df02054' - context_hash: de7cc417de1b3246 -- key: xpcall - kind: - type: doc_block - indent: '' - range: - start: - line: 449 - col: 0 - end: - line: 452 - col: 0 - hash: '8f9f463f8371a2f0' - context_hash: de7cc417de1b3246 -- key: _ENV - kind: - type: doc_block - indent: '' - range: - start: - line: 476 - col: 0 - end: - line: 478 - col: 0 - hash: '450d61ac9994e275' - context_hash: ff924cd5098e706f -- key: setfenv - kind: - type: doc_block - indent: '' - range: - start: - line: 484 - col: 0 - end: - line: 486 - col: 0 - hash: '79ac6333c6044892' - context_hash: de7cc417de1b3246 -- key: setfenv.param.f - kind: - type: line_tail - prefix: '' - range: - start: - line: 486 - col: 29 - end: - line: 486 - col: 81 - hash: '45534d1ea636fab4' - context_hash: d75ed448e13441d6 -- key: setfenv.param.env - kind: - type: line_tail - prefix: '' - range: - start: - line: 487 - col: 20 - end: - line: 487 - col: 68 - hash: '405f4286ea2c7329' - context_hash: '152a311703ac5ea8' -- key: getfenv - kind: - type: doc_block - indent: '' - range: - start: - line: 491 - col: 0 - end: - line: 493 - col: 0 - hash: '3c8b579e5297fcd6' - context_hash: de7cc417de1b3246 -- key: getfenv.param.f - kind: - type: line_tail - prefix: '' - range: - start: - line: 493 - col: 29 - end: - line: 493 - col: 79 - hash: '61d794dd7c91d37d' - context_hash: '467b302d8fefaf8f' -- key: getfenv.return.1 - kind: - type: line_tail - prefix: '' - range: - start: - line: 494 - col: 21 - end: - line: 494 - col: 74 - hash: '41910412fe03bad4' - context_hash: '570039d3c1c43398' diff --git a/crates/glua_ls/std_i18n/global/zh_CN.yaml b/crates/glua_ls/std_i18n/global/zh_CN.yaml deleted file mode 100644 index df1626bb7..000000000 --- a/crates/glua_ls/std_i18n/global/zh_CN.yaml +++ /dev/null @@ -1,297 +0,0 @@ -assert: | - 如果参数 `v` 的值为假(即 **nil** 或 **false**),则调用 error; - 否则返回所有参数。在发生错误的情况下,`message` 将是错误对象; - 如果省略,默认为 "assertion failed!"。 - -std.collectgarbage_opt.item.collect: | - 执行完整的垃圾收集循环。这是默认选项。 - -std.collectgarbage_opt.item.stop: | - 停止垃圾收集器的自动执行。收集器只会在显式调用之前运行,直到调用 restart。 - -std.collectgarbage_opt.item.restart: | - 重启垃圾收集器的自动执行。 - -std.collectgarbage_opt.item.count: | - 以 Kbytes 为单位返回 Lua 使用的总内存数。该值有小数部分,乘以 1024 可得到 Lua 使用的确切字节数(溢出除外)。 - -std.collectgarbage_opt.item.step: | - 执行一次垃圾回收步骤。步进由 `arg` 控制。 - 如果值为 0,收集器将执行一个基础(不可分割)步进。 - 对于非零值,收集器执行的效果等同于 Lua 分配了相应数量(以 KBytes 为单位)的内存。 - 如果步进完成了一个收集循环,则返回 true。 - -std.collectgarbage_opt.item.setpause: | - 将 `arg` 设置为收集器 *pause* (参见 §2.5)的新值。返回 *pause* 的前一个值。 - -std.collectgarbage_opt.item.setstepmul: | - 将 `arg` 设置为收集器 *step multiplier* (参见 §2.5)的新值。返回 *step* 的前一个值。 - -std.collectgarbage_opt.item.incremental: | - 将收集器模式更改为增量模式。此选项后面可以跟三个数字:垃圾收集器暂停、步进倍率和步进大小。 - -std.collectgarbage_opt.item.generational: | - 将收集器模式更改为分代模式。此选项后面可以跟两个数字:垃圾收集器次要倍率和主要倍率。 - -std.collectgarbage_opt.item.isrunning: | - 返回一个布尔值,指示收集器是否正在运行(即未停止)。 - -# minor multiplier -std.collectgarbage.param.item.minormul: "" - -# major/minor ratio -std.collectgarbage.param.item.majorminor: "" - -# minor/major ratio -std.collectgarbage.param.item.minormajor: "" - -# collector pause -std.collectgarbage.param.item.pause: "" - -# step multiplier -std.collectgarbage.param.item.stepmul: "" - -# step size -std.collectgarbage.param.item.stepsize: "" - -collectgarbage: | - 此函数是垃圾收集器的通用接口。它根据第一个参数 `opt` 执行不同的功能。 - - ### opt - - - **"collect"**: 执行完整的垃圾回收。这是默认选项。 - - - **"stop"**: 停止垃圾收集器的自动执行。收集器只会在显式调用之前运行, - 直到调用 restart。 - - - **"restart"**: 重启垃圾收集器的自动执行。 - - - **"count"**: 以 Kbytes 为单位返回 Lua 使用的总内存数。该值有小数部分, - 乘以 1024 可得到 Lua 使用的确切字节数(溢出除外)。 - - - **"step"**: 执行垃圾收集步进。步进 "size" 由 `arg` 控制。 - 如果值为 0,收集器将执行一个基础(不可分割)步进。 - 对于非零值,收集器执行的效果等同于 Lua 分配了相应数量(以 KBytes 为单位)的内存。 - 如果步进完成了一个回收周期,则返回 **true**。 - - - **"setpause"**: 将 `arg` 设置为收集器 *pause* (参见 §2.5)的新值。 - 返回 *pause* 的前一个值。 - - - **"incremental"**: 将收集器模式更改为增量模式。此选项后面可以跟三个数字: - 垃圾收集器暂停、步进倍率和步进大小。 - - - **"generational"**: 将收集器模式更改为分代模式。此选项后面可以跟两个数字: - 垃圾收集器次要倍率和主要倍率。 - - - **"isrunning"**: 返回一个布尔值,指示收集器是否正在运行(即未停止)。 - -dofile: | - 打开指定的文件并将其内容作为 Lua 代码块执行。不带参数调用时, - `dofile` 执行标准输入(`stdin`)的内容。返回代码块返回的所有值。 - 如果发生错误,`dofile` 将错误传播给调用者(即 `dofile` 不在保护模式下运行)。 - -error: | - 终止最后调用的受保护函数,并将 `message` 作为错误对象返回。 - Function `error` 永远不会返回。通常,如果消息是字符串,`error` - 会在消息开头添加一些关于错误位置的信息。`level` 参数指定如何获取 - 错误位置。使用 level 1(默认值),错误位置是调用 `error` 函数的位置。 - Level 2 将错误指向调用 `error` 的函数的调用位置;依此类推。 - 传递 level 0 可以避免向消息添加错误位置信息。 - -_G: | - 一个保存全局环境的全局变量(不是函数)。Lua 本身不使用此变量; - 更改其值不会影响任何环境,反之亦然。 - -getmetatable: | - 如果 `object` 没有元表,则返回 **nil**。否则,如果对象的元表 - 具有 `"__metatable"` 字段,则返回关联的值。否则,返回给定对象的元表。 - -ipairs: | - 返回三个值(一个迭代器函数、表 `t` 和 0),以便构造 - > `for i,v in ipairs(t) do` *body* `end` - 将遍历键值对 (1,`t[1]`), (2,`t[2]`), ...,直到第一个不存在的索引。 - -std.loadmode.item.b: | - 仅二进制块 - -std.loadmode.item.t: | - 仅文本块 - -std.loadmode.item.bt: | - 二进制和文本块 - -load: | - 加载一个代码块。 - 如果 `chunk` 是字符串,则代码块就是该字符串。如果 `chunk` 是函数, - `load` 会重复调用它以获取代码块片段。每次调用 `chunk` 必须返回 - 一个与先前结果连接的字符串。返回空字符串、**nil** 或无值表示代码块结束。 - - 如果没有语法错误,则将编译后的代码块作为函数返回; - 否则,返回 **nil** 加上错误消息。 - - 如果生成的函数有上值,则第一个上值设置为 `env` 的值(如果给定了该参数), - 或者设置为全局环境的值。其他上值初始化为 **nil**。(当你加载主代码块时, - 生成的函数将始终只有一个上值,即 _ENV 变量。但是,当你加载从函数创建的 - 二进制代码块时(参见 string.dump),生成的函数可以有任意数量的上值。) - 所有上值都是新的,即它们不与任何其他函数共享。 - - `chunkname` 用作错误消息和调试信息的代码块名称。如果省略, - 由于 `chunk` 是字符串,默认为 `chunk`,否则默认为 "=(load)"。 - - 字符串 `mode` 控制代码块是文本还是二进制(即预编译代码块)。 - 它可以是字符串 "b"(仅二进制代码块)、"t"(仅文本代码块) - 或 "bt"(二进制和文本)。默认为 "bt"。 - - Lua 不检查二进制代码块的一致性。恶意构造的二进制代码块可能会导致解释器崩溃。 - -loadstring: | - 从给定字符串加载代码块。 - -loadfile: | - 类似于 `load`,但从文件 `filename` 获取代码块,或者如果没有给出文件名, - 则从标准输入获取。 - -module: | - 创建一个模块。 - -next: | - 允许程序遍历表的所有字段。它的第一个参数是一个表,第二个参数是 - 表中的一个索引。`next` 返回表的下一个索引及其关联的值。当使用 **nil** - 作为第二个参数调用时,`next` 返回初始索引及其关联的值。当使用最后一个索引 - 调用,或者在空表中使用 **nil** 调用时,`next` 返回 **nil**。 - 如果省略第二个参数,则将其解释为 **nil**。特别是,你可以使用 `next(t)` - 来检查表是否为空。 - - 索引枚举的顺序未指定,*即使对于数字索引也是如此*。(要按数字顺序 - 遍历表,请使用数字 **for**。) - - 如果在遍历过程中将任何值赋给表中不存在的字段,则 `next` 的行为未定义。 - 但是,你可以修改现有字段。特别是,你可以将现有字段设置为 nil。 - -pairs: | - 如果 `t` 具有元方法 `__pairs`,则以 `t` 作为参数调用它,并返回该调用的前三个结果。 - - 否则,返回三个值:`next` 函数、表 `t` 和 `nil`, - 因此结构 `for k,v in pairs(t) do *body* end` 会遍历表 `t` 的所有键值对。 - - 关于在遍历过程中修改表的注意事项,请参见函数 `next`。 - -pcall: | - 以 *保护模式* 调用给定参数的函数 `f`。这意味着 `f` 内部的任何错误 - 都不会传播;相反,`pcall` 会捕获错误并返回状态码。它的第一个结果是 - 状态码(一个布尔值),如果调用成功且没有错误,则为 true。在这种情况下, - `pcall` 还会返回调用后的所有结果。如果发生任何错误,`pcall` 返回 - **false** 加上错误消息。 - -print: | - 接收任意数量的参数,并使用 `tostring` 函数将它们转换为字符串, - 然后将它们的值打印到 `stdout`。`print` 不用于格式化输出, - 仅作为显示值的快速方式,例如用于调试。为了完全控制输出, - 请使用 `string.format` 和 `io.write`。 - -rawequal: | - 检查 `v1` 是否等于 `v2`,不调用 `__eq` 元方法。返回布尔值。 - -rawget: | - 获取 `table[index]` 的实际值,不调用 `__index` 元方法。`table` - 必须是一个表;`index` 可以是任何值。 - -rawlen: | - 返回对象 `v` 的长度,该对象必须是表或字符串,不调用任何元方法。返回一个整数。 - -rawset: | - 将 `table[index]` 的实际值设置为 `value`,不调用 `__newindex` 元方法。 - `table` 必须是一个表,`index` 是除 **nil** 和 NaN 之外的任何值, - `value` 是任何 Lua 值。 - -require: | - 加载给定的模块。该函数首先在 'package.loaded' 表中查找 `modname` - 是否这就加载。如果是,则 `require` 返回存储在 `package.loaded[modname]` - 中的值。否则,它尝试为模块查找 *加载器*。 - - 为了查找加载器,`require` 由 `package.searchers` 序列引导。 - 通过更改此序列,我们可以改变 `require` 查找模块的方式。 - 以下解释基于 `package.searchers` 的默认配置。 - - 首先 `require` 查询 `package.preload[modname]`。如果它有值, - 该值(应该是一个函数)就是加载器。否则 `require` 使用存储在 - `package.path` 中的路径搜索 Lua 加载器。如果这也失败,它使用 - 存储在 `package.cpath` 中的路径搜索 C 加载器。如果这也失败, - 它尝试 *一体化* 加载器(参见 `package.loaders`)。 - - 一旦找到加载器,`require` 就会使用两个参数调用加载器:`modname` - 和一个取决于它如何获取加载器的额外值。(如果加载器来自文件, - 这个额外值就是文件名。)如果加载器返回任何非 nil 值,require - 将返回的值赋值给 `package.loaded[modname]`。如果加载器没有返回 - 非 nil 值并且没有给 `package.loaded[modname]` 赋值,则 `require` - 将 true 赋值给该条目。在任何情况下,require 返回 - `package.loaded[modname]` 的最终值。 - - 如果加载或运行模块时出现任何错误,或者找不到模块的任何加载器, - 则 `require` 引发错误。 - -select: | - 如果 `index` 是数字,则返回参数编号 `index` 之后的所有参数; - 负数从末尾开始索引(-1 是最后一个参数)。 - 否则,`index` 必须是字符串 "#",`select` 返回接收到的额外参数的总数。 - -setmetatable: | - 为给定表设置元表。(要从 Lua 代码更改其他类型的元表,必须使用调试库。) - 如果 `metatable` 为 **nil**,则移除给定表的元表。如果原始元表 - 具有 `"__metatable"` 字段,则引发错误。 - - 此函数返回 `table`。 - -tonumber: | - 当不带 `base` 调用时,`tonumber` 尝试将其参数转换为数字。 - 如果参数已经是数字或可转换为数字的字符串,则 `tonumber` 返回该数字; - 否则,返回 **nil**。 - - 字符串的转换可以根据 Lua 的词法约定产生整数或浮点数。(字符串可以有 - 前导和尾随空格以及符号。) - - 当带 `base` 调用时,e 必须是一个字符串,被解释为该进制的整数。 - 进制可以是 2 到 36 之间的任何整数(含)。在 10 以上的进制中, - 字母 'A'(大写或小写)表示 10,'B' 表示 11,依此类推,'Z' 表示 35。 - 如果字符串 `e` 不是给定进制的有效数字,则函数返回 **nil**。 - -tostring: | - 接收任何类型的值并将其转换为人类可读格式的字符串。(为了完全控制 - 数字的转换方式,请使用 `string.format`)。 - - 如果 `v` 的元表具有 `__tostring` 字段,则 `tostring` 使用 `v` - 作为参数调用相应的值,并将调用的结果作为其结果。 - -type: | - 返回其唯一参数的类型,编码为字符串。此函数的可能结果是 "`nil`" - (字符串,不是值 **nil**)、"`number`"、"`string`"、"`boolean`"、 - "`table`"、"`function`"、"`thread`" 和 "`userdata`"。 - -_VERSION: | - 一个保存包含运行 Lua 版本字符串的全局变量(不是函数)。 - 此变量的当前值是 "`Lua 5.4`"。 - -xpcall: | - 此函数类似于 `pcall`,只是它设置了一个新的消息处理程序 `msgh`。 - -_ENV: | - 这是一个不正确的注解,但真正支持 _ENV 会完全破坏变量分析路径;目前将其视为一个全局变量。 - -setfenv: | - 设置指定函数的环境。 - -setfenv.param.f: | - 要设置环境的函数。 - -setfenv.param.env: | - 要分配给函数的环境表。 - -getfenv: | - 检索指定函数的环境表。 - -getfenv.param.f: | - 要检索环境的函数。 - -getfenv.return.1: | - 与给定函数关联的环境表。 - diff --git a/crates/glua_ls/std_i18n/io/meta.yaml b/crates/glua_ls/std_i18n/io/meta.yaml deleted file mode 100644 index 9cb4d8e9d..000000000 --- a/crates/glua_ls/std_i18n/io/meta.yaml +++ /dev/null @@ -1,408 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: io.lua -entries: -- key: iolib.close - kind: - type: doc_block - indent: '' - range: - start: - line: 18 - col: 0 - end: - line: 21 - col: 0 - hash: '9fd22cd41017890e' - context_hash: de7cc417de1b3246 -- key: iolib.flush - kind: - type: doc_block - indent: '' - range: - start: - line: 24 - col: 0 - end: - line: 25 - col: 40 - hash: '77b68d0b81e2a6ec' - context_hash: de7cc417de1b3246 -- key: iolib.input - kind: - type: doc_block - indent: '' - range: - start: - line: 28 - col: 0 - end: - line: 36 - col: 0 - hash: '30a4cb7aed17b4e2' - context_hash: de7cc417de1b3246 -- key: iolib.lines - kind: - type: doc_block - indent: '' - range: - start: - line: 40 - col: 0 - end: - line: 53 - col: 0 - hash: '6bf53c14d9a3e328' - context_hash: de7cc417de1b3246 -- key: iolib.open - kind: - type: doc_block - indent: '' - range: - start: - line: 59 - col: 0 - end: - line: 74 - col: 0 - hash: df02368e03dde439 - context_hash: de7cc417de1b3246 -- key: iolib.output - kind: - type: doc_block - indent: '' - range: - start: - line: 80 - col: 0 - end: - line: 82 - col: 0 - hash: '4e45ea3c8fb9457c' - context_hash: de7cc417de1b3246 -- key: iolib.popen - kind: - type: doc_block - indent: '' - range: - start: - line: 86 - col: 0 - end: - line: 92 - col: 0 - hash: be397ad0e73d2933 - context_hash: de7cc417de1b3246 -- key: std.readmode.item.n - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 100 - col: 12 - end: - line: 100 - col: 92 - hash: '09724402d824ca77' - context_hash: '140a64161cb92d4b' -- key: std.readmode.item.a - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 101 - col: 12 - end: - line: 101 - col: 70 - hash: '0b0543502b6fe843' - context_hash: '90ef6cfb35c6bcdc' -- key: std.readmode.item.l - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 102 - col: 12 - end: - line: 102 - col: 61 - hash: '2232740e3160cdc8' - context_hash: df058106ca067ce2 -- key: std.readmode.item.L - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 103 - col: 12 - end: - line: 103 - col: 63 - hash: '4942667b1190a4de' - context_hash: fd41ede7cf5b3288 -- key: std.readmode.item.*n - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 104 - col: 12 - end: - line: 104 - col: 92 - hash: '09724402d824ca77' - context_hash: '2a0d1282b2a177ad' -- key: std.readmode.item.*a - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 105 - col: 12 - end: - line: 105 - col: 70 - hash: '0b0543502b6fe843' - context_hash: eaa243d8bc090b70 -- key: std.readmode.item.*l - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 106 - col: 12 - end: - line: 106 - col: 61 - hash: '2232740e3160cdc8' - context_hash: '83f5df8702ad7b68' -- key: std.readmode.item.*L - kind: - type: line_tail - prefix: ' ' - range: - start: - line: 107 - col: 12 - end: - line: 107 - col: 63 - hash: '4942667b1190a4de' - context_hash: b3bd56472aa2dd1e -- key: iolib.read - kind: - type: doc_block - indent: '' - range: - start: - line: 109 - col: 0 - end: - line: 111 - col: 0 - hash: '771eb15e50f555d7' - context_hash: de7cc417de1b3246 -- key: iolib.tmpfile - kind: - type: doc_block - indent: '' - range: - start: - line: 117 - col: 0 - end: - line: 120 - col: 0 - hash: '8c5fee923fff7b7d' - context_hash: de7cc417de1b3246 -- key: iolib.type - kind: - type: doc_block - indent: '' - range: - start: - line: 123 - col: 0 - end: - line: 127 - col: 0 - hash: a9422cffda6cfcf8 - context_hash: de7cc417de1b3246 -- key: iolib.write - kind: - type: doc_block - indent: '' - range: - start: - line: 131 - col: 0 - end: - line: 133 - col: 0 - hash: '2a7a93b6b492d6e7' - context_hash: de7cc417de1b3246 -- key: file - kind: - type: doc_block - indent: '' - range: - start: - line: 138 - col: 0 - end: - line: 139 - col: 0 - hash: '44f701b90e451a28' - context_hash: '6b5eb10d9792750b' -- key: file.close@>5.2 - kind: - type: doc_block - indent: '' - range: - start: - line: 143 - col: 0 - end: - line: 150 - col: 0 - hash: '506043bf254a978d' - context_hash: de7cc417de1b3246 -- key: file.close@5.1,JIT - kind: - type: doc_block - indent: '' - range: - start: - line: 156 - col: 0 - end: - line: 160 - col: 0 - hash: '8b2730ffb16aacd6' - context_hash: de7cc417de1b3246 -- key: file.flush - kind: - type: doc_block - indent: '' - range: - start: - line: 164 - col: 0 - end: - line: 166 - col: 0 - hash: cb93f3d56af1b6ff - context_hash: de7cc417de1b3246 -- key: file.lines - kind: - type: doc_block - indent: '' - range: - start: - line: 170 - col: 0 - end: - line: 181 - col: 0 - hash: '2f9df86b5b29e9c5' - context_hash: de7cc417de1b3246 -- key: file.read - kind: - type: doc_block - indent: '' - range: - start: - line: 186 - col: 0 - end: - line: 210 - col: 0 - hash: '6748446b86c12534' - context_hash: de7cc417de1b3246 -- key: file.seek - kind: - type: doc_block - indent: '' - range: - start: - line: 216 - col: 0 - end: - line: 233 - col: 0 - hash: bb0f0c4d8ca400b2 - context_hash: de7cc417de1b3246 -- key: file.setvbuf - kind: - type: doc_block - indent: '' - range: - start: - line: 240 - col: 0 - end: - line: 252 - col: 0 - hash: b023f23bc1af6ca7 - context_hash: de7cc417de1b3246 -- key: file.write - kind: - type: doc_block - indent: '' - range: - start: - line: 256 - col: 0 - end: - line: 262 - col: 0 - hash: '868914df6d494b96' - context_hash: de7cc417de1b3246 -- key: iolib.stderr - kind: - type: doc_block - indent: '' - range: - start: - line: 267 - col: 0 - end: - line: 268 - col: 0 - hash: '3f87a382a528ef4c' - context_hash: '241ad9d43dd76137' -- key: iolib.stdin - kind: - type: doc_block - indent: '' - range: - start: - line: 271 - col: 0 - end: - line: 272 - col: 0 - hash: b3683d38bfd93f8f - context_hash: aa04d03ab1feb2c8 -- key: iolib.stdout - kind: - type: doc_block - indent: '' - range: - start: - line: 275 - col: 0 - end: - line: 276 - col: 0 - hash: b969090b3463d8a5 - context_hash: d5100f194d3f55f2 diff --git a/crates/glua_ls/std_i18n/io/zh_CN.yaml b/crates/glua_ls/std_i18n/io/zh_CN.yaml deleted file mode 100644 index a854c2d94..000000000 --- a/crates/glua_ls/std_i18n/io/zh_CN.yaml +++ /dev/null @@ -1,169 +0,0 @@ -iolib.close: | - 相当于 `file:close()`。如果没有指定文件,则关闭默认输出文件。 - -iolib.flush: | - 相当于 `io.output():flush()`。 - -iolib.input: | - 当使用文件名调用时,它打开命名的文件(以文本模式),并将该文件句柄 - 设置为默认输入文件。当使用文件句柄调用时,它只是将此文件句柄设置为 - 默认输入文件。当不带参数调用时,它返回当前的默认输入文件。 - - 如果发生错误,此函数会引发错误,而不是返回错误代码。 - -iolib.lines: | - 以读取模式打开给定的文件名,并返回一个像 `file:lines(...)` 一样 - 在打开的文件上工作的迭代器函数。当迭代器函数检测到文件末尾时, - 它不返回任何值(以结束循环)并自动关闭文件。 - - 调用 `io.lines()`(不带文件名)相当于 `io.input():lines()`; - 也就是说,它遍历默认输入文件的行。在这种情况下,迭代器在循环结束时 - 不会关闭文件。 - - 如果发生错误,此函数会引发错误,而不是返回错误代码。 - -iolib.open: | - 此函数以字符串 `mode` 指定的模式打开文件。如果成功,它返回一个新的 - 文件句柄。`mode` 字符串可以是以下任意一种: - - **"r"**: 读取模式(默认); - **"w"**: 写入模式; - **"a"**: 追加模式; - **"r+"**: 更新模式,保留所有先前的数据; - **"w+"**: 更新模式,擦除所有先前的数据; - **"a+"**: 追加更新模式,保留先前的数据,只允许在文件末尾写入。 - - `mode` 字符串末尾也可以有一个 '`b`',在某些系统中需要它以二进制模式打开文件。 - -iolib.output: | - 类似于 `io.input`,但操作的是默认输出文件。 - -iolib.popen: | - 此函数依赖于系统,并非在所有平台上都可用。 - - 在单独的进程中启动程序 `prog`,并返回一个文件句柄,你可以使用它 - 从此程序读取数据(如果 `mode` 是 "`r`",默认值)或向此程序写入数据 - (如果 `mode` 是 "`w`")。 - -std.readmode.item.n: | - 读取一个数字,根据 Lua 的转换语法返回浮点数或整数。 - -std.readmode.item.a: | - 从当前位置开始读取整个文件。 - -std.readmode.item.l: | - 读取一行并忽略行尾标记。 - -std.readmode.item.L: | - 读取一行并保留行尾标记。 - -std.readmode.item.*n: | - 读取一个数字,根据 Lua 的转换语法返回浮点数或整数。 - -std.readmode.item.*a: | - 从当前位置开始读取整个文件。 - -std.readmode.item.*l: | - 读取一行并忽略行尾标记。 - -std.readmode.item.*L: | - 读取一行并保留行尾标记。 - -iolib.read: | - 相当于 `io.input():read(...)`。 - -iolib.tmpfile: | - 如果成功,返回一个临时文件的句柄。此文件以更新模式打开, - 并在程序结束时自动删除。 - -iolib.type: | - 检查 `obj` 是否为有效的文件句柄。如果 `obj` 是打开的文件句柄, - 返回字符串 "`file`";如果 `obj` 是已关闭的文件句柄, - 返回 "`closed file`";如果 `obj` 不是文件句柄,返回 **nil**。 - -iolib.write: | - 相当于 `io.output():write(...)`。 - -file: | - 文件对象 - -file.close@>5.2: | - 关闭 `file`。请注意,当文件句柄被垃圾收集时,文件会自动关闭, - 但这需要的时间是不可预测的。 - - 当关闭由 `io.popen` 创建的文件句柄时,`file:close` 返回 - 与 `os.execute` 相同的返回值。 - -# Closes `file`. Note that files are automatically closed when their -# handles are garbage collected, but that takes an unpredictable amount of -# time to happen. -file.close@5.1,JIT: "" - -file.flush: | - 将任何写入的数据保存到 `file`。 - -file.lines: | - 返回一个迭代器函数,每次调用时,都会根据给定的格式读取文件。 - 如果没有给出格式,默认使用 "l"。例如,构造 - `for c in file:lines(1) do *body* end` - 将从当前位置开始遍历文件的所有字符。与 `io.lines` 不同, - 此函数在循环结束时不会关闭文件。 - - 如果发生错误,此函数会引发错误,而不是返回错误代码。 - -file.read: | - 根据给定的格式读取文件 `file`,格式指定了要读取的内容。对于每种格式, - 该函数返回一个包含读取字符的字符串或数字,如果无法使用指定格式 - 读取数据,则返回 **nil**。(在后一种情况下,函数不会读取后续格式。) - 当不带参数调用时,它使用默认格式读取下一行(见下文)。 - - - 可用的格式有: - **"n"**: 读取一个数字,并根据 Lua 的词法约定将其作为浮点数或整数返回。 - (数字可以有前导空格和符号。)此格式总是读取作为数字有效前缀的最长 - 输入序列;如果该前缀不构成有效数字(例如空字符串、"`0x`" 或 "`3.4e-`"), - 它将被丢弃,并且格式返回 **nil**; - **"a"**: 从当前位置开始读取整个文件。在文件末尾,它返回空字符串; - **"l"**: 读取下一行,跳过行尾,在文件末尾返回 **nil**。这是默认格式。 - **"L"**: 读取下一行,保留行尾字符(如果存在),在文件末尾返回 **nil**; - *number*: 读取最多包含此字节数的字符串,在文件末尾返回 **nil**。 - 如果 `number` 为零,它不读取任何内容并返回空字符串,或在文件末尾返回 **nil**。 - -file.seek: | - 设置并获取文件位置,位置从文件开头测量,由 `offset` 加上字符串 `whence` - 指定的基准决定,如下所示: - **"set"**: 基准是位置 0(文件开头); - **"cur"**: 基准是当前位置; - **"end"**: 基准是文件末尾; - - 如果成功,`seek` 返回最终文件位置,以从文件开头算起的字节数测量。 - 如果 `seek` 失败,它返回 **nil** 加上描述错误的字符串。 - - `whence` 的默认值为 "`cur`",`offset` 的默认值为 0。因此, - 调用 `file:seek()` 返回当前文件位置而不改变它; - 调用 `file:seek("set")` 将位置设置为文件开头(并返回 0); - 调用 `file:seek("end")` 将位置设置为文件末尾,并返回其大小。 - -file.setvbuf: | - 设置输出文件的缓冲模式。有三种可用模式: - **"no"**: 无缓冲;任何输出操作的结果立即出现。 - **"full"**: 全缓冲;仅当缓冲区已满(或显式 `flush` 文件(见 `io.flush`)) - 时才执行输出操作。 - **"line"**: 行缓冲;输出被缓冲直到输出换行符或从某些特殊文件 - (如终端设备)有任何输入。 - - 对于后两种情况,`size` 指定缓冲区的大小(以字节为单位)。默认是合适的大小。 - -file.write: | - 将其每个参数的值写入 `file`。参数必须是字符串或数字。 - - 如果成功,此函数返回 `file`。否则它返回 **nil** 加上描述错误的字符串。 - -iolib.stderr: | - 标准错误输出。 - -iolib.stdin: | - 标准输入。 - -iolib.stdout: | - 标准输出。 - diff --git a/crates/glua_ls/std_i18n/jit/meta.yaml b/crates/glua_ls/std_i18n/jit/meta.yaml deleted file mode 100644 index 85ca5bb3c..000000000 --- a/crates/glua_ls/std_i18n/jit/meta.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: jit.lua -entries: [] diff --git a/crates/glua_ls/std_i18n/jit/profile/meta.yaml b/crates/glua_ls/std_i18n/jit/profile/meta.yaml deleted file mode 100644 index 6e5273a2c..000000000 --- a/crates/glua_ls/std_i18n/jit/profile/meta.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: jit/profile.lua -entries: [] diff --git a/crates/glua_ls/std_i18n/jit/profile/zh_CN.yaml b/crates/glua_ls/std_i18n/jit/profile/zh_CN.yaml deleted file mode 100644 index 8b1378917..000000000 --- a/crates/glua_ls/std_i18n/jit/profile/zh_CN.yaml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/glua_ls/std_i18n/jit/util/meta.yaml b/crates/glua_ls/std_i18n/jit/util/meta.yaml deleted file mode 100644 index 1a462e30c..000000000 --- a/crates/glua_ls/std_i18n/jit/util/meta.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: jit/util.lua -entries: -- key: util.traceir.return.2 - kind: - type: line_tail - prefix: '' - range: - start: - line: 78 - col: 25 - end: - line: 78 - col: 50 - hash: '0fccc55e3f2e66e3' - context_hash: '1fc12a68a2da7f94' diff --git a/crates/glua_ls/std_i18n/jit/util/zh_CN.yaml b/crates/glua_ls/std_i18n/jit/util/zh_CN.yaml deleted file mode 100644 index a134ab403..000000000 --- a/crates/glua_ls/std_i18n/jit/util/zh_CN.yaml +++ /dev/null @@ -1,3 +0,0 @@ -# spellchecker:disable-line -util.traceir.return.2: "" - diff --git a/crates/glua_ls/std_i18n/jit/zh_CN.yaml b/crates/glua_ls/std_i18n/jit/zh_CN.yaml deleted file mode 100644 index 8b1378917..000000000 --- a/crates/glua_ls/std_i18n/jit/zh_CN.yaml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/glua_ls/std_i18n/math/meta.yaml b/crates/glua_ls/std_i18n/math/meta.yaml deleted file mode 100644 index 21412b977..000000000 --- a/crates/glua_ls/std_i18n/math/meta.yaml +++ /dev/null @@ -1,486 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: math.lua -entries: -- key: mathlib.abs - kind: - type: doc_block - indent: '' - range: - start: - line: 18 - col: 0 - end: - line: 20 - col: 0 - hash: c58920ed7e19d04e - context_hash: de7cc417de1b3246 -- key: mathlib.acos - kind: - type: doc_block - indent: '' - range: - start: - line: 25 - col: 0 - end: - line: 27 - col: 0 - hash: a5dd11d2e52d92bb - context_hash: de7cc417de1b3246 -- key: mathlib.asin - kind: - type: doc_block - indent: '' - range: - start: - line: 31 - col: 0 - end: - line: 33 - col: 0 - hash: a66e0fb56f556f39 - context_hash: de7cc417de1b3246 -- key: mathlib.atan - kind: - type: doc_block - indent: '' - range: - start: - line: 37 - col: 0 - end: - line: 44 - col: 0 - hash: eb7d38ca6a36d7e4 - context_hash: de7cc417de1b3246 -- key: mathlib.ceil - kind: - type: doc_block - indent: '' - range: - start: - line: 49 - col: 0 - end: - line: 51 - col: 0 - hash: '27f3c017a9d3c924' - context_hash: de7cc417de1b3246 -- key: mathlib.cos - kind: - type: doc_block - indent: '' - range: - start: - line: 55 - col: 0 - end: - line: 57 - col: 0 - hash: '3dcf75884877dfdf' - context_hash: de7cc417de1b3246 -- key: mathlib.deg - kind: - type: doc_block - indent: '' - range: - start: - line: 61 - col: 0 - end: - line: 63 - col: 0 - hash: f054a9819e67ad41 - context_hash: de7cc417de1b3246 -- key: mathlib.exp - kind: - type: doc_block - indent: '' - range: - start: - line: 67 - col: 0 - end: - line: 69 - col: 0 - hash: '0ba32fcbfe06e7ac' - context_hash: de7cc417de1b3246 -- key: mathlib.floor - kind: - type: doc_block - indent: '' - range: - start: - line: 73 - col: 0 - end: - line: 75 - col: 0 - hash: '0217672176f9061c' - context_hash: de7cc417de1b3246 -- key: mathlib.fmod - kind: - type: doc_block - indent: '' - range: - start: - line: 79 - col: 0 - end: - line: 82 - col: 0 - hash: af18ff844942e1a1 - context_hash: de7cc417de1b3246 -- key: mathlib.huge - kind: - type: doc_block - indent: '' - range: - start: - line: 87 - col: 0 - end: - line: 90 - col: 0 - hash: b6cee43c3c9752e4 - context_hash: de7cc417de1b3246 -- key: mathlib.log - kind: - type: doc_block - indent: '' - range: - start: - line: 93 - col: 0 - end: - line: 96 - col: 0 - hash: '4c3d322fa253de6d' - context_hash: de7cc417de1b3246 -- key: mathlib.max - kind: - type: doc_block - indent: '' - range: - start: - line: 101 - col: 0 - end: - line: 104 - col: 0 - hash: '03a9e9e4fef2c659' - context_hash: de7cc417de1b3246 -- key: mathlib.maxinteger - kind: - type: doc_block - indent: '' - range: - start: - line: 111 - col: 0 - end: - line: 113 - col: 0 - hash: '28ba2ba24ff4a272' - context_hash: de7cc417de1b3246 -- key: mathlib.min - kind: - type: doc_block - indent: '' - range: - start: - line: 116 - col: 0 - end: - line: 119 - col: 0 - hash: b3b85d0ea6475cef - context_hash: de7cc417de1b3246 -- key: mathlib.mininteger - kind: - type: doc_block - indent: '' - range: - start: - line: 126 - col: 0 - end: - line: 128 - col: 0 - hash: a57b428c91d0e758 - context_hash: de7cc417de1b3246 -- key: mathlib.modf - kind: - type: doc_block - indent: '' - range: - start: - line: 131 - col: 0 - end: - line: 134 - col: 0 - hash: '91b7162d1bad4494' - context_hash: de7cc417de1b3246 -- key: mathlib.pi - kind: - type: doc_block - indent: '' - range: - start: - line: 139 - col: 0 - end: - line: 140 - col: 20 - hash: a3cd679841b59bc3 - context_hash: de7cc417de1b3246 -- key: mathlib.rad - kind: - type: doc_block - indent: '' - range: - start: - line: 143 - col: 0 - end: - line: 145 - col: 0 - hash: d221083d438800ff - context_hash: de7cc417de1b3246 -- key: mathlib.random - kind: - type: doc_block - indent: '' - range: - start: - line: 149 - col: 0 - end: - line: 155 - col: 0 - hash: b1cc3cd05011681a - context_hash: de7cc417de1b3246 -- key: mathlib.randomseed - kind: - type: doc_block - indent: '' - range: - start: - line: 162 - col: 0 - end: - line: 165 - col: 0 - hash: '93e6af65e993e003' - context_hash: de7cc417de1b3246 -- key: mathlib.sin - kind: - type: doc_block - indent: '' - range: - start: - line: 168 - col: 0 - end: - line: 170 - col: 0 - hash: f3156b9eee524b75 - context_hash: de7cc417de1b3246 -- key: mathlib.sqrt - kind: - type: doc_block - indent: '' - range: - start: - line: 174 - col: 0 - end: - line: 177 - col: 0 - hash: '9c47f0e74f561c41' - context_hash: de7cc417de1b3246 -- key: mathlib.tan - kind: - type: doc_block - indent: '' - range: - start: - line: 181 - col: 0 - end: - line: 183 - col: 0 - hash: '4e92d05f5d7eea2f' - context_hash: de7cc417de1b3246 -- key: mathlib.tointeger - kind: - type: doc_block - indent: '' - range: - start: - line: 188 - col: 0 - end: - line: 191 - col: 0 - hash: e006425ac24cd772 - context_hash: de7cc417de1b3246 -- key: mathlib.type - kind: - type: doc_block - indent: '' - range: - start: - line: 196 - col: 0 - end: - line: 199 - col: 0 - hash: '963bb5a28f874620' - context_hash: de7cc417de1b3246 -- key: mathlib.ult - kind: - type: doc_block - indent: '' - range: - start: - line: 204 - col: 0 - end: - line: 207 - col: 0 - hash: '2fa6304df1df9a20' - context_hash: de7cc417de1b3246 -- key: mathlib.pow - kind: - type: doc_block - indent: '' - range: - start: - line: 213 - col: 0 - end: - line: 215 - col: 0 - hash: '42724ba1a56090e7' - context_hash: de7cc417de1b3246 -- key: mathlib.pow.param.x - kind: - type: line_tail - prefix: '' - range: - start: - line: 215 - col: 19 - end: - line: 215 - col: 27 - hash: fa1421b96f3eabc1 - context_hash: a43ffd9c02dc61f0 -- key: mathlib.pow.param.y - kind: - type: line_tail - prefix: '' - range: - start: - line: 216 - col: 19 - end: - line: 216 - col: 31 - hash: '5e06156f6c04648b' - context_hash: '7d56446027d998a5' -- key: mathlib.atan2 - kind: - type: doc_block - indent: '' - range: - start: - line: 221 - col: 0 - end: - line: 227 - col: 0 - hash: '97077dd764611e12' - context_hash: de7cc417de1b3246 -- key: mathlib.log10 - kind: - type: doc_block - indent: '' - range: - start: - line: 233 - col: 0 - end: - line: 235 - col: 0 - hash: '7eeb848cac8c5c80' - context_hash: de7cc417de1b3246 -- key: mathlib.cosh - kind: - type: doc_block - indent: '' - range: - start: - line: 240 - col: 0 - end: - line: 242 - col: 0 - hash: '3f0d7a29939c3308' - context_hash: de7cc417de1b3246 -- key: mathlib.sinh - kind: - type: doc_block - indent: '' - range: - start: - line: 247 - col: 0 - end: - line: 249 - col: 0 - hash: '16630a06699d002a' - context_hash: de7cc417de1b3246 -- key: mathlib.tanh - kind: - type: doc_block - indent: '' - range: - start: - line: 254 - col: 0 - end: - line: 256 - col: 0 - hash: '05129308e84b8d36' - context_hash: de7cc417de1b3246 -- key: mathlib.frexp - kind: - type: doc_block - indent: '' - range: - start: - line: 261 - col: 0 - end: - line: 264 - col: 0 - hash: '03448afbb8de9e62' - context_hash: de7cc417de1b3246 -- key: mathlib.ldexp - kind: - type: doc_block - indent: '' - range: - start: - line: 269 - col: 0 - end: - line: 271 - col: 0 - hash: fdabbcfb70f65661 - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/math/zh_CN.yaml b/crates/glua_ls/std_i18n/math/zh_CN.yaml deleted file mode 100644 index 5be15e9a9..000000000 --- a/crates/glua_ls/std_i18n/math/zh_CN.yaml +++ /dev/null @@ -1,125 +0,0 @@ -mathlib.abs: | - 返回 `x` 的绝对值。(整数/浮点数) - -mathlib.acos: | - 返回 `x` 的反余弦值(以弧度为单位)。 - -mathlib.asin: | - 返回 `x` 的反正弦值(以弧度为单位)。 - -mathlib.atan: | - 返回 `y/x` 的反正切值(以弧度为单位),通过两个参数的符号来确定 - 结果所在的象限。(它也正确处理 `x` 为零的情况。) - - `x` 的默认值为 1,因此调用 `math.atan(y)` 返回 `y` 的反正切值。 - -mathlib.ceil: | - 返回大于或等于 `x` 的最小整数。 - -mathlib.cos: | - 返回 `x` 的余弦值(假设为弧度)。 - -mathlib.deg: | - 将角度 `x` 从弧度转换为角度。 - -mathlib.exp: | - 返回 *e^x* 的值(其中 e 是自然对数的底)。 - -mathlib.floor: | - 返回小于或等于 `x` 的最大整数。 - -mathlib.fmod: | - 返回 `x` 除以 `y` 的余数,该除法将商向零取整。(整数/浮点数) - -mathlib.huge: | - 浮点值 `HUGE_VAL`,一个大于任何其他数值的值。 - 它是 INF 值,大于 math.maxinteger。 - -mathlib.log: | - 返回 `x` 在给定底数下的对数。`base` 的默认值为 *e* - (因此函数返回 `x` 的自然对数)。 - -mathlib.max: | - 返回具有最大值的参数,根据 Lua 运算符 `<` 比较。(整数/浮点数) - -mathlib.maxinteger: | - 整数类型的最大值。 - -mathlib.min: | - 返回具有最小值的参数,根据 Lua 运算符 `<` 比较。(整数/浮点数) - -mathlib.mininteger: | - 整数类型的最小值。 - -mathlib.modf: | - 返回 `x` 的整数部分和小数部分。其第二个结果始终为浮点数。 - -mathlib.pi: | - π 的值。 - -mathlib.rad: | - 将角度 `x` 从角度转换为弧度。 - -mathlib.random: | - 当不带参数调用时,返回一个在范围 *[0,1)* 内均匀分布的伪随机浮点数。 - 当带两个整数 `m` 和 `n` 调用时,`math.random` 返回一个在范围 *[m, n]* - 内均匀分布的伪随机整数。调用 `math.random(n)` 相当于 `math.random(1,n)`。 - -mathlib.randomseed: | - 将 `x` 设置为伪随机生成器的“种子”:相同的种子产生相同的数字序列。 - -mathlib.sin: | - 返回 `x` 的正弦值(假设为弧度)。 - -mathlib.sqrt: | - 返回 `x` 的平方根。(你也可以使用表达式 `x^0.5` 来计算此值。) - -mathlib.tan: | - 返回 `x` 的正切值(假设为弧度)。 - -mathlib.tointeger: | - 如果值 `x` 可转换为整数,则返回该整数。 - 否则,返回 `nil`。 - -mathlib.type: | - 如果 `x` 是整数,返回 "`integer`";如果它是浮点数,返回 "`float`"; - 如果 `x` 不是数字,返回 **nil**。 - -mathlib.ult: | - 返回一个布尔值,当且仅当整数 `m` 在作为无符号整数与 `n` 比较时 - 小于 `n` 时为 true。 - -mathlib.pow: | - 返回 `x` 的 `y` 次幂。(x^y) - -mathlib.pow.param.x: | - 底数 - -mathlib.pow.param.y: | - 指数 - -mathlib.atan2: | - 返回 `y/x` 的反正切值(以弧度为单位),通过两个参数的符号来确定 - 结果所在的象限。(它也正确处理 `x` 为零的情况。) - - 注意:在某些 Lua 实现中,此函数相当于 `math.atan(y, x)`。 - -mathlib.log10: | - 返回 `x` 的以 10 为底的对数。 - -mathlib.cosh: | - 返回 `x` 的双曲余弦值。 - -mathlib.sinh: | - 返回 `x` 的双曲正弦值。 - -mathlib.tanh: | - 返回 `x` 的双曲正切值。 - -mathlib.frexp: | - 返回 `m` 和 `e`,使得 *x = m2^e*,`e` 是整数,且 `m` 的绝对值 - 在范围 *[0.5, 1)* 内(或者当 `x` 为零时为零)。 - -mathlib.ldexp: | - 返回 *m2e* (`e` 应该是整数)。 - diff --git a/crates/glua_ls/std_i18n/os/meta.yaml b/crates/glua_ls/std_i18n/os/meta.yaml deleted file mode 100644 index bcf9ef757..000000000 --- a/crates/glua_ls/std_i18n/os/meta.yaml +++ /dev/null @@ -1,408 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: os.lua -entries: -- key: oslib.clock - kind: - type: doc_block - indent: '' - range: - start: - line: 18 - col: 0 - end: - line: 21 - col: 0 - hash: '9b2a917ccb76530b' - context_hash: de7cc417de1b3246 -- key: std.osdateparam.field.year - kind: - type: line_tail - prefix: '' - range: - start: - line: 25 - col: 30 - end: - line: 25 - col: 41 - hash: '30212e7efed3b741' - context_hash: '0a791cb2212aaf50' -- key: std.osdateparam.field.month - kind: - type: line_tail - prefix: '' - range: - start: - line: 26 - col: 31 - end: - line: 26 - col: 35 - hash: '38cea8f11113c9ac' - context_hash: df63c840e75bc2b4 -- key: std.osdateparam.field.day - kind: - type: line_tail - prefix: '' - range: - start: - line: 27 - col: 29 - end: - line: 27 - col: 33 - hash: '38c79ff1110d9bb3' - context_hash: c7ee7b25b9a1b737 -- key: std.osdateparam.field.hour - kind: - type: line_tail - prefix: '' - range: - start: - line: 28 - col: 33 - end: - line: 28 - col: 37 - hash: c772bdf95d76417d - context_hash: '1e3e8ce1df18ca99' -- key: std.osdateparam.field.min - kind: - type: line_tail - prefix: '' - range: - start: - line: 29 - col: 32 - end: - line: 29 - col: 36 - hash: c76fabf95d73ed10 - context_hash: a3b24a481fcf318e -- key: std.osdateparam.field.sec - kind: - type: line_tail - prefix: '' - range: - start: - line: 30 - col: 32 - end: - line: 30 - col: 57 - hash: '7de6ed0043babdcb' - context_hash: '20d6a60fad2254b4' -- key: std.osdateparam.field.wday - kind: - type: line_tail - prefix: '' - range: - start: - line: 31 - col: 33 - end: - line: 31 - col: 49 - hash: d3245cd249132d0f - context_hash: '6fffb0e100303b1e' -- key: std.osdateparam.field.yday - kind: - type: line_tail - prefix: '' - range: - start: - line: 32 - col: 33 - end: - line: 32 - col: 38 - hash: '88e1499ffa2db2cc' - context_hash: e6bdbe72de1add39 -- key: std.osdateparam.field.isdst - kind: - type: line_tail - prefix: '' - range: - start: - line: 33 - col: 25 - end: - line: 33 - col: 57 - hash: '0dca085c7f2cce30' - context_hash: '6c5109bc12943f87' -- key: std.osdate.field.year - kind: - type: line_tail - prefix: '' - range: - start: - line: 36 - col: 30 - end: - line: 36 - col: 41 - hash: '30212e7efed3b741' - context_hash: '0a791cb2212aaf50' -- key: std.osdate.field.month - kind: - type: line_tail - prefix: '' - range: - start: - line: 37 - col: 31 - end: - line: 37 - col: 35 - hash: '38cea8f11113c9ac' - context_hash: df63c840e75bc2b4 -- key: std.osdate.field.day - kind: - type: line_tail - prefix: '' - range: - start: - line: 38 - col: 29 - end: - line: 38 - col: 33 - hash: '38c79ff1110d9bb3' - context_hash: c7ee7b25b9a1b737 -- key: std.osdate.field.hour - kind: - type: line_tail - prefix: '' - range: - start: - line: 39 - col: 30 - end: - line: 39 - col: 34 - hash: c772bdf95d76417d - context_hash: '7a266681ff6b0381' -- key: std.osdate.field.min - kind: - type: line_tail - prefix: '' - range: - start: - line: 40 - col: 29 - end: - line: 40 - col: 33 - hash: c76fabf95d73ed10 - context_hash: '7e16ebb1362bff6e' -- key: std.osdate.field.sec - kind: - type: line_tail - prefix: '' - range: - start: - line: 41 - col: 29 - end: - line: 41 - col: 54 - hash: '7de6ed0043babdcb' - context_hash: '7086cca1b94085fe' -- key: std.osdate.field.wday - kind: - type: line_tail - prefix: '' - range: - start: - line: 42 - col: 30 - end: - line: 42 - col: 46 - hash: d3245cd249132d0f - context_hash: a5762a773c0510d8 -- key: std.osdate.field.yday - kind: - type: line_tail - prefix: '' - range: - start: - line: 43 - col: 30 - end: - line: 43 - col: 35 - hash: '88e1499ffa2db2cc' - context_hash: '6f5f168e7a2ba7fb' -- key: std.osdate.field.isdst - kind: - type: line_tail - prefix: '' - range: - start: - line: 44 - col: 24 - end: - line: 44 - col: 56 - hash: '0dca085c7f2cce30' - context_hash: '7a31b4e4dcf538ee' -- key: oslib.date - kind: - type: doc_block - indent: '' - range: - start: - line: 46 - col: 0 - end: - line: 78 - col: 0 - hash: '74c81b78019554e4' - context_hash: de7cc417de1b3246 -- key: oslib.difftime - kind: - type: doc_block - indent: '' - range: - start: - line: 85 - col: 0 - end: - line: 89 - col: 0 - hash: '6a0b703ea28839f7' - context_hash: de7cc417de1b3246 -- key: oslib.execute@>5.2 - kind: - type: doc_block - indent: '' - range: - start: - line: 95 - col: 0 - end: - line: 108 - col: 0 - hash: '534ea24d2e6da6c0' - context_hash: de7cc417de1b3246 -- key: oslib.execute@5.1,JIT - kind: - type: doc_block - indent: '' - range: - start: - line: 116 - col: 0 - end: - line: 121 - col: 0 - hash: cfd1f1e32f605f58 - context_hash: de7cc417de1b3246 -- key: oslib.exit@>5.2,JIT - kind: - type: doc_block - indent: '' - range: - start: - line: 126 - col: 0 - end: - line: 134 - col: 0 - hash: '4fda0e69acf947bb' - context_hash: de7cc417de1b3246 -- key: oslib.exit@5.1 - kind: - type: doc_block - indent: '' - range: - start: - line: 140 - col: 0 - end: - line: 143 - col: 0 - hash: ef5d094abaa551fd - context_hash: de7cc417de1b3246 -- key: oslib.getenv - kind: - type: doc_block - indent: '' - range: - start: - line: 147 - col: 0 - end: - line: 150 - col: 0 - hash: '101bdb2422c380c0' - context_hash: de7cc417de1b3246 -- key: oslib.remove - kind: - type: doc_block - indent: '' - range: - start: - line: 154 - col: 0 - end: - line: 158 - col: 0 - hash: c3e36a31c22ce5ea - context_hash: de7cc417de1b3246 -- key: oslib.rename - kind: - type: doc_block - indent: '' - range: - start: - line: 163 - col: 0 - end: - line: 167 - col: 0 - hash: '400a03f66b702215' - context_hash: de7cc417de1b3246 -- key: oslib.setlocale - kind: - type: doc_block - indent: '' - range: - start: - line: 173 - col: 0 - end: - line: 190 - col: 0 - hash: d4332152c62608ba - context_hash: de7cc417de1b3246 -- key: oslib.time - kind: - type: doc_block - indent: '' - range: - start: - line: 195 - col: 0 - end: - line: 217 - col: 0 - hash: fcaa6c7521e56af4 - context_hash: de7cc417de1b3246 -- key: oslib.tmpname - kind: - type: doc_block - indent: '' - range: - start: - line: 221 - col: 0 - end: - line: 234 - col: 0 - hash: '7d4f7ce2378cf7da' - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/os/zh_CN.yaml b/crates/glua_ls/std_i18n/os/zh_CN.yaml deleted file mode 100644 index c3f035a34..000000000 --- a/crates/glua_ls/std_i18n/os/zh_CN.yaml +++ /dev/null @@ -1,176 +0,0 @@ -oslib.clock: | - 返回程序使用的 CPU 时间(以秒为单位)的近似值。 - -std.osdateparam.field.year: | - 四位年份 - -std.osdateparam.field.month: | - 月 (1-12) - -std.osdateparam.field.day: | - 日 (1-31) - -std.osdateparam.field.hour: | - 时 (0-23) - -std.osdateparam.field.min: | - 分 (0-59) - -std.osdateparam.field.sec: | - 秒 (0-61,包含闰秒) - -std.osdateparam.field.wday: | - 星期几 (1-7,星期日为 1) - -std.osdateparam.field.yday: | - 一年中的哪一天 (1-366) - -std.osdateparam.field.isdst: | - 夏令时标志,布尔值。 - -std.osdate.field.year: | - 四位年份 - -std.osdate.field.month: | - 月 (1-12) - -std.osdate.field.day: | - 日 (1-31) - -std.osdate.field.hour: | - 时 (0-23) - -std.osdate.field.min: | - 分 (0-59) - -std.osdate.field.sec: | - 秒 (0-61,包含闰秒) - -std.osdate.field.wday: | - 星期几 (1-7,星期日为 1) - -std.osdate.field.yday: | - 一年中的哪一天 (1-366) - -std.osdate.field.isdst: | - 夏令时标志,布尔值。 - -oslib.date: | - 返回包含日期和时间的字符串或表,格式由字符串 `format` 指定。 - - 如果提供了 `time` 参数,则格式化该时间(有关此值的描述,请参见 - `os.time` 函数)。否则,`date` 格式化当前时间。 - - 如果 `format` 以 '`!`' 开头,则日期按协调世界时(UTC)格式化。 - 在此可选字符之后,如果 `format` 是字符串 "`*t`",则 `date` - 返回一个包含以下字段的表: - - **`year`** (四位年份) - **`month`** (1-12) - **`day`** (1-31) - **`hour`** (0-23) - **`min`** (0-59) - **`sec`** (0-61, 包含闰秒) - **`wday`** (星期几, 1-7, 星期日为 1) - **`yday`** (一年中的哪一天, 1-366) - **`isdst`** (夏令时标志, 布尔值). 如果信息不可用,可能会缺少此字段。 - - 如果 `format` 不是 "`*t`",则 `date` 将日期作为字符串返回, - 其格式化规则与 ISO C 函数 `strftime` 相同。 - - 当不带参数调用时,`date` 返回依赖于主机系统和当前区域设置的 - 合理日期和时间表示。(更具体地说,`os.date()` 相当于 `os.date("%c")`。) - - 在非 POSIX 系统上,此函数可能不是线程安全的,因为它依赖于 - C 函数 `gmtime` 和 `localtime`。 - -oslib.difftime: | - 返回从时间 `t1` 到时间 `t2` 的差值(以秒为单位)。(其中时间是 - `os.time` 返回的值)。在 POSIX、Windows 和其他一些系统中, - 此值正好是 `t2`-`t1`。 - -oslib.execute@>5.2: | - 此函数相当于 C 函数 `system`。它传递 `command` 给操作系统 shell 执行。 - 如果命令成功终止,它的第一个结果是 **true**,否则是 **nil**。 - - 在第一个结果之后,函数返回一个字符串加上一个数字: - - **"exit"**: 命令正常终止;后面的数字是命令的退出状态。 - - **"signal"**: 命令被信号终止;后面的数字是终止命令的信号。 - - 当不带命令调用时,如果 shell 可用,`os.execute` 返回 true。 - -# This function is equivalent to the C function system. It passes command to -# be executed by an operating system shell. It returns a status code, which is -# system-dependent. If command is absent, then it returns nonzero if a shell -# is available and zero otherwise. -oslib.execute@5.1,JIT: "" - -oslib.exit@>5.2,JIT: | - 调用 ISO C 函数 `exit` 以终止宿主程序。 - - 如果 `code` 为 **true**,返回的状态是 `EXIT_SUCCESS`; - - 如果 `code` 为 **false**,返回的状态是 `EXIT_FAILURE`; - - 如果 `code` 是数字,返回的状态就是该数字。 - - `code` 的默认值为 **true**。 - - 如果可选的第二个参数 `close` 为 true,则在退出前关闭 Lua 状态。 - -# Calls the C function exit, with an optional `code`, to terminate the host -# program. The default value for `code` is the success code. -oslib.exit@5.1: "" - -oslib.getenv: | - 返回进程环境变量 `varname` 的值,如果未定义该变量,则返回 **nil**。 - -oslib.remove: | - 删除具有给定名称的文件(或在 POSIX 系统上的空目录)。 - 如果此函数失败,它返回 **nil**,加上描述错误的字符串和错误代码。 - 否则,它返回 true。 - -oslib.rename: | - 将名为 `oldname` 的文件或目录重命名为 `newname`。如果此函数失败, - 它返回 **nil**,加上描述错误的字符串和错误代码。否则,它返回 true。 - -oslib.setlocale: | - 设置程序的当前区域设置。`locale` 是一个依赖于系统的字符串,指定区域设置; - `category` 是一个可选字符串,描述要更改的类别:`"all"`, `"collate"`, - `"ctype"`, `"monetary"`, `"numeric"`, 或 `"time"`;默认类别是 `"all"`。 - 该函数返回新区域设置的名称,如果无法满足请求,则返回 **nil**。 - - 如果 `locale` 是空字符串,当前区域设置将设置为实现定义的本机区域设置。 - 如果 `locale` 是字符串 "`C`",当前区域设置将设置为标准 C 区域设置。 - - 当使用 **nil** 作为第一个参数调用时,此函数仅返回给定类别的当前区域设置名称。 - - 此函数可能不是线程安全的,因为它依赖于 C 函数 `setlocale`。 - -oslib.time: | - 当不带参数调用时返回当前时间,或者返回由给定表指定的日期和时间对应的时间。 - 此表必须有 `year`、`month` 和 `day` 字段,并且可以有 `hour`(默认为 12)、 - `min`(默认为 0)、`sec`(默认为 0)和 `isdst`(默认为 **nil**)。 - 忽略其他字段。有关这些字段的描述,请参见 `os.date` 函数。 - - 调用该函数时,这些字段中的值不需要在其有效范围内。例如,如果 `sec` 为 -10, - 则表示在其他字段指定的时间之前 10 秒;如果 `hour` 为 1000,则表示在 - 其他字段指定的时间之后 1000 小时。 - - 返回的值是一个数字,其含义取决于你的系统。在 POSIX、Windows 和其他一些系统中, - 此数字计算自某个给定开始时间("epoch")以来的秒数。在其他系统中, - 含义未指定,`time` 返回的数字只能用作 `os.date` 和 `os.difftime` 的参数。 - - 当使用表调用时,`os.time` 还会规范化 `os.date` 函数中记录的所有字段, - 以便它们表示与调用前相同的时间,但值在其有效范围内。 - -oslib.tmpname: | - 返回一个包含可用于临时文件文件名的字符串。该文件必须在使用前显式打开, - 不再需要时显式删除。 - - 在某些系统(POSIX)上,此函数还会创建一个具有该名称的文件,以避免安全风险。 - (其他人可能会在获取名称和创建文件之间的时间内创建具有错误权限的文件。) - 你仍然必须打开文件才能使用它并删除它(即使你不使用它)。 - - 如果可能,你可能更喜欢使用 `io.tmpfile`,它在程序结束时自动删除文件。 - diff --git a/crates/glua_ls/std_i18n/package/meta.yaml b/crates/glua_ls/std_i18n/package/meta.yaml deleted file mode 100644 index 1f49337a7..000000000 --- a/crates/glua_ls/std_i18n/package/meta.yaml +++ /dev/null @@ -1,122 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: package.lua -entries: -- key: packagelib.config - kind: - type: doc_block - indent: '' - range: - start: - line: 18 - col: 0 - end: - line: 31 - col: 43 - hash: '99dcc3d5945c2f62' - context_hash: de7cc417de1b3246 -- key: packagelib.cpath - kind: - type: doc_block - indent: '' - range: - start: - line: 39 - col: 0 - end: - line: 45 - col: 16 - hash: e9dcc7a0e4b7131c - context_hash: de7cc417de1b3246 -- key: packagelib.loaded - kind: - type: doc_block - indent: '' - range: - start: - line: 49 - col: 0 - end: - line: 55 - col: 55 - hash: '43e40c3e74a6b5b1' - context_hash: de7cc417de1b3246 -- key: packagelib.loadlib - kind: - type: doc_block - indent: '' - range: - start: - line: 58 - col: 0 - end: - line: 77 - col: 0 - hash: '3ba962f5ba70f54e' - context_hash: de7cc417de1b3246 -- key: packagelib.path - kind: - type: doc_block - indent: '' - range: - start: - line: 82 - col: 0 - end: - line: 89 - col: 21 - hash: '71bb304c9691ddb4' - context_hash: de7cc417de1b3246 -- key: packagelib.preload - kind: - type: doc_block - indent: '' - range: - start: - line: 92 - col: 0 - end: - line: 96 - col: 55 - hash: '32d7fb054c43a96e' - context_hash: de7cc417de1b3246 -- key: packagelib.searchers - kind: - type: doc_block - indent: '' - range: - start: - line: 103 - col: 0 - end: - line: 145 - col: 46 - hash: '9715fac064e1ab65' - context_hash: de7cc417de1b3246 -- key: packagelib.searchpath - kind: - type: doc_block - indent: '' - range: - start: - line: 149 - col: 0 - end: - line: 166 - col: 0 - hash: e3a24a24ba7ced35 - context_hash: de7cc417de1b3246 -- key: packagelib.seeall - kind: - type: doc_block - indent: '' - range: - start: - line: 174 - col: 0 - end: - line: 177 - col: 0 - hash: dac20d6d3e907ba3 - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/package/zh_CN.yaml b/crates/glua_ls/std_i18n/package/zh_CN.yaml deleted file mode 100644 index 75618134a..000000000 --- a/crates/glua_ls/std_i18n/package/zh_CN.yaml +++ /dev/null @@ -1,84 +0,0 @@ -packagelib.config: | - 一个字符串,用于描述一些与包相关的编译期配置。该字符串由多行组成: - - - 第一行:目录分隔符字符串。Windows 默认为 `\`,其他系统默认为 `/`。 - - 第二行:路径中分隔模板的字符。默认为 `;`。 - - 第三行:标记模板中替换点的字符串。默认为 `?`。 - - 第四行:在 Windows 的路径中,会被替换为可执行文件目录的字符串。默认为 `!`。 - - 第五行:在构建 `luaopen_` 函数名时,用于忽略其后的所有文本的标记。默认为 `-`。 - -packagelib.cpath: | - `require` 用于搜索 C 加载器的路径。 - - Lua 初始化 `package.cpath` 的方式与初始化 Lua 路径 `package.path` 类似:使用环境变量 - `LUA_CPATH_5_4` 或 `LUA_CPATH`,或者使用 `luaconf.h` 中定义的默认路径。 - -packagelib.loaded: | - 一个表,供 `require` 用于控制哪些模块已经被加载。 - - 当你 `require` 模块 `modname` 且 `package.loaded[modname]` 不为 false 时, - `require` 会直接返回其中存储的值。 - - 该变量只是对真实表的引用;对该变量赋值不会改变 `require` 使用的表。 - -packagelib.loadlib: | - 将宿主程序与 C 库 `libname` 进行动态链接。 - - 如果 `funcname` 为 `"*"`,则只链接该库,使库导出的符号可供其他动态链接库使用。 - 否则,它会在库中查找函数 `funcname`,并以 C 函数形式返回该函数。因此,`funcname` - 必须符合 `lua_CFunction` 原型(参见 `lua_CFunction`)。 - - 这是一个底层函数,它完全绕过包与模块系统。与 `require` 不同,它不会执行任何路径搜索, - 也不会自动添加扩展名。`libname` 必须是 C 库的完整文件名(必要时包含路径与扩展名)。 - `funcname` 必须是该 C 库导出的精确名称(可能依赖所使用的 C 编译器与链接器)。 - - 标准 C 不支持此函数,因此它仅在部分平台可用(Windows、Linux、Mac OS X、Solaris、BSD, - 以及其他支持 `dlfcn` 标准的 Unix 系统)。 - -packagelib.path: | - `require` 用于搜索 Lua 加载器的路径。 - - 启动时,如果环境变量已定义,Lua 会用环境变量 `LUA_PATH_5_4` 或 `LUA_PATH` 的值来初始化该变量; - 否则使用 `luaconf.h` 中定义的默认路径。环境变量值中的任何 ";;" 都会被替换为默认路径。 - -packagelib.preload: | - 一个表,用于存放特定模块的加载器(参见 `require`)。 - - 该变量只是对真实表的引用;对该变量赋值不会改变 `require` 使用的表。 - -packagelib.searchers: | - `require` 用于控制如何加载模块的表。 - - 该表中的每个条目都是一个 *搜索器函数*。查找模块时,`require` 会按升序依次调用这些搜索器, - 并将模块名(传给 `require` 的参数)作为唯一参数。搜索器可以返回另一个函数(模块 *加载器*) - 以及一个会传给该加载器的额外值;或者返回一个字符串来解释为什么没有找到该模块(如果无话可说则返回 **nil**)。 - - Lua 会用四个搜索器初始化该表: - - 第一个搜索器:在 `package.preload` 表中查找加载器。 - - 第二个搜索器:使用 `package.path`,按 `package.searchpath` 的规则将模块作为 Lua 库进行查找。 - - 第三个搜索器:使用 `package.cpath`,按 `package.searchpath` 的规则将模块作为 C 库进行查找; - 找到 C 库后先进行动态链接,然后按规则构造并查找 `luaopen_...` 的 C 函数作为加载器。 - - 第四个搜索器:尝试 *一体化加载器*;在 C 路径中查找给定模块根名的库(例如 `a.b.c` 会查找 `a`), - 若找到则在其中查找子模块的 open 函数(例如 `luaopen_a_b_c`)。 - - 除第一个(preload)外,其他搜索器都会把 `package.searchpath` 返回的“模块被找到的文件名”作为额外值返回; - 第一个搜索器不返回额外值。 - -packagelib.searchpath: | - 在给定路径中搜索指定名称。 - - 路径是一个字符串,其中包含一系列由分号分隔的 *模板*。对每个模板,本函数会将模板中的每个问号(如有) - 替换为名称的一个副本,并将其中所有出现的 `sep`(默认是点号 `.`)替换为 `rep` - (默认是系统的目录分隔符),然后尝试打开得到的文件名。 - - 例如,如果路径为 - > "`./?.lua;./?.lc;/usr/local/?/init.lua`" - 搜索名称 `foo.a` 时会按顺序尝试打开 `./foo/a.lua`、`./foo/a.lc`、`/usr/local/foo/a/init.lua`。 - - 返回第一个能够以只读模式打开的文件(在关闭文件后)的最终文件名;如果都失败,则返回 **nil** 以及错误消息 - (错误消息会列出所有尝试打开的文件名)。 - -packagelib.seeall: | - 为 `module` 设置一个元表,其 `__index` 字段指向全局环境,使该模块从全局环境继承值。 - 用作 `module` 函数的一个选项。 - diff --git a/crates/glua_ls/std_i18n/string/buffer/meta.yaml b/crates/glua_ls/std_i18n/string/buffer/meta.yaml deleted file mode 100644 index c7de9179e..000000000 --- a/crates/glua_ls/std_i18n/string/buffer/meta.yaml +++ /dev/null @@ -1,330 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: string/buffer.lua -entries: -- key: buffer - kind: - type: doc_block - indent: '' - range: - start: - line: 5 - col: 0 - end: - line: 119 - col: 464 - hash: '3f1fb290916e15e3' - context_hash: '0ffffd55da93e0dd' -- key: buf - kind: - type: doc_block - indent: '' - range: - start: - line: 122 - col: 0 - end: - line: 130 - col: 0 - hash: b876ec9e1fe5d76e - context_hash: '1cdc5f30a0bc1868' -- key: string.buffer.data - kind: - type: doc_block - indent: '' - range: - start: - line: 134 - col: 0 - end: - line: 136 - col: 0 - hash: '4921c42ac9b4e6ba' - context_hash: bf68051f04c19a9f -- key: buf.put - kind: - type: doc_block - indent: '' - range: - start: - line: 139 - col: 0 - end: - line: 143 - col: 0 - hash: '6759e329f138d41c' - context_hash: '7f31a0cf523f80b6' -- key: buf.putf - kind: - type: doc_block - indent: '' - range: - start: - line: 149 - col: 0 - end: - line: 151 - col: 0 - hash: '134720a52ebbce50' - context_hash: b7465420402bad59 -- key: buf.putcdata - kind: - type: doc_block - indent: '' - range: - start: - line: 157 - col: 0 - end: - line: 159 - col: 0 - hash: '8d9ba1e277b6ffd1' - context_hash: '05d9961b521b6552' -- key: buf.set - kind: - type: doc_block - indent: '' - range: - start: - line: 165 - col: 0 - end: - line: 173 - col: 0 - hash: baf4bbe32512a07f - context_hash: a1772f562f581e13 -- key: buf.reset - kind: - type: doc_block - indent: '' - range: - start: - line: 178 - col: 0 - end: - line: 179 - col: 0 - hash: '6383ef1d75c1fbd4' - context_hash: '4e35a14b657e62d6' -- key: buf.free - kind: - type: doc_block - indent: '' - range: - start: - line: 183 - col: 0 - end: - line: 185 - col: 222 - hash: b79f21749a622ecf - context_hash: a267daf18bd68655 -- key: buf.reserve - kind: - type: doc_block - indent: '' - range: - start: - line: 189 - col: 0 - end: - line: 207 - col: 0 - hash: '5b5a385de244ace2' - context_hash: '18f3e3808bf6d879' -- key: buf.reserve.return.1 - kind: - type: line_tail - prefix: '' - range: - start: - line: 208 - col: 26 - end: - line: 208 - col: 84 - hash: '606a76765af0a5fe' - context_hash: '8e6f550d60044373' -- key: buf.reserve.return.2 - kind: - type: line_tail - prefix: '' - range: - start: - line: 209 - col: 26 - end: - line: 209 - col: 52 - hash: c9ff7b760da02fe1 - context_hash: '7058075639983595' -- key: buf.commit - kind: - type: doc_block - indent: '' - range: - start: - line: 213 - col: 0 - end: - line: 214 - col: 0 - hash: '6f6b683663e06a74' - context_hash: '294d8a1b3240a9b6' -- key: buf.skip - kind: - type: doc_block - indent: '' - range: - start: - line: 219 - col: 0 - end: - line: 220 - col: 0 - hash: '2baf988594a98193' - context_hash: c65bbb20161dd0ab -- key: buf.get - kind: - type: doc_block - indent: '' - range: - start: - line: 224 - col: 0 - end: - line: 228 - col: 0 - hash: b695a85360eb9976 - context_hash: dfe1b276c890bc83 -- key: buf.tostring - kind: - type: doc_block - indent: '' - range: - start: - line: 233 - col: 0 - end: - line: 236 - col: 0 - hash: be09e35cc640a966 - context_hash: '23d9f8a403da35b3' -- key: buf.ref - kind: - type: doc_block - indent: '' - range: - start: - line: 240 - col: 0 - end: - line: 258 - col: 0 - hash: ec59ac6395339033 - context_hash: '1f64bfaedf7538d6' -- key: buf.ref.return.1 - kind: - type: line_tail - prefix: '' - range: - start: - line: 258 - col: 26 - end: - line: 258 - col: 90 - hash: '14518ff520084405' - context_hash: '0b00828802cffd98' -- key: buf.ref.return.2 - kind: - type: line_tail - prefix: '' - range: - start: - line: 259 - col: 23 - end: - line: 259 - col: 59 - hash: afbd7c3182b59c06 - context_hash: ccd63ba92ed9427c -- key: buf.encode - kind: - type: doc_block - indent: '' - range: - start: - line: 262 - col: 0 - end: - line: 265 - col: 0 - hash: a38b8742aae7dbe7 - context_hash: c397c8c88a124c49 -- key: buf.decode - kind: - type: doc_block - indent: '' - range: - start: - line: 270 - col: 0 - end: - line: 280 - col: 0 - hash: '4e30c26d847cb8a9' - context_hash: '1f16a1deba621209' -- key: buffer.encode - kind: - type: doc_block - indent: '' - range: - start: - line: 284 - col: 0 - end: - line: 287 - col: 0 - hash: cc0765fe93da76b2 - context_hash: a78581f832d11af0 -- key: buffer.decode - kind: - type: doc_block - indent: '' - range: - start: - line: 291 - col: 0 - end: - line: 300 - col: 0 - hash: '33e0c0c0e0812524' - context_hash: '4247bd38d3781068' -- key: buffer.new - kind: - type: doc_block - indent: '' - range: - start: - line: 307 - col: 0 - end: - line: 313 - col: 0 - hash: '570e5bee37744a67' - context_hash: '2a2cd9d61df2a763' -- key: string.buffer.serialization.opts - kind: - type: doc_block - indent: '' - range: - start: - line: 318 - col: 0 - end: - line: 349 - col: 0 - hash: e021fbee65992fe5 - context_hash: '556dcc131bdfaac4' diff --git a/crates/glua_ls/std_i18n/string/buffer/zh_CN.yaml b/crates/glua_ls/std_i18n/string/buffer/zh_CN.yaml deleted file mode 100644 index a75968b17..000000000 --- a/crates/glua_ls/std_i18n/string/buffer/zh_CN.yaml +++ /dev/null @@ -1,227 +0,0 @@ -buffer: | - 字符串缓冲区库允许对类字符串数据进行高性能操作。 - - 与 Lua 字符串(常量)不同,字符串缓冲区是可变的 8 位(二进制透明)字符序列。数据可以存储、格式化并编码到字符串缓冲区中,之后再转换、提取或解码。 - - 便捷的字符串缓冲区 API 简化了常见的字符串处理任务,否则这些任务通常需要创建大量中间字符串。字符串缓冲区通过消除冗余的内存拷贝、对象创建、字符串驻留以及垃圾回收开销来提升性能;与 FFI 库结合时,还能实现零拷贝操作。 - - 字符串缓冲区库还包含一个用于 Lua 对象的高性能序列化器。 - - ## 流式序列化 - - 在某些场景下,希望对大型数据集进行分片序列化(也称 streaming)。该序列化格式可安全拼接并支持流式:可以把多个编码简单地追加到一个缓冲区,之后再分别解码: - - ```lua - local buf = buffer.new() - buf:encode(obj1) - buf:encode(obj2) - local copy1 = buf:decode() - local copy2 = buf:decode() - ``` - - 下面展示如何迭代一个流: - - ```lua - while #buf ~= 0 do - local obj = buf:decode() - -- 对 obj 做些处理。 - end - ``` - - 由于该序列化格式不会在编码前附加长度信息,网络应用可能还需要同时传输长度。 - - 该序列化格式主要供 LuaJIT 应用内部使用。序列化数据向上兼容,并可在所有支持的 LuaJIT 平台间移植。 - - 它是 8 位二进制格式,非人类可读,可能包含嵌入的零字节;并且会原样存储嵌入的 Lua 字符串对象(同样是 8 位透明)。编码后的数据可以安全拼接以用于流式处理,并可在之后按“顶层对象”逐个解码。 - - 该编码相对紧凑,但优化目标是最大性能而非最小空间占用;它也能很好地被常见的按字节数据压缩算法压缩。 - - 尽管本文档给出了说明,但该格式明确不打算作为跨语言结构化数据交换的“公共标准”(如 JSON 或 MessagePack);请不要这样使用。 - - 规范以无上下文文法给出,以顶层对象为起点。备选项以 `|` 分隔,`*` 表示重复。分组是隐式的,或用 `{…}` 表示。终结符要么是以字节编码的十六进制数,要么带有 `.format` 后缀。 - - ``` - object → nil | false | true - ``` - -buf: | - 缓冲区对象是由垃圾回收管理的 Lua 对象。通过 `buffer.new()` 创建后,它可以(也应该)在多个操作中复用。当最后一个对缓冲区对象的引用消失后,它最终会由垃圾回收器释放,包括其分配的缓冲区空间。 - - 缓冲区以 FIFO(先进先出)数据结构方式工作:数据可以被追加(写入)到缓冲区末尾,也可以从缓冲区头部被消费(读取);这些操作可以任意混合。 - - 用于保存字符的缓冲区空间由系统自动管理——按需增长,并会回收已消费的空间。如需更细粒度的控制,可使用 `buffer.new(size)` 与 `buf:free()`。 - - 单个缓冲区的最大大小与 Lua 字符串的最大大小相同,略低于 2GB。对于超大数据规模,字符串和缓冲区都不是合适的数据结构——应使用 FFI 库直接映射内存或文件,直到操作系统的虚拟内存限制。 - -string.buffer.data: | - 要写入缓冲区的数据:字符串、数字,或带有 `__tostring` 元方法的对象 `obj`。 - -buf.put: | - 向缓冲区追加一个字符串 `str`、数字 `num`,或任何带有 `__tostring` 元方法的对象 `obj`。多个参数会按给定顺序依次追加。 - - 将一个缓冲区追加到另一个缓冲区是可行的,并会在内部进行短路处理,但仍然会发生一次拷贝;更好的做法是合并写入,尽量使用同一个缓冲区完成写入。 - -buf.putf: | - 将格式化后的参数追加到缓冲区。格式字符串支持与 `string.format()` 相同的选项。 - -buf.putcdata: | - 将 FFI cdata 对象指向的内存中 `len` 个字节追加到缓冲区。该对象需要可转换为(常量)指针。 - -buf.set: | - 该方法允许将一个字符串或 FFI cdata 对象以“零拷贝”的方式作为缓冲区来消费。它会在缓冲区中保存对传入字符串 `str` 或 FFI cdata 对象的引用,并释放原本为缓冲区分配的任何空间。 - - 调用该方法后,缓冲区的行为等同于调用 `buf:free():put(str)` 或 `buf:free():put(cdata, len)`,但只要缓冲区只被消费(读取),数据仅被引用而不会被复制。 - - 如果之后对缓冲区进行写入,则会把被引用的数据拷贝到内部缓冲区,并移除对象引用(写时复制语义)。 - - 保存的引用会作为垃圾回收器的锚点,确保原本传入的字符串或 FFI cdata 对象保持存活。 - -buf.reset: | - 重置(清空)缓冲区。已分配的缓冲区空间不会被释放,并可被复用。 - -buf.free: | - 释放缓冲区对象所占用的缓冲区空间。对象本身仍然存在,处于空状态并可被复用。 - - 注意:通常不需要使用该方法。缓冲区对象被回收时,垃圾回收器会自动释放其缓冲区空间。只有在你需要立即释放关联内存时才应使用此方法。 - -buf.reserve: | - `reserve` 方法会在缓冲区中至少预留 `size` 字节的写入空间,并返回一个指向该空间的 `uint8_t *` FFI cdata 指针 `ptr`。 - - 可用长度(字节)会通过 `len` 返回:它至少为 `size`,但为了便于高效扩容,可能会更大。你可以利用这部分额外空间,也可以忽略 `len`,只使用 `size` 字节。 - - 该方法与 `buf:commit()` 配合,可对 C 风格的 read API 实现零拷贝写入: - - ```lua - local MIN_SIZE = 65536 - repeat - local ptr, len = buf:reserve(MIN_SIZE) - local n = C.read(fd, ptr, len) - if n == 0 then break end -- EOF. - if n < 0 then error("read error") end - buf:commit(n) - until false - ``` - - 预留的写入空间不会被初始化。在调用 `commit` 之前,至少需要把实际使用的 `used` 字节写入该空间。如果没有向缓冲区添加任何内容(例如发生错误),则无需调用 `commit`。 - -buf.reserve.return.1: | - 指向该空间的 `uint8_t *` FFI cdata 指针。 - -buf.reserve.return.2: | - 可用长度(字节)。 - -buf.commit: | - 将先前 `reserve` 返回的写入空间中实际使用的 `used` 字节追加到缓冲区数据中。 - -buf.skip: | - 从缓冲区中跳过(消费)`len` 个字节,最多到当前缓冲区数据长度为止。 - -buf.get: | - 消费缓冲区数据并返回一个或多个字符串。多个参数会按给定顺序依次消费缓冲区数据。 - - - 不带参数调用时,消费整个缓冲区数据。 - - 以数字参数调用时,最多消费 `len` 字节。 - - 参数为 `nil` 时,会消费剩余的缓冲区空间(这通常只应作为最后一个参数使用)。 - - 注意:当长度为 0 或没有剩余缓冲区数据时,返回的是空字符串而不是 `nil`。 - -buf.tostring: | - 从缓冲区数据创建一个字符串,但不会消费数据;缓冲区保持不变。 - - 缓冲区对象还定义了 `__tostring` 元方法。这意味着缓冲区可以传给全局 `tostring()` 函数,以及许多可以用它代替字符串的函数。在诸如 `io.write()` 之类的函数中,一些关键的内部用法会被短路处理,以避免创建中间字符串对象。 - -buf.ref: | - 返回一个指向缓冲区数据的 `uint8_t *` FFI cdata 指针 `ptr`,并通过 `len` 返回缓冲区数据的字节长度。 - - 返回的指针可直接传给需要“缓冲区 + 长度”的 C 函数。你也可以对缓冲区数据进行逐字节读取(`local x = ptr[i]`)或写入(`ptr[i] = 0x40`)。 - - 结合 `buf:skip()` 方法,可以对 C 风格的 write API 实现零拷贝写入: - - ```lua - repeat - local ptr, len = buf:ref() - if len == 0 then break end - local n = C.write(fd, ptr, len) - if n < 0 then error("write error") end - buf:skip(n) - until n >= len - ``` - - 与 Lua 字符串不同,缓冲区数据不会隐式以 0 结尾。把 `ptr` 传给期望以 0 结尾字符串的 C 函数并不安全。如果你不使用 `len`,那很可能是在做错事。 - -buf.ref.return.1: | - 指向缓冲区数据的 `uint8_t *` FFI cdata 指针。 - -buf.ref.return.2: | - 缓冲区数据的字节长度。 - -buf.encode: | - 将 Lua 对象序列化(编码)到缓冲区。 - - 尝试序列化不支持的对象类型、循环引用或嵌套很深的表时,该函数可能会抛出错误。 - -buf.decode: | - 从缓冲区反序列化(解码)一个对象。 - - 返回的对象可以是任意受支持的 Lua 类型——甚至可以是 `nil`。 - - 当输入的数据格式错误或编码数据不完整时,该函数可能会抛出错误。 - - 解码后会把未消费的剩余数据保留在缓冲区中。 - - 如果尝试反序列化 FFI 类型,而 FFI 库未内建或尚未加载,则会抛出错误。 - -buffer.encode: | - 序列化(编码)Lua 对象 `obj`。 - - 尝试序列化不支持的对象类型、循环引用或嵌套很深的表时,该函数可能会抛出错误。 - -buffer.decode: | - 将字符串反序列化(解码)为 Lua 对象。 - - 返回的对象可以是任意受支持的 Lua 类型——甚至可以是 `nil`。 - - 当输入的数据格式错误或编码数据不完整时会抛出错误。 - 当解码单个顶层对象后仍有剩余数据时也会抛出错误。 - - 如果尝试反序列化 FFI 类型,而 FFI 库未内建或尚未加载,则会抛出错误。 - -buffer.new: | - 创建一个新的缓冲区对象。 - - 可选参数 `size` 用于确保最小的初始缓冲区大小。当你预先知道所需缓冲区大小时,这严格来说只是一次优化;无论如何,缓冲区空间都会按需增长。 - - 可选的表参数 `options` 用于设置各种序列化选项。 - -string.buffer.serialization.opts: | - 序列化选项 - - 传给 `buffer.new()` 的 `options` 表可以包含以下成员(全部可选): - - - `dict`:一个 Lua 表,保存一组在你要序列化的对象中经常作为表键出现的字符串字典。序列化时这些键会被紧凑地编码为索引。选择合适的字典可以节省空间并提升序列化性能。 - - `metatable`:一个 Lua 表,保存一组用于你要序列化的表对象的元表字典。 - - `dict` 需要是字符串数组,`metatable` 需要是表数组;两者都从索引 1 开始且不能有空洞(中间不能出现 nil)。这些表会被锚定在缓冲区对象中,并在内部被改造为双向索引(不要自己这么做,只需传入普通数组)。在把它们传给 `buffer.new()` 之后,这些表不得再被修改。 - - 编码器与解码器使用的 `dict` 与 `metatable` 必须一致。把最常见的条目放在前面,并在末尾扩展以保证向后兼容——这样旧的编码仍然能被读取。你也可以将某些索引设为 false 来显式放弃向后兼容;解码使用了这些索引的旧编码时会抛出错误。 - - 编码时,如果某个元表不在 `metatable` 字典中,则会被忽略;解码会返回一个元表为 nil 的表。 - - 注意:解析并准备 `options` 表的开销相对较高。建议只创建一次缓冲区对象并在多次使用中复用。避免混用编码与解码缓冲区,因为 `buf:set()` 会释放已分配的缓冲区空间: - - ```lua - local options = { - dict = { "commonly", "used", "string", "keys" }, - } - local buf_enc = buffer.new(options) - local buf_dec = buffer.new(options) - - local function encode(obj) - return buf_enc:reset():encode(obj):get() - end - - local function decode(str) - return buf_dec:set(str):decode() - end - ``` - diff --git a/crates/glua_ls/std_i18n/string/meta.yaml b/crates/glua_ls/std_i18n/string/meta.yaml deleted file mode 100644 index b725be02d..000000000 --- a/crates/glua_ls/std_i18n/string/meta.yaml +++ /dev/null @@ -1,239 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: string.lua -entries: -- key: string - kind: - type: doc_block - indent: '' - range: - start: - line: 15 - col: 0 - end: - line: 20 - col: 0 - hash: a72ac7db3f7d4603 - context_hash: de7cc417de1b3246 -- key: string.byte - kind: - type: doc_block - indent: '' - range: - start: - line: 23 - col: 0 - end: - line: 30 - col: 0 - hash: '5ca99caefe62dd0b' - context_hash: de7cc417de1b3246 -- key: string.char - kind: - type: doc_block - indent: '' - range: - start: - line: 36 - col: 0 - end: - line: 42 - col: 0 - hash: '39697fcb2bdf094f' - context_hash: de7cc417de1b3246 -- key: string.dump - kind: - type: doc_block - indent: '' - range: - start: - line: 46 - col: 0 - end: - line: 57 - col: 0 - hash: '7a9424d0441a129a' - context_hash: de7cc417de1b3246 -- key: string.find - kind: - type: doc_block - indent: '' - range: - start: - line: 62 - col: 0 - end: - line: 75 - col: 0 - hash: '7ab6f8359e5d19c2' - context_hash: de7cc417de1b3246 -- key: string.format - kind: - type: doc_block - indent: '' - range: - start: - line: 85 - col: 0 - end: - line: 114 - col: 0 - hash: '1b6b640f12c7c5ee' - context_hash: de7cc417de1b3246 -- key: string.gmatch - kind: - type: doc_block - indent: '' - range: - start: - line: 120 - col: 0 - end: - line: 144 - col: 0 - hash: '78ff847ad504f943' - context_hash: de7cc417de1b3246 -- key: string.gsub - kind: - type: doc_block - indent: '' - range: - start: - line: 149 - col: 0 - end: - line: 192 - col: 0 - hash: f241d32e2a8fb75f - context_hash: de7cc417de1b3246 -- key: string.len - kind: - type: doc_block - indent: '' - range: - start: - line: 200 - col: 0 - end: - line: 203 - col: 0 - hash: cddd67e2a2b28c2e - context_hash: de7cc417de1b3246 -- key: string.lower - kind: - type: doc_block - indent: '' - range: - start: - line: 207 - col: 0 - end: - line: 211 - col: 0 - hash: '4c234ee3d950bad4' - context_hash: de7cc417de1b3246 -- key: string.match - kind: - type: doc_block - indent: '' - range: - start: - line: 215 - col: 0 - end: - line: 221 - col: 0 - hash: '73a921b42b7c11d4' - context_hash: de7cc417de1b3246 -- key: string.pack - kind: - type: doc_block - indent: '' - range: - start: - line: 228 - col: 0 - end: - line: 231 - col: 0 - hash: '5da2ac260adbb234' - context_hash: de7cc417de1b3246 -- key: string.packsize - kind: - type: doc_block - indent: '' - range: - start: - line: 239 - col: 0 - end: - line: 243 - col: 0 - hash: f5740841b9e9c4b1 - context_hash: de7cc417de1b3246 -- key: string.rep - kind: - type: doc_block - indent: '' - range: - start: - line: 247 - col: 0 - end: - line: 255 - col: 0 - hash: ee87e632f2e1a738 - context_hash: de7cc417de1b3246 -- key: string.reverse - kind: - type: doc_block - indent: '' - range: - start: - line: 261 - col: 0 - end: - line: 263 - col: 0 - hash: c0411ed95baa29f4 - context_hash: de7cc417de1b3246 -- key: string.sub - kind: - type: doc_block - indent: '' - range: - start: - line: 267 - col: 0 - end: - line: 279 - col: 0 - hash: a25451cdfc2b643e - context_hash: de7cc417de1b3246 -- key: string.unpack - kind: - type: doc_block - indent: '' - range: - start: - line: 287 - col: 0 - end: - line: 292 - col: 0 - hash: df66a1c8efef1986 - context_hash: de7cc417de1b3246 -- key: string.upper - kind: - type: doc_block - indent: '' - range: - start: - line: 299 - col: 0 - end: - line: 303 - col: 0 - hash: '77d737c7349fb1f7' - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/string/zh_CN.yaml b/crates/glua_ls/std_i18n/string/zh_CN.yaml deleted file mode 100644 index caf6a3825..000000000 --- a/crates/glua_ls/std_i18n/string/zh_CN.yaml +++ /dev/null @@ -1,163 +0,0 @@ -string: | - *string* 类型表示不可变的字节序列。Lua 使用 8 位编码:字符串可以包含任意 8 位值, - 包括嵌入的零(`\0`)。Lua 也与编码无关;它不会对字符串的内容做任何假设。 - -string.byte: | - 返回字符 `s[i]`、`s[i+1]`、...、`s[j]` 的内部数值码。`i` 的默认值为 1,`j` 的默认值为 `i`。 - 这些索引会按 `string.sub` 的相同规则进行修正。 - - 注意:这些数值码并不一定能跨平台移植。 - -string.char: | - 接收零个或多个整数。返回一个长度等于参数个数的字符串,其中每个字符的内部数值码 - 等于其对应的参数。 - - 注意:这些数值码并不一定能跨平台移植。 - -string.dump: | - 返回一个字符串,包含给定函数的二进制表示(*二进制块*)。之后对该字符串调用 `load`, - 会得到该函数的一个副本(但其上值是新的实例)。如果 `strip` 为真,则为了节省空间, - 二进制表示可能不会包含该函数的全部调试信息。 - - 对于带上值的函数,只会保存上值的数量;(重新)加载后,这些上值会变成包含 **nil** - 的新实例。(你可以使用 debug 库按需序列化并恢复函数的上值。) - -string.find: | - 在字符串 `s` 中查找 `pattern` 的第一次匹配。如果找到,则返回该匹配在 `s` 中的起始与结束索引; - 否则返回 **nil**。 - - 可选的第三个数字参数 `init` 指定从哪里开始搜索;默认值为 1,也可以为负数。 - 可选的第四个参数 `plain` 为 **true** 时,会关闭模式匹配机制:函数将进行普通的“查找子串”操作, - 此时 `pattern` 中不会有任何字符被视为具有特殊含义的“魔法字符”。注意:如果提供了 `plain`,则也必须提供 `init`。 - - 如果 `pattern` 含有捕获,则匹配成功时也会在两个索引之后返回捕获到的值。 - -string.format: | - 按第一个参数(必须是字符串)中的描述,对后续可变数量的参数进行格式化,并返回格式化后的字符串。 - 格式字符串遵循 ISO C 的 `sprintf` 的规则,区别在于:不支持选项/修饰符 `*`、`h`、`L`、`l`、`n`、`p`, - 且额外支持一个 `q` 选项。 - - `q` 选项会以“可被 Lua 源码当作常量读回”的形式格式化布尔值、nil、数字和字符串: - 布尔值与 nil 会直接写作 `true`、`false`、`nil`;浮点数会用十六进制表示以保留全部精度; - 字符串会放在双引号中,并在必要时使用转义序列以确保 Lua 解释器可以安全读回。 - - 例如: - ```lua - string.format('%q', 'a string with "quotes" and \n new line') - ``` - 可能产生: - ```lua - "a string with \"quotes\" and \n new line" - ``` - - 选项 `A`、`a`、`E`、`e`、`f`、`g`、`G` 和 `g` 需要一个数字参数。 - 选项 `c`、`d`、`i`、`o`、`u`、`X`、`x` 需要一个整数参数。 - 当 Lua 使用 C89 编译器编译时,选项 `A` 和 `a`(十六进制浮点数)不支持任何修饰符(标志、宽度、长度)。 - - 选项 `s` 需要一个字符串;若参数不是字符串,则按与 `tostring` 相同的规则转换为字符串。 - 如果 `s` 选项带有任何修饰符(标志、宽度、长度),则字符串参数不应包含嵌入的零字节。 - -string.gmatch: | - 返回一个迭代器函数:每次调用时,它都会在字符串 `s` 上返回 `pattern` 的下一组捕获结果。 - 若 `pattern` 未指定捕获,则每次返回整个匹配内容。 - - 例如,下面的循环会遍历字符串 `s` 中的所有单词,并逐行打印: - - ```lua - s = "hello world from Lua" - for w in string.gmatch(s, "%a+") do - print(w) - end - ``` - - 下一个示例把给定字符串中的所有 `key=value` 对收集到表中: - - ```lua - t = {} - s = "from=world, to=Lua" - for k, v in string.gmatch(s, "(%w+)=(%w+)") do - t[k] = v - end - ``` - - 注意:对该函数而言,模式开头的插入符 '`^`' 不会作为锚点使用,因为那会阻止迭代。 - -string.gsub: | - 返回 `s` 的一个副本,其中所有(或前 `n` 个,如果给出)匹配 `pattern` 的内容都会被 `repl` 替换。 - `repl` 可以是字符串、表或函数。`gsub` 还会把发生的匹配总数作为第二个返回值返回。 - - 若 `repl` 为字符串,则直接用该字符串替换。此时字符 `%` 用作转义:`repl` 中形如 `%n`(*n* 为 1~9) - 的序列表示第 *n* 个捕获子串;`%0` 表示整个匹配;`%%` 表示单个 `%`。 - - 若 `repl` 为表,则每次匹配都会用“第一个捕获”作为键查询该表;如果 `pattern` 没有捕获,则用整个匹配作为键。 - - 若 `repl` 为函数,则每次匹配都会调用该函数,并按顺序把所有捕获子串作为参数传入;如果没有捕获,则把整个匹配作为唯一参数传入。 - - 若表查询或函数调用的返回值是字符串或数字,则用它作为替换内容;否则,如果返回 false 或 nil,则表示不替换(即保留原始匹配)。 - - # 示例: - ```lua - x = string.gsub("hello world", "(%w+)", "%1 %1") - -- > x="hello hello world world" - x = string.gsub("hello world", "%w+", "%0 %0", 1) - -- > x="hello hello world" - x = string.gsub("hello world from Lua", "(%w+)%s*(%w+)", "%2 %1") - -- > x="world hello Lua from" - x = string.gsub("home = $HOME, user = $USER", "%$(%w+)", os.getenv) - -- > x="home = /home/roberto, user = roberto" - x = string.gsub("4+5 = $return 4+5$", "%$(.-)%$", function (s) - return loadstring(s)() - end) - -- > x="4+5 = 9" - local t = {name="lua", version="5.3"} - x = string.gsub("$name-$version.tar.gz", "%$(%w+)", t) - -- > x="lua-5.3.tar.gz" - ``` - -string.len: | - 接收一个字符串并返回其长度。空字符串 `""` 的长度为 0。 - 嵌入的零字节也会计入长度,因此 `"a\000bc\000"` 的长度为 5。 - -string.lower: | - 接收一个字符串并返回该字符串的副本,将其中所有大写字母转换为小写;其余字符保持不变。 - 什么算作大写字母取决于当前的区域设置。 - -string.match: | - 在字符串 `s` 中查找 `pattern` 的第一次*匹配*。如果匹配成功,则返回模式中的捕获;否则返回 **nil**。 - 如果 `pattern` 没有捕获,则返回整个匹配内容。可选的第三个数字参数 `init` 指定从哪里开始搜索;默认值为 1,可为负数。 - -string.pack: | - 按格式字符串 `fmt` 将 `v1`、`v2` 等值打包(即以二进制形式序列化),并返回得到的二进制字符串。 - -string.packsize: | - 返回使用给定格式调用 `string.pack` 时生成的字符串大小。 - 格式字符串不能包含变长选项 '`s`' 或 '`z`'。 - -string.rep: | - 返回 `n` 个以字符串 `sep` 为分割符连在一起的字符串。 - 默认的 `sep` 值为空字符串(即没有分割符)。如果 `n` 不是正数则返回空串。 - - 请注意,单次调用此函数极易耗尽机器的内存。 - -string.reverse: | - 返回将字符串 `s` 反转后的新字符串。 - -string.sub: | - 返回 `s` 的子串:从 `i` 开始直到 `j` 结束(包含两端)。`i` 与 `j` 均可为负数。 - 若省略 `j`,则默认为 -1(等同于字符串长度)。 - - 特别地,`string.sub(s, 1, j)` 返回 `s` 的一个前缀,其长度为 `j`; - `string.sub(s, -i)`(`i` 为正)返回 `s` 的一个后缀,其长度为 `i`。 - - 将负索引换算后,若 `i` 小于 1,则修正为 1;若 `j` 大于字符串长度,则修正为该长度。 - 若修正后 `i` 大于 `j`,则返回空字符串。 - -string.unpack: | - 按格式字符串 `fmt` 从字符串 `s` 中解包并返回其中的值。 - 可选参数 `pos` 指定从 `s` 的哪个位置开始读取(默认值为 1)。 - 除了解包得到的值之外,本函数还会返回 `s` 中第一个未读字节的索引。 - -string.upper: | - 接收一个字符串并返回该字符串的副本,将其中所有小写字母转换为大写;其余字符保持不变。 - 什么算作小写字母取决于当前的区域设置。 - diff --git a/crates/glua_ls/std_i18n/table/clear/meta.yaml b/crates/glua_ls/std_i18n/table/clear/meta.yaml deleted file mode 100644 index 468a9ab03..000000000 --- a/crates/glua_ls/std_i18n/table/clear/meta.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: table/clear.lua -entries: -- key: clear - kind: - type: doc_block - indent: '' - range: - start: - line: 5 - col: 0 - end: - line: 15 - col: 0 - hash: '95aff6d9fd7ac7bc' - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/table/clear/zh_CN.yaml b/crates/glua_ls/std_i18n/table/clear/zh_CN.yaml deleted file mode 100644 index 5b3a95de7..000000000 --- a/crates/glua_ls/std_i18n/table/clear/zh_CN.yaml +++ /dev/null @@ -1,15 +0,0 @@ -clear: | - 清除表中的所有键和值,但保留已分配的数组/哈希容量。 - - 当一个表被多个地方引用而需要被清空,或需要在同一上下文中回收复用表时,这很有用。 - 这样可以避免维护反向引用、节省一次分配,以及减少数组/哈希部分逐步增长的开销。 - 使用前需要先 require: - - ```lua - require("table.clear") - ``` - - 注意:此函数仅适用于非常特定的场景。多数情况下,更好的做法是用一个新表替换(通常唯一的)引用,并让 GC 自行回收旧表。 - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-table.clear"]) - diff --git a/crates/glua_ls/std_i18n/table/meta.yaml b/crates/glua_ls/std_i18n/table/meta.yaml deleted file mode 100644 index 73d8b580a..000000000 --- a/crates/glua_ls/std_i18n/table/meta.yaml +++ /dev/null @@ -1,161 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: table.lua -entries: -- key: tablelib.concat - kind: - type: doc_block - indent: '' - range: - start: - line: 18 - col: 0 - end: - line: 23 - col: 0 - hash: '504e6d9e125a441e' - context_hash: de7cc417de1b3246 -- key: tablelib.insert - kind: - type: doc_block - indent: '' - range: - start: - line: 31 - col: 0 - end: - line: 36 - col: 0 - hash: '3f149377193529ce' - context_hash: de7cc417de1b3246 -- key: tablelib.move - kind: - type: doc_block - indent: '' - range: - start: - line: 44 - col: 0 - end: - line: 51 - col: 0 - hash: '4eaddc2518d941b3' - context_hash: de7cc417de1b3246 -- key: tablelib.maxn - kind: - type: doc_block - indent: '' - range: - start: - line: 61 - col: 0 - end: - line: 64 - col: 0 - hash: a28a3f83a36c528d - context_hash: de7cc417de1b3246 -- key: tablelib.remove - kind: - type: doc_block - indent: '' - range: - start: - line: 69 - col: 0 - end: - line: 79 - col: 0 - hash: a7e87624e3fd9616 - context_hash: de7cc417de1b3246 -- key: tablelib.sort - kind: - type: doc_block - indent: '' - range: - start: - line: 85 - col: 0 - end: - line: 99 - col: 0 - hash: b11a809b5964de0f - context_hash: de7cc417de1b3246 -- key: tablelib.unpack - kind: - type: doc_block - indent: '' - range: - start: - line: 106 - col: 0 - end: - line: 110 - col: 0 - hash: ae6b75e5905939b2 - context_hash: de7cc417de1b3246 -- key: tablelib.pack - kind: - type: doc_block - indent: '' - range: - start: - line: 118 - col: 0 - end: - line: 121 - col: 0 - hash: c6fe784fff53117d - context_hash: de7cc417de1b3246 -- key: tablelib.foreach - kind: - type: doc_block - indent: '' - range: - start: - line: 128 - col: 0 - end: - line: 132 - col: 0 - hash: '8a83776ff1bb2be4' - context_hash: de7cc417de1b3246 -- key: tablelib.foreachi - kind: - type: doc_block - indent: '' - range: - start: - line: 140 - col: 0 - end: - line: 143 - col: 0 - hash: ed156847af70b07b - context_hash: de7cc417de1b3246 -- key: tablelib.getn - kind: - type: doc_block - indent: '' - range: - start: - line: 151 - col: 0 - end: - line: 155 - col: 0 - hash: a33b9ea6feb84cdb - context_hash: de7cc417de1b3246 -- key: tablelib.create - kind: - type: doc_block - indent: '' - range: - start: - line: 162 - col: 0 - end: - line: 166 - col: 0 - hash: b4aa98c172166a6d - context_hash: '32106038437480b2' diff --git a/crates/glua_ls/std_i18n/table/new/meta.yaml b/crates/glua_ls/std_i18n/table/new/meta.yaml deleted file mode 100644 index c69d7e525..000000000 --- a/crates/glua_ls/std_i18n/table/new/meta.yaml +++ /dev/null @@ -1,18 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: table/new.lua -entries: -- key: new - kind: - type: doc_block - indent: '' - range: - start: - line: 5 - col: 0 - end: - line: 14 - col: 0 - hash: '9eab27b4cb7ecc47' - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/table/new/zh_CN.yaml b/crates/glua_ls/std_i18n/table/new/zh_CN.yaml deleted file mode 100644 index addd857ab..000000000 --- a/crates/glua_ls/std_i18n/table/new/zh_CN.yaml +++ /dev/null @@ -1,13 +0,0 @@ -new: | - 创建一个预设大小的表,等价于 C API 的 `lua_createtable()`。 - - 当需要创建大表且已知最终表大小、而自动扩容开销过高时,这很有用。 - 参数 `narray` 指定类数组部分的元素数量,参数 `nhash` 指定类哈希部分的元素数量。 - 使用前需要先 require: - - ```lua - require("table.new") - ``` - - [查看文档](command:extension.lua.doc?["en-us/54/manual.html/pdf-table.new"]) - diff --git a/crates/glua_ls/std_i18n/table/zh_CN.yaml b/crates/glua_ls/std_i18n/table/zh_CN.yaml deleted file mode 100644 index f1d4126b5..000000000 --- a/crates/glua_ls/std_i18n/table/zh_CN.yaml +++ /dev/null @@ -1,69 +0,0 @@ -tablelib.concat: | - 给定一个列表,其中所有元素都是字符串或数字,返回字符串 - `list[i]..sep..list[i+1] ... sep..list[j]`。 - - `sep` 的默认值为空字符串,`i` 的默认值为 1,`j` 的默认值为 `#list`。 - 如果 `i` 大于 `j`,则返回空字符串。 - -tablelib.insert: | - 在 `list` 的位置 `pos` 处插入元素 `value`,并将 - `list[pos]`、`list[pos+1]`、`···`、`list[#list]` 这些元素向后移动。 - - `pos` 的默认值为 `#list+1`,因此调用 `table.insert(t, x)` 会把 `x` 插入到列表 `t` 的末尾。 - -tablelib.move: | - 将元素从表 `a1` 移动到表 `a2`,等价于进行如下的多重赋值: - `a2[t]`, `···` = `a1[f]`, `···`, `a1[e]`。 - - `a2` 的默认值为 `a1`。目标区间可以与源区间重叠。要移动的元素数量必须能放入 Lua 整数。 - - 返回目标表 `a2`。 - -tablelib.maxn: | - 返回给定表的最大正整数索引;如果表没有正整数索引,则返回 0。 - -tablelib.remove: | - 移除 `list` 中位置 `pos` 处的元素,并返回被移除元素的值。 - - 当 `pos` 是 1 到 `#list` 之间的整数时,它会将 `list[pos+1]`、`list[pos+2]`、`···`、 - `list[#list]` 向前移动,并清除 `list[#list]`;当 `#list` 为 0 时,`pos` 也可以为 0, - 或者 `#list + 1`;这些情况下,函数会清除 `list[pos]`。 - - `pos` 的默认值为 `#list`,因此调用 `table.remove(l)` 会移除列表 `l` 的最后一个元素。 - -tablelib.sort: | - 在表内从 `list[1]` 到 `list[#list]` 之间,按指定顺序 *原地* 排序列表元素。 - - 如果提供了 `comp`,它必须是一个接收两个列表元素并返回布尔值的函数:当第一个元素在最终排序中应位于第二个元素之前时返回 true - (因此,排序后 `i < j` 蕴含 `not comp(list[j], list[i])`)。如果不提供 `comp`,则使用标准 Lua 运算符 `<`。 - - 注意:`comp` 必须在列表元素上定义一个严格偏序(即非对称且传递);否则可能无法得到有效排序。 - - 排序算法不稳定:在给定顺序下被认为相等的元素,其相对位置可能在排序后发生变化。 - -tablelib.unpack: | - 返回给定列表中的元素。该函数等价于: - `return list[i], list[i+1], ···, list[j]`。 - - 默认情况下,`i` 为 1,`j` 为 `#list`。 - -tablelib.pack: | - 返回一个新表,将所有参数依次存入键 `1`、`2` 等,并设置字段 `"n"` 为参数总数。 - -tablelib.foreach: | - 对表的所有元素执行给定函数 `f`。对每个元素,`f` 会以索引与对应值作为参数被调用。 - 如果 `f` 返回非 nil 值,则中断循环,并将该值作为 `foreach` 的最终返回值。 - -tablelib.foreachi: | - 对表的数值索引执行给定函数 `f`。对每个索引,`f` 会以索引与对应值作为参数被调用。 - 索引会按顺序访问,从 1 到 n,其中 n 为表的大小。 - 如果 `f` 返回非 nil 值,则中断循环,并将该值作为 `foreachi` 的结果返回。 - -tablelib.getn: | - 返回表中的元素数量。该函数等价于 `#list`。 - -tablelib.create: | - 创建一个新的空表,并预分配内存。当你事先知道表会包含多少元素时,这种预分配可能有助于提升性能并节省内存。 - - 参数 `nseq` 用于提示该表作为序列会包含多少元素;可选参数 `nrec` 用于提示该表还会包含多少其他元素,默认值为 0。 - diff --git a/crates/glua_ls/std_i18n/utf8/meta.yaml b/crates/glua_ls/std_i18n/utf8/meta.yaml deleted file mode 100644 index 208859b20..000000000 --- a/crates/glua_ls/std_i18n/utf8/meta.yaml +++ /dev/null @@ -1,83 +0,0 @@ -version: 1 -line_base: 0 -col_base: 0 -file: utf8.lua -entries: -- key: utf8lib.char - kind: - type: doc_block - indent: '' - range: - start: - line: 21 - col: 0 - end: - line: 25 - col: 0 - hash: c5a661c65ae1030a - context_hash: de7cc417de1b3246 -- key: utf8lib.charpattern - kind: - type: doc_block - indent: '' - range: - start: - line: 28 - col: 0 - end: - line: 32 - col: 0 - hash: a11c6144655d15b2 - context_hash: de7cc417de1b3246 -- key: utf8lib.codes - kind: - type: doc_block - indent: '' - range: - start: - line: 35 - col: 0 - end: - line: 41 - col: 0 - hash: c57cefb975fbd3bb - context_hash: de7cc417de1b3246 -- key: utf8lib.codepoint - kind: - type: doc_block - indent: '' - range: - start: - line: 45 - col: 0 - end: - line: 50 - col: 0 - hash: eff46239f32f51bb - context_hash: de7cc417de1b3246 -- key: utf8lib.len - kind: - type: doc_block - indent: '' - range: - start: - line: 57 - col: 0 - end: - line: 62 - col: 0 - hash: '6f1924e1e29490a7' - context_hash: de7cc417de1b3246 -- key: utf8lib.offset - kind: - type: doc_block - indent: '' - range: - start: - line: 71 - col: 0 - end: - line: 83 - col: 0 - hash: '373cd5d885e68978' - context_hash: de7cc417de1b3246 diff --git a/crates/glua_ls/std_i18n/utf8/zh_CN.yaml b/crates/glua_ls/std_i18n/utf8/zh_CN.yaml deleted file mode 100644 index 86a34e431..000000000 --- a/crates/glua_ls/std_i18n/utf8/zh_CN.yaml +++ /dev/null @@ -1,35 +0,0 @@ -utf8lib.char: | - 接收零个或多个整数,将每个整数转换为对应的 UTF-8 字节序列,并返回将这些序列连接在一起的字符串。 - -utf8lib.charpattern: | - 模式(一个字符串,而非函数)`[\0-\x7F\xC2-\xF4][\x80-\xBF]*`, - 在假定被匹配对象是合法 UTF-8 字符串的情况下,它精确匹配一个 UTF-8 字节序列。 - -utf8lib.codes: | - 返回一些值,使得结构 - > `for p, c in utf8.codes(s) do` *body* `end` - 能遍历字符串 `s` 中的所有字符,其中 `p` 为位置(按字节计),`c` 为每个字符的码点。 - 如果遇到任何无效的字节序列,会抛出错误。 - -utf8lib.codepoint: | - 以整数形式返回 `s` 中从字节位置 `i` 到 `j`(两端都包含)开始的所有字符的码点。 - `i` 的默认值为 1,`j` 的默认值为 `i`。如果遇到任何无效的字节序列,会抛出错误。 - -utf8lib.len: | - 返回字符串 `s` 中从位置 `i` 到 `j`(两端都包含)开始的 UTF-8 字符数量。 - `i` 的默认值为 1,`j` 的默认值为 -1。 - - 如果发现任何无效的字节序列,则返回 `false` 以及第一个无效字节的位置。 - -utf8lib.offset: | - 返回 `s` 中第 `n` 个字符的编码起始位置(按字节计)(从位置 `i` 开始计数)。 - 负的 `n` 表示取 `i` 之前的字符。 - - `n` 非负时,`i` 的默认值为 1;否则 `i` 的默认值为 `#s + 1`,因此 - `utf8.offset(s, -n)` 可获得从字符串末尾数第 `n` 个字符的偏移。 - - 如果指定字符既不在字符串中也不在其末尾紧接处,则函数返回 nil。 - 特殊情况:当 `n` 为 0 时,函数返回包含 `s` 的第 `i` 个字节的那个字符的编码起始位置。 - - 此函数假定 `s` 是合法的 UTF-8 字符串。 - diff --git a/crates/glua_parser/Cargo.toml b/crates/glua_parser/Cargo.toml index fc62e9f4f..588af8d63 100644 --- a/crates/glua_parser/Cargo.toml +++ b/crates/glua_parser/Cargo.toml @@ -16,9 +16,5 @@ workspace = true [dependencies] rowan.workspace = true -rust-i18n.workspace = true serde.workspace = true -[package.metadata.i18n] -available-locales = ["en", "zh_CN", "zh_HK"] -default-locale = "en" diff --git a/crates/glua_parser/locales/app.yml b/crates/glua_parser/locales/app.yml deleted file mode 100644 index 4be4e230b..000000000 --- a/crates/glua_parser/locales/app.yml +++ /dev/null @@ -1,557 +0,0 @@ -_version: 2 -Invalid escape sequence '\%{char}': - en: Invalid escape sequence '\%{char}' - zh_CN: 无效的转义序列 '\%{char}' - zh_HK: 無效的轉義序列 '\%{char}' -Invalid hex escape sequence '\x%{hex}': - en: Invalid hex escape sequence '\x%{hex}' - zh_CN: 无效的十六进制转义序列 '\x%{hex}' - zh_HK: 無效的十六進制轉義序列 '\x%{hex}' -Invalid long string end, expected '%{eq}]': - en: Invalid long string end, expected '%{eq}]' - zh_CN: 无效的长字符串结尾,期望 '%{eq}]' - zh_HK: 無效的長字符串結尾,期望 '%{eq}]' -Invalid long string start: - en: Invalid long string start - zh_CN: 无效的长字符串开始 - zh_HK: 無效的長字符串開始 -Invalid long string start, expected '[', found '%{char}': - en: Invalid long string start, expected '[', found '%{char}' - zh_CN: 无效的长字符串开始,期望 '[', 但找到 '%{char}' - zh_HK: 無效的長字符串開始,期望 '[', 但找到 '%{char}' -Invalid long string start, expected '[', found end of input: - en: Invalid long string start, expected '[', found end of input - zh_CN: 无效的长字符串开始,期望 '[', 但找到输入结束 - zh_HK: 無效的長字符串開始,期望 '[', 但找到輸入結束 -Invalid unicode escape sequence '\u{{%{unicode_hex}}}': - en: Invalid unicode escape sequence '\u{{%{unicode_hex}}}' - zh_CN: 无效的Unicode转义序列 '\u{{%{unicode_hex}}}' - zh_HK: 無效的Unicode轉義序列 '\u{{%{unicode_hex}}}' -String too short: - en: String too short - zh_CN: 字符串太短 - zh_HK: 字符串太短 -The float literal '%{text}' is invalid, %{err}: - en: The float literal '%{text}' is invalid, %{err} - zh_CN: 浮点字面量 '%{text}' 无效, %{err} - zh_HK: 浮點字面量 '%{text}' 無效, %{err} -The integer literal '%{text}' is invalid, %{err}: - en: The integer literal '%{text}' is invalid, %{err} - zh_CN: 整数字面量 '%{text}' 无效, %{err} - zh_HK: 整數字面量 '%{text}' 無效, %{err} -The integer literal '%{text}' is too large to be represented in type 'long': - en: The integer literal '%{text}' is too large to be represented in type 'long' - zh_CN: 整数字面量 '%{text}' 太大,无法用 'long' 类型表示 - zh_HK: 整數字面量 '%{text}' 太大,無法用 'long' 類型表示 -binary operator not followed by expression: - en: binary operator not followed by expression - zh_CN: 二元运算符后没有表达式 - zh_HK: 二元運算符後沒有表達式 -binary operator not followed by type: - en: binary operator not followed by type - zh_CN: 二元运算符后没有类型 - zh_HK: 二元運算符後沒有類型 -bitwise operation is not supported: - en: bitwise operation is not supported - zh_CN: 不支持位运算 - zh_HK: 不支持位運算 -expect args: - en: expect args - zh_CN: 需要参数 - zh_HK: 需要參數 -expect field name or '[', but get %{current}: - en: expect field name or '[', but get %{current} - zh_CN: 需要字段名或 '[', 但得到 %{current} - zh_HK: 需要字段名或 '[', 但得到 %{current} -expect fun: - en: expect fun - zh_CN: 需要函数 - zh_HK: 需要函數 -expect index struct: - en: expect index struct - zh_CN: 需要索引结构 - zh_HK: 需要索引結構 -expect name or ...: - en: expect name or ... - zh_CN: 需要名称或 ... - zh_HK: 需要名稱或 ... -expect name or [] or []: - en: expect name or [] or [] - zh_CN: 需要名称或 [<数字>] 或 [<字符串>] - zh_HK: 需要名稱或 [<數字>] 或 [<字符串>] -expect param name or '...', but get %{current}: - en: expect param name or '...', but get %{current} - zh_CN: 需要参数名或 '...', 但得到 %{current} - zh_HK: 需要參數名或 '...', 但得到 %{current} -expect parameter name: - en: expect parameter name - zh_CN: 需要参数名 - zh_HK: 需要參數名 -expect primary expression: - en: expect primary expression - zh_CN: 需要主表达式 - zh_HK: 需要主表達式 -expect type: - en: expect type - zh_CN: 需要类型 - zh_HK: 需要類型 -expected %{token}, but get %{current}: - en: expected %{token}, but get %{current} - zh_CN: 期望 %{token}, 但得到 %{current} - zh_HK: 期望 %{token}, 但得到 %{current} -integer division is not supported: - en: integer division is not supported - zh_CN: 不支持整数除法 - zh_HK: 不支持整數除法 -integer power operation is not supported: - en: integer power operation is not supported - zh_CN: 不支持整数幂运算 - zh_HK: 不支持整數冪運算 -invalid long string delimiter: - en: invalid long string delimiter - zh_CN: 无效的长字符串定界符 - zh_HK: 無效的長字符串定界符 -'local attribute is not supported for current version: %{level}': - en: 'local attribute is not supported for current version: %{level}' - zh_CN: '当前版本不支持本地属性: %{level}' - zh_HK: '當前版本不支持本地屬性: %{level}' -unary operator not followed by expression: - en: unary operator not followed by expression - zh_CN: 一元运算符后没有表达式 - zh_HK: 一元運算符後沒有表達式 -unary operator not followed by type: - en: unary operator not followed by type - zh_CN: 一元运算符后没有类型 - zh_HK: 一元運算符後沒有類型 -unexpected expr for varList: - en: unexpected expr for varList - zh_CN: varList 中的意外表达式 - zh_HK: varList 中的意外表達式 -unexpected token: - en: unexpected token - zh_CN: 意外的标记 - zh_HK: 意外的標記 -unexpected token %{token}: - en: unexpected token %{token} - zh_CN: 意外的标记 %{token} - zh_HK: 意外的標記 %{token} -unfinished long string or comment: - en: unfinished long string or comment - zh_CN: 未完成的长字符串或注释 - zh_HK: 未完成的長字符串或註釋 -unfinished stat: - en: unfinished stat - zh_CN: 未完成的语句 - zh_HK: 未完成的語句 -unfinished string: - en: unfinished string - zh_CN: 未完成的字符串 - zh_HK: 未完成的字符串 -colon accessor must be followed by a function call or table constructor or string literal: - en: colon accessor must be followed by a function call or table constructor or string literal - zh_CN: 冒号访问器后必须跟随函数调用、表构造或字符串字面量 - zh_HK: 冒號訪問器後必須跟隨函數調用、表構造或字符串字面量 -expected '}' to close table: - en: expected '}' to close table - zh_CN: 期望 '}' 关闭表 - zh_HK: 期望 '}' 關閉表 -expected ']': - en: expected ']' - zh_CN: 期望 ']' - zh_HK: 期望 ']' -expected '=': - en: expected '=' - zh_CN: 期望 '=' - zh_HK: 期望 '=' -The integer literal '%{text}' is too large to be represented: - en: The integer literal '%{text}' is too large to be represented - zh_CN: 整数字面量 '%{text}' 太大无法表示 - zh_HK: 整數字面量 '%{text}' 太大無法表示 -binary operator '%{op}' is not followed by an expression: - en: binary operator '%{op}' is not followed by an expression - zh_CN: 二元运算符 '%{op}' 后面没有跟表达式 - zh_HK: 二元運算符 '%{op}' 後面沒有跟表達式 -expect primary expression (identifier or parenthesized expression): - en: expect primary expression (identifier or parenthesized expression) - zh_CN: 期望主表达式(标识符或带括号的表达式) - zh_HK: 期望主表達式(標識符或帶括號的表達式) -expected '(' to start parameter list: - en: expected '(' to start parameter list - zh_CN: 期望 '(' 来开始参数列表 - zh_HK: 期望 '(' 來開始參數列表 -expected '(', string, or table constructor for function call: - en: expected '(', string, or table constructor for function call - zh_CN: 期望 '('、字符串或表构造器用于函数调用 - zh_HK: 期望 '('、字符串或表構造器用於函數調用 -expected ')' to close argument list: - en: expected ')' to close argument list - zh_CN: 期望 ')' 来关闭参数列表 - zh_HK: 期望 ')' 來關閉參數列表 -expected ')' to close parameter list: - en: expected ')' to close parameter list - zh_CN: 期望 ')' 来关闭参数列表 - zh_HK: 期望 ')' 來關閉參數列表 -expected ')' to close parentheses: - en: expected ')' to close parentheses - zh_CN: 期望 ')' 来关闭括号 - zh_HK: 期望 ')' 來關閉括號 -expected ',' after start value in numeric for loop: - en: expected ',' after start value in numeric for loop - zh_CN: 期望在数值for循环的起始值后有 ',' - zh_HK: 期望在數值for迴圈的起始值後有 ',' -expected '::' after label name: - en: expected '::' after label name - zh_CN: 期望在标签名后有 '::' - zh_HK: 期望在標籤名後有 '::' -expected '=' after table index: - en: expected '=' after table index - zh_CN: 期望在表索引后有 '=' - zh_HK: 期望在表索引後有 '=' -expected '=' for assignment or this is an incomplete statement: - en: expected '=' for assignment or this is an incomplete statement - zh_CN: 期望 '=' 用于赋值或这是一个不完整的语句 - zh_HK: 期望 '=' 用於賦值或這是一個不完整的語句 -expected '=' for numeric for loop or ',' or 'in' for generic for loop: - en: expected '=' for numeric for loop or ',' or 'in' for generic for loop - zh_CN: 期望 '=' 用于数值for循环,或 ',' 或 'in' 用于通用for循环 - zh_HK: 期望 '=' 用於數值for迴圈,或 ',' 或 'in' 用於通用for迴圈 -expected '>' after attribute name: - en: expected '>' after attribute name - zh_CN: 期望在属性名后有 '>' - zh_HK: 期望在屬性名後有 '>' -expected ']' to close table index: - en: expected ']' to close table index - zh_CN: 期望 ']' 来关闭表索引 - zh_HK: 期望 ']' 來關閉表索引 -expected 'do' after while condition: - en: expected 'do' after while condition - zh_CN: 期望在while条件后有 'do' - zh_HK: 期望在while條件後有 'do' -expected 'do' in for statement: - en: expected 'do' in for statement - zh_CN: 期望在for语句中有 'do' - zh_HK: 期望在for語句中有 'do' -expected 'end' after 'do' block: - en: expected 'end' after 'do' block - zh_CN: 期望在 'do' 块后有 'end' - zh_HK: 期望在 'do' 塊後有 'end' -expected 'end' to close for statement: - en: expected 'end' to close for statement - zh_CN: 期望 'end' 来关闭for语句 - zh_HK: 期望 'end' 來關閉for語句 -expected 'end' to close function definition: - en: expected 'end' to close function definition - zh_CN: 期望 'end' 来关闭函数定义 - zh_HK: 期望 'end' 來關閉函數定義 -expected 'end' to close if statement: - en: expected 'end' to close if statement - zh_CN: 期望 'end' 来关闭if语句 - zh_HK: 期望 'end' 來關閉if語句 -expected 'end' to close while statement: - en: expected 'end' to close while statement - zh_CN: 期望 'end' 来关闭while语句 - zh_HK: 期望 'end' 來關閉while語句 -expected 'function', variable name, or attribute after 'local': - en: expected 'function', variable name, or attribute after 'local' - zh_CN: 期望在 'local' 后有 'function'、变量名或属性 - zh_HK: 期望在 'local' 後有 'function'、變數名或屬性 -expected 'in' after variable list in generic for loop: - en: expected 'in' after variable list in generic for loop - zh_CN: 期望在通用for循环的变量列表后有 'in' - zh_HK: 期望在通用for迴圈的變數列表後有 'in' -expected 'then' after 'elseif' condition: - en: expected 'then' after 'elseif' condition - zh_CN: 期望在 'elseif' 条件后有 'then' - zh_HK: 期望在 'elseif' 條件後有 'then' -expected 'then' after if condition: - en: expected 'then' after if condition - zh_CN: 期望在if条件后有 'then' - zh_HK: 期望在if條件後有 'then' -expected 'until' after repeat block: - en: expected 'until' after repeat block - zh_CN: 期望在repeat块后有 'until' - zh_HK: 期望在repeat塊後有 'until' -expected '}' to close table constructor: - en: expected '}' to close table constructor - zh_CN: 期望 '}' 来关闭表构造器 - zh_HK: 期望 '}' 來關閉表構造器 -expected argument expression: - en: expected argument expression - zh_CN: 期望参数表达式 - zh_HK: 期望參數表達式 -expected attribute name after '<': - en: expected attribute name after '<' - zh_CN: 期望在 '<' 后有属性名 - zh_HK: 期望在 '<' 後有屬性名 -expected condition expression after 'elseif': - en: expected condition expression after 'elseif' - zh_CN: 期望在 'elseif' 后有条件表达式 - zh_HK: 期望在 'elseif' 後有條件表達式 -expected condition expression after 'if': - en: expected condition expression after 'if' - zh_CN: 期望在 'if' 后有条件表达式 - zh_HK: 期望在 'if' 後有條件表達式 -expected condition expression after 'until': - en: expected condition expression after 'until' - zh_CN: 期望在 'until' 后有条件表达式 - zh_HK: 期望在 'until' 後有條件表達式 -expected condition expression after 'while': - en: expected condition expression after 'while' - zh_CN: 期望在 'while' 后有条件表达式 - zh_HK: 期望在 'while' 後有條件表達式 -expected end value expression in numeric for loop: - en: expected end value expression in numeric for loop - zh_CN: 期望在数值for循环中有结束值表达式 - zh_HK: 期望在數值for迴圈中有結束值表達式 -expected expression after ',': - en: expected expression after ',' - zh_CN: 期望在 ',' 后有表达式 - zh_HK: 期望在 ',' 後有表達式 -expected expression after '=' in assignment: - en: expected expression after '=' in assignment - zh_CN: 期望在赋值中的 '=' 后有表达式 - zh_HK: 期望在賦值中的 '=' 後有表達式 -expected expression in assignment or statement: - en: expected expression in assignment or statement - zh_CN: 期望在赋值或语句中有表达式 - zh_HK: 期望在賦值或語句中有表達式 -expected expression in return statement: - en: expected expression in return statement - zh_CN: 期望在return语句中有表达式 - zh_HK: 期望在return語句中有表達式 -expected expression inside parentheses: - en: expected expression inside parentheses - zh_CN: 期望在括号内有表达式 - zh_HK: 期望在括號內有表達式 -expected expression inside table index brackets: - en: expected expression inside table index brackets - zh_CN: 期望在表索引括号内有表达式 - zh_HK: 期望在表索引括號內有表達式 -expected field name after '.': - en: expected field name after '.' - zh_CN: 期望在 '.' 后有字段名 - zh_HK: 期望在 '.' 後有字段名 -expected function name after 'function': - en: expected function name after 'function' - zh_CN: 期望在 'function' 后有函数名 - zh_HK: 期望在 'function' 後有函數名 -expected function name after 'local function': - en: expected function name after 'local function' - zh_CN: 期望在 'local function' 后有函数名 - zh_HK: 期望在 'local function' 後有函數名 -expected initialization expression after '=': - en: expected initialization expression after '=' - zh_CN: 期望在 '=' 后有初始化表达式 - zh_HK: 期望在 '=' 後有初始化表達式 -expected iterator expression after 'in': - en: expected iterator expression after 'in' - zh_CN: 期望在 'in' 后有迭代器表达式 - zh_HK: 期望在 'in' 後有迭代器表達式 -expected label name after 'goto': - en: expected label name after 'goto' - zh_CN: 期望在 'goto' 后有标签名 - zh_HK: 期望在 'goto' 後有標籤名 -expected method name after ':': - en: expected method name after ':' - zh_CN: 期望在 ':' 后有方法名 - zh_HK: 期望在 ':' 後有方法名 -expected name after '.': - en: expected name after '.' - zh_CN: 期望在 '.' 后有名称 - zh_HK: 期望在 '.' 後有名稱 -expected name after ':': - en: expected name after ':' - zh_CN: 期望在 ':' 后有名称 - zh_HK: 期望在 ':' 後有名稱 -expected parameter name: - en: expected parameter name - zh_CN: 期望参数名 - zh_HK: 期望參數名 -expected parameter name after ',': - en: expected parameter name after ',' - zh_CN: 期望在 ',' 后有参数名 - zh_HK: 期望在 ',' 後有參數名 -expected parameter name or '...' (vararg): - en: expected parameter name or '...' (vararg) - zh_CN: 期望参数名或 '...'(可变参数) - zh_HK: 期望參數名或 '...'(可變參數) -expected start value expression in numeric for loop: - en: expected start value expression in numeric for loop - zh_CN: 期望在数值for循环中有起始值表达式 - zh_HK: 期望在數值for迴圈中有起始值表達式 -expected step value expression in numeric for loop: - en: expected step value expression in numeric for loop - zh_CN: 期望在数值for循环中有步长值表达式 - zh_HK: 期望在數值for迴圈中有步長值表達式 -expected value expression after '=': - en: expected value expression after '=' - zh_CN: 期望在 '=' 后有值表达式 - zh_HK: 期望在 '=' 後有值表達式 -expected value expression after field name: - en: expected value expression after field name - zh_CN: 期望在字段名后有值表达式 - zh_HK: 期望在字段名後有值表達式 -expected variable after ',' in assignment: - en: expected variable after ',' in assignment - zh_CN: 期望在赋值中的 ',' 后有变量 - zh_HK: 期望在賦值中的 ',' 後有變數 -expected variable name after ',': - en: expected variable name after ',' - zh_CN: 期望在 ',' 后有变量名 - zh_HK: 期望在 ',' 後有變數名 -expected variable name after 'for': - en: expected variable name after 'for' - zh_CN: 期望在 'for' 后有变量名 - zh_HK: 期望在 'for' 後有變數名 -expected variable name after 'local': - en: expected variable name after 'local' - zh_CN: 期望在 'local' 后有变量名 - zh_HK: 期望在 'local' 後有變數名 -invalid attribute syntax: - en: invalid attribute syntax - zh_CN: 无效的属性语法 - zh_HK: 無效的屬性語法 -invalid function definition: - en: invalid function definition - zh_CN: 无效的函数定义 - zh_HK: 無效的函數定義 -invalid left-hand side in assignment (expected variable or table index): - en: invalid left-hand side in assignment (expected variable or table index) - zh_CN: 赋值中的左侧无效(期望变量或表索引) - zh_HK: 賦值中的左側無效(期望變數或表索引) -invalid parameter list in function definition: - en: invalid parameter list in function definition - zh_CN: 函数定义中的参数列表无效 - zh_HK: 函數定義中的參數列表無效 -invalid table constructor in function call: - en: invalid table constructor in function call - zh_CN: 函数调用中的表构造器无效 - zh_HK: 函數調用中的表構造器無效 -invalid table field after '%{sep}': - en: invalid table field after '%{sep}' - zh_CN: "'%{sep}' 后的表字段无效" - zh_HK: "'%{sep}' 後的表字段無效" -invalid table field expression: - en: invalid table field expression - zh_CN: 无效的表字段表达式 - zh_HK: 無效的表字段表達式 -invalid table field, expected expression, field assignment, or table end: - en: invalid table field, expected expression, field assignment, or table end - zh_CN: 无效的表字段,期望表达式、字段赋值或表结束 - zh_HK: 無效的表字段,期望表達式、字段賦值或表結束 -local attributes are not supported in Lua version %{level}: - en: local attributes are not supported in Lua version %{level} - zh_CN: Lua版本 %{level} 不支持局部属性 - zh_HK: Lua版本 %{level} 不支援局部屬性 -table constructor was not properly closed: - en: table constructor was not properly closed - zh_CN: 表构造器没有正确关闭 - zh_HK: 表構造器沒有正確關閉 -unary operator '%{op}' is not followed by an expression: - en: unary operator '%{op}' is not followed by an expression - zh_CN: 一元运算符 '%{op}' 后面没有跟表达式 - zh_HK: 一元運算符 '%{op}' 後面沒有跟表達式 -unexpected ')' - missing opening '(' or extra closing parenthesis: - en: unexpected ')' - missing opening '(' or extra closing parenthesis - zh_CN: 意外的 ')' - 缺少开放的 '(' 或多余的关闭括号 - zh_HK: 意外的 ')' - 缺少開放的 '(' 或多餘的關閉括號 -unexpected ')', expected expression: - en: unexpected ')', expected expression - zh_CN: 意外的 ')',期望表达式 - zh_HK: 意外的 ')',期望表達式 -unexpected ',', expected expression: - en: unexpected ',', expected expression - zh_CN: 意外的 ',',期望表达式 - zh_HK: 意外的 ',',期望表達式 -unexpected ';', expected expression: - en: unexpected ';', expected expression - zh_CN: 意外的 ';',期望表达式 - zh_HK: 意外的 ';',期望表達式 -unexpected ']' - missing opening '[' or extra closing bracket: - en: unexpected ']' - missing opening '[' or extra closing bracket - zh_CN: 意外的 ']' - 缺少开放的 '[' 或多余的关闭方括号 - zh_HK: 意外的 ']' - 缺少開放的 '[' 或多餘的關閉方括號 -unexpected ']', expected expression: - en: unexpected ']', expected expression - zh_CN: 意外的 ']',期望表达式 - zh_HK: 意外的 ']',期望表達式 -unexpected 'do' - missing corresponding loop statement: - en: unexpected 'do' - missing corresponding loop statement - zh_CN: 意外的 'do' - 缺少对应的循环语句 - zh_HK: 意外的 'do' - 缺少對應的迴圈語句 -unexpected 'do', expected expression: - en: unexpected 'do', expected expression - zh_CN: 意外的 'do',期望表达式 - zh_HK: 意外的 'do',期望表達式 -unexpected 'else' - missing corresponding 'if' statement: - en: unexpected 'else' - missing corresponding 'if' statement - zh_CN: 意外的 'else' - 缺少对应的 'if' 语句 - zh_HK: 意外的 'else' - 缺少對應的 'if' 語句 -unexpected 'else', expected expression: - en: unexpected 'else', expected expression - zh_CN: 意外的 'else',期望表达式 - zh_HK: 意外的 'else',期望表達式 -unexpected 'elseif' - missing corresponding 'if' statement: - en: unexpected 'elseif' - missing corresponding 'if' statement - zh_CN: 意外的 'elseif' - 缺少对应的 'if' 语句 - zh_HK: 意外的 'elseif' - 缺少對應的 'if' 語句 -unexpected 'elseif', expected expression: - en: unexpected 'elseif', expected expression - zh_CN: 意外的 'elseif',期望表达式 - zh_HK: 意外的 'elseif',期望表達式 -unexpected 'end' - missing corresponding block statement: - en: unexpected 'end' - missing corresponding block statement - zh_CN: 意外的 'end' - 缺少对应的块语句 - zh_HK: 意外的 'end' - 缺少對應的塊語句 -unexpected 'end', expected expression: - en: unexpected 'end', expected expression - zh_CN: 意外的 'end',期望表达式 - zh_HK: 意外的 'end',期望表達式 -unexpected 'then' - missing corresponding 'if' statement: - en: unexpected 'then' - missing corresponding 'if' statement - zh_CN: 意外的 'then' - 缺少对应的 'if' 语句 - zh_HK: 意外的 'then' - 缺少對應的 'if' 語句 -unexpected 'then', expected expression: - en: unexpected 'then', expected expression - zh_CN: 意外的 'then',期望表达式 - zh_HK: 意外的 'then',期望表達式 -unexpected 'until' - missing corresponding 'repeat' statement: - en: unexpected 'until' - missing corresponding 'repeat' statement - zh_CN: 意外的 'until' - 缺少对应的 'repeat' 语句 - zh_HK: 意外的 'until' - 缺少對應的 'repeat' 語句 -unexpected 'until', expected expression: - en: unexpected 'until', expected expression - zh_CN: 意外的 'until',期望表达式 - zh_HK: 意外的 'until',期望表達式 -unexpected '}' - missing opening '{{' or extra closing brace: - en: unexpected '}' - missing opening '{{' or extra closing brace - zh_CN: 意外的 '}' - 缺少开放的 '{' 或多余的关闭大括号 - zh_HK: 意外的 '}' - 缺少開放的 '{' 或多餘的關閉大括號 -unexpected '}', expected expression: - en: unexpected '}', expected expression - zh_CN: 意外的 '}',期望表达式 - zh_HK: 意外的 '}',期望表達式 -unexpected character '%{ch}' after number literal: - en: unexpected character '%{ch}' after number literal - zh_CN: 数字字面量后的意外字符 '%{ch}' - zh_HK: 數字字面量後的意外字符 '%{ch}' -unexpected end of file, expected expression: - en: unexpected end of file, expected expression - zh_CN: 意外的文件结束,期望表达式 - zh_HK: 意外的檔案結束,期望表達式 -unexpected end of table field: - en: unexpected end of table field - zh_CN: 意外的表字段结束 - zh_HK: 意外的表字段結束 -unexpected token '%{token}' - expected statement: - en: unexpected token '%{token}' - expected statement - zh_CN: 意外的标记 '%{token}' - 期望语句 - zh_HK: 意外的標記 '%{token}' - 期望語句 -unexpected token '%{token}', expected expression: - en: unexpected token '%{token}', expected expression - zh_CN: 意外的标记 '%{token}',期望表达式 - zh_HK: 意外的標記 '%{token}',期望表達式 -unfinished long comment: - en: unfinished long comment - zh_CN: 未完成的长注释 - zh_HK: 未完成的長註釋 diff --git a/crates/glua_parser/src/grammar/doc/mod.rs b/crates/glua_parser/src/grammar/doc/mod.rs index c33690006..1c8df6d03 100644 --- a/crates/glua_parser/src/grammar/doc/mod.rs +++ b/crates/glua_parser/src/grammar/doc/mod.rs @@ -111,11 +111,7 @@ fn expect_token(p: &mut LuaDocParser, token: LuaTokenKind) -> Result<(), LuaPars Ok(()) } else { Err(LuaParseError::syntax_error_from( - &t!( - "expected %{token}, but get %{current}", - token = token, - current = p.current_token() - ), + &format!("expected {}, but get {}", token, p.current_token()), p.current_token_range(), )) } diff --git a/crates/glua_parser/src/grammar/doc/tag.rs b/crates/glua_parser/src/grammar/doc/tag.rs index 92b2bbbd3..863956211 100644 --- a/crates/glua_parser/src/grammar/doc/tag.rs +++ b/crates/glua_parser/src/grammar/doc/tag.rs @@ -286,10 +286,10 @@ fn parse_tag_module(p: &mut LuaDocParser) -> DocParseResult { } _ => { return Err(LuaParseError::syntax_error_from( - &t!( - "expected %{token}, but get %{current}", - token = LuaTokenKind::TkString, - current = p.current_token() + &format!( + "expected {}, but get {}", + LuaTokenKind::TkString, + p.current_token() ), p.current_token_range(), )); @@ -330,10 +330,7 @@ fn parse_tag_field(p: &mut LuaDocParser) -> DocParseResult { } _ => { return Err(LuaParseError::doc_error_from( - &t!( - "expect field name or '[', but get %{current}", - current = p.current_token() - ), + &format!("expect field name or '[', but get {}", p.current_token()), p.current_token_range(), )); } @@ -386,10 +383,7 @@ fn parse_tag_param(p: &mut LuaDocParser) -> DocParseResult { p.bump(); } else { return Err(LuaParseError::doc_error_from( - &t!( - "expect param name or '...', but get %{current}", - current = p.current_token() - ), + &format!("expect param name or '...', but get {}", p.current_token()), p.current_token_range(), )); } @@ -462,7 +456,7 @@ fn parse_doc_default_value(p: &mut LuaDocParser) -> DocParseResult { LuaTokenKind::TkInt | LuaTokenKind::TkFloat => p.bump(), _ => { return Err(LuaParseError::doc_error_from( - &t!("expect numeric default literal value"), + "expect numeric default literal value", p.current_token_range(), )); } @@ -470,7 +464,7 @@ fn parse_doc_default_value(p: &mut LuaDocParser) -> DocParseResult { } _ => { return Err(LuaParseError::doc_error_from( - &t!("expect default literal value"), + "expect default literal value", p.current_token_range(), )); } diff --git a/crates/glua_parser/src/grammar/doc/types.rs b/crates/glua_parser/src/grammar/doc/types.rs index 48984eca6..ee7b76fcd 100644 --- a/crates/glua_parser/src/grammar/doc/types.rs +++ b/crates/glua_parser/src/grammar/doc/types.rs @@ -66,7 +66,7 @@ fn parse_sub_type(p: &mut LuaDocParser, limit: i32) -> DocParseResult { Ok(_) => {} Err(err) => { p.push_error(LuaParseError::doc_error_from( - &t!("unary operator not followed by type"), + "unary operator not followed by type", range, )); return Err(err); @@ -114,7 +114,7 @@ pub fn parse_binary_operator( Ok(_) => {} Err(err) => { p.push_error(LuaParseError::doc_error_from( - &t!("binary operator not followed by type"), + "binary operator not followed by type", range, )); @@ -170,7 +170,7 @@ fn parse_primary_type(p: &mut LuaDocParser) -> DocParseResult { LuaTokenKind::TkDots => parse_vararg_type(p), LuaTokenKind::TkDocNew => parse_constructor_type(p), _ => Err(LuaParseError::doc_error_from( - &t!("expect type"), + "expect type", p.current_token_range(), )), } @@ -192,7 +192,7 @@ fn parse_mapped_type(p: &mut LuaDocParser, m: Marker) -> DocParseResult { LuaTokenKind::TkLeftBracket => {} _ => { return Err(LuaParseError::doc_error_from( - &t!("expect mapped field"), + "expect mapped field", p.current_token_range(), )); } @@ -320,7 +320,7 @@ fn parse_typed_field(p: &mut LuaDocParser) -> DocParseResult { } _ => { return Err(LuaParseError::doc_error_from( - &t!("expect name or [] or []"), + "expect name or [] or []", p.current_token_range(), )); } @@ -384,7 +384,7 @@ pub fn parse_fun_type(p: &mut LuaDocParser) -> DocParseResult { if p.current_token_text() != "fun" { return Err(LuaParseError::doc_error_from( - &t!("expect fun"), + "expect fun", p.current_token_range(), )); } @@ -468,7 +468,7 @@ pub fn parse_typed_param(p: &mut LuaDocParser) -> DocParseResult { } _ => { return Err(LuaParseError::doc_error_from( - &t!("expect name or ..."), + "expect name or ...", p.current_token_range(), )); } @@ -608,7 +608,7 @@ fn parse_constructor_type(p: &mut LuaDocParser) -> DocParseResult { Ok(cm) => { if cm.kind != LuaSyntaxKind::TypeFun { let err = LuaParseError::doc_error_from( - &t!("new keyword must be followed by function type"), + "new keyword must be followed by function type", new_range, ); p.push_error(err.clone()); diff --git a/crates/glua_parser/src/grammar/lua/expr.rs b/crates/glua_parser/src/grammar/lua/expr.rs index b3afb6d04..39a3de1ca 100644 --- a/crates/glua_parser/src/grammar/lua/expr.rs +++ b/crates/glua_parser/src/grammar/lua/expr.rs @@ -23,9 +23,9 @@ fn parse_sub_expr(p: &mut LuaParser, limit: i32) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!( - "unary operator '%{op}' is not followed by an expression", - op = op_token + &format!( + "unary operator '{}' is not followed by an expression", + op_token ), op_range, )); @@ -46,9 +46,9 @@ fn parse_sub_expr(p: &mut LuaParser, limit: i32) -> ParseResult { Ok(_) => {} Err(err) => { p.push_error(LuaParseError::syntax_error_from( - &t!( - "binary operator '%{op}' is not followed by an expression", - op = op_token + &format!( + "binary operator '{}' is not followed by an expression", + op_token ), op_range, )); @@ -85,21 +85,21 @@ fn parse_simple_expr(p: &mut LuaParser) -> ParseResult { _ => { // Provide more specific error information let error_msg = match p.current_token() { - LuaTokenKind::TkEof => t!("unexpected end of file, expected expression"), - LuaTokenKind::TkRightParen => t!("unexpected ')', expected expression"), - LuaTokenKind::TkRightBrace => t!("unexpected '}', expected expression"), - LuaTokenKind::TkRightBracket => t!("unexpected ']', expected expression"), - LuaTokenKind::TkComma => t!("unexpected ',', expected expression"), - LuaTokenKind::TkSemicolon => t!("unexpected ';', expected expression"), - LuaTokenKind::TkEnd => t!("unexpected 'end', expected expression"), - LuaTokenKind::TkElse => t!("unexpected 'else', expected expression"), - LuaTokenKind::TkElseIf => t!("unexpected 'elseif', expected expression"), - LuaTokenKind::TkThen => t!("unexpected 'then', expected expression"), - LuaTokenKind::TkDo => t!("unexpected 'do', expected expression"), - LuaTokenKind::TkUntil => t!("unexpected 'until', expected expression"), - _ => t!( - "unexpected token '%{token}', expected expression", - token = p.current_token() + LuaTokenKind::TkEof => "unexpected end of file, expected expression".to_string(), + LuaTokenKind::TkRightParen => "unexpected ')', expected expression".to_string(), + LuaTokenKind::TkRightBrace => "unexpected '}', expected expression".to_string(), + LuaTokenKind::TkRightBracket => "unexpected ']', expected expression".to_string(), + LuaTokenKind::TkComma => "unexpected ',', expected expression".to_string(), + LuaTokenKind::TkSemicolon => "unexpected ';', expected expression".to_string(), + LuaTokenKind::TkEnd => "unexpected 'end', expected expression".to_string(), + LuaTokenKind::TkElse => "unexpected 'else', expected expression".to_string(), + LuaTokenKind::TkElseIf => "unexpected 'elseif', expected expression".to_string(), + LuaTokenKind::TkThen => "unexpected 'then', expected expression".to_string(), + LuaTokenKind::TkDo => "unexpected 'do', expected expression".to_string(), + LuaTokenKind::TkUntil => "unexpected 'until', expected expression".to_string(), + _ => format!( + "unexpected token '{}', expected expression", + p.current_token() ), }; @@ -127,7 +127,7 @@ pub fn parse_closure_expr(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected 'end' to close function definition"), + "expected 'end' to close function definition", p.current_token_range(), )); } @@ -142,7 +142,7 @@ fn parse_param_list(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected '(' to start parameter list"), + "expected '(' to start parameter list", p.current_token_range(), )); } @@ -155,7 +155,7 @@ fn parse_param_list(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected parameter name"), + "expected parameter name", p.current_token_range(), )); // Try to recover to next comma or right parenthesis @@ -177,7 +177,7 @@ fn parse_param_list(p: &mut LuaParser) -> ParseResult { // Check if there is a parameter after comma if p.current_token() == LuaTokenKind::TkRightParen { p.push_error(LuaParseError::syntax_error_from( - &t!("expected parameter name after ','"), + "expected parameter name after ','", p.current_token_range(), )); break; @@ -185,7 +185,7 @@ fn parse_param_list(p: &mut LuaParser) -> ParseResult { if is_vararg && !reported_trailing_after_vararg { p.push_error(LuaParseError::syntax_error_from( - &t!("vararg '...' must be the last parameter"), + "vararg '...' must be the last parameter", p.current_token_range(), )); reported_trailing_after_vararg = true; @@ -200,7 +200,7 @@ fn parse_param_list(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected ')' to close parameter list"), + "expected ')' to close parameter list", p.current_token_range(), )); } @@ -225,7 +225,7 @@ fn parse_param_name(p: &mut LuaParser, is_vararg: &mut bool) -> ParseResult { } _ => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected parameter name or '...' (vararg)"), + "expected parameter name or '...' (vararg)", p.current_token_range(), )); m.complete(p); @@ -283,7 +283,7 @@ fn parse_table_expr(p: &mut LuaParser) -> ParseResult { } Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("invalid table field after '%{sep}'", sep = separator_token), + &format!("invalid table field after '{}'", separator_token), p.current_token_range(), )); // Recover to next field boundary @@ -300,7 +300,7 @@ fn parse_table_expr(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected '}' to close table constructor"), + "expected '}' to close table constructor", p.current_token_range(), )); @@ -344,7 +344,7 @@ fn parse_table_expr(p: &mut LuaParser) -> ParseResult { if !found_brace { // 如果没有找到闭合括号,在当前位置创建一个错误标记 p.push_error(LuaParseError::syntax_error_from( - &t!("table constructor was not properly closed"), + "table constructor was not properly closed", p.current_token_range(), )); } @@ -366,7 +366,7 @@ fn parse_field_with_recovery(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected expression inside table index brackets"), + "expected expression inside table index brackets", p.current_token_range(), )); // 恢复到边界 @@ -388,7 +388,7 @@ fn parse_field_with_recovery(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected ']' to close table index"), + "expected ']' to close table index", p.current_token_range(), )); } @@ -397,7 +397,7 @@ fn parse_field_with_recovery(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected '=' after table index"), + "expected '=' after table index", p.current_token_range(), )); } @@ -406,7 +406,7 @@ fn parse_field_with_recovery(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected value expression after '='"), + "expected value expression after '='", p.current_token_range(), )); } @@ -422,7 +422,7 @@ fn parse_field_with_recovery(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected value expression after field name"), + "expected value expression after field name", p.current_token_range(), )); } @@ -433,7 +433,7 @@ fn parse_field_with_recovery(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("invalid table field expression"), + "invalid table field expression", p.current_token_range(), )); } @@ -443,7 +443,7 @@ fn parse_field_with_recovery(p: &mut LuaParser) -> ParseResult { // 表示表实际上已经结束的token LuaTokenKind::TkEof | LuaTokenKind::TkLocal => { p.push_error(LuaParseError::syntax_error_from( - &t!("unexpected end of table field"), + "unexpected end of table field", p.current_token_range(), )); } @@ -453,7 +453,7 @@ fn parse_field_with_recovery(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("invalid table field, expected expression, field assignment, or table end"), + "invalid table field, expected expression, field assignment, or table end", p.current_token_range(), )); } @@ -488,7 +488,7 @@ fn parse_suffixed_expr(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(err) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected expression inside parentheses"), + "expected expression inside parentheses", paren_range, )); m.complete(p); @@ -499,7 +499,7 @@ fn parse_suffixed_expr(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected ')' to close parentheses"), + "expected ')' to close parentheses", paren_range, )); } @@ -507,7 +507,7 @@ fn parse_suffixed_expr(p: &mut LuaParser) -> ParseResult { } _ => { p.push_error(LuaParseError::syntax_error_from( - &t!("expect primary expression (identifier or parenthesized expression)"), + "expect primary expression (identifier or parenthesized expression)", p.current_token_range(), )); return Err(ParseFailReason::UnexpectedToken); @@ -585,7 +585,7 @@ fn parse_index_struct(p: &mut LuaParser) -> Result<(), ParseFailReason> { Ok(_) => {} Err(err) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected expression inside table index brackets"), + "expected expression inside table index brackets", index_op_range, )); return Err(err); @@ -595,7 +595,7 @@ fn parse_index_struct(p: &mut LuaParser) -> Result<(), ParseFailReason> { Ok(_) => {} Err(err) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected ']' to close table index"), + "expected ']' to close table index", index_op_range, )); return Err(err); @@ -608,7 +608,7 @@ fn parse_index_struct(p: &mut LuaParser) -> Result<(), ParseFailReason> { Ok(_) => {} Err(err) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected field name after '.'"), + "expected field name after '.'", index_op_range, )); return Err(err); @@ -622,7 +622,7 @@ fn parse_index_struct(p: &mut LuaParser) -> Result<(), ParseFailReason> { Ok(_) => {} Err(err) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected method name after ':'"), + "expected method name after ':'", index_op_range, )); return Err(err); @@ -636,9 +636,7 @@ fn parse_index_struct(p: &mut LuaParser) -> Result<(), ParseFailReason> { | LuaTokenKind::TkLongString ) { p.push_error(LuaParseError::syntax_error_from( - &t!( - "colon accessor must be followed by a function call or table constructor or string literal" - ), + "colon accessor must be followed by a function call or table constructor or string literal", name_token_range, )); @@ -647,7 +645,7 @@ fn parse_index_struct(p: &mut LuaParser) -> Result<(), ParseFailReason> { } _ => { p.push_error(LuaParseError::syntax_error_from( - &t!("expect index struct"), + "expect index struct", p.current_token_range(), )); @@ -669,7 +667,7 @@ fn parse_args(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected argument expression"), + "expected argument expression", p.current_token_range(), )); // 跳过到下一个逗号或右括号 @@ -695,7 +693,7 @@ fn parse_args(p: &mut LuaParser) -> ParseResult { p.bump(); if p.current_token() == LuaTokenKind::TkRightParen { p.push_error(LuaParseError::syntax_error_from( - &t!("expected expression after ','"), + "expected expression after ','", p.current_token_range(), )); break; @@ -710,7 +708,7 @@ fn parse_args(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected ')' to close argument list"), + "expected ')' to close argument list", p.current_token_range(), )); } @@ -719,7 +717,7 @@ fn parse_args(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(err) => { p.push_error(LuaParseError::syntax_error_from( - &t!("invalid table constructor in function call"), + "invalid table constructor in function call", p.current_token_range(), )); m.complete(p); @@ -733,7 +731,7 @@ fn parse_args(p: &mut LuaParser) -> ParseResult { } _ => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected '(', string, or table constructor for function call"), + "expected '(', string, or table constructor for function call", p.current_token_range(), )); diff --git a/crates/glua_parser/src/grammar/lua/mod.rs b/crates/glua_parser/src/grammar/lua/mod.rs index effcfc3ba..2e5ff2b82 100644 --- a/crates/glua_parser/src/grammar/lua/mod.rs +++ b/crates/glua_parser/src/grammar/lua/mod.rs @@ -29,36 +29,36 @@ pub fn parse_chunk(p: &mut LuaParser) { // Provide more detailed error information let error_msg = match p.current_token() { LuaTokenKind::TkRightBrace => { - t!("unexpected '}' - missing opening '{{' or extra closing brace") + "unexpected '}' - missing opening '{' or extra closing brace".to_string() } LuaTokenKind::TkRightParen => { - t!("unexpected ')' - missing opening '(' or extra closing parenthesis") + "unexpected ')' - missing opening '(' or extra closing parenthesis".to_string() } LuaTokenKind::TkRightBracket => { - t!("unexpected ']' - missing opening '[' or extra closing bracket") + "unexpected ']' - missing opening '[' or extra closing bracket".to_string() } LuaTokenKind::TkElse => { - t!("unexpected 'else' - missing corresponding 'if' statement") + "unexpected 'else' - missing corresponding 'if' statement".to_string() } LuaTokenKind::TkElseIf => { - t!("unexpected 'elseif' - missing corresponding 'if' statement") + "unexpected 'elseif' - missing corresponding 'if' statement".to_string() } LuaTokenKind::TkEnd => { - t!("unexpected 'end' - missing corresponding block statement") + "unexpected 'end' - missing corresponding block statement".to_string() } LuaTokenKind::TkUntil => { - t!("unexpected 'until' - missing corresponding 'repeat' statement") + "unexpected 'until' - missing corresponding 'repeat' statement".to_string() } LuaTokenKind::TkThen => { - t!("unexpected 'then' - missing corresponding 'if' statement") + "unexpected 'then' - missing corresponding 'if' statement".to_string() } LuaTokenKind::TkDo => { - t!("unexpected 'do' - missing corresponding loop statement") + "unexpected 'do' - missing corresponding loop statement".to_string() } _ => { - t!( - "unexpected token '%{token}' - expected statement", - token = p.current_token() + format!( + "unexpected token '{}' - expected statement", + p.current_token() ) } }; diff --git a/crates/glua_parser/src/grammar/lua/stat.rs b/crates/glua_parser/src/grammar/lua/stat.rs index 69359e4bf..ea0eefb9c 100644 --- a/crates/glua_parser/src/grammar/lua/stat.rs +++ b/crates/glua_parser/src/grammar/lua/stat.rs @@ -14,7 +14,7 @@ use super::{ /// Push expression parsing error with lazy error message generation fn push_expr_error_lazy(p: &mut LuaParser, error_msg_fn: F) where - F: FnOnce() -> std::borrow::Cow<'static, str>, + F: FnOnce() -> String, { let error_msg = error_msg_fn(); p.push_error(LuaParseError::syntax_error_from( @@ -30,7 +30,7 @@ fn expect_keyword_with_recovery( error_msg_fn: F, ) -> bool where - F: FnOnce() -> std::borrow::Cow<'static, str>, + F: FnOnce() -> String, { if p.current_token() == expected { p.bump(); @@ -50,7 +50,7 @@ where /// Expect 'end' keyword, report error at start keyword location if missing fn expect_end_keyword(p: &mut LuaParser, start_range: crate::text::SourceRange, error_msg_fn: F) where - F: FnOnce() -> std::borrow::Cow<'static, str>, + F: FnOnce() -> String, { if p.current_token() == LuaTokenKind::TkEnd { p.bump(); @@ -148,7 +148,7 @@ fn parse_variable_name_list(p: &mut LuaParser, support_attrib: bool) -> ParseRes Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected variable name after ','"), + "expected variable name after ','", p.current_token_range(), )); } @@ -231,14 +231,14 @@ fn parse_if(p: &mut LuaParser) -> ParseResult { // Parse condition expression if parse_expr(p).is_err() { - push_expr_error_lazy(p, || t!("expected condition expression after 'if'")); + push_expr_error_lazy(p, || "expected condition expression after 'if'".to_string()); // 尝试恢复到 'then' 或语句开始 recover_to_keywords(p, &[LuaTokenKind::TkThen, LuaTokenKind::TkEnd]); } // Expect 'then' if !expect_keyword_with_recovery(p, LuaTokenKind::TkThen, || { - t!("expected 'then' after if condition") + "expected 'then' after if condition".to_string() }) { // 如果没有找到 'then',尝试恢复 recover_to_keywords( @@ -269,7 +269,7 @@ fn parse_if(p: &mut LuaParser) -> ParseResult { // Use new end expectation function to associate error with 'if' keyword expect_end_keyword(p, if_start_range, || { - t!("expected 'end' to close if statement") + "expected 'end' to close if statement".to_string() }); if_token_bump(p, LuaTokenKind::TkSemicolon); @@ -281,11 +281,13 @@ fn parse_elseif_clause(p: &mut LuaParser) -> ParseResult { p.bump(); if parse_expr(p).is_err() { - push_expr_error_lazy(p, || t!("expected condition expression after 'elseif'")); + push_expr_error_lazy(p, || { + "expected condition expression after 'elseif'".to_string() + }); } expect_keyword_with_recovery(p, LuaTokenKind::TkThen, || { - t!("expected 'then' after 'elseif' condition") + "expected 'then' after 'elseif' condition".to_string() }); parse_block(p)?; @@ -308,13 +310,15 @@ fn parse_while(p: &mut LuaParser) -> ParseResult { // Parse condition expression if parse_expr(p).is_err() { - push_expr_error_lazy(p, || t!("expected condition expression after 'while'")); + push_expr_error_lazy(p, || { + "expected condition expression after 'while'".to_string() + }); recover_to_keywords(p, &[LuaTokenKind::TkDo, LuaTokenKind::TkEnd]); } // Expect 'do' if !expect_keyword_with_recovery(p, LuaTokenKind::TkDo, || { - t!("expected 'do' after while condition") + "expected 'do' after while condition".to_string() }) { recover_to_keywords(p, &[LuaTokenKind::TkEnd]); } @@ -326,7 +330,7 @@ fn parse_while(p: &mut LuaParser) -> ParseResult { // Use new end expectation function to associate error with 'while' keyword expect_end_keyword(p, while_start_range, || { - t!("expected 'end' to close while statement") + "expected 'end' to close while statement".to_string() }); if_token_bump(p, LuaTokenKind::TkSemicolon); @@ -340,7 +344,9 @@ fn parse_do(p: &mut LuaParser) -> ParseResult { parse_block(p)?; - expect_end_keyword(p, do_start_range, || t!("expected 'end' after 'do' block")); + expect_end_keyword(p, do_start_range, || { + "expected 'end' after 'do' block".to_string() + }); if_token_bump(p, LuaTokenKind::TkSemicolon); Ok(m.complete(p)) @@ -356,7 +362,7 @@ fn parse_for(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected variable name after 'for'"), + "expected variable name after 'for'", p.current_token_range(), )); // Try to recover: skip to '=' or 'in' @@ -379,7 +385,7 @@ fn parse_for(p: &mut LuaParser) -> ParseResult { // Start value if parse_expr(p).is_err() { push_expr_error_lazy(p, || { - t!("expected start value expression in numeric for loop") + "expected start value expression in numeric for loop".to_string() }); } @@ -387,7 +393,7 @@ fn parse_for(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected ',' after start value in numeric for loop"), + "expected ',' after start value in numeric for loop", p.current_token_range(), )); } @@ -395,7 +401,7 @@ fn parse_for(p: &mut LuaParser) -> ParseResult { // End value if parse_expr(p).is_err() { push_expr_error_lazy(p, || { - t!("expected end value expression in numeric for loop") + "expected end value expression in numeric for loop".to_string() }); } @@ -404,7 +410,7 @@ fn parse_for(p: &mut LuaParser) -> ParseResult { p.bump(); if parse_expr(p).is_err() { push_expr_error_lazy(p, || { - t!("expected step value expression in numeric for loop") + "expected step value expression in numeric for loop".to_string() }); } } @@ -418,7 +424,7 @@ fn parse_for(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected variable name after ','"), + "expected variable name after ','", p.current_token_range(), )); } @@ -428,19 +434,19 @@ fn parse_for(p: &mut LuaParser) -> ParseResult { p.bump(); } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected 'in' after variable list in generic for loop"), + "expected 'in' after variable list in generic for loop", p.current_token_range(), )); } // Iterator expression list if parse_expr_list_impl(p).is_err() { - push_expr_error_lazy(p, || t!("expected iterator expression after 'in'")); + push_expr_error_lazy(p, || "expected iterator expression after 'in'".to_string()); } } _ => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected '=' for numeric for loop or ',' or 'in' for generic for loop"), + "expected '=' for numeric for loop or ',' or 'in' for generic for loop", p.current_token_range(), )); } @@ -448,7 +454,7 @@ fn parse_for(p: &mut LuaParser) -> ParseResult { // Expect 'do' if !expect_keyword_with_recovery(p, LuaTokenKind::TkDo, || { - t!("expected 'do' in for statement") + "expected 'do' in for statement".to_string() }) { recover_to_keywords(p, &[LuaTokenKind::TkEnd]); } @@ -459,7 +465,7 @@ fn parse_for(p: &mut LuaParser) -> ParseResult { } expect_end_keyword(p, for_start_range, || { - t!("expected 'end' to close for statement") + "expected 'end' to close for statement".to_string() }); if_token_bump(p, LuaTokenKind::TkSemicolon); @@ -481,7 +487,7 @@ fn parse_func_name(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected function name after 'function'"), + "expected function name after 'function'", p.current_token_range(), )); return Err(ParseFailReason::UnexpectedToken); @@ -498,7 +504,7 @@ fn parse_func_name(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected name after '.'"), + "expected name after '.'", p.current_token_range(), )); } @@ -513,7 +519,7 @@ fn parse_func_name(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected name after ':'"), + "expected name after ':'", p.current_token_range(), )); } @@ -542,7 +548,7 @@ fn parse_local(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected function name after 'local function'"), + "expected function name after 'local function'", p.current_token_range(), )); } @@ -552,7 +558,7 @@ fn parse_local(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("invalid function definition"), + "invalid function definition", p.current_token_range(), )); } @@ -565,13 +571,15 @@ fn parse_local(p: &mut LuaParser) -> ParseResult { if p.current_token().is_assign_op() { p.bump(); if parse_expr_list_impl(p).is_err() { - push_expr_error_lazy(p, || t!("expected initialization expression after '='")); + push_expr_error_lazy(p, || { + "expected initialization expression after '='".to_string() + }); } } } _ => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected 'function' or variable name after 'local'"), + "expected 'function' or variable name after 'local'", p.current_token_range(), )); @@ -589,7 +597,7 @@ fn parse_local_name(p: &mut LuaParser, support_attrib: bool) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected variable name after 'local'"), + "expected variable name after 'local'", p.current_token_range(), )); } @@ -609,7 +617,7 @@ fn parse_attrib(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected attribute name after '<'"), + "expected attribute name after '<'", p.current_token_range(), )); } @@ -618,16 +626,16 @@ fn parse_attrib(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected '>' after attribute name"), + "expected '>' after attribute name", p.current_token_range(), )); } } if !p.parse_config.support_local_attrib() { p.errors.push(LuaParseError::syntax_error_from( - &t!( - "local attribute is not supported for current version: %{level}", - level = p.parse_config.level + &format!( + "local attribute is not supported for current version: {}", + p.parse_config.level ), range, )); @@ -643,7 +651,7 @@ fn parse_return(p: &mut LuaParser) -> ParseResult { && p.current_token() != LuaTokenKind::TkSemicolon && parse_expr_list_impl(p).is_err() { - push_expr_error_lazy(p, || t!("expected expression in return statement")); + push_expr_error_lazy(p, || "expected expression in return statement".to_string()); } if_token_bump(p, LuaTokenKind::TkSemicolon); @@ -665,13 +673,15 @@ fn parse_repeat(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected 'until' after repeat block"), + "expected 'until' after repeat block", p.current_token_range(), )); } } if parse_expr(p).is_err() { - push_expr_error_lazy(p, || t!("expected condition expression after 'until'")); + push_expr_error_lazy(p, || { + "expected condition expression after 'until'".to_string() + }); } if_token_bump(p, LuaTokenKind::TkSemicolon); Ok(m.complete(p)) @@ -684,7 +694,7 @@ fn parse_goto(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected label name after 'goto'"), + "expected label name after 'goto'", p.current_token_range(), )); } @@ -708,7 +718,7 @@ fn parse_assign_or_expr_or_global_stat(p: &mut LuaParser) -> ParseResult { Ok(cm) => cm, Err(err) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected expression in assignment or statement"), + "expected expression in assignment or statement", range, )); return Err(err); @@ -733,7 +743,7 @@ fn parse_assign_or_expr_or_global_stat(p: &mut LuaParser) -> ParseResult { // 验证左值 if !matches!(cm.kind, LuaSyntaxKind::NameExpr | LuaSyntaxKind::IndexExpr) { p.push_error(LuaParseError::syntax_error_from( - &t!("invalid left-hand side in assignment (expected variable or table index)"), + "invalid left-hand side in assignment (expected variable or table index)", range, )); @@ -750,9 +760,7 @@ fn parse_assign_or_expr_or_global_stat(p: &mut LuaParser) -> ParseResult { LuaSyntaxKind::NameExpr | LuaSyntaxKind::IndexExpr ) { p.push_error(LuaParseError::syntax_error_from( - &t!( - "invalid left-hand side in assignment (expected variable or table index)" - ), + "invalid left-hand side in assignment (expected variable or table index)", p.current_token_range(), )); return Err(ParseFailReason::UnexpectedToken); @@ -760,7 +768,7 @@ fn parse_assign_or_expr_or_global_stat(p: &mut LuaParser) -> ParseResult { } Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected variable after ',' in assignment"), + "expected variable after ',' in assignment", p.current_token_range(), )); } @@ -773,11 +781,13 @@ fn parse_assign_or_expr_or_global_stat(p: &mut LuaParser) -> ParseResult { // 解析右值表达式列表 if parse_expr_list_impl(p).is_err() { - push_expr_error_lazy(p, || t!("expected expression after '=' in assignment")); + push_expr_error_lazy(p, || { + "expected expression after '=' in assignment".to_string() + }); } } else { p.push_error(LuaParseError::syntax_error_from( - &t!("expected '=' for assignment or this is an incomplete statement"), + "expected '=' for assignment or this is an incomplete statement", p.previous_token_range(), )); @@ -795,7 +805,7 @@ fn parse_label_stat(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected label name after 'goto'"), + "expected label name after 'goto'", p.current_token_range(), )); } @@ -804,7 +814,7 @@ fn parse_label_stat(p: &mut LuaParser) -> ParseResult { Ok(_) => {} Err(_) => { p.push_error(LuaParseError::syntax_error_from( - &t!("expected '::' after label name"), + "expected '::' after label name", p.current_token_range(), )); } diff --git a/crates/glua_parser/src/lexer/lua_lexer.rs b/crates/glua_parser/src/lexer/lua_lexer.rs index 982b77f75..5feb12030 100644 --- a/crates/glua_parser/src/lexer/lua_lexer.rs +++ b/crates/glua_parser/src/lexer/lua_lexer.rs @@ -160,7 +160,7 @@ impl<'a> LuaLexer<'a> { return LuaTokenKind::TkLeftBracket; } if self.reader.current_char() != '[' { - self.error(|| t!("invalid long string delimiter")); + self.error(|| "invalid long string delimiter"); return LuaTokenKind::TkLongString; } @@ -185,7 +185,7 @@ impl<'a> LuaLexer<'a> { } '<' => { if !self.lexer_config.support_integer_operation() { - self.error(|| t!("bitwise operation is not supported")); + self.error(|| "bitwise operation is not supported"); } self.reader.bump(); @@ -209,7 +209,7 @@ impl<'a> LuaLexer<'a> { } '>' => { if !self.lexer_config.support_integer_operation() { - self.error(|| t!("bitwise operation is not supported")); + self.error(|| "bitwise operation is not supported"); } self.reader.bump(); @@ -228,7 +228,7 @@ impl<'a> LuaLexer<'a> { self.reader.bump(); if self.reader.current_char() != '=' { if !self.lexer_config.support_integer_operation() { - self.error(|| t!("bitwise operation is not supported")); + self.error(|| "bitwise operation is not supported"); } return LuaTokenKind::TkBitXor; } @@ -289,7 +289,7 @@ impl<'a> LuaLexer<'a> { } } _ if self.reader.is_eof() => { - self.error(|| t!("unfinished long comment")); + self.error(|| "unfinished long comment"); return LuaTokenKind::TkLongComment; } _ => { @@ -311,7 +311,7 @@ impl<'a> LuaLexer<'a> { } _ => { if !self.lexer_config.support_integer_operation() { - self.error(|| t!("integer division is not supported")); + self.error(|| "integer division is not supported"); } self.reader.bump(); @@ -400,7 +400,7 @@ impl<'a> LuaLexer<'a> { } if !self.lexer_config.support_integer_operation() { - self.error(|| t!("bitwise operation is not supported")); + self.error(|| "bitwise operation is not supported"); } LuaTokenKind::TkBitAnd } @@ -421,7 +421,7 @@ impl<'a> LuaLexer<'a> { } if !self.lexer_config.support_integer_operation() { - self.error(|| t!("bitwise operation is not supported")); + self.error(|| "bitwise operation is not supported"); } LuaTokenKind::TkBitOr } @@ -535,7 +535,7 @@ impl<'a> LuaLexer<'a> { } if self.reader.current_char() != quote { - self.error(|| t!("unfinished string")); + self.error(|| "unfinished string"); return LuaTokenKind::TkString; } @@ -567,7 +567,7 @@ impl<'a> LuaLexer<'a> { } if !end { - self.error(|| t!("unfinished long string or comment")); + self.error(|| "unfinished long string or comment"); } LuaTokenKind::TkLongString @@ -687,7 +687,7 @@ impl<'a> LuaLexer<'a> { if self.reader.current_char().is_alphabetic() { let ch = self.reader.current_char(); - self.error(|| t!("unexpected character '%{ch}' after number literal", ch = ch)); + self.error(|| format!("unexpected character '{}' after number literal", ch)); } match state { diff --git a/crates/glua_parser/src/lib.rs b/crates/glua_parser/src/lib.rs index 2bd7da505..8a1d845cc 100644 --- a/crates/glua_parser/src/lib.rs +++ b/crates/glua_parser/src/lib.rs @@ -13,12 +13,3 @@ pub use parser_error::{LuaParseError, LuaParseErrorKind}; pub use syntax::*; pub use text::LineIndex; pub use text::{Reader, SourceRange}; - -#[macro_use] -extern crate rust_i18n; - -rust_i18n::i18n!("./locales", fallback = "en"); - -pub fn set_locale(locale: &str) { - rust_i18n::set_locale(locale); -} diff --git a/crates/glua_parser/src/syntax/node/token/number_analyzer.rs b/crates/glua_parser/src/syntax/node/token/number_analyzer.rs index 29cb44297..73526d668 100644 --- a/crates/glua_parser/src/syntax/node/token/number_analyzer.rs +++ b/crates/glua_parser/src/syntax/node/token/number_analyzer.rs @@ -61,11 +61,7 @@ pub fn float_token_value(token: &LuaSyntaxToken) -> Result { let mut value = float_part.parse::().map_err(|e| { LuaParseError::new( LuaParseErrorKind::SyntaxError, - &t!( - "The float literal '%{text}' is invalid, %{err}", - text = text, - err = e - ), + &format!("The float literal '{}' is invalid, {}", text, e), token.text_range(), ) })?; @@ -240,17 +236,13 @@ pub fn int_token_value(token: &LuaSyntaxToken) -> Result Result { if text.len() < 4 { return Err(LuaParseError::new( LuaParseErrorKind::SyntaxError, - &t!("String too short"), + "String too short", range, )); } @@ -32,9 +32,9 @@ fn long_string_value(token: &LuaSyntaxToken) -> Result { if first_char != '[' { return Err(LuaParseError::new( LuaParseErrorKind::SyntaxError, - &t!( - "Invalid long string start, expected '[', found '%{char}'", - char = first_char + &format!( + "Invalid long string start, expected '[', found '{}'", + first_char ), range, )); @@ -42,7 +42,7 @@ fn long_string_value(token: &LuaSyntaxToken) -> Result { } else { return Err(LuaParseError::new( LuaParseErrorKind::SyntaxError, - &t!("Invalid long string start, expected '[', found end of input"), + "Invalid long string start, expected '[', found end of input", range, )); } @@ -57,7 +57,7 @@ fn long_string_value(token: &LuaSyntaxToken) -> Result { } else { return Err(LuaParseError::new( LuaParseErrorKind::SyntaxError, - &t!("Invalid long string start"), + "Invalid long string start", range, )); } @@ -67,9 +67,9 @@ fn long_string_value(token: &LuaSyntaxToken) -> Result { if text.len() < i + equal_num + 2 { return Err(LuaParseError::new( LuaParseErrorKind::SyntaxError, - &t!( - "Invalid long string end, expected '%{eq}]'", - eq = "=".repeat(equal_num) + &format!( + "Invalid long string end, expected '{}]'", + "=".repeat(equal_num) ), range, )); @@ -127,7 +127,7 @@ fn normal_string_value(token: &LuaSyntaxToken) -> Result } else { return Err(LuaParseError::new( LuaParseErrorKind::SyntaxError, - &t!("Invalid hex escape sequence '\\x%{hex}'", hex = hex), + &format!("Invalid hex escape sequence '\\x{}'", hex), token.text_range(), )); } @@ -143,9 +143,9 @@ fn normal_string_value(token: &LuaSyntaxToken) -> Result } else { return Err(LuaParseError::new( LuaParseErrorKind::SyntaxError, - &t!( - "Invalid unicode escape sequence '\\u{{%{unicode_hex}}}'", - unicode_hex = unicode_hex + &format!( + "Invalid unicode escape sequence '\\u{{{{{}}}}}'", + unicode_hex ), token.text_range(), )); @@ -187,7 +187,7 @@ fn normal_string_value(token: &LuaSyntaxToken) -> Result _ => { return Err(LuaParseError::new( LuaParseErrorKind::SyntaxError, - &t!("Invalid escape sequence '\\%{char}'", char = next_char), + &format!("Invalid escape sequence '\\{}'", next_char), token.text_range(), )); } diff --git a/crates/glua_parser/src/syntax/tree/test.rs b/crates/glua_parser/src/syntax/tree/test.rs index 2c50114ca..56eb567de 100644 --- a/crates/glua_parser/src/syntax/tree/test.rs +++ b/crates/glua_parser/src/syntax/tree/test.rs @@ -1,8 +1,6 @@ #[cfg(test)] mod test { - use crate::{ - LuaAstNode, LuaLanguageLevel, LuaNonStdSymbolSet, LuaParser, ParserConfig, set_locale, - }; + use crate::{LuaAstNode, LuaLanguageLevel, LuaNonStdSymbolSet, LuaParser, ParserConfig}; // use std::time::Instant; use std::{collections::HashMap, thread}; @@ -67,7 +65,6 @@ end let code = r#" local "#; - set_locale("zh_CN"); let tree = LuaParser::parse(code, ParserConfig::default()); let errors = tree.get_errors(); for error in errors { diff --git a/docs/mintlify/annotations/call-arg.mdx b/docs/mintlify/annotations/call-arg.mdx new file mode 100644 index 000000000..e23a30728 --- /dev/null +++ b/docs/mintlify/annotations/call-arg.mdx @@ -0,0 +1,201 @@ +--- +title: "@call_arg" +description: Mark string and callback parameters so GLuaLS can understand custom GMod wrappers. +--- + +## Overview + +`@call_arg` marks a function parameter that requires special handling. Use it when you write a wrapper around base gmod functions and still want GLuaLS to understand the names, callbacks, colors, panels, skins, or scripted-class metadata passed through that wrapper. This system was implemented to replace previously hardcoded handling of special base gmod functions / methods / classes to instead implement a more generic system that can be used for other advanced use cases. + +Most addons do not need this annotation. The official GMod annotations already apply it to built-in functions like `util.AddNetworkString`, `net.Start`, `hook.Add`, `vgui.Register`, and `derma.DefineSkin`. + +It is expected that you know what you are doing if you use this annotation. + +--- + +## Syntax + +```lua +---@[call_arg("domain", "role")] +---@param paramName type +``` + +Put `@call_arg` directly above the `@param` it describes. + +```lua +---@[call_arg("gmod.net_message", "start")] +---@param messageName string +---@param ply Player +local function StartMessage(messageName, ply) + net.Start(messageName) + net.Send(ply) +end +``` + +After this, `StartMessage("...")` gets the same net message lookup, completion, hover, and navigation as `net.Start("...")`. While the net system already does try to detect cases similar to the above code, this is more of an example of how it could be used to add detection for wrappers that aren't currently autodetected. + +For overloaded signatures, use `@overload_call_arg` directly above the `@overload` it describes: + +```lua +---@[overload_call_arg(0, "gmod.network_var", "type")] +---@[overload_call_arg(1, "gmod.network_var", "define")] +---@overload fun(typeName: string, name: string, extended?: table) +---@[call_arg("gmod.network_var", "type")] +---@param typeName string +---@param slot number +---@[call_arg("gmod.network_var", "define")] +---@param name string +local function AddVar(typeName, slot, name) end +``` + +The first argument is the zero-based parameter index inside that overload. + +--- + +## Supported roles + +| Domain | Roles | Used for | +|---|---|---| +| `gmod.net_message` | `define`, `start`, `receive`, `callback`, `reference` | Net message definitions, sends, receives, callbacks, and name lookups | +| `gmod.hook` | `add`, `emit`, `callback`, `gamemode_table`, `remove`, `reference` | Hook registration, hook calls, callback inference, and hook name lookup | +| `gmod.concommand` | `define`, `callback` | Console command registration and callback tracking | +| `gmod.convar` | `define`, `define_server`, `define_client` | Server and client ConVar registration | +| `gmod.timer` | `define`, `callback`, `simple` | Timer creation and callback tracking | +| `gmod.vgui_panel` | `define`, `define_control`, `table`, `base`, `reference` | VGUI panel registration, inheritance, and panel name lookup | +| `gmod.derma_skin` | `define`, `reference` | Derma skin registration and skin name lookup | +| `gmod.network_var` | `type`, `define`, `define_element` | Entity `NetworkVar` and `NetworkVarElement` accessor generation | +| `gmod.class_base` | `reference` | `DEFINE_BASECLASS` inheritance metadata | +| `gmod.gamemode` | `reference` | `DeriveGamemode` inheritance metadata | +| `gmod.color` | `r`, `g`, `b`, `a` | Color previews for custom color constructors | + +Unknown domains and roles are stored as metadata but ignored by GMod-specific features until GLuaLS adds a consumer for them. This is intentional to prevent errors for outdated clients. + +--- + +## Hooks with callbacks + +Use one role for the hook name and one role for the callback function: + +```lua +---@[call_arg("gmod.hook", "add")] +---@param eventName string +---@param id any +---@[call_arg("gmod.hook", "callback")] +---@param callback fun(...) +local function AddHook(eventName, id, callback) + hook.Add(eventName, id, callback) +end +``` + +GLuaLS uses the hook name to infer callback parameters: + +```lua +AddHook("PlayerSpawn", "my-addon", function(ply) + ply:SteamID() -- ply is Player +end) +``` + +Use `emit` for functions that call hooks: + +```lua +---@[call_arg("gmod.hook", "emit")] +---@param eventName string +local function RunHook(eventName, ...) + return hook.Run(eventName, ...) +end +``` + +--- + +## VGUI panels + +Use `define`, `table`, and `base` for a `vgui.Register` wrapper: + +```lua +---@[call_arg("gmod.vgui_panel", "define")] +---@param className string +---@[call_arg("gmod.vgui_panel", "table")] +---@param panel table +---@[call_arg("gmod.vgui_panel", "base")] +---@param baseName string +local function RegisterPanel(className, panel, baseName) + vgui.Register(className, panel, baseName) +end +``` + +Use `define_control` instead of `define` for `derma.DefineControl` wrappers. Use `reference` for functions that create or look up panels by name. + +--- + +## Derma skins + +Use `gmod.derma_skin` when wrapping Derma skin functions: + +```lua +---@[call_arg("gmod.derma_skin", "define")] +---@param name string +---@param description string +---@param skin table +local function DefineSkin(name, description, skin) + derma.DefineSkin(name, description, skin) +end + +---@[call_arg("gmod.derma_skin", "reference")] +---@param name string +local function UseSkin(name) + return derma.GetNamedSkin(name) +end +``` + +GLuaLS can then find references between `DefineSkin("MySkin", ...)` and `UseSkin("MySkin")`. + +--- + +## Scripted classes + +Use `gmod.network_var` when wrapping entity NetworkVar helpers: + +```lua +---@[call_arg("gmod.network_var", "define")] +---@param name string +---@[call_arg("gmod.network_var", "type")] +---@param typeName string +local function AddVar(name, typeName) + ENT:NetworkVar(typeName, 0, name) +end +``` + +Use `define_element` for wrappers around `NetworkVarElement`, because element accessors always return numbers. + +For gamemode inheritance wrappers, mark the base-name parameter: + +```lua +---@[call_arg("gmod.gamemode", "reference")] +---@param base string +local function Derive(base) + DeriveGamemode(base) +end +``` + +Use `gmod.class_base` `reference` for wrappers around `DEFINE_BASECLASS`. + +--- + +## Priority + +An optional third argument breaks ties when one parameter can inherit multiple roles: + +```lua +---@[call_arg("gmod.hook", "emit", 10)] +---@param name string +``` + +Higher priority wins. You normally do not need this unless you are annotating a generic wrapper that can behave like several GMod APIs. + +--- + +## See also + +- [Network analysis](/language/network-analysis): net message tracking +- [Hook intelligence](/language/hook-intelligence): hook completion and callback inference +- [VGUI support](/language/vgui-support): panels and Derma skins diff --git a/docs/mintlify/annotations/index.mdx b/docs/mintlify/annotations/index.mdx index 7618209c9..75ef0119c 100644 --- a/docs/mintlify/annotations/index.mdx +++ b/docs/mintlify/annotations/index.mdx @@ -54,6 +54,7 @@ GLuaLS follows EmmyLua-style annotations and adds support for GMod-specific case | [`@outparam`](/annotations/outparam) | Document a parameter table field modified by a function | `---@outparam cfg.output TraceResult` | | [`@fileparam`](/annotations/fileparam) | Set a file-wide parameter type default | `---@fileparam ply Player` | | [`@accessorfunc`](/annotations/accessorfunc) | Mark a function as an accessor generator | `---@accessorfunc` | +| [`@call_arg`](/annotations/call-arg) | Mark wrapper parameters as GMod names or callbacks | `---@[call_arg("gmod.hook", "add")]` | ## Control and metadata annotations diff --git a/docs/mintlify/docs.json b/docs/mintlify/docs.json index 23bfb4f28..005202e4b 100644 --- a/docs/mintlify/docs.json +++ b/docs/mintlify/docs.json @@ -114,7 +114,8 @@ "annotations/realm", "annotations/hook", "annotations/fileparam", - "annotations/accessorfunc" + "annotations/accessorfunc", + "annotations/call-arg" ] }, { diff --git a/docs/mintlify/language/vgui-support.mdx b/docs/mintlify/language/vgui-support.mdx index 605fb5a26..6bf006d58 100644 --- a/docs/mintlify/language/vgui-support.mdx +++ b/docs/mintlify/language/vgui-support.mdx @@ -81,6 +81,43 @@ After `vgui.Register(name, PANEL, base)`, GLuaLS recognizes `"MyPanel"` in later --- +## Derma controls and skins + +GLuaLS also understands `derma.DefineControl`, `derma.DefineSkin`, `derma.GetNamedSkin`, `derma.GetSkinTable()`, and panel skin setters: + +```lua +local SKIN = {} + +function SKIN:PaintFrame(panel, w, h) + draw.RoundedBox(0, 0, 0, w, h, color_black) +end + +derma.DefineSkin("MySkin", "Dark UI skin", SKIN) + +local frame = vgui.Create("DFrame") +frame:SetSkin("MySkin") -- navigates to the skin definition +``` + +Skin names support completion, hover, references, and go-to-definition in the same way as panel names. + +--- + +## Custom wrappers + +If your framework wraps VGUI or Derma APIs, annotate the wrapper parameters with [`@call_arg`](/annotations/call-arg). This lets GLuaLS recognize the wrapped calls without matching the wrapper by name. + +```lua +---@[call_arg("gmod.derma_skin", "define")] +---@param name string +---@param description string +---@param skin table +function MyUI.DefineSkin(name, description, skin) + derma.DefineSkin(name, description, skin) +end +``` + +--- + ## Scripted Entity Explorer The Scripted Class Explorer lists VGUI panels for navigation. Click a panel class in the explorer to open its definition. diff --git a/tools/benchmark/src/main.rs b/tools/benchmark/src/main.rs index 2ac3df482..20a7889d3 100644 --- a/tools/benchmark/src/main.rs +++ b/tools/benchmark/src/main.rs @@ -13,12 +13,6 @@ use glua_code_analysis::{ }; use tokio_util::sync::CancellationToken; -/// Default paths — override with env vars -const DEFAULT_LARGE_CODEBASE: &str = - r"A:\Misc\FearlessSRCDS\steamapps\common\GarrysModDS\garrysmod\gamemodes\cityrp"; -const DEFAULT_ANNOTATIONS: &str = - r"C:\Users\Pollux\Documents\glualangserver\emmylua-rust\glua-api-snippets\output"; - fn setup_logger() { let log_file = std::env::var("BENCH_LOG").unwrap_or_else(|_| "benchmark_profile.log".to_string()); @@ -76,10 +70,14 @@ fn discover_config_files(root: &Path) -> Vec { async fn main() { setup_logger(); - let large_codebase = - std::env::var("BENCH_CODEBASE").unwrap_or_else(|_| DEFAULT_LARGE_CODEBASE.to_string()); - let annotations = - std::env::var("BENCH_ANNOTATIONS").unwrap_or_else(|_| DEFAULT_ANNOTATIONS.to_string()); + let large_codebase = std::env::var("BENCH_CODEBASE").unwrap_or_else(|_| { + eprintln!("ERROR: BENCH_CODEBASE env var is required"); + std::process::exit(1); + }); + let annotations = std::env::var("BENCH_ANNOTATIONS").unwrap_or_else(|_| { + eprintln!("ERROR: BENCH_ANNOTATIONS env var is required"); + std::process::exit(1); + }); let large_path = PathBuf::from(&large_codebase); let annotations_path = PathBuf::from(&annotations); @@ -124,7 +122,7 @@ async fn main() { let t = Instant::now(); let mut analysis = EmmyLuaAnalysis::new(); analysis.update_config(Arc::new(emmyrc.clone())); - analysis.init_std_lib(None); + analysis.init_std_lib(); // Add annotations as library workspace analysis.add_library_workspace(annotations_path.clone()); @@ -197,15 +195,26 @@ async fn main() { // Precompute shared diagnostic data once (avoids per-file workspace-wide scans) let shared_data = analysis.precompute_diagnostic_shared_data(); - let parallelism = std::env::var("BENCH_THREADS") - .ok() - .and_then(|s| s.parse::().ok()) - .unwrap_or_else(|| { - std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(1) - .min(16) - }); + let parallelism = match std::env::var("BENCH_THREADS") { + Ok(val) => match val.parse::() { + Ok(n) if n > 0 => n, + Ok(_) => { + eprintln!( + "ERROR: BENCH_THREADS must be a positive integer, got: {}", + val + ); + std::process::exit(1); + } + Err(_) => { + eprintln!("ERROR: BENCH_THREADS is not a valid integer: {}", val); + std::process::exit(1); + } + }, + Err(_) => std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1) + .min(16), + }; eprintln!( "Diagnostics: {} files, {} threads", diag_file_count, parallelism diff --git a/tools/std_i18n/Cargo.toml b/tools/std_i18n/Cargo.toml deleted file mode 100644 index f103f998c..000000000 --- a/tools/std_i18n/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "std_i18n" -version = "0.1.0" -edition = "2024" - -[dependencies] -# local -glua_parser.workspace = true - -# external -serde.workspace = true -serde_yml.workspace = true -walkdir.workspace = true diff --git a/tools/std_i18n/src/comment_syntax.rs b/tools/std_i18n/src/comment_syntax.rs deleted file mode 100644 index 0bd36838a..000000000 --- a/tools/std_i18n/src/comment_syntax.rs +++ /dev/null @@ -1,216 +0,0 @@ -use std::collections::HashMap; - -#[derive(Debug, Clone, Copy)] -pub struct LineInfo { - pub start: usize, - pub end: usize, // 不含换行(也不含 CR) -} - -impl LineInfo { - pub fn text<'a>(&self, raw: &'a str) -> &'a str { - raw.get(self.start..self.end).unwrap_or("") - } - - pub fn indent(&self, raw: &str) -> String { - self.text(raw) - .chars() - .take_while(|c| c.is_whitespace()) - .collect() - } - - pub fn trim_start_text<'a>(&self, raw: &'a str) -> &'a str { - self.text(raw).trim_start() - } -} - -pub fn split_lines_with_offsets(s: &str) -> Vec { - let bytes = s.as_bytes(); - let mut out: Vec = Vec::new(); - - let mut line_start = 0usize; - let mut i = 0usize; - while i < bytes.len() { - if bytes[i] == b'\n' { - let mut line_end = i; - if line_end > line_start && bytes[line_end - 1] == b'\r' { - line_end -= 1; - } - out.push(LineInfo { - start: line_start, - end: line_end, - }); - line_start = i + 1; - } - i += 1; - } - - if line_start <= bytes.len() { - out.push(LineInfo { - start: line_start, - end: bytes.len(), - }); - } - - out -} - -pub fn normalize_optional_name(s: &str) -> String { - s.trim() - .trim_end_matches('?') - .trim_end_matches(',') - .to_string() -} - -pub fn normalize_field_key_token(token: &str) -> String { - let t = token.trim(); - if let Some(inner) = t.strip_prefix("[\"").and_then(|s| s.strip_suffix("\"]")) { - return inner.to_string(); - } - if let Some(inner) = t.strip_prefix("['").and_then(|s| s.strip_suffix("']")) { - return inner.to_string(); - } - if (t.starts_with('"') && t.ends_with('"')) || (t.starts_with('\'') && t.ends_with('\'')) { - return t[1..t.len() - 1].to_string(); - } - t.to_string() -} - -pub fn parse_param_name_from_line(trimmed: &str) -> Option { - let after = doc_tag_payload(trimmed, "@param")?; - let name = after.split_whitespace().next()?; - Some(normalize_optional_name(name)) -} - -pub fn parse_field_name_from_line(trimmed: &str) -> Option { - let after = doc_tag_payload(trimmed, "@field")?; - let token = after.split_whitespace().next()?; - Some(normalize_optional_name(&normalize_field_key_token(token))) -} - -pub fn is_doc_tag_line(line_trim_start: &str) -> bool { - let t = line_trim_start.trim_start(); - let Some(after) = t.strip_prefix("---") else { - return false; - }; - let after = after.trim_start(); - after.starts_with('@') -} - -pub fn find_desc_block_line_range(raw: &str, lines: &[LineInfo]) -> Option<(usize, usize)> { - let mut start_idx: Option = None; - for (i, li) in lines.iter().enumerate() { - let t = li.trim_start_text(raw); - if is_doc_tag_line(t) || t.starts_with("---|") { - continue; - } - if t.starts_with("---") { - start_idx = Some(i); - break; - } - } - let start = start_idx?; - - let mut end = start; - while end < lines.len() { - let t = lines[end].trim_start_text(raw); - if is_doc_tag_line(t) || t.starts_with("---|") { - break; - } - if t.starts_with("---") { - end += 1; - continue; - } - break; - } - Some((start, end)) -} - -/// 解析 union item 行的 value 部分。 -/// -/// 支持: -/// - `---| "n" # ...` -/// - `---|>"collect" # ...` -/// - `---|+"n" # ...` -/// - `---|>+"n" # ...` -pub fn parse_union_item_value_from_line_trim(line_trim_start: &str) -> Option { - let after = line_trim_start.strip_prefix("---|")?.trim_start(); - let after = after.strip_prefix('>').unwrap_or(after).trim_start(); - let after = after.strip_prefix('+').unwrap_or(after).trim_start(); - - if let Some(rest) = after.strip_prefix('"') { - let end = rest.find('"')?; - return Some(rest[..end].to_string()); - } - if let Some(rest) = after.strip_prefix('\'') { - let end = rest.find('\'')?; - return Some(rest[..end].to_string()); - } - - let end = after - .find(|c: char| c.is_whitespace() || c == '#') - .unwrap_or(after.len()); - if end == 0 { - None - } else { - Some(after[..end].to_string()) - } -} - -pub fn build_tag_line_indexes(raw: &str, lines: &[LineInfo]) -> TagLineIndexes { - let default_indent = lines.first().map(|l| l.indent(raw)).unwrap_or_default(); - - let desc_block = find_desc_block_line_range(raw, lines); - - let mut param_line: HashMap = HashMap::new(); - let mut field_line: HashMap = HashMap::new(); - let mut return_lines: Vec = Vec::new(); - let mut union_line: HashMap = HashMap::new(); - - for (i, li) in lines.iter().enumerate() { - let t = li.trim_start_text(raw); - - if let Some(name) = parse_param_name_from_line(t) { - param_line.entry(name).or_insert(i); - continue; - } - if let Some(name) = parse_field_name_from_line(t) { - field_line.entry(name).or_insert(i); - continue; - } - if doc_tag_payload(t, "@return").is_some() { - return_lines.push(i); - continue; - } - if t.starts_with("---|") - && let Some(value) = parse_union_item_value_from_line_trim(t) - { - union_line.entry(value).or_insert(i); - } - } - - TagLineIndexes { - default_indent, - desc_block, - param_line, - field_line, - return_lines, - union_line, - } -} - -pub struct TagLineIndexes { - pub default_indent: String, - pub desc_block: Option<(usize, usize)>, - pub param_line: HashMap, - pub field_line: HashMap, - pub return_lines: Vec, - pub union_line: HashMap, -} - -fn doc_tag_payload<'a>(line_trim_start: &'a str, tag: &str) -> Option<&'a str> { - // 支持 `---@param ...` 以及 `--- @param ...`(中间允许空格)。 - let t = line_trim_start.trim_start(); - let after = t.strip_prefix("---")?.trim_start(); - let after = after.strip_prefix(tag)?; - Some(after.trim_start()) -} diff --git a/tools/std_i18n/src/extractor.rs b/tools/std_i18n/src/extractor.rs deleted file mode 100644 index 8f253ee16..000000000 --- a/tools/std_i18n/src/extractor.rs +++ /dev/null @@ -1,918 +0,0 @@ -use crate::comment_syntax::{is_doc_tag_line, parse_union_item_value_from_line_trim}; -use crate::keys::{ - build_module_table_to_class_map, locale_key_desc, locale_key_field, locale_key_item, - locale_key_param, locale_key_return, locale_key_return_item, map_symbol_for_locale_key, -}; -use crate::model::{ - AnalyzedLuaDocFile, ExtractedComment, ExtractedEntry, ExtractedFile, ExtractedKind, SourceSpan, -}; -use glua_parser::{ - LuaAst, LuaAstNode, LuaAstToken, LuaComment, LuaDocDescriptionOwner, LuaDocMultiLineUnionType, - LuaDocTag, LuaDocType, LuaExpr, LuaIndexExpr, LuaLiteralToken, LuaParser, LuaVarExpr, - ParserConfig, -}; -use std::collections::{HashMap, HashSet}; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; -use walkdir::WalkDir; - -#[derive(Debug, Clone)] -struct RootContext { - span: SourceSpan, - symbol: String, - base: String, - version_suffix: Option, -} - -type LocaleBaseMap = HashMap<(SourceSpan, String), String>; - -/// 从 `std_dir/*.lua` 提取 i18n 条目,并尽量保持“分析顺序”: -/// - 文件顺序:按相对路径排序(稳定、可复现) -/// - 文件内:按注释在源码中的位置(text_range.start)排序 -/// - 注释内:按 tag/item 在源码中的出现顺序输出 -pub fn extract_std_dir( - std_dir: &Path, - include_empty: bool, -) -> Result, Box> { - let mut files: Vec<(PathBuf, PathBuf)> = WalkDir::new(std_dir) - .min_depth(1) - .max_depth(2) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_file()) - .map(|e| e.into_path()) - .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("lua")) - .filter_map(|full| { - let rel = full.strip_prefix(std_dir).ok()?.to_path_buf(); - Some((rel, full)) - }) - .collect(); - - files.sort_by(|(a, _), (b, _)| a.cmp(b)); - - let mut out: Vec = Vec::new(); - for (rel_path, full_path) in files { - let file_name = rel_path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or_default() - .to_string(); - let content = std::fs::read_to_string(&full_path)?; - let analyzed = analyze_lua_doc_file(&file_name, &content, include_empty); - out.push(ExtractedFile { - path: rel_path, - comments: analyzed.comments, - entries: analyzed.entries, - }); - } - - Ok(out) -} - -pub fn analyze_lua_doc_file( - file_name: &str, - content: &str, - include_empty: bool, -) -> AnalyzedLuaDocFile { - let module_map = build_module_table_to_class_map(content); - - let tree = LuaParser::parse(content, ParserConfig::default()); - let chunk = tree.get_chunk_node(); - let mut comments: Vec = chunk.descendants::().collect(); - comments.sort_by_key(|c| c.syntax().text_range().start()); - - #[derive(Debug, Clone)] - struct CommentRecord { - comment: LuaComment, - span: SourceSpan, - raw: String, - effective_version: Option, - } - - // 有些 std 文档会把 `@version` 单独放在上一条注释里(紧挨着真正的 doc block)。 - // 这种 `@version` 注释通常没有 owner,因此当它与下一条“有 owner 的注释”相邻且中间只有空白时, - // 我们把 version 后缀透传给下一条注释。 - let mut pending_version: Option<(String, usize)> = None; // (suffix, end_offset) - - let mut records: Vec = Vec::with_capacity(comments.len()); - let mut roots: Vec = Vec::new(); - - for comment in comments { - let range = comment.syntax().text_range(); - let start: usize = range.start().into(); - let end: usize = range.end().into(); - let span = SourceSpan { start, end }; - - let raw_slice = content.get(start..end).unwrap_or(""); - let raw_comment = raw_slice.to_string(); - - let direct_version = extract_version_suffix(&comment, &raw_comment); - let has_owner = comment.get_owner().is_some(); - - let mut effective_version = direct_version.clone(); - if effective_version.is_none() - && let Some((pending, pending_end)) = pending_version.as_ref() - && has_owner - && is_whitespace_between(content, *pending_end, start) - { - effective_version = Some(pending.clone()); - } - - for symbol in root_symbols_for_comment(&comment, &raw_comment) { - let base = map_symbol_for_locale_key(&symbol, &module_map); - roots.push(RootContext { - span, - symbol, - base, - version_suffix: effective_version.clone(), - }); - } - - records.push(CommentRecord { - comment, - span, - raw: raw_comment, - effective_version, - }); - - if has_owner { - pending_version = None; - } else if let Some(v) = direct_version { - pending_version = Some((v, end)); - } - } - - let locale_base_map = build_locale_base_map(&roots); - - let mut out_comments: Vec = Vec::new(); - let mut out_entries: Vec = Vec::new(); - let mut seen: HashSet = HashSet::new(); - - for r in records { - let mut comment_entries: Vec = Vec::new(); - extract_from_comment( - file_name, - &r.comment, - &r.raw, - include_empty, - r.effective_version.as_deref(), - r.span, - &module_map, - &locale_base_map, - &mut comment_entries, - &mut seen, - ); - - if !comment_entries.is_empty() { - out_entries.extend(comment_entries.iter().cloned()); - out_comments.push(ExtractedComment { - span: r.span, - raw: r.raw, - entries: comment_entries, - }); - } - } - - AnalyzedLuaDocFile { - module_map, - comments: out_comments, - entries: out_entries, - } -} - -fn build_locale_base_map(roots: &[RootContext]) -> LocaleBaseMap { - let mut groups: HashMap> = HashMap::new(); - for (i, r) in roots.iter().enumerate() { - groups.entry(r.base.clone()).or_default().push(i); - } - - let mut map: LocaleBaseMap = HashMap::new(); - for (base, idxs) in groups { - if idxs.len() <= 1 { - if let Some(i) = idxs.first() { - let r = &roots[*i]; - map.insert((r.span, r.symbol.clone()), base.clone()); - } - continue; - } - - let mut used_suffix: HashMap = HashMap::new(); - let mut no_version_seq: usize = 0; - for i in idxs { - let r = &roots[i]; - let mut suffix = if let Some(v) = &r.version_suffix { - v.clone() - } else { - no_version_seq += 1; - format!("@@{no_version_seq}") - }; - - let c = used_suffix.entry(suffix.clone()).or_insert(0); - *c += 1; - if *c > 1 { - suffix.push_str(&format!("@@{c}")); - } - - map.insert((r.span, r.symbol.clone()), format!("{base}{suffix}")); - } - } - - map -} - -fn extract_from_comment( - file_name: &str, - comment: &LuaComment, - raw_comment: &str, - include_empty: bool, - version_suffix: Option<&str>, - comment_span: SourceSpan, - module_map: &HashMap, - locale_base_map: &LocaleBaseMap, - out: &mut Vec, - seen: &mut HashSet, -) { - let owner_symbol = comment.get_owner().and_then(owner_symbol_from_ast); - let tags: Vec = comment.get_doc_tags().collect(); - - let mut class_name: Option = None; - let mut alias_name: Option = None; - for tag in &tags { - match tag { - LuaDocTag::Class(class_tag) => { - let text = class_tag.syntax().text().to_string(); - class_name = class_name.or_else(|| parse_tag_primary_name(&text)); - } - LuaDocTag::Alias(alias_tag) => { - let text = alias_tag.syntax().text().to_string(); - alias_name = alias_name.or_else(|| parse_tag_primary_name(&text)); - } - _ => {} - } - } - - // 优先从原始源码行提取(支持 `std.readmode` 这类带点的名字)。 - alias_name = alias_name.or_else(|| extract_tag_name_from_raw(raw_comment, "alias")); - class_name = class_name.or_else(|| extract_tag_name_from_raw(raw_comment, "class")); - - // 1) owner/函数文档:desc/param/return(以及 return 多行 union item) - if let Some(symbol) = owner_symbol.as_deref() { - let base = map_symbol_for_locale_key(symbol, module_map); - let locale_base = locale_base_map - .get(&(comment_span, symbol.to_string())) - .cloned() - .unwrap_or_else(|| base.clone()); - let desc_raw = comment - .get_description() - .map(|d| d.get_description_text()) - .or_else(|| extract_owner_description_fallback(raw_comment)) - .unwrap_or_default(); - let desc_text = preprocess_description(&desc_raw); - push_entry( - out, - seen, - ExtractedEntry { - key: make_key(file_name, symbol, version_suffix, "desc", None), - locale_key: locale_key_desc(&locale_base), - kind: ExtractedKind::Desc, - symbol: symbol.to_string(), - base: base.clone(), - version_suffix: version_suffix.map(|s| s.to_string()), - comment_span, - raw: desc_raw, - value: desc_text, - }, - include_empty, - ); - - let mut return_index: usize = 0; - for tag in &tags { - match tag { - LuaDocTag::Param(param) => { - let Some(name_token) = param.get_name_token() else { - continue; - }; - let name = name_token.get_name_text().to_string(); - let raw_desc = param - .get_description() - .map(|d| d.get_description_text()) - .unwrap_or_default(); - let text = preprocess_description(&raw_desc); - push_entry( - out, - seen, - ExtractedEntry { - key: make_key(file_name, symbol, version_suffix, "param", Some(&name)), - locale_key: locale_key_param(&locale_base, &name), - kind: ExtractedKind::Param { name: name.clone() }, - symbol: symbol.to_string(), - base: base.clone(), - version_suffix: version_suffix.map(|s| s.to_string()), - comment_span, - raw: raw_desc, - value: text, - }, - include_empty, - ); - } - LuaDocTag::Return(ret) => { - return_index += 1; - let ident = return_index.to_string(); - let raw_desc = ret - .get_description() - .map(|d| d.get_description_text()) - .unwrap_or_default(); - let text = preprocess_description(&raw_desc); - push_entry( - out, - seen, - ExtractedEntry { - key: make_key( - file_name, - symbol, - version_suffix, - "return", - Some(&ident), - ), - locale_key: locale_key_return(&locale_base, &ident), - kind: ExtractedKind::Return { - index: return_index, - }, - symbol: symbol.to_string(), - base: base.clone(), - version_suffix: version_suffix.map(|s| s.to_string()), - comment_span, - raw: raw_desc, - value: text, - }, - include_empty, - ); - - for (value, raw_item_desc) in - return_union_items_for_index(raw_comment, return_index) - { - let item_key = format!("{ident}.{value}"); - let text = preprocess_description(&raw_item_desc); - push_entry( - out, - seen, - ExtractedEntry { - key: make_key( - file_name, - symbol, - version_suffix, - "return_item", - Some(&item_key), - ), - locale_key: locale_key_return_item(&locale_base, &ident, &value), - kind: ExtractedKind::ReturnItem { - index: return_index, - value: value.clone(), - }, - symbol: symbol.to_string(), - base: base.clone(), - version_suffix: version_suffix.map(|s| s.to_string()), - comment_span, - raw: raw_item_desc, - value: text, - }, - include_empty, - ); - } - } - _ => {} - } - } - - return; - } - - // 2) class/table 文档:desc/field - if let Some(class_symbol) = class_name.as_deref() { - let base = map_symbol_for_locale_key(class_symbol, module_map); - let locale_base = locale_base_map - .get(&(comment_span, class_symbol.to_string())) - .cloned() - .unwrap_or_else(|| base.clone()); - let desc_raw = comment - .get_description() - .map(|d| d.get_description_text()) - .or_else(|| extract_owner_description_fallback(raw_comment)) - .unwrap_or_default(); - let text = preprocess_description(&desc_raw); - push_entry( - out, - seen, - ExtractedEntry { - key: make_key(file_name, class_symbol, version_suffix, "desc", None), - locale_key: locale_key_desc(&locale_base), - kind: ExtractedKind::Desc, - symbol: class_symbol.to_string(), - base: base.clone(), - version_suffix: version_suffix.map(|s| s.to_string()), - comment_span, - raw: desc_raw, - value: text, - }, - include_empty, - ); - - for tag in &tags { - if let LuaDocTag::Field(field) = tag { - let field_key = field.get_field_key(); - let Some(field_name) = field_key.and_then(format_doc_field_key) else { - continue; - }; - let raw_desc = field - .get_description() - .map(|d| d.get_description_text()) - .unwrap_or_default(); - let text = preprocess_description(&raw_desc); - push_entry( - out, - seen, - ExtractedEntry { - key: make_key( - file_name, - class_symbol, - version_suffix, - "field", - Some(&field_name), - ), - locale_key: locale_key_field(&locale_base, &field_name), - kind: ExtractedKind::Field { - name: field_name.clone(), - }, - symbol: class_symbol.to_string(), - base: base.clone(), - version_suffix: version_suffix.map(|s| s.to_string()), - comment_span, - raw: raw_desc, - value: text, - }, - include_empty, - ); - } - } - } - - // 3) alias:desc + 多行 union 枚举项(item.) - if let Some(alias_symbol) = alias_name.as_deref() { - let base = map_symbol_for_locale_key(alias_symbol, module_map); - let locale_base = locale_base_map - .get(&(comment_span, alias_symbol.to_string())) - .cloned() - .unwrap_or_else(|| base.clone()); - let desc_raw = comment - .get_description() - .map(|d| d.get_description_text()) - .or_else(|| extract_owner_description_fallback(raw_comment)) - .unwrap_or_default(); - let text = preprocess_description(&desc_raw); - push_entry( - out, - seen, - ExtractedEntry { - key: make_key(file_name, alias_symbol, version_suffix, "desc", None), - locale_key: locale_key_desc(&locale_base), - kind: ExtractedKind::Desc, - symbol: alias_symbol.to_string(), - base: base.clone(), - version_suffix: version_suffix.map(|s| s.to_string()), - comment_span, - raw: desc_raw, - value: text, - }, - include_empty, - ); - - if let Some(union) = comment.descendants::().next() { - for field in union.get_fields() { - let Some(field_type) = field.get_type() else { - continue; - }; - let Some(value) = literal_value_from_doc_type(&field_type) else { - continue; - }; - let raw_desc = field - .get_description() - .map(|d| d.get_description_text()) - .unwrap_or_default(); - let text = preprocess_description(&raw_desc); - push_entry( - out, - seen, - ExtractedEntry { - key: make_key( - file_name, - alias_symbol, - version_suffix, - "item", - Some(&value), - ), - locale_key: locale_key_item(&locale_base, &value), - kind: ExtractedKind::Item { - value: value.clone(), - }, - symbol: alias_symbol.to_string(), - base: base.clone(), - version_suffix: version_suffix.map(|s| s.to_string()), - comment_span, - raw: raw_desc, - value: text, - }, - include_empty, - ); - } - } - } -} - -fn push_entry( - out: &mut Vec, - seen: &mut HashSet, - entry: ExtractedEntry, - include_empty: bool, -) { - if entry.value.trim().is_empty() && !include_empty { - return; - } - if !seen.insert(entry.locale_key.clone()) { - let _ = writeln!( - io::stderr(), - "warning: duplicate locale_key {} (kept first, ignored new value)", - entry.locale_key - ); - return; - } - out.push(entry); -} - -fn root_symbols_for_comment(comment: &LuaComment, raw_comment: &str) -> Vec { - let owner_symbol = comment.get_owner().and_then(owner_symbol_from_ast); - if let Some(symbol) = owner_symbol { - return vec![symbol]; - } - - let tags: Vec = comment.get_doc_tags().collect(); - let mut class_name: Option = None; - let mut alias_name: Option = None; - for tag in &tags { - match tag { - LuaDocTag::Class(class_tag) => { - let text = class_tag.syntax().text().to_string(); - class_name = class_name.or_else(|| parse_tag_primary_name(&text)); - } - LuaDocTag::Alias(alias_tag) => { - let text = alias_tag.syntax().text().to_string(); - alias_name = alias_name.or_else(|| parse_tag_primary_name(&text)); - } - _ => {} - } - } - - // 优先从原始源码行提取(支持 `std.readmode` 这类带点的名字)。 - alias_name = alias_name.or_else(|| extract_tag_name_from_raw(raw_comment, "alias")); - class_name = class_name.or_else(|| extract_tag_name_from_raw(raw_comment, "class")); - - let mut out: Vec = Vec::new(); - if let Some(class_symbol) = class_name { - out.push(class_symbol); - } - if let Some(alias_symbol) = alias_name { - out.push(alias_symbol); - } - out -} - -fn return_union_items_for_index(raw_comment: &str, index: usize) -> Vec<(String, String)> { - let mut return_idx = 0usize; - let mut lines: Vec<&str> = raw_comment.lines().collect(); - if raw_comment.contains("\r\n") { - lines = raw_comment.split("\r\n").collect(); - } - - for i in 0..lines.len() { - let t = lines[i].trim_start(); - let Some(after_triple) = t.strip_prefix("---") else { - continue; - }; - let after_triple = after_triple.trim_start(); - let Some(after_return) = after_triple.strip_prefix("@return") else { - continue; - }; - return_idx += 1; - if return_idx != index { - continue; - } - - let after = after_return.trim(); - // 仅处理 `@return`(无类型列表)+ 后续 `---| ... # ...` 的写法。 - if !after.is_empty() { - return Vec::new(); - } - - let mut out: Vec<(String, String)> = Vec::new(); - let mut j = i + 1; - while j < lines.len() && lines[j].trim().is_empty() { - j += 1; - } - while j < lines.len() { - let lt = lines[j].trim_start(); - if is_doc_tag_line(lt) { - break; - } - if lt.starts_with("---|") { - if let Some(value) = parse_union_item_value_from_line_trim(lt) { - let desc = lt - .split_once('#') - .map(|(_, after)| after.trim().to_string()) - .unwrap_or_default(); - out.push((value, desc)); - } - j += 1; - continue; - } - break; - } - return out; - } - - Vec::new() -} - -fn make_key( - file_name: &str, - symbol: &str, - version_suffix: Option<&str>, - section: &str, - ident: Option<&str>, -) -> String { - let mut key = String::new(); - key.push_str(file_name); - key.push_str("::"); - key.push_str(symbol); - if let Some(v) = version_suffix { - key.push_str(v); - } - key.push_str("::"); - key.push_str(section); - if let Some(ident) = ident { - key.push('.'); - key.push_str(ident); - } - key -} - -fn preprocess_description(description: &str) -> String { - // 行为尽量对齐 crates/glua_code_analysis/src/compilation/analyzer/doc/mod.rs - let mut description = description; - if description.starts_with(['#', '@']) { - description = description.trim_start_matches(['#', '@']); - } - - let mut result = String::new(); - let lines = description.lines(); - let mut start_with_one_space: Option = None; - for mut line in lines { - let indent_count = line.chars().take_while(|c| c.is_whitespace()).count(); - if indent_count == line.len() { - result.push('\n'); - continue; - } - - if start_with_one_space.is_none() { - start_with_one_space = Some(indent_count == 1); - } - - if let Some(true) = start_with_one_space { - let mut chars = line.chars(); - if let Some(first) = chars.next() - && first.is_whitespace() - { - line = chars.as_str(); - } - } - - result.push_str(line); - result.push('\n'); - } - - result.trim_end().to_string() -} - -fn extract_version_suffix(comment: &LuaComment, raw_comment: &str) -> Option { - // 优先从 AST tag 里提取;无法提取时再从 raw 做简单兜底。 - for tag in comment.get_doc_tags() { - if let LuaDocTag::Version(version_tag) = tag { - let raw = version_tag.syntax().text().to_string(); - if let Some(remainder) = extract_version_remainder(&raw) - && !remainder.is_empty() - { - let compact = remainder.split_whitespace().collect::(); - return Some(format!("@{compact}")); - } - } - } - - // 兜底:逐行解析 `---@version ...` / `--- @version ...`(避免把整段 comment 都当成版本后缀)。 - for line in raw_comment.lines() { - let t = line.trim_start(); - let remainder = if let Some(rest) = t.strip_prefix("@version") { - rest - } else if let Some(rest) = t.strip_prefix("---@version") { - rest - } else if let Some(rest) = t.strip_prefix("--- @version") { - rest - } else { - // 通用形式:`---` 后允许若干空格,再跟 `@version` - let Some(after) = t.strip_prefix("---") else { - continue; - }; - let after = after.trim_start(); - let Some(after) = after.strip_prefix("@version") else { - continue; - }; - after - }; - let remainder = remainder.trim(); - if remainder.is_empty() { - return None; - } - let compact = remainder.split_whitespace().collect::(); - return Some(format!("@{compact}")); - } - - None -} - -fn extract_version_remainder(tag_text: &str) -> Option { - let s = tag_text.trim(); - if let Some(after) = s.strip_prefix("@version") { - return Some(after.trim().to_string()); - } - if let Some(after) = s.strip_prefix("version") { - return Some(after.trim().to_string()); - } - if let Some(at) = s.find("@version") { - let after = &s[(at + "@version".len())..]; - return Some(after.trim().to_string()); - } - None -} - -fn parse_tag_primary_name(tag_text: &str) -> Option { - // 从 tag 的语法文本中提取 `@` 后面的第一个符号: - // 例:`---@alias std.readmode` -> `std.readmode` - // 例:`---@class file` -> `file` - // 例:`---@class foo:bar` -> `foo` - let s = tag_text.trim(); - let at = s.find('@')?; - let after_at = &s[at + 1..]; - let mut iter = after_at.split_whitespace(); - let _tag_name = iter.next()?; // alias/class 等 - let name = iter.next()?; - let name = name.trim_end_matches(['\r', '\n']).trim_end_matches(','); - let stop_at = name.find([':', '<']).unwrap_or(name.len()); - Some(name[..stop_at].to_string()) -} - -fn extract_tag_name_from_raw(raw_comment: &str, tag: &str) -> Option { - let needle = format!("@{tag}"); - for line in raw_comment.lines() { - let Some(at) = line.find(&needle) else { - continue; - }; - let after = &line[(at + needle.len())..]; - let after = after.trim(); - if after.is_empty() { - continue; - } - let end = after - .find(|c: char| c.is_whitespace() || matches!(c, ':' | '<')) - .unwrap_or(after.len()); - return Some(after[..end].to_string()); - } - None -} - -fn is_whitespace_between(content: &str, from: usize, to: usize) -> bool { - if from >= to { - return true; - } - let Some(slice) = content.get(from..to) else { - return false; - }; - slice.chars().all(|c| c.is_whitespace()) -} - -fn format_doc_field_key(key: glua_parser::LuaDocFieldKey) -> Option { - match key { - glua_parser::LuaDocFieldKey::Name(name) => Some(name.get_name_text().to_string()), - glua_parser::LuaDocFieldKey::String(s) => Some(s.get_value()), - glua_parser::LuaDocFieldKey::Integer(i) => Some(i.get_number_value().to_string()), - glua_parser::LuaDocFieldKey::Type(t) => Some(t.syntax().text().to_string()), - } -} - -fn literal_value_from_doc_type(typ: &LuaDocType) -> Option { - match typ { - LuaDocType::Literal(lit) => match lit.get_literal()? { - LuaLiteralToken::String(s) => Some(s.get_value()), - LuaLiteralToken::Number(n) => Some(n.get_number_value().to_string()), - LuaLiteralToken::Bool(b) => Some(b.syntax().text().to_string()), - LuaLiteralToken::Nil(n) => Some(n.syntax().text().to_string()), - other => Some(other.syntax().text().to_string()), - }, - _ => None, - } -} - -fn extract_owner_description_fallback(raw_comment: &str) -> Option { - // 从源码行做一次兜底提取(尽力而为): - // - 跳过开头的 `---@...` tag 行 - // - 收集连续的 `--- ...` 描述行(但不包含 `---@`、也不包含 `---|` union 行) - // - 遇到 tag/union/非 doc 行则停止 - let mut lines = raw_comment.lines().peekable(); - while let Some(line) = lines.peek() { - let t = line.trim_start(); - if is_doc_tag_line(t) { - let _ = lines.next(); - continue; - } - break; - } - - let mut buf: Vec = Vec::new(); - while let Some(line) = lines.peek() { - let t = line.trim_start(); - if is_doc_tag_line(t) || t.starts_with("---|") { - break; - } - if t.starts_with("---") { - let mut s = t.trim_start_matches("---"); - if let Some(rest) = s.strip_prefix(' ') { - s = rest; - } - buf.push(s.to_string()); - let _ = lines.next(); - continue; - } - break; - } - - if buf.is_empty() { - None - } else { - Some(buf.join("\n")) - } -} - -pub(crate) fn owner_symbol_from_ast(owner: LuaAst) -> Option { - match owner { - LuaAst::LuaFuncStat(func) => { - let var = func.get_func_name()?; - format_var_expr_path_var(&var) - } - LuaAst::LuaLocalFuncStat(local_func) => { - let local_name = local_func.get_local_name()?; - Some(local_name.get_name_token()?.get_name_text().to_string()) - } - LuaAst::LuaAssignStat(assign) => { - let (vars, _) = assign.get_var_and_expr_list(); - let v = vars.first()?; - format_var_expr_path_var(v) - } - LuaAst::LuaLocalStat(local_stat) => { - let name = local_stat.get_local_name_list().next()?; - Some(name.get_name_token()?.get_name_text().to_string()) - } - _ => None, - } -} - -fn format_var_expr_path_var(var: &LuaVarExpr) -> Option { - match var { - LuaVarExpr::NameExpr(name) => Some(name.get_name_token()?.get_name_text().to_string()), - LuaVarExpr::IndexExpr(index) => format_index_expr_path(index), - } -} - -fn format_expr_path(expr: &LuaExpr) -> Option { - match expr { - LuaExpr::NameExpr(name) => Some(name.get_name_token()?.get_name_text().to_string()), - LuaExpr::IndexExpr(index) => format_index_expr_path(index), - _ => None, - } -} - -fn format_index_expr_path(index: &LuaIndexExpr) -> Option { - let prefix = format_expr_path(&index.get_prefix_expr()?)?; - let key = index.get_index_key()?; - match key { - glua_parser::LuaIndexKey::Name(name) => Some(format!("{prefix}.{}", name.get_name_text())), - glua_parser::LuaIndexKey::String(s) => Some(format!("{prefix}.{}", s.get_value())), - glua_parser::LuaIndexKey::Integer(i) => Some(format!("{prefix}.{}", i.get_number_value())), - _ => None, - } -} diff --git a/tools/std_i18n/src/keys.rs b/tools/std_i18n/src/keys.rs deleted file mode 100644 index 093b5cc7a..000000000 --- a/tools/std_i18n/src/keys.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::extractor::owner_symbol_from_ast; -use glua_parser::{LuaAst, LuaAstNode, LuaComment, LuaDocTag, LuaParser, ParserConfig}; -use std::collections::HashMap; - -/// 从 std 源文件中构建“模块表 -> class 名”的映射。 -/// -/// 典型例子(io.lua): -/// - `---@class iolib` + `io = {}` -> `io -> iolib` -pub fn build_module_table_to_class_map(lua_content: &str) -> HashMap { - let tree = LuaParser::parse(lua_content, ParserConfig::default()); - let chunk = tree.get_chunk_node(); - - let mut map: HashMap = HashMap::new(); - for comment in chunk.descendants::() { - let Some(owner_ast) = comment.get_owner() else { - continue; - }; - - // 仅对“模块表/全局对象”做映射(例如 `io = {}` -> `io -> iolib`)。 - // 对 `local x` 这类局部变量的 `---@class ...` 不做映射: - // - 局部变量的成员(`function x:set() end`)应以代码标识符为前缀生成 key(`x.set`), - // 而不是类型名(例如 `ffi.cb*`)。 - if matches!( - owner_ast, - LuaAst::LuaLocalStat(_) | LuaAst::LuaLocalFuncStat(_) - ) { - continue; - } - - let Some(owner) = owner_symbol_from_ast(owner_ast) else { - continue; - }; - for tag in comment.get_doc_tags() { - if let LuaDocTag::Class(class_tag) = tag { - let Some(class_name) = class_tag - .get_name_token() - .map(|t| t.get_name_text().to_string()) - else { - continue; - }; - map.insert(owner.clone(), class_name); - } - } - } - map -} - -/// 将源码中的符号名映射为“locale key 中使用的符号路径”。 -/// -/// 规则: -/// - `io.open` -> `iolib.open`(根据 `io -> iolib` 映射) -/// - `io` -> `iolib`(表本身) -/// - `file:close` -> `file.close` -/// - `std.readmode` -> `std.readmode`(符号本身包含 `std.` 时不做特殊处理) -pub fn map_symbol_for_locale_key(symbol: &str, module_map: &HashMap) -> String { - let mut s = symbol.to_string(); - if let Some(class) = module_map.get(symbol) { - s = class.clone(); - } - if let Some((first, rest)) = s.split_once('.') - && let Some(class) = module_map.get(first) - { - s = format!("{class}.{rest}"); - } - s.replace(':', ".") -} - -pub fn locale_key_desc(base: &str) -> String { - base.to_string() -} - -pub fn locale_key_param(base: &str, name: &str) -> String { - format!("{base}.param.{name}") -} - -pub fn locale_key_return(base: &str, index: &str) -> String { - format!("{base}.return.{index}") -} - -pub fn locale_key_return_item(base: &str, index: &str, value: &str) -> String { - format!("{base}.return.{index}.{value}") -} - -pub fn locale_key_field(base: &str, name: &str) -> String { - format!("{base}.field.{name}") -} - -pub fn locale_key_item(base: &str, value: &str) -> String { - format!("{base}.item.{value}") -} diff --git a/tools/std_i18n/src/main.rs b/tools/std_i18n/src/main.rs deleted file mode 100644 index ab56f692a..000000000 --- a/tools/std_i18n/src/main.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::path::PathBuf; - -mod comment_syntax; -mod extractor; -mod keys; -mod merger; -mod meta; -mod model; -mod translator; - -fn main() { - // 是否全量输出翻译条目: - // - `true`:输出所有提取到的 key(包含没有原文的条目) - // - `false`:不输出不包含原文的 key(默认,压缩输出体积) - let full_output = false; - - let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .and_then(|p| p.parent()) - .expect("tools/std_i18n is two levels under repo root") - .to_path_buf(); - - let std_dir = repo_root.join("crates/glua_code_analysis/resources/std"); - let out_root = repo_root.join("crates/glua_ls/std_i18n"); - - // zh_CN - merger::write_std_locales_yaml(&std_dir, "zh_CN", &out_root, full_output) - .expect("write std zh_CN locales should succeed"); - // meta - meta::write_std_meta_yaml(&std_dir, &out_root, full_output) - .expect("write std meta.yaml should succeed"); -} diff --git a/tools/std_i18n/src/merger.rs b/tools/std_i18n/src/merger.rs deleted file mode 100644 index bfcce94cc..000000000 --- a/tools/std_i18n/src/merger.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::model::ExtractedFile; -use std::collections::{HashMap, HashSet}; -use std::fs; -use std::path::Path; - -/// 将提取到的 key 输出为 YAML(翻译文件),写入到当前工具根目录的 `locales/std` 下。 -/// -/// 输出路径形如: -/// - `locales/std/global/zh_CN.yaml` -/// - `locales/std/io/zh_CN.yaml` -/// - `locales/std/jit/profile/zh_CN.yaml` -/// -/// 行为(尽量贴近“同步”): -/// - 按分析顺序生成 key 列表 -/// - 若目标 YAML 已存在,则保留已有翻译;新增 key 的值为空串 -/// - 若目标 YAML 不存在,则生成仅含 key 的空值模板 -pub fn write_std_locales_yaml( - std_dir: &Path, - locale: &str, - out_root: &Path, - full_output: bool, -) -> Result<(), Box> { - // `full_output=false` 时,extractor 会过滤掉不包含原文(value 为空)的条目。 - let files = crate::extractor::extract_std_dir(std_dir, full_output)?; - for file in files { - write_one_file(out_root, &file, locale)?; - } - - Ok(()) -} - -fn write_one_file( - out_root: &Path, - file: &ExtractedFile, - locale: &str, -) -> Result<(), Box> { - // 输出目录:去掉 `.lua` 扩展名作为目录名(支持子目录) - let mut dir_rel = file.path.clone(); - if dir_rel.extension().and_then(|e| e.to_str()) == Some("lua") { - dir_rel.set_extension(""); - } - let out_dir = out_root.join(&dir_rel); - fs::create_dir_all(&out_dir)?; - let out_file = out_dir.join(format!("{locale}.yaml")); - - let existing = read_yaml_string_map(&out_file).unwrap_or_default(); - - let mut ordered = Vec::::new(); - let mut seen = HashSet::::new(); - - for entry in &file.entries { - let yaml_key = entry.locale_key.clone(); - if !seen.insert(yaml_key.clone()) { - continue; - } - let translated = existing.get(&yaml_key).cloned().unwrap_or_default(); - ordered.push(YamlOutEntry { - key: yaml_key, - translated, - origin: entry.value.clone(), - }); - } - - write_yaml_string_map_in_order(&out_file, &ordered)?; - Ok(()) -} - -#[allow(clippy::type_complexity)] -fn read_yaml_string_map( - path: &Path, -) -> Result, Box> { - if !path.exists() { - return Ok(HashMap::new()); - } - let raw = fs::read_to_string(path)?; - let map: HashMap = serde_yml::from_str(&raw)?; - Ok(map) -} - -#[derive(Debug, Clone)] -struct YamlOutEntry { - key: String, - translated: String, - origin: String, -} - -fn write_yaml_string_map_in_order(path: &Path, entries: &[YamlOutEntry]) -> std::io::Result<()> { - let mut out = String::new(); - for entry in entries { - let key = yaml_escape_key(&entry.key); - - // 同时输出原始英文文本,便于翻译时对照。 - // 仅在尚未翻译时输出(避免污染已维护的翻译文件)。 - if entry.translated.is_empty() && !entry.origin.trim().is_empty() { - let normalized = entry.origin.replace("\r\n", "\n"); - for line in normalized.lines() { - out.push_str("# "); - out.push_str(line); - out.push('\n'); - } - } - - if entry.translated.is_empty() { - out.push_str(&format!("{key}: \"\"\n\n")); - continue; - } - - out.push_str(&format!("{key}: |\n")); - let normalized = entry.translated.replace("\r\n", "\n"); - for line in normalized.lines() { - out.push_str(" "); - out.push_str(line); - out.push('\n'); - } - out.push('\n'); - } - - if !out.ends_with('\n') { - out.push('\n'); - } - fs::write(path, out) -} - -fn yaml_escape_key(key: &str) -> String { - // 绝大多数 key(如 `iolib.open` / `std.readmode.item.n`)可以直接写为 plain scalar。 - // 为稳妥起见,碰到空白、冒号等特殊字符时用单引号包裹。 - let needs_quote = key.is_empty() - || key.chars().any(|c| c.is_whitespace()) - || key.contains(':') - || key.starts_with(['*', '?', '-', '!', '&']) - || key.contains('#'); - if !needs_quote { - return key.to_string(); - } - let escaped = key.replace('\'', "''"); - format!("'{escaped}'") -} diff --git a/tools/std_i18n/src/meta.rs b/tools/std_i18n/src/meta.rs deleted file mode 100644 index f77016f1f..000000000 --- a/tools/std_i18n/src/meta.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::translator::{ReplaceStrategy, compute_replace_targets}; -use serde::Serialize; -use std::fs; -use std::path::{Path, PathBuf}; -use walkdir::WalkDir; - -#[derive(Debug, Clone, Serialize)] -pub struct MetaFile { - pub version: u32, - pub line_base: u32, - pub col_base: u32, - pub file: String, - pub entries: Vec, -} - -#[derive(Debug, Clone, Serialize)] -pub struct MetaEntry { - pub key: String, - pub kind: MetaKind, - pub range: MetaRange, - pub hash: String, - pub context_hash: String, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum MetaKind { - DocBlock { indent: String }, - LineTail { prefix: String }, -} - -#[derive(Debug, Clone, Serialize)] -pub struct MetaRange { - pub start: MetaPos, - pub end: MetaPos, -} - -#[derive(Debug, Clone, Copy, Serialize)] -pub struct MetaPos { - pub line: u32, - pub col: u32, -} - -/// 基于 std 源文件生成 `meta.yaml`(一次生成,供运行时做快速替换)。 -/// -/// 输出路径形如: -/// - `/global/meta.yaml` -/// - `/jit/profile/meta.yaml` -/// -/// `out_root` 目录结构与 `write_std_locales_yaml` 一致:以去掉 `.lua` 扩展名后的相对路径作为目录。 -pub fn write_std_meta_yaml( - std_dir: &Path, - out_root: &Path, - full_output: bool, -) -> Result<(), Box> { - let mut files: Vec = WalkDir::new(std_dir) - .min_depth(1) - .max_depth(2) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_file()) - .map(|e| e.into_path()) - .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("lua")) - .collect(); - - files.sort(); - - for full_path in files { - let rel_path = full_path.strip_prefix(std_dir)?.to_path_buf(); - let mut dir_rel = rel_path.clone(); - if dir_rel.extension().and_then(|e| e.to_str()) == Some("lua") { - dir_rel.set_extension(""); - } - - let file_name = rel_path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or_default() - .to_string(); - let content = fs::read_to_string(&full_path)?; - - let targets = compute_replace_targets(&content, &file_name, full_output); - let line_starts = build_line_start_offsets(&content); - - let mut entries: Vec = Vec::with_capacity(targets.len()); - for t in targets { - let kind = match &t.strategy { - ReplaceStrategy::DocBlock { indent } => MetaKind::DocBlock { - indent: indent.clone(), - }, - ReplaceStrategy::LineCommentTail { prefix } => MetaKind::LineTail { - prefix: prefix.clone(), - }, - }; - - let (start_line, start_col) = offset_to_line_col(&line_starts, t.start); - let (end_line, end_col) = offset_to_line_col(&line_starts, t.end); - - let replaced_slice = content.get(t.start..t.end).unwrap_or(""); - let hash = fnv1a64_hex(replaced_slice); - - let context_line = line_slice_at_offset(&content, &line_starts, t.start); - let context_hash = fnv1a64_hex(context_line); - - entries.push(MetaEntry { - key: t.key, - kind, - range: MetaRange { - start: MetaPos { - line: start_line as u32, - col: start_col as u32, - }, - end: MetaPos { - line: end_line as u32, - col: end_col as u32, - }, - }, - hash, - context_hash, - }); - } - - let out_dir = out_root.join(&dir_rel); - fs::create_dir_all(&out_dir)?; - let meta_path = out_dir.join("meta.yaml"); - - let meta = MetaFile { - version: 1, - line_base: 0, - col_base: 0, - file: rel_path.to_string_lossy().replace('\\', "/"), - entries, - }; - - let yaml = serde_yml::to_string(&meta)?; - fs::write(meta_path, yaml)?; - } - - Ok(()) -} - -fn build_line_start_offsets(s: &str) -> Vec { - let mut out = Vec::new(); - out.push(0); - for (i, b) in s.as_bytes().iter().enumerate() { - if *b == b'\n' { - out.push(i + 1); - } - } - out -} - -fn offset_to_line_col(line_starts: &[usize], offset: usize) -> (usize, usize) { - // upper_bound(line_starts, offset) - 1 - let idx = match line_starts.binary_search(&offset) { - Ok(i) => i, - Err(i) => i.saturating_sub(1), - }; - let line = idx.min(line_starts.len().saturating_sub(1)); - let col = offset.saturating_sub(line_starts[line]); - (line, col) -} - -fn line_slice_at_offset<'a>(s: &'a str, line_starts: &[usize], offset: usize) -> &'a str { - if s.is_empty() { - return ""; - } - let (line, _) = offset_to_line_col(line_starts, offset.min(s.len())); - let line_start = *line_starts.get(line).unwrap_or(&0); - let next_start = line_starts.get(line + 1).copied().unwrap_or(s.len()); - let mut line_end = next_start; - if line_end > line_start && s.as_bytes().get(line_end - 1) == Some(&b'\n') { - line_end -= 1; - if line_end > line_start && s.as_bytes().get(line_end - 1) == Some(&b'\r') { - line_end -= 1; - } - } - s.get(line_start..line_end).unwrap_or("") -} - -fn fnv1a64_hex(s: &str) -> String { - let mut hash: u64 = 0xcbf29ce484222325; - for b in s.as_bytes() { - hash ^= *b as u64; - hash = hash.wrapping_mul(0x00000100000001B3); - } - format!("{hash:016x}") -} diff --git a/tools/std_i18n/src/model.rs b/tools/std_i18n/src/model.rs deleted file mode 100644 index 4982ce7a3..000000000 --- a/tools/std_i18n/src/model.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct SourceSpan { - pub start: usize, - pub end: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ExtractedKind { - Desc, - Param { name: String }, - Return { index: usize }, - ReturnItem { index: usize, value: String }, - Field { name: String }, - Item { value: String }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtractedEntry { - pub key: String, - pub locale_key: String, - pub kind: ExtractedKind, - /// 原始符号名(owner/class/alias),不做 module 映射。 - pub symbol: String, - /// module 映射后的 base(用于生成 locale key)。 - pub base: String, - /// 来自 `@version` 的后缀(含 `@`,例如 `@>5.2`)。 - pub version_suffix: Option, - /// 该条目所属注释块在文件中的范围。 - pub comment_span: SourceSpan, - /// 源码中的原始描述文本(未做 preprocess),用于 translator 做行内替换定位。 - pub raw: String, - /// 预处理后的描述文本(用于输出 YAML 的英文原文对照)。 - pub value: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtractedFile { - pub path: PathBuf, - /// 按源码顺序的注释块(每个注释块内含若干条目)。 - pub comments: Vec, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExtractedComment { - pub span: SourceSpan, - pub raw: String, - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AnalyzedLuaDocFile { - pub module_map: HashMap, - pub comments: Vec, - pub entries: Vec, -} diff --git a/tools/std_i18n/src/translator.rs b/tools/std_i18n/src/translator.rs deleted file mode 100644 index 0d3426bd5..000000000 --- a/tools/std_i18n/src/translator.rs +++ /dev/null @@ -1,346 +0,0 @@ -use crate::comment_syntax::{ - LineInfo, build_tag_line_indexes, is_doc_tag_line, normalize_optional_name, - split_lines_with_offsets, -}; -use crate::extractor::analyze_lua_doc_file; -use crate::model::{ExtractedEntry, ExtractedKind, SourceSpan}; -use std::collections::{HashMap, HashSet}; - -#[derive(Debug, Clone)] -pub(crate) struct ReplaceTarget { - pub key: String, - pub start: usize, - pub end: usize, - pub strategy: ReplaceStrategy, -} - -/// 基于 analyzer 的精确 span,为单文件计算“可替换的目标列表”。 -/// -/// 注意:这里不关心具体翻译文本,仅计算每个 key 对应的替换范围和替换策略。 -pub(crate) fn compute_replace_targets( - content: &str, - file_name: &str, - include_empty: bool, -) -> Vec { - let analyzed = analyze_lua_doc_file(file_name, content, include_empty); - - let mut targets: Vec = Vec::new(); - let mut used_spans: HashSet<(usize, usize)> = HashSet::new(); - - let mut comment_ctx: HashMap = HashMap::new(); - for c in analyzed.comments { - let ctx = CommentReplaceContext::new(c.raw); - comment_ctx.insert(c.span, ctx); - } - - for entry in analyzed.entries { - let Some((start, end, strategy)) = compute_replace_target(content, &comment_ctx, &entry) - else { - continue; - }; - if !used_spans.insert((start, end)) { - continue; - } - targets.push(ReplaceTarget { - key: entry.locale_key, - start, - end, - strategy, - }); - } - - targets.sort_by_key(|t| t.start); - targets -} - -#[derive(Debug, Clone)] -pub(crate) enum ReplaceStrategy { - DocBlock { indent: String }, - LineCommentTail { prefix: String }, -} - -struct CommentReplaceContext { - raw: String, - lines: Vec, - indexes: crate::comment_syntax::TagLineIndexes, -} - -impl CommentReplaceContext { - fn new(raw: String) -> Self { - let lines = split_lines_with_offsets(&raw); - let indexes = build_tag_line_indexes(&raw, &lines); - Self { - raw, - lines, - indexes, - } - } -} - -fn compute_replace_target( - file_content: &str, - ctx_map: &HashMap, - entry: &ExtractedEntry, -) -> Option<(usize, usize, ReplaceStrategy)> { - let ctx = ctx_map.get(&entry.comment_span)?; - match &entry.kind { - ExtractedKind::Desc => desc_replace_target(ctx, entry.comment_span), - ExtractedKind::Param { name } => tag_attached_replace_target_for_param( - ctx, - entry.comment_span, - name, - &entry.raw, - file_content, - ), - ExtractedKind::Return { index } => tag_attached_replace_target_for_return( - ctx, - entry.comment_span, - *index, - &entry.raw, - file_content, - ), - ExtractedKind::Field { name } => tag_attached_replace_target_for_field( - ctx, - entry.comment_span, - name, - &entry.raw, - file_content, - ), - ExtractedKind::Item { value } => union_item_replace_target(ctx, entry.comment_span, value), - ExtractedKind::ReturnItem { value, .. } => { - union_item_replace_target(ctx, entry.comment_span, value) - } - } -} - -fn desc_replace_target( - ctx: &CommentReplaceContext, - comment_span: SourceSpan, -) -> Option<(usize, usize, ReplaceStrategy)> { - let (rel_start, rel_end) = if let Some((start, end)) = ctx.indexes.desc_block { - let start_off = ctx.lines.get(start)?.start; - let end_off = if end < ctx.lines.len() { - ctx.lines.get(end)?.start - } else { - ctx.raw.len() - }; - (start_off, end_off) - } else { - (0, 0) - }; - - let start = comment_span.start + rel_start; - let end = comment_span.start + rel_end; - Some(( - start, - end, - ReplaceStrategy::DocBlock { - indent: ctx.indexes.default_indent.clone(), - }, - )) -} - -fn tag_attached_replace_target_for_param( - ctx: &CommentReplaceContext, - comment_span: SourceSpan, - name: &str, - raw_desc: &str, - file_content: &str, -) -> Option<(usize, usize, ReplaceStrategy)> { - let name = normalize_optional_name(name); - let tag_idx = *ctx.indexes.param_line.get(&name)?; - - tag_attached_replace_target_after(ctx, comment_span, tag_idx, raw_desc, file_content) -} - -fn tag_attached_replace_target_for_field( - ctx: &CommentReplaceContext, - comment_span: SourceSpan, - name: &str, - raw_desc: &str, - file_content: &str, -) -> Option<(usize, usize, ReplaceStrategy)> { - let name = normalize_optional_name(name); - let tag_idx = *ctx.indexes.field_line.get(&name)?; - tag_attached_replace_target_after(ctx, comment_span, tag_idx, raw_desc, file_content) -} - -fn tag_attached_replace_target_for_return( - ctx: &CommentReplaceContext, - comment_span: SourceSpan, - index: usize, - raw_desc: &str, - file_content: &str, -) -> Option<(usize, usize, ReplaceStrategy)> { - let tag_idx = ctx - .indexes - .return_lines - .get(index.saturating_sub(1)) - .copied()?; - tag_attached_replace_target_after(ctx, comment_span, tag_idx, raw_desc, file_content) -} - -fn tag_attached_replace_target_after( - ctx: &CommentReplaceContext, - comment_span: SourceSpan, - tag_idx: usize, - raw_desc: &str, - file_content: &str, -) -> Option<(usize, usize, ReplaceStrategy)> { - if let Some(inline) = - inline_tag_description_replace_target(ctx, comment_span, tag_idx, raw_desc) - { - return Some(inline); - } - - if raw_desc.trim().is_empty() - && let Some(insert) = inline_tag_description_insert_target(ctx, comment_span, tag_idx) - { - return Some(insert); - } - - attached_doc_block_target_after(ctx, comment_span, tag_idx, file_content) -} - -fn inline_tag_description_replace_target( - ctx: &CommentReplaceContext, - comment_span: SourceSpan, - tag_idx: usize, - raw_desc: &str, -) -> Option<(usize, usize, ReplaceStrategy)> { - let desc = raw_desc.trim(); - if desc.is_empty() || desc.contains('\n') { - return None; - } - let li = *ctx.lines.get(tag_idx)?; - let line_text = li.text(&ctx.raw); - let pos = line_text.rfind(desc)?; - let start = comment_span.start + li.start + pos; - let end = start + desc.len(); - Some(( - start, - end, - ReplaceStrategy::LineCommentTail { - prefix: "".to_string(), - }, - )) -} - -fn inline_tag_description_insert_target( - ctx: &CommentReplaceContext, - comment_span: SourceSpan, - tag_idx: usize, -) -> Option<(usize, usize, ReplaceStrategy)> { - let li = *ctx.lines.get(tag_idx)?; - let line_text = li.text(&ctx.raw); - let start = comment_span.start + li.end; - let prefix = if line_text.ends_with(|c: char| c.is_whitespace()) { - "" - } else { - " " - }; - Some(( - start, - start, - ReplaceStrategy::LineCommentTail { - prefix: prefix.to_string(), - }, - )) -} - -fn attached_doc_block_target_after( - ctx: &CommentReplaceContext, - comment_span: SourceSpan, - tag_idx: usize, - file_content: &str, -) -> Option<(usize, usize, ReplaceStrategy)> { - let indent = ctx.lines.get(tag_idx)?.indent(&ctx.raw); - - let mut start = tag_idx + 1; - while start < ctx.lines.len() && ctx.lines[start].text(&ctx.raw).trim().is_empty() { - start += 1; - } - - let mut end = start; - while end < ctx.lines.len() { - let t = ctx.lines[end].trim_start_text(&ctx.raw); - if is_doc_tag_line(t) || t.starts_with("---|") { - break; - } - if t.starts_with("---") { - end += 1; - continue; - } - break; - } - - let (abs_start, abs_end) = if start < end { - let rel_s = ctx.lines.get(start)?.start; - let rel_e = if end < ctx.lines.len() { - ctx.lines.get(end)?.start - } else { - ctx.raw.len() - }; - (comment_span.start + rel_s, comment_span.start + rel_e) - } else { - let rel_insert = if start < ctx.lines.len() { - ctx.lines.get(start)?.start - } else { - ctx.raw.len() - }; - let mut abs_insert = comment_span.start + rel_insert; - if abs_insert == comment_span.end { - abs_insert = advance_past_line_break(file_content, abs_insert); - } - (abs_insert, abs_insert) - }; - - Some((abs_start, abs_end, ReplaceStrategy::DocBlock { indent })) -} - -fn union_item_replace_target( - ctx: &CommentReplaceContext, - comment_span: SourceSpan, - value: &str, -) -> Option<(usize, usize, ReplaceStrategy)> { - let line_idx = ctx.indexes.union_line.get(value).copied()?; - if ctx.lines.is_empty() { - return None; - } - let li = ctx.lines.get(line_idx.min(ctx.lines.len() - 1))?; - let line_text = li.text(&ctx.raw); - if let Some(hash_pos) = line_text.find('#') { - let start = comment_span.start + li.start + hash_pos + 1; - let end = comment_span.start + li.end; - Some(( - start, - end, - ReplaceStrategy::LineCommentTail { - prefix: " ".to_string(), - }, - )) - } else { - let start = comment_span.start + li.end; - Some(( - start, - start, - ReplaceStrategy::LineCommentTail { - prefix: " # ".to_string(), - }, - )) - } -} - -fn advance_past_line_break(s: &str, offset: usize) -> usize { - let bytes = s.as_bytes(); - if offset < bytes.len() && bytes[offset] == b'\r' { - if offset + 1 < bytes.len() && bytes[offset + 1] == b'\n' { - return offset + 2; - } - return offset + 1; - } - if offset < bytes.len() && bytes[offset] == b'\n' { - return offset + 1; - } - offset -}