From 67bd0e6c9ec8f650524ef14721ace894f2b6f647 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Fri, 13 Mar 2026 15:25:53 +0800 Subject: feat: auto-generate annotations for LSP settings --- scripts/gen_annotations.lua | 308 +++++++++++++++++++++++++++++++++++++++++++ scripts/gen_json_schemas.lua | 247 ++++++++++++++++++++++++++++++++++ 2 files changed, 555 insertions(+) create mode 100644 scripts/gen_annotations.lua create mode 100644 scripts/gen_json_schemas.lua (limited to 'scripts') diff --git a/scripts/gen_annotations.lua b/scripts/gen_annotations.lua new file mode 100644 index 00000000..a8f4d1b8 --- /dev/null +++ b/scripts/gen_annotations.lua @@ -0,0 +1,308 @@ +---Merge maps recursively and replace non-map values. +---@param ... any Values to merge in order. +---@return any +local function merge(...) + ---Return whether a value can be merged as a map. + ---@param value any Candidate value. + ---@return boolean + local function is_mergeable(value) + return type(value) == 'table' and (vim.tbl_isempty(value) or not vim.islist(value)) + end + + local values = { ... } + local merged = values[1] + for i = 2, #values, 1 do + local next_value = values[i] + if is_mergeable(merged) and is_mergeable(next_value) then + for key, item in pairs(next_value) do + merged[key] = merge(merged[key], item) + end + else + merged = next_value + end + end + return merged +end + +---@class Settings +---@field _settings table +---@field file string +local Settings = {} +Settings.__index = Settings + +---Create a settings store from dotted keys. +---@param settings? table Initial dotted-key values. +---@return Settings +function Settings.new(settings) + local self = setmetatable({ _settings = {} }, Settings) + for key, value in pairs(settings or {}) do + self:set(key, value) + end + return self +end + +---Expand dotted keys in a table into nested tables. +---@param value any Source value. +---@return any +function Settings.expand_keys(value) + if type(value) ~= 'table' then + return value + end + local expanded = Settings.new() + for key, item in pairs(value) do + expanded:set(key, item) + end + return expanded:get() +end + +---Split a dotted key into path segments. +---@param key any Dotted key or raw key value. +---@return any[] +function Settings.split_key(key) + if not key or key == '' then + return {} + end + if type(key) ~= 'string' then + return { key } + end + local parts = {} + for part in string.gmatch(key, '[^.]+') do + table.insert(parts, part) + end + return parts +end + +---Store a value at a dotted key path. +---@param key any Target dotted key. +---@param value any Value to store. +function Settings:set(key, value) + local parts = Settings.split_key(key) + + if #parts == 0 then + self._settings = value + return + end + + local node = self._settings + for i = 1, #parts - 1, 1 do + local part = parts[i] + if type(node[part]) ~= 'table' then + node[part] = {} + end + node = node[part] + end + node[parts[#parts]] = value +end + +---Read a value from the settings store. +---@param key? any Dotted key to read. +---@param opts? {defaults?:table, expand?:boolean} Read options. +---@return any +function Settings:get(key, opts) + ---@type table|nil + local node = self._settings + + for _, part in ipairs(Settings.split_key(key)) do + if type(node) ~= 'table' then + node = nil + break + end + node = node[part] + end + + if opts and opts.expand and type(node) == 'table' then + node = Settings.expand_keys(node) + end + + if opts and opts.defaults then + if node == nil then + return vim.deepcopy(opts.defaults) + end + if type(node) ~= 'table' then + return node + end + node = merge({}, opts.defaults, node) + end + + return node +end + +---Format schema text as Lua comments. +---@param desc? string Comment body. +---@param prefix? string Optional line prefix. +---@return string? +local function format_comment(desc, prefix) + if desc then + prefix = (prefix or '') .. '---' + return prefix .. desc:gsub('\n', '\n' .. prefix) + end +end + +---Append a property's description comment. +---@param lines string[] Output buffer. +---@param prop table Schema property. +---@param prefix? string Optional line prefix. +local function append_description(lines, prop, prefix) + local description = prop.markdownDescription or prop.description + if type(description) == 'table' and description.message then + description = description.message + end + if prop.default then + if prop.default == vim.NIL then + prop.default = nil + end + if type(prop.default) == 'table' and vim.tbl_isempty(prop.default) then + prop.default = {} + end + description = (description and (description .. '\n\n') or '') + .. '```lua\ndefault = ' + .. vim.inspect(prop.default) + .. '\n```' + end + if description then + table.insert(lines, format_comment(description, prefix)) + end +end + +---Wrap nested schema nodes as object properties. +---@param node table Schema node tree. +---@return table +local function normalize_properties(node) + return node.leaf and node + or { + type = 'object', + properties = vim.tbl_map(function(child) + return normalize_properties(child) + end, node), + } +end + +---Build the Lua class name for a schema node. +---@param name string Node name. +---@param root_class string Root class name. +---@return string +local function class_name_for(name, root_class) + if name == root_class then + return name + end + local class_name = { '_', root_class } + for word in string.gmatch(name, '([^_]+)') do + table.insert(class_name, word:sub(1, 1):upper() .. word:sub(2)) + end + return table.concat(class_name, '.') +end + +---Convert a schema property into a Lua type. +---@param prop table Schema property. +---@return string +local function lua_type_for(prop) + if prop.enum then + return table.concat( + vim.tbl_map(function(e) + return vim.inspect(e) + end, prop.enum), + ' | ' + ) + end + local types = type(prop.type) == 'table' and prop.type or { prop.type } + if vim.tbl_isempty(types) and type(prop.anyOf) == 'table' then + return table.concat( + vim.tbl_map(function(p) + return lua_type_for(p) + end, prop.anyOf), + '|' + ) + end + types = vim.tbl_map(function(t) + if t == 'null' then + return + end + if t == 'array' then + if prop.items and prop.items.type then + if type(prop.items.type) == 'table' then + prop.items.type = 'any' + end + return prop.items.type .. '[]' + end + return 'any[]' + end + if t == 'object' then + return 'table' + end + return t + end, types) + if vim.tbl_isempty(types) then + types = { 'any' } + end + return table.concat(vim.iter(types):flatten():totable(), '|') +end + +---Append annotations for an object node and its children. +---@param lines string[] Output buffer. +---@param name string Node name. +---@param prop table Object property schema. +---@param root_class string Root class name. +local function append_object(lines, name, prop, root_class) + local object_lines = {} + append_description(object_lines, prop) + table.insert(object_lines, '---@class ' .. class_name_for(name, root_class)) + if prop.properties then + local props = vim.tbl_keys(prop.properties) + table.sort(props) + for _, field in ipairs(props) do + local child = prop.properties[field] + append_description(object_lines, child) + + if child.type == 'object' and child.properties then + table.insert(object_lines, '---@field ' .. field .. ' ' .. class_name_for(field, root_class)) + append_object(lines, field, child, root_class) + else + table.insert(object_lines, '---@field ' .. field .. ' ' .. lua_type_for(child)) + end + end + end + table.insert(lines, '') + vim.list_extend(lines, object_lines) +end + +---Generate annotation lines for one schema file. +---@param file string Schema file path. +---@return string[] +local function generate_file_annotations(file) + local name = vim.fn.fnamemodify(file, ':t:r') + local json = vim.json.decode(vim.fn.readblob(file), { luanil = { array = true, object = true } }) or {} + local class_name = 'lspconfig.settings.' .. name + local lines = { '---@meta' } + + local schema = Settings.new() + for key, prop in pairs(json.properties) do + prop.leaf = true + schema:set(key, prop) + end + + append_object(lines, class_name, normalize_properties(schema:get()), class_name) + return vim.tbl_filter(function(v) + return v ~= nil + end, lines) +end + +---Generate Lua annotation files from the schemas directory. +---@return nil +local function generate_all_annotations() + local schema_dir = vim.fs.joinpath(vim.uv.cwd(), 'schemas') + local output_dir = vim.fs.joinpath(vim.uv.cwd(), 'lua', 'lspconfig', 'types', 'lsp') + + vim.fn.delete(output_dir, 'rf') + vim.fn.mkdir(output_dir, 'p') + + for name, type in vim.fs.dir(schema_dir) do + if type == 'file' and vim.endswith(name, '.json') then + local file = vim.fs.joinpath(schema_dir, name) + local lines = generate_file_annotations(file) + local output_file = vim.fs.joinpath(output_dir, vim.fn.fnamemodify(name, ':r') .. '.lua') + vim.fn.writefile(vim.split(table.concat(lines, '\n') .. '\n', '\n', { plain = true }), output_file, 'b') + end + end +end + +generate_all_annotations() diff --git a/scripts/gen_json_schemas.lua b/scripts/gen_json_schemas.lua new file mode 100644 index 00000000..9f62ff48 --- /dev/null +++ b/scripts/gen_json_schemas.lua @@ -0,0 +1,247 @@ +-- modified from https://gist.githubusercontent.com/williamboman/a01c3ce1884d4b57cc93422e7eae7702/raw/lsp-packages.json +local index = { + -- nickel_ls = "https://raw.githubusercontent.com/tweag/nickel/master/lsp/client-extension/package.json", + ada_ls = 'https://raw.githubusercontent.com/AdaCore/ada_language_server/master/integration/vscode/ada/package.json', + astro = 'https://raw.githubusercontent.com/withastro/language-tools/main/packages/vscode/package.json', + awk_ls = 'https://raw.githubusercontent.com/Beaglefoot/awk-language-server/master/client/package.json', + basedpyright = 'https://raw.githubusercontent.com/DetachHead/basedpyright/main/packages/vscode-pyright/package.json', + bashls = 'https://raw.githubusercontent.com/bash-lsp/bash-language-server/master/vscode-client/package.json', + clangd = 'https://raw.githubusercontent.com/clangd/vscode-clangd/master/package.json', + cssls = 'https://raw.githubusercontent.com/microsoft/vscode/main/extensions/css-language-features/package.json', + dartls = 'https://raw.githubusercontent.com/Dart-Code/Dart-Code/master/package.json', + denols = 'https://raw.githubusercontent.com/denoland/vscode_deno/main/package.json', + elixirls = 'https://raw.githubusercontent.com/elixir-lsp/vscode-elixir-ls/master/package.json', + elmls = 'https://raw.githubusercontent.com/elm-tooling/elm-language-client-vscode/master/package.json', + eslint = 'https://raw.githubusercontent.com/microsoft/vscode-eslint/main/package.json', + flow = 'https://raw.githubusercontent.com/flowtype/flow-for-vscode/master/package.json', + fsautocomplete = 'https://raw.githubusercontent.com/ionide/ionide-vscode-fsharp/main/release/package.json', + grammarly = 'https://raw.githubusercontent.com/znck/grammarly/main/extension/package.json', + hhvm = 'https://raw.githubusercontent.com/slackhq/vscode-hack/master/package.json', + hie = 'https://raw.githubusercontent.com/alanz/vscode-hie-server/master/package.json', + html = 'https://raw.githubusercontent.com/microsoft/vscode/main/extensions/html-language-features/package.json', + intelephense = 'https://raw.githubusercontent.com/bmewburn/vscode-intelephense/master/package.json', + java_language_server = 'https://raw.githubusercontent.com/georgewfraser/java-language-server/master/package.json', + jdtls = 'https://raw.githubusercontent.com/redhat-developer/vscode-java/master/package.json', + jsonls = 'https://raw.githubusercontent.com/microsoft/vscode/master/extensions/json-language-features/package.json', + julials = 'https://raw.githubusercontent.com/julia-vscode/julia-vscode/master/package.json', + kotlin_language_server = 'https://raw.githubusercontent.com/fwcd/vscode-kotlin/master/package.json', + ltex = 'https://raw.githubusercontent.com/valentjn/vscode-ltex/develop/package.json', + lua_ls = 'https://raw.githubusercontent.com/LuaLS/vscode-lua/master/package.json', + luau_lsp = 'https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/editors/code/package.json', + omnisharp = 'https://raw.githubusercontent.com/OmniSharp/omnisharp-vscode/master/package.json', + perlls = 'https://raw.githubusercontent.com/richterger/Perl-LanguageServer/master/clients/vscode/perl/package.json', + perlnavigator = 'https://raw.githubusercontent.com/bscan/PerlNavigator/main/package.json', + perlpls = 'https://raw.githubusercontent.com/FractalBoy/perl-language-server/master/client/package.json', + powershell_es = 'https://raw.githubusercontent.com/PowerShell/vscode-powershell/main/package.json', + psalm = 'https://raw.githubusercontent.com/psalm/psalm-vscode-plugin/master/package.json', + puppet = 'https://raw.githubusercontent.com/puppetlabs/puppet-vscode/main/package.json', + purescriptls = 'https://raw.githubusercontent.com/nwolverson/vscode-ide-purescript/master/package.json', + pylsp = 'https://raw.githubusercontent.com/python-lsp/python-lsp-server/develop/pylsp/config/schema.json', + pyright = 'https://raw.githubusercontent.com/microsoft/pyright/master/packages/vscode-pyright/package.json', + r_language_server = 'https://raw.githubusercontent.com/REditorSupport/vscode-r-lsp/master/package.json', + rescriptls = 'https://raw.githubusercontent.com/rescript-lang/rescript-vscode/master/package.json', + rls = 'https://raw.githubusercontent.com/rust-lang/vscode-rust/master/package.json', + rome = 'https://raw.githubusercontent.com/rome/tools/main/editors/vscode/package.json', + ruff_lsp = 'https://raw.githubusercontent.com/astral-sh/ruff-vscode/main/package.json', + rust_analyzer = 'https://raw.githubusercontent.com/rust-analyzer/rust-analyzer/master/editors/code/package.json', + solargraph = 'https://raw.githubusercontent.com/castwide/vscode-solargraph/master/package.json', + solidity_ls = 'https://raw.githubusercontent.com/juanfranblanco/vscode-solidity/master/package.json', + sorbet = 'https://raw.githubusercontent.com/sorbet/sorbet/master/vscode_extension/package.json', + sourcekit = 'https://raw.githubusercontent.com/swift-server/vscode-swift/main/package.json', + spectral = 'https://raw.githubusercontent.com/stoplightio/vscode-spectral/master/package.json', + stylelint_lsp = 'https://raw.githubusercontent.com/bmatcuk/coc-stylelintplus/master/package.json', + svelte = 'https://raw.githubusercontent.com/sveltejs/language-tools/master/packages/svelte-vscode/package.json', + svlangserver = 'https://raw.githubusercontent.com/eirikpre/VSCode-SystemVerilog/master/package.json', + tailwindcss = 'https://raw.githubusercontent.com/tailwindlabs/tailwindcss-intellisense/master/packages/vscode-tailwindcss/package.json', + terraformls = 'https://raw.githubusercontent.com/hashicorp/vscode-terraform/master/package.json', + tinymist = 'https://raw.githubusercontent.com/Myriad-Dreamin/tinymist/refs/heads/main/editors/vscode/package.json', + ts_ls = 'https://raw.githubusercontent.com/microsoft/vscode/main/extensions/typescript-language-features/package.json', + typst_lsp = 'https://raw.githubusercontent.com/nvarner/typst-lsp/refs/heads/master/editors/vscode/package.json', + volar = 'https://raw.githubusercontent.com/vuejs/language-tools/master/extensions/vscode/package.json', + vtsls = 'https://raw.githubusercontent.com/yioneko/vtsls/main/packages/service/configuration.schema.json', + vue_ls = 'https://raw.githubusercontent.com/vuejs/vetur/master/package.json', + yamlls = 'https://raw.githubusercontent.com/redhat-developer/vscode-yaml/master/package.json', + zls = 'https://raw.githubusercontent.com/zigtools/zls-vscode/master/package.json', +} + +---@param url string +---@return string +local function request(url) + local done = false + local err + local body + + vim.net.request(url, nil, function(request_err, response) + err = request_err + body = response and response.body or nil + done = true + end) + + vim.wait(30000, function() + return done + end, 50) + + if not done then + error(('Timed out downloading %s'):format(url)) + end + + if err then + error(('Could not download %s: %s'):format(url, err)) + end + + return body +end + +---@class LspSchema +---@field package_url string url of the package.json of the LSP server +---@field settings_file string file of the settings json schema of the LSP server +---@field translate? boolean + +--- @type table +local overrides = { + lua_ls = { + translate = true, + }, + jsonls = { + translate = true, + }, + ts_ls = { + translate = true, + }, + ltex = { + translate = true, + }, + html = { + translate = true, + }, + cssls = { + translate = true, + }, +} + +---Builds the effective schema configuration table for each server. +---@return table +local function resolve_schema_configs() + ---@type table + local schemas = {} + + for server, package_json in pairs(index) do + schemas[server] = { + package_url = package_json, + settings_file = vim.fs.joinpath(vim.uv.cwd(), 'schemas', server .. '.json'), + } + end + + return vim.tbl_deep_extend('force', schemas, overrides) +end + +---Replaces localized documentation placeholders in a schema tree in place. +--- +---This is used for schemas whose documentation strings are stored in a +---separate `package.nls.json` file instead of inline in `package.json`. +---@param props table +---@param nls_url string +local function translate_schema_docs(props, nls_url) + local localization = vim.json.decode(request(nls_url), { luanil = { array = true, object = true } }) or {} + + ---Resolves a single description value from the downloaded NLS payload. + ---@param desc string + ---@return string + local function resolve_doc_text(desc) + desc = localization[desc:gsub('%%', '')] or desc + if type(desc) == 'table' then + local message_parts = vim.tbl_values(desc) + message_parts = vim.iter(message_parts):flatten():totable() + table.sort(message_parts) + desc = table.concat(message_parts, '\n\n') + end + return desc + end + + ---Walks a schema node recursively and rewrites localized doc fields in place. + ---@param node any + local function rewrite_doc_tree(node) + if type(node) == 'table' then + for k, v in pairs(node) do + if + k == 'description' + or k == 'markdownDescription' + or k == 'markdownDeprecationMessage' + or k == 'deprecationMessage' + then + node[k] = resolve_doc_text(v) + end + if k == 'enumDescriptions' or k == 'markdownEnumDescriptions' then + for i, d in ipairs(v) do + v[i] = resolve_doc_text(d) + end + end + rewrite_doc_tree(v) + end + end + end + rewrite_doc_tree(props) +end + +---Downloads and normalizes the JSON schema for a single server. +---@param schema LspSchema +---@return table +local function generate_server_schema(schema) + local package_json = vim.json.decode(request(schema.package_url)) or {} + local config_schema = package_json.contributes and package_json.contributes.configuration + or package_json.properties and package_json + + local properties = vim.empty_dict() + + if vim.islist(config_schema) then + for _, config_section in pairs(config_schema) do + if config_section.properties then + for k, v in pairs(config_section.properties) do + properties[k] = v + end + end + end + elseif config_schema.properties then + properties = config_schema.properties + end + + local schema_json = { + ['$schema'] = 'http://json-schema.org/draft-07/schema#', + description = package_json.description, + properties = properties, + } + + if schema.translate then + translate_schema_docs(schema_json, schema.package_url:gsub('package%.json$', 'package.nls.json')) + end + + return schema_json +end + +---Regenerates all schema files under the local `schemas/` directory. +local function generate_all_schemas() + ---@diagnostic disable-next-line: param-type-mismatch + vim.fn.delete(vim.fs.joinpath(vim.uv.cwd(), 'schemas'), 'rf') + + local schemas = resolve_schema_configs() + local names = vim.tbl_keys(schemas) + table.sort(names) + for _, name in ipairs(names) do + local schema_config = schemas[name] + print(('Generating schema for %s'):format(name)) + + if not vim.uv.fs_stat(schema_config.settings_file) then + local ok, schema_json = pcall(generate_server_schema, schema_config) + if ok then + vim.fn.mkdir(vim.fn.fnamemodify(schema_config.settings_file, ':h'), 'p') + vim.fn.writefile( + vim.split(vim.json.encode(schema_json, { indent = ' ', sort_keys = true }), '\n', { plain = true }), + schema_config.settings_file, + 'b' + ) + end + end + end +end + +generate_all_schemas() -- cgit v1.3 From c321e501410aabd7f3748bddfea6e7480b768799 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Fri, 13 Mar 2026 20:20:08 +0800 Subject: fix: respect schema required fields --- scripts/gen_annotations.lua | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/gen_annotations.lua b/scripts/gen_annotations.lua index a8f4d1b8..d8e4c301 100644 --- a/scripts/gen_annotations.lua +++ b/scripts/gen_annotations.lua @@ -237,6 +237,30 @@ local function lua_type_for(prop) return table.concat(vim.iter(types):flatten():totable(), '|') end +---Return whether a field is required by its parent schema. +---@param parent table +---@param field string +---@param child table +---@return boolean +local function is_required_field(parent, field, child) + if child.required == true then + return true + end + + local required = parent.required + if type(required) ~= 'table' then + return false + end + + for _, required_field in ipairs(required) do + if required_field == field then + return true + end + end + + return false +end + ---Append annotations for an object node and its children. ---@param lines string[] Output buffer. ---@param name string Node name. @@ -251,13 +275,14 @@ local function append_object(lines, name, prop, root_class) table.sort(props) for _, field in ipairs(props) do local child = prop.properties[field] + local optional_marker = is_required_field(prop, field, child) and '' or '?' append_description(object_lines, child) if child.type == 'object' and child.properties then - table.insert(object_lines, '---@field ' .. field .. ' ' .. class_name_for(field, root_class)) + table.insert(object_lines, '---@field ' .. field .. optional_marker .. ' ' .. class_name_for(field, root_class)) append_object(lines, field, child, root_class) else - table.insert(object_lines, '---@field ' .. field .. ' ' .. lua_type_for(child)) + table.insert(object_lines, '---@field ' .. field .. optional_marker .. ' ' .. lua_type_for(child)) end end end -- cgit v1.3 From 39fdfdfe1169b05beb9cc5b8a55716205243af2d Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Fri, 13 Mar 2026 22:43:47 +0800 Subject: fix: avoid duplicate class names for nested schemas --- scripts/gen_annotations.lua | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) (limited to 'scripts') diff --git a/scripts/gen_annotations.lua b/scripts/gen_annotations.lua index d8e4c301..08ed2310 100644 --- a/scripts/gen_annotations.lua +++ b/scripts/gen_annotations.lua @@ -178,16 +178,20 @@ local function normalize_properties(node) end ---Build the Lua class name for a schema node. ----@param name string Node name. +---@param path string[] Path segments from the schema root. ---@param root_class string Root class name. ---@return string -local function class_name_for(name, root_class) - if name == root_class then - return name +local function class_name_for(path, root_class) + if #path == 0 then + return root_class end local class_name = { '_', root_class } - for word in string.gmatch(name, '([^_]+)') do - table.insert(class_name, word:sub(1, 1):upper() .. word:sub(2)) + for _, segment in ipairs(path) do + local words = {} + for word in string.gmatch(segment, '([^_]+)') do + table.insert(words, word:sub(1, 1):upper() .. word:sub(2)) + end + table.insert(class_name, table.concat(words)) end return table.concat(class_name, '.') end @@ -263,13 +267,13 @@ end ---Append annotations for an object node and its children. ---@param lines string[] Output buffer. ----@param name string Node name. +---@param path string[] Path segments from the schema root. ---@param prop table Object property schema. ---@param root_class string Root class name. -local function append_object(lines, name, prop, root_class) +local function append_object(lines, path, prop, root_class) local object_lines = {} append_description(object_lines, prop) - table.insert(object_lines, '---@class ' .. class_name_for(name, root_class)) + table.insert(object_lines, '---@class ' .. class_name_for(path, root_class)) if prop.properties then local props = vim.tbl_keys(prop.properties) table.sort(props) @@ -279,8 +283,13 @@ local function append_object(lines, name, prop, root_class) append_description(object_lines, child) if child.type == 'object' and child.properties then - table.insert(object_lines, '---@field ' .. field .. optional_marker .. ' ' .. class_name_for(field, root_class)) - append_object(lines, field, child, root_class) + local child_path = vim.deepcopy(path) + table.insert(child_path, field) + table.insert( + object_lines, + '---@field ' .. field .. optional_marker .. ' ' .. class_name_for(child_path, root_class) + ) + append_object(lines, child_path, child, root_class) else table.insert(object_lines, '---@field ' .. field .. optional_marker .. ' ' .. lua_type_for(child)) end @@ -305,7 +314,7 @@ local function generate_file_annotations(file) schema:set(key, prop) end - append_object(lines, class_name, normalize_properties(schema:get()), class_name) + append_object(lines, {}, normalize_properties(schema:get()), class_name) return vim.tbl_filter(function(v) return v ~= nil end, lines) -- cgit v1.3 From 45679130eab4efe19066dd55d71513a6ec208f1d Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Fri, 13 Mar 2026 23:14:07 +0800 Subject: fix: map array object items to table[] --- scripts/gen_annotations.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/gen_annotations.lua b/scripts/gen_annotations.lua index 08ed2310..d71c88b8 100644 --- a/scripts/gen_annotations.lua +++ b/scripts/gen_annotations.lua @@ -224,9 +224,9 @@ local function lua_type_for(prop) if t == 'array' then if prop.items and prop.items.type then if type(prop.items.type) == 'table' then - prop.items.type = 'any' + return 'any[]' end - return prop.items.type .. '[]' + return (prop.items.type == 'object' and 'table' or prop.items.type) .. '[]' end return 'any[]' end -- cgit v1.3 From fce4448bb7158b740fc6298839a463b70446a92e Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Fri, 13 Mar 2026 23:26:40 +0800 Subject: fix: sanitize invalid schema keys in type names --- scripts/gen_annotations.lua | 62 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 7 deletions(-) (limited to 'scripts') diff --git a/scripts/gen_annotations.lua b/scripts/gen_annotations.lua index d71c88b8..1612d82b 100644 --- a/scripts/gen_annotations.lua +++ b/scripts/gen_annotations.lua @@ -177,6 +177,20 @@ local function normalize_properties(node) } end +---Normalize one schema path segment into a valid LuaLS class name segment. +---@param segment string +---@return string +local function segment_name_for_annotation(segment) + local words = {} + for word in string.gmatch(segment, '[%w]+') do + if word:match('^%d') then + word = '_' .. word + end + table.insert(words, word:sub(1, 1):upper() .. word:sub(2)) + end + return #words > 0 and table.concat(words) or '_' +end + ---Build the Lua class name for a schema node. ---@param path string[] Path segments from the schema root. ---@param root_class string Root class name. @@ -187,15 +201,48 @@ local function class_name_for(path, root_class) end local class_name = { '_', root_class } for _, segment in ipairs(path) do - local words = {} - for word in string.gmatch(segment, '([^_]+)') do - table.insert(words, word:sub(1, 1):upper() .. word:sub(2)) - end - table.insert(class_name, table.concat(words)) + table.insert(class_name, segment_name_for_annotation(segment)) end return table.concat(class_name, '.') end +---@type table +local lua_keywords = { + ['and'] = true, + ['break'] = true, + ['do'] = true, + ['else'] = true, + ['elseif'] = true, + ['end'] = true, + ['false'] = true, + ['for'] = true, + ['function'] = true, + ['goto'] = true, + ['if'] = true, + ['in'] = true, + ['local'] = true, + ['nil'] = true, + ['not'] = true, + ['or'] = true, + ['package'] = true, + ['repeat'] = true, + ['return'] = true, + ['then'] = true, + ['true'] = true, + ['until'] = true, + ['while'] = true, +} + +---Format a schema field name for use in a LuaLS `---@field` annotation. +---@param field string +---@return string +local function field_name_for_annotation(field) + if field:match('^[A-Za-z_][A-Za-z0-9_]*$') and not lua_keywords[field] then + return field + end + return '[' .. vim.inspect(field) .. ']' +end + ---Convert a schema property into a Lua type. ---@param prop table Schema property. ---@return string @@ -280,6 +327,7 @@ local function append_object(lines, path, prop, root_class) for _, field in ipairs(props) do local child = prop.properties[field] local optional_marker = is_required_field(prop, field, child) and '' or '?' + local field_name = field_name_for_annotation(field) append_description(object_lines, child) if child.type == 'object' and child.properties then @@ -287,11 +335,11 @@ local function append_object(lines, path, prop, root_class) table.insert(child_path, field) table.insert( object_lines, - '---@field ' .. field .. optional_marker .. ' ' .. class_name_for(child_path, root_class) + '---@field ' .. field_name .. optional_marker .. ' ' .. class_name_for(child_path, root_class) ) append_object(lines, child_path, child, root_class) else - table.insert(object_lines, '---@field ' .. field .. optional_marker .. ' ' .. lua_type_for(child)) + table.insert(object_lines, '---@field ' .. field_name .. optional_marker .. ' ' .. lua_type_for(child)) end end end -- cgit v1.3