Skip to content

Instantly share code, notes, and snippets.

@johannesrld
Last active November 12, 2023 03:27
Show Gist options
  • Save johannesrld/078d9ffba1eb1947cc9bc6b6a536cead to your computer and use it in GitHub Desktop.
Save johannesrld/078d9ffba1eb1947cc9bc6b6a536cead to your computer and use it in GitHub Desktop.
local lddextractor = require("lddextractor")
local typeNameSubstitutes = {
[""] = "any",
["unknown"] = "any",
}
local metamethods = {
__add = true,
__sub = true,
__mul = true,
__div = true,
__mod = true,
__pow = true,
__unm = true,
__idiv = true,
__band = true,
__bor = true,
__bxor = true,
__bnot = true,
__shl = true,
__shr = true,
__concat = true,
__len = true,
__eq = true,
__lt = true,
__le = true,
__index = true,
__newindex = true,
__call = true,
}
local function valuesSortedByName(map)
local result = {}
for _, v in pairs(map) do
table.insert(result, v)
end
table.sort(result, function(a, b)
return a.name < b.name
end)
return result
end
local indentStyle = " "
local function moduleName(path)
-- expect packages to always be mentioned by the same path
return path:gsub("[^%a]", "_")
end
local TealModule = {
__tostring = function(self)
return table.concat(self.__written)
end,
}
TealModule.__index = {
write = function(self, ...)
local t = self.__written
local lastText = t[#t] or "\n"
local lineStart = lastText:sub(#lastText) == "\n"
for _, s in ipairs({ ... }) do
if #s > 0 then
local anchor = 1
while anchor <= #s do
local lineEnd = s:find("\n", anchor) or #s
if lineStart then
table.insert(t, self.__indent)
end
table.insert(t, s:sub(anchor, lineEnd))
lineStart = s:sub(lineEnd, lineEnd) == "\n"
anchor = lineEnd + 1
end
end
end
end,
type = function(self, name)
return name and self.module.types[name]
end,
isFunction = function(_, type)
if not type then
return false
elseif type.tag == "functiontypedef" then
return true
end
end,
writeHeader = function(self, unit)
local shortdescription = unit.shortdescription or ""
local longDescription = unit.description or ""
local usage = unit.metadata and unit.metadata.usage
local usageDescription = usage and usage[1] and usage[1].description or ""
local empty = (#shortdescription + #longDescription + #usageDescription) == 0
if empty then
return
end
if #shortdescription > 0 then
self:write("---", shortdescription:gsub("\n", " "):gsub("^@", ""), "\n")
end
if #longDescription > 0 then
self:write("---", longDescription:gsub("\n", " "):gsub("^@", ""), "\n")
end
if #usageDescription > 0 then
self:write("---", usageDescription:gsub("\n", " "):gsub("^@", ""), "\n")
end
end,
writeNote = function(self, unit, printName)
local name = printName and unit.name or ""
local desc = unit.description or ""
if #name + #desc == 0 then
return
end
if #name > 0 then
self:write((name:gsub("\n", "<br>"):gsub("@", "")))
end
self:write((desc:gsub("\n", "<br>"):gsub("@", "")))
end,
writeReference = function(self, ref, parent)
local name = ref and (ref.typename or (ref.def and ref.def.name)) or ""
local substitute = typeNameSubstitutes[name]
if substitute then
self:write(substitute)
return
end
if name == "" then
error("Empty name")
end
local typeT = self:type(name)
if not typeT then
if ref.modulename then
self:write(name)
elseif ref.def and ref.def.structurekind then
if ref.def.structurekind == "list" then
self:writeReference(ref.def.defaultvaluetyperef)
self:write("[]")
elseif ref.def.structurekind == "map" then
self:write("table<")
self:writeReference(ref.def.defaulkeytyperef)
self:write(", ")
self:writeReference(ref.def.defaultvaluetyperef)
self:write(">")
else
error("Unknown structure kind ", ref.def.structurekind)
end
else
self:write(name)
end
elseif typeT.tag == "recordtypedef" then
self:write(name)
elseif typeT.tag == "functiontypedef" then
else
error(table.concat({ 'Unknown type declaration tag: "', typeT.tag, '" (', name, ")" }))
end
end,
writeReturns = function(self, unit)
if #unit.returns == 0 then
return
end
local prev = nil
for _, ret in ipairs(unit.returns) do
if #ret.types == 0 then
self:write("---@return any", "\n")
end
for i, ref in ipairs(ret.types) do
self:writeHeader(ref, true)
self:write("---@return ")
self:writeReference(ref, unit)
self:write("\n")
end
end
end,
writeFunction = function(self, func, funcdata, parent)
local isSelf = false
if #func.params > 0 then
for _, param in ipairs(func.params) do
if not param.type and param.name == "self" and funcdata then
isSelf = true
else
self:write("---@param ", param.name, " ")
self:writeReference(param.type, funcdata)
self:write(" ")
self:writeNote(param, false)
self:write("\n")
end
end
end
if #func.returns > 0 then
self:writeReturns(func)
end
self:write("function ")
self:write(parent.name)
if isSelf then
self:write(":")
else
self:write(".")
end
self:write(funcdata.name)
self:write("(")
if #func.params > 0 then
for i, param in ipairs(func.params) do
if param.name ~= "self" then
self:write(param.name)
if #func.params ~= i then
self:write(", ")
end
end
end
end
self:write(") end")
self:write("\n")
end,
writeVariable = function(self, var, parent)
self:writeHeader(var)
if metamethods[var.name] and self:isFunction(self:type(var.type.typename)) then
--FIXME: lls uses ---@operator {operator name}({type}): {return type} or ---@operator {operator name}: {return type}
self:write("metamethod ")
end
self:write("---@field ", var.name, " ")
self:writeReference(var.type, parent)
self:write("\n")
end,
writeDeclaration = function(self, typeT)
local functions = {}
for _, field in ipairs(valuesSortedByName(typeT.fields)) do
local name = field.type and (field.type.typename or (field.def and field.def.name)) or ""
local typeB = self:type(name)
if not typeB then
self:writeVariable(field, typeT)
elseif typeB.tag ~= "functiontypedef" then
self:writeVariable(field, typeT)
elseif typeB.tag == "functiontypedef" then
table.insert(functions, field)
end
end
self:writeHeader(typeT)
self:write("local ", typeT.name, " = {}", "\n\n")
for _, field in ipairs(valuesSortedByName(functions)) do
local name = field.type and (field.type.typename or (field.def and field.def.name)) or ""
local typeB = self:type(name)
self:writeFunction(typeB, field, typeT)
end
end,
writeRequires = function(self)
local moduleSet = {}
local function requiredInUnits(refs)
for _, ref in pairs(refs) do
local refTypes = ref.types or { ref.type }
for _, refType in ipairs(refTypes) do
local name = refType and refType.modulename
if name then
moduleSet[name] = true
end
end
end
end
for _, typeT in pairs(self.module.types) do
if typeT.fields then
requiredInUnits(typeT.fields)
end
if typeT.params then
requiredInUnits(typeT.params)
end
if typeT.returns then
requiredInUnits(typeT.returns)
end
end
requiredInUnits(self.module.globalvars)
local moduleList = {}
for module, _ in pairs(moduleSet) do
table.insert(moduleList, module)
end
table.sort(moduleList)
for _, module in ipairs(moduleList) do
self:write("---@module '", string.match(module, "%.(.+)"), "'", "\n")
end
self:write("\n")
end,
generateDeclaration = function(self)
local module = self.module
local moduleRef = module:moduletyperef()
local moduleType = moduleRef and self:type(moduleRef.typename)
self.moduleType = moduleType
self:write("---@meta")
self:write("\n")
self:writeHeader(module)
self:write("\n")
self:writeRequires()
if moduleType then
self:write("---@class ", moduleType.name, "\n")
self:writeDeclaration(moduleType)
end
-- resolve conflicts between field names and child type names by promoting those types to global
if moduleType then
for _, field in pairs(moduleType.fields) do
local typeName = field.type and field.type.typename
local typeT = typeName and self:type(typeName)
local notAnonymous = typeName and typeName:sub(1, 2) ~= "__"
if typeT and notAnonymous then
self.promotedTypes[typeT] = true
end
end
self.promotedTypes[moduleType] = true
end
local function writeType(typeT)
local isAnonymous = typeT.name:sub(1, 2) == "__"
if typeT ~= moduleType and not isAnonymous then
self:write("---@class ", typeT.name)
self:write("\n")
self:writeDeclaration(typeT)
end
end
local promotedList = {}
for typeT in pairs(self.promotedTypes) do
table.insert(promotedList, typeT)
end
promotedList = valuesSortedByName(promotedList)
for _, typeT in ipairs(promotedList) do
writeType(typeT)
end
for _, typeT in ipairs(valuesSortedByName(module.types)) do
if not self.promotedTypes[typeT] then
writeType(typeT)
end
end
-- for _, variable in ipairs(valuesSortedByName(module.globalvars)) do
-- self:writeVariable(variable)
-- end
self:write("return ", moduleType.name)
end,
}
TealModule.new = function(apiModule)
return setmetatable({
module = apiModule,
__written = {},
__indent = "",
promotedTypes = {},
moduleType = nil,
}, TealModule)
end
local function generateforfiles(filenames)
local definitions = {}
local failedFiles = {}
for _, filename in pairs(filenames) do
local file, err = io.open(filename, "r")
if not file then
return nil, 'Unable to read "' .. filename .. '"\n' .. err
end
local code = file:read("*all")
file:close()
local apimodule, err = lddextractor.generateapimodule(filename, code, false)
if apimodule and apimodule.name then
local tealModule = TealModule.new(apimodule)
tealModule:generateDeclaration()
definitions[filename] = tostring(tealModule)
else
table.insert(failedFiles, table.concat({ 'Failed to parse "', filename, '" with "', err, '"' }))
end
end
return definitions, failedFiles
end
return {
generateforfiles = generateforfiles,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment