--[[ LuaDoctor Генерирует файлы аннотаций EmmyLua для Java-классов в среде LuaJ, чтобы Lua Language Server мог предлагать автодополнение методов. Путь по умолчанию: config/neoscripts/scripts/lsp --]] local LuaDoctor = {} local DEFAULT_OUTPUT_DIR = "config/neoscripts/scripts/lsp" -- Битовые маски для java.lang.reflect.Modifier local MOD_PUBLIC = 1 local MOD_PRIVATE = 2 local MOD_STATIC = 8 local MOD_TRANSIENT = 128 local function isPublic(mods) return math.floor(mods / MOD_PUBLIC) % 2 == 1 end local function isPrivate(mods) return math.floor(mods / MOD_PRIVATE) % 2 == 1 end local function isStatic(mods) return math.floor(mods / MOD_STATIC) % 2 == 1 end local function isTransient(mods) return math.floor(mods / MOD_TRANSIENT) % 2 == 1 end -- Таблица соответствия типов Java и EmmyLua local TYPE_MAP = { ['boolean'] = 'boolean', ['Boolean'] = 'boolean', ['byte'] = 'integer', ['Byte'] = 'integer', ['short'] = 'integer', ['Short'] = 'integer', ['int'] = 'integer', ['Integer'] = 'integer', ['long'] = 'integer', ['Long'] = 'integer', ['float'] = 'number', ['Float'] = 'number', ['double'] = 'number', ['Double'] = 'number', ['char'] = 'string', ['Character'] = 'string', ['String'] = 'string', ['void'] = 'void', ['Object'] = 'any', ['java.lang.String'] = 'string', ['java.lang.Object'] = 'any', ['java.lang.Boolean'] = 'boolean', ['java.lang.Byte'] = 'integer', ['java.lang.Short'] = 'integer', ['java.lang.Integer'] = 'integer', ['java.lang.Long'] = 'integer', ['java.lang.Float'] = 'number', ['java.lang.Double'] = 'number', ['java.lang.Character'] = 'string', ['java.lang.Number'] = 'number' } local LUA_KEYWORDS = { ["and"] = true, ["break"] = true, ["do"] = true, ["else"] = true, ["elseif"] = true, ["end"] = true, ["false"] = true, ["for"] = true, ["function"] = true, ["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true, ["not"] = true, ["or"] = true, ["repeat"] = true, ["return"] = true, ["then"] = true, ["true"] = true, ["until"] = true, ["while"] = true } local NON_EXTENDABLE = { ['java.lang.Object'] = true, ['java.lang.Boolean'] = true, ['java.lang.Byte'] = true, ['java.lang.Short'] = true, ['java.lang.Integer'] = true, ['java.lang.Long'] = true, ['java.lang.Float'] = true, ['java.lang.Double'] = true, ['java.lang.Character'] = true, ['java.lang.String'] = true, ['java.lang.Enum'] = true, ['java.lang.Record'] = true } -- Вспомогательная функция перевода Java-массивов в Lua-таблицы local function javaArrayToTable(javaArr) if not javaArr then return {} end local t = {} local ArrayClass = luajava.bindClass("java.lang.reflect.Array") local ok, len = pcall(function() return ArrayClass.getLength(javaArr) end) if ok then for i = 0, len - 1 do table.insert(t, ArrayClass.get(javaArr, i)) end else for i = 1, #javaArr do table.insert(t, javaArr[i]) end end return t end local function resolveClass(target) local Class = luajava.bindClass('java.lang.Class') if type(target) == 'string' then -- 1. Сначала используем bindClass и получаем реальный java.lang.Class через наше новое свойство .javaClass local ok_bind, binded = pcall(luajava.bindClass, target) if ok_bind and binded then local realCls = binded.javaClass if realCls then return realCls end end -- 2. Если не вышло (например, примитив), пробуем системный Class.forName local ok_forName, cls = pcall(function() return Class.forName(target) end) if ok_forName and cls then return cls end error("Could not resolve class: " .. tostring(target)) end if type(target) == 'userdata' then -- Проверяем, является ли userdata уже настоящим java.lang.Class (имеет метод getName) local ok_name, _ = pcall(function() return target:getName() end) if ok_name then return target end -- Пробуем получить класс, если передан класс-прокси local ok_class_prop, realCls = pcall(function() return target.javaClass end) if ok_class_prop and realCls then return realCls end -- Если это обычный объект, берем его класс local ok_class, cls = pcall(function() return target:getClass() end) if ok_class and cls then return cls end end return target end local function checkParamName(pName) if LUA_KEYWORDS[pName] then return "_" .. pName -- Превращает "end" в "_end" end return pName end local function safeGetName(clazz) clazz = resolveClass(clazz) local str = tostring(clazz) -- Парсит строки вида "class имя.пакета.Класс" или "interface имя.пакета.Интерфейс" local name = str:match("^class%s+(.+)$") or str:match("^interface%s+(.+)$") or str return name end local function safeGetSimpleName(clazz) clazz = resolveClass(clazz) local ok, name = pcall(function() return clazz:getSimpleName() end) if ok and name and name ~= "" then return name:gsub("%$", "_") end -- Резервный парсинг имени local full = safeGetName(clazz) local lastDot = full:match("^.*()%.") local base = lastDot and full:sub(lastDot + 1) or full local lastDollar = base:match("^.*()%$") local finalName = lastDollar and base:sub(lastDollar + 1) or base if finalName == "" or finalName:match("^%d") then finalName = "AnonymousClass_" .. finalName end return finalName:gsub("%$", "_") end local function safeGetDeclaredFields(clazz) clazz = resolveClass(clazz) local ok, res = pcall(function() return clazz:getDeclaredFields() end) return ok and javaArrayToTable(res) or {} end local function safeGetDeclaredConstructors(clazz) clazz = resolveClass(clazz) local ok, res = pcall(function() return clazz:getDeclaredConstructors() end) return ok and javaArrayToTable(res) or {} end local function safeGetDeclaredMethods(clazz) clazz = resolveClass(clazz) local ok, res = pcall(function() return clazz:getDeclaredMethods() end) return ok and javaArrayToTable(res) or {} end local function safeGetInterfaces(clazz) clazz = resolveClass(clazz) local ok, res = pcall(function() return clazz:getInterfaces() end) if ok and res then local tbl = javaArrayToTable(res) for i, v in ipairs(tbl) do tbl[i] = resolveClass(v) end return tbl end return {} end local function safeGetSuperclass(clazz) clazz = resolveClass(clazz) local ok, res = pcall(function() return clazz:getSuperclass() end) return ok and res and resolveClass(res) or nil end local function safeGetAllMethods(clazz) clazz = resolveClass(clazz) local all = {} local seen = {} local queue = { clazz } local head = 1 while head <= #queue do local node = queue[head] head = head + 1 if node then node = resolveClass(node) local nodeName = safeGetName(node) if nodeName ~= 'java.lang.Object' and not seen[nodeName] then seen[nodeName] = true local ok, declared = pcall(function() return node:getDeclaredMethods() end) if ok and declared then for _, m in ipairs(javaArrayToTable(declared)) do local mods = m:getModifiers() -- Оборачиваем возвращаемый класс в resolveClass для разблокировки методов local declClass = resolveClass(m:getDeclaringClass()) if declClass:getName() == clazz:getName() or not isPrivate(mods) then table.insert(all, m) end end end local sup = safeGetSuperclass(node) if sup then table.insert(queue, sup) end for _, iface in ipairs(safeGetInterfaces(node)) do table.insert(queue, iface) end end end end return all end local function pkgDirs(fqcn) local dirs = {} for part in fqcn:gmatch("[^%.]+") do table.insert(dirs, part) end if #dirs > 0 then table.remove(dirs) end return dirs end local function packagePath(fqcn) return table.concat(pkgDirs(fqcn), "/") end local function tsType(typeClass, ctx) if not typeClass then return 'any' end typeClass = resolveClass(typeClass) -- Если класс анонимный, приводим его тип к суперклассу или интерфейсу local ok_anon, is_anon = pcall(function() return typeClass:isAnonymousClass() end) if ok_anon and is_anon then local ok_sup, sup = pcall(function() return typeClass:getSuperclass() end) if ok_sup and sup and safeGetName(sup) ~= "java.lang.Object" then return tsType(sup, ctx) end -- Если суперкласс Object, пробуем взять первый интерфейс local ok_ifaces, ifaces = pcall(function() return typeClass:getInterfaces() end) if ok_ifaces and ifaces then local ifaceTable = javaArrayToTable(ifaces) if #ifaceTable > 0 then return tsType(ifaceTable[1], ctx) end end return "any" end local ok, isArr = pcall(function() return typeClass:isArray() end) if ok and isArr then return tsType(typeClass:getComponentType(), ctx) .. '[]' end local ok2, isPrim = pcall(function() return typeClass:isPrimitive() end) if ok2 and isPrim then return TYPE_MAP[typeClass:getName()] or 'number' end -- Заменяем $ на _ во всех генерируемых типах local name = safeGetName(typeClass):gsub("%$", "_") local simple = safeGetSimpleName(typeClass):gsub("%$", "_") if TYPE_MAP[simple] then return TYPE_MAP[simple] end if TYPE_MAP[name] then return TYPE_MAP[name] end if ctx.registry[typeClass:getName()] then ctx.imports[typeClass:getName()] = true return name end return name end local function collectRelatedTypes(clazz) clazz = resolveClass(clazz) local seen = {} local results = {} local function consider(t) if not t then return end t = resolveClass(t) -- Пропускаем анонимные классы и заменяем их на их суперклассы при обходе связей local ok_anon, is_anon = pcall(function() return t:isAnonymousClass() end) if ok_anon and is_anon then local ok_sup, sup = pcall(function() return t:getSuperclass() end) if ok_sup and sup and safeGetName(sup) ~= "java.lang.Object" then consider(sup) end return end local ok, isPrim = pcall(function() return t:isPrimitive() end) if ok and isPrim then return end local ok2, isArr = pcall(function() return t:isArray() end) if ok2 and isArr then consider(t:getComponentType()) return end local name = safeGetName(t) if seen[name] then return end if name:find("^java%.") or name:find("^javax%.") or name:find("^jdk%.") then return end if TYPE_MAP[name] then return end if name == clazz:getName() then return end seen[name] = true table.insert(results, t) end pcall(function() local sup = safeGetSuperclass(clazz) if sup and not NON_EXTENDABLE[sup:getName()] then consider(sup) end for _, iface in ipairs(safeGetInterfaces(clazz)) do consider(iface) end for _, f in ipairs(safeGetDeclaredFields(clazz)) do consider(f:getType()) end for _, m in ipairs(safeGetDeclaredMethods(clazz)) do consider(m:getReturnType()) for _, p in ipairs(javaArrayToTable(m:getParameters())) do consider(p:getType()) end end for _, c in ipairs(safeGetDeclaredConstructors(clazz)) do for _, p in ipairs(javaArrayToTable(c:getParameters())) do consider(p:getType()) end end end) return results end local function renderMethods(clazz, ctx) clazz = resolveClass(clazz) local out = {} local methods = safeGetAllMethods(clazz) local grouped = {} for _, m in ipairs(methods) do local mods = m:getModifiers() local isSynthetic, isBridge = false, false pcall(function() isSynthetic = m:isSynthetic() end) pcall(function() isBridge = m:isBridge() end) if not isSynthetic and not isBridge then local name = m:getName() if name ~= 'wait' and name ~= 'notify' and name ~= 'notifyAll' and name ~= 'getClass' and not name:find('%$') then if not grouped[name] then grouped[name] = {} end table.insert(grouped[name], m) end end end local methodNames = {} for k in pairs(grouped) do table.insert(methodNames, k) end table.sort(methodNames) for _, name in ipairs(methodNames) do local overloads = grouped[name] local sigs = {} for _, m in ipairs(overloads) do local isStatic = isStatic(m:getModifiers()) local params = javaArrayToTable(m:getParameters()) local paramDecls = {} local paramNames = {} for j, p in ipairs(params) do local pName = p:getName() if not pName or pName:find("^arg%d+$") or pName == "" then pName = "arg" .. (j - 1) end pName = checkParamName(pName) -- ЭКРАНИРУЕМ ключевые слова в аргументах local pType = tsType(p:getType(), ctx) if j == #params and m:isVarArgs() then local comp = resolveClass(p:getType()):getComponentType() pType = tsType(comp or p:getType(), ctx) .. "[]" pName = "..." end table.insert(paramDecls, { name = pName, type = pType }) table.insert(paramNames, pName) end table.insert(sigs, { isStatic = isStatic, params = paramDecls, paramNames = paramNames, retType = tsType(m:getReturnType(), ctx) }) end for i = 1, #sigs - 1 do local sig = sigs[i] local overloadParams = {} for _, p in ipairs(sig.params) do table.insert(overloadParams, string.format("%s: %s", p.name, p.type)) end table.insert(out, string.format("---@overload fun(%s): %s", table.concat(overloadParams, ", "), sig.retType)) end local mainSig = sigs[#sigs] for _, p in ipairs(mainSig.params) do if p.name ~= "..." then table.insert(out, string.format("---@param %s %s", p.name, p.type)) end end table.insert(out, string.format("---@return %s", mainSig.retType)) local simpleName = safeGetSimpleName(clazz):gsub("%$", "_") local isKeyword = LUA_KEYWORDS[name] ~= nil if isKeyword then -- Если имя метода совпадает с ключевым словом Lua (например, "end") local paramList = table.concat(mainSig.paramNames, ", ") if not mainSig.isStatic then if #mainSig.paramNames > 0 then paramList = "self, " .. paramList else paramList = "self" end end table.insert(out, string.format("function %s[\"%s\"](%s) end", simpleName, name, paramList)) else -- Стандартный вызов. NeoScripts вызывает методы (включая статические) через ":" local separator = ":" local paramList = table.concat(mainSig.paramNames, ", ") table.insert(out, string.format("function %s%s%s(%s) end", simpleName, separator, name, paramList)) end end return out end local function renderEnum(clazz, ctx) clazz = resolveClass(clazz) local fqcn = safeGetName(clazz) local simple = safeGetSimpleName(clazz) local lines = { "---@meta", "--- Auto-generated enum для " .. fqcn .. ".", "--- Сгенерировано LuaDoctor — не редактировать вручную.", "" } table.insert(lines, "---@class " .. fqcn) local constants = {} local ok, vals = pcall(function() return clazz:getEnumConstants() end) if ok and vals then for _, c in ipairs(javaArrayToTable(vals)) do local name = tostring(c) if name and not name:find('%$') then table.insert(constants, name) end end end for _, name in ipairs(constants) do table.insert(lines, string.format("---@field %s %s", name, fqcn)) end table.insert(lines, "local " .. simple .. " = {}") table.insert(lines, "return " .. simple) return table.concat(lines, "\n") end local function renderDeclaration(clazz, ctx) clazz = resolveClass(clazz) local fqcn = safeGetName(clazz):gsub("%$", "_") -- Заменяем $ на _ в названии класса -- Безопасная проверка на Enum local is_enum = false local ok_enum, res_enum = pcall(function() return clazz:isEnum() end) if ok_enum and res_enum then is_enum = true end if is_enum then return renderEnum(clazz, ctx) end local simple = safeGetSimpleName(clazz):gsub("%$", "_") local lines = { "---@meta", "--- Auto-generated declaration для " .. clazz:getName() .. ".", "--- Сгенерировано LuaDoctor — не редактировать вручную.", "" } -- LuaLS (LuaCATS) признаёт только `--@class [: [, ...]]`. -- `@implements` и произвольные суффиксы после суперкласса LuaLS не понимает -- и трактует их как имена родителей -> ломает весь класс. -- Наследование для language server не нужно: safeGetAllMethods и так -- собирает все методы суперклассов и интерфейсов прямо в этот файл. table.insert(lines, "---@class " .. fqcn) local fields = safeGetDeclaredFields(clazz) local instanceFields = {} local staticFields = {} for _, f in ipairs(fields) do local mods = f:getModifiers() local isSynthetic = false pcall(function() isSynthetic = f:isSynthetic() end) if not isSynthetic and not isTransient(mods) and isPublic(mods) then local name = f:getName() if not name:find('%$') then local fType = tsType(f:getType(), ctx) if isStatic(mods) then table.insert(staticFields, { name = name, type = fType }) else table.insert(instanceFields, string.format("---@field %s %s", name, fType)) end end end end for _, fLine in ipairs(instanceFields) do table.insert(lines, fLine) end table.insert(lines, "local " .. simple .. " = {}") for _, sf in ipairs(staticFields) do table.insert(lines, string.format("---@type %s", sf.type)) if LUA_KEYWORDS[sf.name] then table.insert(lines, string.format("%s[\"%s\"] = nil", simple, sf.name)) else table.insert(lines, string.format("%s.%s = nil", simple, sf.name)) end end local ctors = safeGetDeclaredConstructors(clazz) local ctorSigs = {} for _, c in ipairs(ctors) do if not isPrivate(c:getModifiers()) then local params = javaArrayToTable(c:getParameters()) local paramDecls = {} local paramNames = {} for j, p in ipairs(params) do local pName = p:getName() if not pName or pName:find("^arg%d+$") or pName == "" then pName = "arg" .. (j - 1) end pName = checkParamName(pName) -- ЭКРАНИРУЕМ ключевые слова в аргументах конструктора local pType = tsType(p:getType(), ctx) table.insert(paramDecls, { name = pName, type = pType }) table.insert(paramNames, pName) end table.insert(ctorSigs, { params = paramDecls, paramNames = paramNames }) end end if #ctorSigs > 0 then for i = 1, #ctorSigs - 1 do local sig = ctorSigs[i] local overloadParams = {} for _, p in ipairs(sig.params) do table.insert(overloadParams, string.format("%s: %s", p.name, p.type)) end table.insert(lines, string.format("---@overload fun(%s): %s", table.concat(overloadParams, ", "), fqcn)) end local mainSig = ctorSigs[#ctorSigs] for _, p in ipairs(mainSig.params) do table.insert(lines, string.format("---@param %s %s", p.name, p.type)) end table.insert(lines, string.format("---@return %s", fqcn)) table.insert(lines, string.format("function %s.new(%s) end", simple, table.concat(mainSig.paramNames, ", "))) end local methodsLines = renderMethods(clazz, ctx) for _, mLine in ipairs(methodsLines) do table.insert(lines, mLine) end table.insert(lines, "return " .. simple) return table.concat(lines, "\n") end local function writeFile(dir, fileName, content) local FileClass = luajava.bindClass('java.io.File') local FileWriterClass = luajava.bindClass('java.io.FileWriter') -- Создаем директорию через java.io.File local dirFile = luajava.new(FileClass, dir) dirFile:mkdirs() -- Создаем файл local fileObj = luajava.new(FileClass, dirFile, fileName) -- Пишем контент через FileWriter (надежно работает на любой Java и ОС) local ok, err = pcall(function() local writer = luajava.new(FileWriterClass, fileObj) writer:write(content) writer:close() end) if not ok then print("[LuaDoctor] Error writing file " .. fileName .. ": " .. tostring(err)) end return fileObj:getAbsolutePath() end local function readLuaJavaMap(filePath) local result = {} local FileClass = luajava.bindClass('java.io.File') local FileReaderClass = luajava.bindClass('java.io.FileReader') local BufferedReaderClass = luajava.bindClass('java.io.BufferedReader') local fileObj = luajava.new(FileClass, filePath) if not fileObj:exists() then return result end local ok, err = pcall(function() local reader = luajava.new(BufferedReaderClass, luajava.new(FileReaderClass, fileObj)) local line = reader:readLine() while line do local fqcn = line:match('className:%s*"(.-)"') if fqcn then result[fqcn] = true end line = reader:readLine() end reader:close() end) if not ok then print("[LuaDoctor] Error reading map: " .. tostring(err)) end return result end local function ensureLuarcJson(rootDir) local FileClass = luajava.bindClass('java.io.File') local FileReaderClass = luajava.bindClass('java.io.FileReader') local BufferedReaderClass = luajava.bindClass('java.io.BufferedReader') local fileObj = luajava.new(FileClass, rootDir .. "/.luarc.json") -- Дефолтный JSON, если файла не было local defaultJson = [[{ "runtime": { "version": "Lua 5.3" }, "workspace": { "library": [ "lsp" ], "checkThirdParty": false } }]] if not fileObj:exists() then writeFile(rootDir, ".luarc.json", defaultJson) print("[LuaDoctor] Created missing .luarc.json in " .. rootDir) else local ok, content = pcall(function() local reader = luajava.new(BufferedReaderClass, luajava.new(FileReaderClass, fileObj)) local lines = {} local line = reader:readLine() while line do table.insert(lines, line) line = reader:readLine() end reader:close() return table.concat(lines, "\n") end) if ok and content then -- Парсим JSON в Lua-таблицу local json = require("json") local parse_ok, data = pcall(function() return json.parse(content) end) if parse_ok and data then if not data.workspace then data.workspace = {} end if not data.workspace.library then data.workspace.library = {} end -- Проверяем, есть ли уже путь "lsp" в библиотеках local has_lsp = false for _, path in ipairs(data.workspace.library) do if path == "lsp" then has_lsp = true break end end -- Если пути "lsp" нет, добавляем его if not has_lsp then table.insert(data.workspace.library, "lsp") -- Преобразуем таблицу обратно в красивую строку JSON local stringify_ok, newContent = pcall(function() return json.stringify(data, true) end) if stringify_ok and newContent then writeFile(rootDir, ".luarc.json", newContent) print("[LuaDoctor] Updated .luarc.json library path.") end end end end end end --[[ Основной метод генерации API определений. --]] function LuaDoctor.generate(target, options) options = options or {} local outputDir = options.outputDir or DEFAULT_OUTPUT_DIR local recursive = options.recursive == true local maxDepth = options.maxDepth or 1 local generatedDir = outputDir .. '/generated' local strippedDir = outputDir:gsub("/+$", "") local lastDirSep = strippedDir:match("^.*()/") local parentDir = lastDirSep and strippedDir:sub(1, lastDirSep - 1) or "" local workspaceRoot = parentDir local registry = {} local entry = resolveClass(target) local visited = {} local queue = { { clazz = entry, depth = 0 } } while #queue > 0 do local node = table.remove(queue, 1) local fullName = safeGetName(node.clazz) if not visited[fullName] then visited[fullName] = true registry[fullName] = { fqcn = fullName, clazz = node.clazz, simpleName = safeGetSimpleName(node.clazz), pkgPath = packagePath(fullName) } if recursive and node.depth < maxDepth then local related = collectRelatedTypes(node.clazz) for _, rel in ipairs(related) do table.insert(queue, { clazz = rel, depth = node.depth + 1 }) end end end end -- Генерация .lua файлов для каждого класса for _, meta in pairs(registry) do local cleanPkgPath = meta.pkgPath:gsub("%$", "_") -- Экранируем $ в путях папок local dir = cleanPkgPath ~= "" and (generatedDir .. "/" .. cleanPkgPath) or generatedDir local ctx = { registry = registry, fqcn = meta.fqcn, imports = {} } local declaration = renderDeclaration(meta.clazz, ctx) local cleanFileName = meta.simpleName:gsub("%$", "_") -- Экранируем $ в названии файла local dtsPath = writeFile(dir, cleanFileName .. ".lua", declaration) print(string.format("[LuaDoctor] Generated %s.lua -> %s", cleanFileName, dtsPath)) end local mapPath = outputDir .. "/luajava.lua" local prior = readLuaJavaMap(mapPath) for fqcn in pairs(registry) do prior[fqcn] = true end local sortedKeys = {} for k in pairs(prior) do table.insert(sortedKeys, k) end table.sort(sortedKeys) local mapLines = { "---@meta", "--- Auto-generated mapping для luajava API.", "--- Сгенерировано LuaDoctor — не редактировать вручную.", "", "---@class luajava", "local luajava = {}", "" } -- 1. Перегрузки для bindClass for _, k in ipairs(sortedKeys) do local emmyK = k:gsub("%$", "_") table.insert(mapLines, string.format('---@overload fun(className: "%s"): %s', k, emmyK)) end table.insert(mapLines, "---@param className string") table.insert(mapLines, "---@return any") table.insert(mapLines, "function luajava.bindClass(className) end\n") -- 2. Перегрузки для new for _, k in ipairs(sortedKeys) do local emmyK = k:gsub("%$", "_") table.insert(mapLines, string.format('---@overload fun(className: "%s", ...): %s', k, emmyK)) table.insert(mapLines, string.format('---@overload fun(classObject: %s, ...): %s', emmyK, emmyK)) end table.insert(mapLines, "---@param classOrName any") table.insert(mapLines, "---@return any") table.insert(mapLines, "function luajava.new(classOrName, ...) end\n") -- 3. Перегрузки для newInstance for _, k in ipairs(sortedKeys) do local emmyK = k:gsub("%$", "_") table.insert(mapLines, string.format('---@overload fun(className: "%s", ...): %s', k, emmyK)) end table.insert(mapLines, "---@param className string") table.insert(mapLines, "---@return any") table.insert(mapLines, "function luajava.newInstance(className, ...) end\n") -- Глобальная привязка luajava для lua-language-server table.insert(mapLines, "---@type luajava") table.insert(mapLines, "_G.luajava = luajava") table.insert(mapLines, "") table.insert(mapLines, "return luajava") writeFile(outputDir, "luajava.lua", table.concat(mapLines, "\n")) print("[LuaDoctor] Updated " .. mapPath) ensureLuarcJson(workspaceRoot) return mapPath end return LuaDoctor