Last active
November 12, 2023 03:27
-
-
Save johannesrld/078d9ffba1eb1947cc9bc6b6a536cead to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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