Files
Basalt2/tools/generate-annotations.lua
2025-03-14 17:00:13 +01:00

338 lines
13 KiB
Lua

local args = {...}
local commentTypes = {
"module",
"class",
"param",
"return",
"usage",
"function",
"local",
"property",
"combinedProperty",
"event",
"private",
"protected",
"field",
"vararg",
"shortDescription",
}
local elementList = {}
local function getFilesInCC(path)
local files = {}
local function scanDir(dir)
for _, item in ipairs(fs.list(dir)) do
local fullPath = fs.combine(dir, item)
if fs.isDir(fullPath) then
scanDir(fullPath)
elseif item:match("%.lua$") then
if(fullPath:find("elements"))then
local itemName = item:gsub("%.lua$", "")
table.insert(elementList, itemName)
end
local file = fs.open(fullPath, "r")
if file then
files[fullPath] = file.readAll()
file.close()
end
end
end
end
scanDir(path)
return files
end
local function getFilesInLua(path)
local files = {}
local function scanDir(dir)
local p = io.popen('find "'..dir..'" -name "*.lua"')
if not p then print("Couldn't find "..dir) return end
for file in p:lines() do
if file:match("%.lua$") then
if(file:find("elements"))then
local itemName = file:gsub("%.lua$", "")
itemName = itemName:match("([^/\\]+)$")
table.insert(elementList, itemName)
end
local f = io.open(file, "r")
if f then
files[file] = f:read("*a")
f:close()
end
end
end
p:close()
end
scanDir(path)
return files
end
local function extractComment(line)
local tripleContent = line:match("^%-%-%-%s*(.*)")
if tripleContent then
return tripleContent, true
end
local doubleContent = line:match("^%-%- %s*(.*)")
if doubleContent then
return doubleContent, false
end
return nil, false
end
local function getCommentType(comment)
for _, pattern in pairs(commentTypes) do
if comment:match("^@"..pattern) then
local content = comment:sub(#pattern + 2):gsub("^%s*", "")
return pattern, content
end
end
return "desc", comment
end
local function hasBlockContent(block)
for key, _ in pairs(block) do
if(key~="type")and(key~="desc")then
return true
end
end
if(#block.desc > 0)then
return true
end
return false
end
local function getFunctionName(line)
local pattern = "^function%s+([%w_%.:]-)%s*%("
return line:match(pattern)
end
local function split(str, delimiter)
local result = {}
for match in (str..delimiter):gmatch("(.-)"..delimiter) do
table.insert(result, match)
end
return result
end
local function getClassName(content)
return split(content:gsub("^%s*", ""):gsub("%s*$", ""):gsub(" ", ""), ":")
end
local function parseEventParams(content)
local eventName, paramStr, desc = content:match("^([%w_]+)%s*{([^}]+)}%s*(.*)$")
if eventName then
local params = {}
for name, typ in paramStr:gmatch("([%w_]+)%s+([%w_]+)") do
table.insert(params, {name=name, type=typ})
end
return eventName, params, desc
end
return nil, nil, nil
end
local function parseFile(content)
local fileContent = {}
local class = {functions = {}, properties = {}, events = {}, fields = {}}
local func = {params={}, returns={}, desc=""}
local skipNextFunction = false
for line in content:gsub("\r\n", "\n"):gmatch("([^\n]*)\n?") do
if line:match("^%s*$") or line == "" then
-- Skip empty lines
func = {params={}, returns={}, desc=""}
else
local comment, isDoc = extractComment(line)
if comment then
local commentType, content = getCommentType(comment)
if(commentType=="module")then
class.module = content
elseif(commentType=="class")then
if(class.class)then
fileContent[class.class] = class
class = {functions = {}, properties = {}, events = {}, fields = {}}
end
class.class, class.parent = table.unpack(getClassName(content))
if(class.class=="Container")then
for _,v in ipairs(elementList)do
class.functions["Container:add"..v] = {params={{name="self", type="Container", desc="self"}, {name="props", type="table", desc="Optional: properties for the element.", optional=true}}, returns={{type=v, desc="element A new "..v.." element."}}, desc="Creates a new "..v.." element.\n"}
end
end
elseif(commentType=="param")then
if func then
local paramName, paramType, paramDesc = content:match("^%s*([%w_]+)%s+([%w_]+)%s*(.*)$")
if paramName then
table.insert(func.params, {name=paramName, type=paramType, desc=paramDesc or ""})
end
end
elseif(commentType=="return")then
if func then
local returnType, returnDesc = content:match("^%s*([%w_]+)%s*(.*)$")
if returnType then
table.insert(func.returns, {type=returnType, desc=returnDesc or ""})
end
end
elseif(commentType=="usage")then
if func then
func.usage = content
end
elseif(commentType=="desc")then
if func then
func.desc = (func.desc or "") .. content .. "\n"
end
elseif(commentType=="private")then
skipNextFunction = true
elseif(commentType=="protected")then
if func then
func.protected = true
end
elseif(commentType=="shortDescription")then
-- skip
elseif(commentType=="property")then
local propertyName, propertyType, propertyDesc = content:match("^%s*([%w_]+)%s+([%w_]+)%s*(.*)$")
if propertyName then
class.fields[propertyName] = {type=propertyType, desc=propertyDesc:gsub("^%S+%s*", "") or ""}
propertyName = propertyName:sub(1,1):upper() .. propertyName:sub(2)
class.functions[class.class..":get" .. propertyName] = {params={{name="self", type=class.class, desc="self"}}, returns={{type=propertyType, desc=propertyDesc or ""}}, desc="Gets the value of the " .. propertyName .. " property.\n"}
class.functions[class.class..":set" .. propertyName] = {params={{name="self", type=class.class, desc="self"}, {name=propertyName, type=propertyType, desc=propertyDesc:gsub("^%S+%s*", "") or ""}}, returns={}, desc="Sets the value of the " .. propertyName .. " property.\n"}
end
elseif(commentType=="combinedProperty")then
local propertyName, propertyType, propertyDesc = content:match("^%s*([%w_]+)%s+([%w_]+)%s*(.*)$")
if propertyName then
class.fields[propertyName] = {type=propertyType, desc=propertyDesc:gsub("^%S+%s*", "") or ""}
propertyName = propertyName:sub(1,1):upper() .. propertyName:sub(2)
class.functions[class.class..":get" .. propertyName] = {params={{name="self", type=class.class, desc="self"}}, returns={{type=propertyType, desc=propertyDesc or ""}}, desc="Gets the value of the " .. propertyName .. " property.\n"}
class.functions[class.class..":set" .. propertyName] = {params={{name="self", type=class.class, desc="self"}, {name=propertyName, type=propertyType, desc=propertyDesc:gsub("^%S+%s*", "") or ""}}, returns={}, desc="Sets the value of the " .. propertyName .. " property.\n"}
end
elseif(commentType=="event")then
local eventName, params, eventDesc = parseEventParams(content)
if eventName then
class.events[eventName] = {params=params, desc=eventDesc or ""}
local paramStr = ""
for _, param in ipairs(params) do
paramStr = paramStr .. string.format("---@param %s %s\n", param.name, param.type)
end
class.functions[class.class..":on" .. eventName:sub(1,1):upper() .. eventName:sub(2)] = {
params={{name="self", type=class.class, desc="self"}, {name="func", type="function", desc="The function to be called when the event fires"}},
returns={},
desc="Registers a function to handle the " .. eventName .. " event.\n" .. paramStr
}
end
end
else
if not skipNextFunction then
local functionName = getFunctionName(line)
if functionName then
if func and hasBlockContent(func) then
class.functions[functionName] = func
end
func = {params={}, returns={}, desc=""}
end
else
skipNextFunction = false
end
end
end
end
if(class.class)then
fileContent[class.class] = class
end
return fileContent
end
local function generateLuaLS(finalContent)
local output = {"---@meta\n\n"}
for filepath, block in pairs(finalContent) do
if block.class then
if(block.parent~=nil)then
table.insert(output, string.format("---@class %s : %s\n", block.class, block.parent))
else
table.insert(output, string.format("---@class %s\n", block.class))
end
for k,v in pairs(block.fields)do
table.insert(output, string.format("---@field %s %s %s\n", k, v.type, v.desc))
end
table.insert(output, string.format("local %s = {}\n\n", block.class))
for funcName, funcData in pairs(block.functions) do
if funcData.desc~="" then
table.insert(output, string.format("---%s", funcData.desc))
end
if(funcData.protected)then
table.insert(output, string.format("---This function is protected and should not be called outside of basalt, however you can overwrite it if you know what you're doing.\n"))
end
for _, param in ipairs(funcData.params) do
table.insert(output, string.format("---@param %s %s %s\n", (param.optional and param.name.."?" or param.name), param.type, param.desc))
end
for _, ret in ipairs(funcData.returns) do
table.insert(output, string.format("---@return %s %s\n", ret.type, ret.desc))
end
if(funcData.protected)then
table.insert(output, string.format("---@protected\n"))
end
local paramNames = {}
for _, param in ipairs(funcData.params) do
table.insert(paramNames, param.name)
end
table.insert(output, string.format("function %s(%s) end\n\n",
funcName,
table.concat(paramNames, ", ")))
end
end
end
return table.concat(output)
end
local function mergeTables(t1, t2)
local merged = {}
for k, v in pairs(t1) do
merged[k] = v
end
for k, v in pairs(t2) do
if type(v) == "table" and type(merged[k]) == "table" then
merged[k] = mergeTables(merged[k], v)
else
merged[k] = v
end
end
return merged
end
local function parseFiles(files)
local finalContent = {}
for k,v in pairs(files)do
local fileContent = parseFile(v)
if fileContent then
finalContent = mergeTables(finalContent, fileContent)
end
end
local lualsContent = generateLuaLS(finalContent)
local outFile = io.open("BasaltLS.lua", "w")
if outFile then
outFile:write(lualsContent)
outFile:close()
end
end
if _G.fs then
parseFiles(getFilesInCC(args[1]))
else
parseFiles(getFilesInLua(args[1]))
end