dotfiles from arch
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
local lang = require 'language'
|
||||
local platform = require 'bee.platform'
|
||||
local subprocess = require 'bee.subprocess'
|
||||
local json = require 'json'
|
||||
local jsonb = require 'json-beautify'
|
||||
local util = require 'utility'
|
||||
|
||||
local export = {}
|
||||
|
||||
local function logFileForThread(threadId)
|
||||
return LOGPATH .. '/check-partial-' .. threadId .. '.json'
|
||||
end
|
||||
|
||||
local function buildArgs(exe, numThreads, threadId, format, quiet)
|
||||
local args = {exe}
|
||||
local skipNext = false
|
||||
for i = 1, #arg do
|
||||
local arg = arg[i]
|
||||
-- --check needs to be transformed into --check_worker
|
||||
if arg:lower():match('^%-%-check$') or arg:lower():match('^%-%-check=') then
|
||||
args[#args + 1] = arg:gsub('%-%-%w*', '--check_worker')
|
||||
-- --check_out_path needs to be removed if we have more than one thread
|
||||
elseif arg:lower():match('%-%-check_out_path') and numThreads > 1 then
|
||||
if not arg:match('%-%-[%w_]*=') then
|
||||
skipNext = true
|
||||
end
|
||||
else
|
||||
if skipNext then
|
||||
skipNext = false
|
||||
else
|
||||
args[#args + 1] = arg
|
||||
end
|
||||
end
|
||||
end
|
||||
args[#args + 1] = '--thread_id'
|
||||
args[#args + 1] = tostring(threadId)
|
||||
if numThreads > 1 then
|
||||
if quiet then
|
||||
args[#args + 1] = '--quiet'
|
||||
end
|
||||
if format then
|
||||
args[#args + 1] = '--check_format=' .. format
|
||||
end
|
||||
args[#args + 1] = '--check_out_path'
|
||||
args[#args + 1] = logFileForThread(threadId)
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
function export.runCLI()
|
||||
local numThreads = tonumber(NUM_THREADS or 1)
|
||||
|
||||
local exe
|
||||
local minIndex = -1
|
||||
while arg[minIndex] do
|
||||
exe = arg[minIndex]
|
||||
minIndex = minIndex - 1
|
||||
end
|
||||
-- TODO: is this necessary? got it from the shell.lua helper in bee.lua tests
|
||||
if platform.os == 'windows' and not exe:match('%.[eE][xX][eE]$') then
|
||||
exe = exe..'.exe'
|
||||
end
|
||||
|
||||
if not QUIET and numThreads > 1 then
|
||||
print(lang.script('CLI_CHECK_MULTIPLE_WORKERS', numThreads))
|
||||
end
|
||||
|
||||
local procs = {}
|
||||
for i = 1, numThreads do
|
||||
local process, err = subprocess.spawn({buildArgs(exe, numThreads, i, CHECK_FORMAT, QUIET)})
|
||||
if err then
|
||||
print(err)
|
||||
end
|
||||
if process then
|
||||
procs[#procs + 1] = process
|
||||
end
|
||||
end
|
||||
|
||||
local checkPassed = true
|
||||
for _, process in ipairs(procs) do
|
||||
checkPassed = process:wait() == 0 and checkPassed
|
||||
end
|
||||
|
||||
if numThreads > 1 then
|
||||
local mergedResults = {}
|
||||
local count = 0
|
||||
for i = 1, numThreads do
|
||||
local result = json.decode(util.loadFile(logFileForThread(i)) or '[]')
|
||||
for k, v in pairs(result) do
|
||||
local entries = mergedResults[k] or {}
|
||||
mergedResults[k] = entries
|
||||
for _, entry in ipairs(v) do
|
||||
entries[#entries + 1] = entry
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local outpath = nil
|
||||
|
||||
if CHECK_FORMAT == 'json' or CHECK_OUT_PATH then
|
||||
outpath = CHECK_OUT_PATH or LOGPATH .. '/check.json'
|
||||
util.saveFile(outpath, jsonb.beautify(mergedResults))
|
||||
end
|
||||
|
||||
if not QUIET then
|
||||
if count == 0 then
|
||||
print(lang.script('CLI_CHECK_SUCCESS'))
|
||||
elseif outpath then
|
||||
print(lang.script('CLI_CHECK_RESULTS_OUTPATH', count, outpath))
|
||||
else
|
||||
print(lang.script('CLI_CHECK_RESULTS_PRETTY', count))
|
||||
end
|
||||
end
|
||||
end
|
||||
return checkPassed and 0 or 1
|
||||
end
|
||||
|
||||
return export
|
||||
@@ -0,0 +1,295 @@
|
||||
local lclient = require 'lclient'()
|
||||
local furi = require 'file-uri'
|
||||
local ws = require 'workspace'
|
||||
local files = require 'files'
|
||||
local diag = require 'provider.diagnostic'
|
||||
local util = require 'utility'
|
||||
local jsonb = require 'json-beautify'
|
||||
local lang = require 'language'
|
||||
local define = require 'proto.define'
|
||||
local protoDiag = require 'proto.diagnostic'
|
||||
local config = require 'config.config'
|
||||
local fs = require 'bee.filesystem'
|
||||
local provider = require 'provider'
|
||||
local await = require 'await'
|
||||
require 'plugin'
|
||||
require 'vm'
|
||||
|
||||
local export = {}
|
||||
|
||||
local colors
|
||||
|
||||
if not os.getenv('NO_COLOR') then
|
||||
colors = {
|
||||
red = '\27[31m',
|
||||
green = '\27[32m',
|
||||
yellow = '\27[33m',
|
||||
blue = '\27[34m',
|
||||
magenta = '\27[35m',
|
||||
white = '\27[37m',
|
||||
grey = '\27[90m',
|
||||
reset = '\27[0m'
|
||||
}
|
||||
else
|
||||
colors = {
|
||||
red = '',
|
||||
green = '',
|
||||
yellow = '',
|
||||
blue = '',
|
||||
magenta = '',
|
||||
white = '',
|
||||
grey = '',
|
||||
reset = ''
|
||||
}
|
||||
end
|
||||
|
||||
--- @type table<DiagnosticSeverity, string>
|
||||
local severity_colors = {
|
||||
Error = colors.red,
|
||||
Warning = colors.yellow,
|
||||
Information = colors.white,
|
||||
Hint = colors.white,
|
||||
}
|
||||
|
||||
local severity_str = {} --- @type table<integer,DiagnosticSeverity>
|
||||
for k, v in pairs(define.DiagnosticSeverity) do
|
||||
severity_str[v] = k
|
||||
end
|
||||
|
||||
local pwd
|
||||
|
||||
---@param path string
|
||||
---@return string
|
||||
local function relpath(path)
|
||||
if not pwd then
|
||||
pwd = furi.decode(furi.encode(fs.current_path():string()))
|
||||
end
|
||||
if pwd and path:sub(1, #pwd) == pwd then
|
||||
path = path:sub(#pwd + 2)
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
local function report_pretty(uri, diags)
|
||||
local path = relpath(furi.decode(uri))
|
||||
|
||||
local lines = {} --- @type string[]
|
||||
pcall(function()
|
||||
for line in io.lines(path) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
end)
|
||||
|
||||
for _, d in ipairs(diags) do
|
||||
local rstart = d.range.start
|
||||
local rend = d.range['end']
|
||||
local severity = severity_str[d.severity]
|
||||
print(
|
||||
('%s%s:%s:%s%s [%s%s%s] %s %s(%s)%s'):format(
|
||||
colors.blue,
|
||||
path,
|
||||
rstart.line + 1, -- Use 1-based indexing
|
||||
rstart.character + 1, -- Use 1-based indexing
|
||||
colors.reset,
|
||||
severity_colors[severity],
|
||||
severity,
|
||||
colors.reset,
|
||||
d.message,
|
||||
colors.magenta,
|
||||
d.code,
|
||||
colors.reset
|
||||
)
|
||||
)
|
||||
if #lines > 0 then
|
||||
io.write(' ', lines[rstart.line + 1], '\n')
|
||||
io.write(' ', colors.grey, (' '):rep(rstart.character), '^')
|
||||
if rstart.line == rend.line then
|
||||
io.write(('^'):rep(rend.character - rstart.character - 1))
|
||||
end
|
||||
io.write(colors.reset, '\n')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function clear_line()
|
||||
-- Write out empty space to ensure that the previous lien is cleared.
|
||||
io.write('\x0D', (' '):rep(80), '\x0D')
|
||||
end
|
||||
|
||||
--- @param i integer
|
||||
--- @param max integer
|
||||
--- @param results table<string, table[]>
|
||||
local function report_progress(i, max, results)
|
||||
local filesWithErrors = 0
|
||||
local errors = 0
|
||||
for _, diags in pairs(results) do
|
||||
filesWithErrors = filesWithErrors + 1
|
||||
errors = errors + #diags
|
||||
end
|
||||
|
||||
clear_line()
|
||||
io.write(
|
||||
('>'):rep(math.ceil(i / max * 20)),
|
||||
('='):rep(20 - math.ceil(i / max * 20)),
|
||||
' ',
|
||||
('0'):rep(#tostring(max) - #tostring(i)),
|
||||
tostring(i),
|
||||
'/',
|
||||
tostring(max)
|
||||
)
|
||||
if errors > 0 then
|
||||
io.write(' [', lang.script('CLI_CHECK_PROGRESS', errors, filesWithErrors), ']')
|
||||
end
|
||||
io.flush()
|
||||
end
|
||||
|
||||
--- @param uri string
|
||||
--- @param checkLevel integer
|
||||
local function apply_check_level(uri, checkLevel)
|
||||
local config_disables = util.arrayToHash(config.get(uri, 'Lua.diagnostics.disable'))
|
||||
local config_severities = config.get(uri, 'Lua.diagnostics.severity')
|
||||
for name, serverity in pairs(define.DiagnosticDefaultSeverity) do
|
||||
serverity = config_severities[name] or serverity
|
||||
if serverity:sub(-1) == '!' then
|
||||
serverity = serverity:sub(1, -2)
|
||||
end
|
||||
if define.DiagnosticSeverity[serverity] > checkLevel then
|
||||
config_disables[name] = true
|
||||
end
|
||||
end
|
||||
config.set(uri, 'Lua.diagnostics.disable', util.getTableKeys(config_disables, true))
|
||||
end
|
||||
|
||||
local function downgrade_checks_to_opened(uri)
|
||||
local diagStatus = config.get(uri, 'Lua.diagnostics.neededFileStatus')
|
||||
for d, status in pairs(diagStatus) do
|
||||
if status == 'Any' or status == 'Any!' then
|
||||
diagStatus[d] = 'Opened!'
|
||||
end
|
||||
end
|
||||
for d, status in pairs(protoDiag.getDefaultStatus()) do
|
||||
if status == 'Any' or status == 'Any!' then
|
||||
diagStatus[d] = 'Opened!'
|
||||
end
|
||||
end
|
||||
config.set(uri, 'Lua.diagnostics.neededFileStatus', diagStatus)
|
||||
end
|
||||
|
||||
function export.runCLI()
|
||||
lang(LOCALE)
|
||||
|
||||
local numThreads = tonumber(NUM_THREADS or 1)
|
||||
local threadId = tonumber(THREAD_ID or 1)
|
||||
local quiet = QUIET or numThreads > 1
|
||||
|
||||
if type(CHECK_WORKER) ~= 'string' then
|
||||
print(lang.script('CLI_CHECK_ERROR_TYPE', type(CHECK_WORKER)))
|
||||
return
|
||||
end
|
||||
|
||||
local rootPath = fs.canonical(fs.path(CHECK_WORKER)):string()
|
||||
local rootUri = furi.encode(rootPath)
|
||||
if not rootUri then
|
||||
print(lang.script('CLI_CHECK_ERROR_URI', rootPath))
|
||||
return
|
||||
end
|
||||
rootUri = rootUri:gsub("/$", "")
|
||||
|
||||
if CHECKLEVEL and not define.DiagnosticSeverity[CHECKLEVEL] then
|
||||
print(lang.script('CLI_CHECK_ERROR_LEVEL', 'Error, Warning, Information, Hint'))
|
||||
return
|
||||
end
|
||||
local checkLevel = define.DiagnosticSeverity[CHECKLEVEL] or define.DiagnosticSeverity.Warning
|
||||
|
||||
util.enableCloseFunction()
|
||||
|
||||
local lastClock = os.clock()
|
||||
local results = {} --- @type table<string, table[]>
|
||||
|
||||
local function errorhandler(err)
|
||||
print(err)
|
||||
print(debug.traceback())
|
||||
end
|
||||
|
||||
---@async
|
||||
xpcall(lclient.start, errorhandler, lclient, function (client)
|
||||
await.disable()
|
||||
client:registerFakers()
|
||||
|
||||
client:initialize {
|
||||
rootUri = rootUri,
|
||||
}
|
||||
|
||||
client:register('textDocument/publishDiagnostics', function (params)
|
||||
results[params.uri] = params.diagnostics
|
||||
if not QUIET and (CHECK_FORMAT == nil or CHECK_FORMAT == 'pretty') then
|
||||
clear_line()
|
||||
report_pretty(params.uri, params.diagnostics)
|
||||
end
|
||||
end)
|
||||
|
||||
if not quiet then
|
||||
io.write(lang.script('CLI_CHECK_INITING'))
|
||||
end
|
||||
|
||||
provider.updateConfig(rootUri)
|
||||
|
||||
ws.awaitReady(rootUri)
|
||||
|
||||
-- Disable any diagnostics that are above the check level
|
||||
apply_check_level(rootUri, checkLevel)
|
||||
|
||||
-- Downgrade file opened status to Opened for everything to avoid
|
||||
-- reporting during compilation on files that do not belong to this thread
|
||||
downgrade_checks_to_opened(rootUri)
|
||||
|
||||
local uris = files.getChildFiles(rootUri)
|
||||
local max = #uris
|
||||
table.sort(uris) -- sort file list to ensure the work distribution order across multiple threads
|
||||
for i, uri in ipairs(uris) do
|
||||
if (i % numThreads + 1) == threadId and not ws.isIgnored(uri) then
|
||||
files.open(uri)
|
||||
diag.doDiagnostic(uri, true)
|
||||
-- Print regularly but always print the last entry to ensure
|
||||
-- that logs written to files don't look incomplete.
|
||||
if not quiet and (os.clock() - lastClock > 0.2 or i == #uris) then
|
||||
lastClock = os.clock()
|
||||
client:update()
|
||||
report_progress(i, max, results)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not quiet then
|
||||
clear_line()
|
||||
end
|
||||
end)
|
||||
|
||||
local count = 0
|
||||
for uri, result in pairs(results) do
|
||||
count = count + #result
|
||||
if #result == 0 then
|
||||
results[uri] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local outpath = nil
|
||||
|
||||
if CHECK_FORMAT == 'json' or CHECK_OUT_PATH then
|
||||
outpath = CHECK_OUT_PATH or LOGPATH .. '/check.json'
|
||||
-- Always write result, even if it's empty to make sure no one accidentally looks at an old output after a successful run.
|
||||
util.saveFile(outpath, jsonb.beautify(results))
|
||||
end
|
||||
|
||||
if not quiet then
|
||||
if count == 0 then
|
||||
print(lang.script('CLI_CHECK_SUCCESS'))
|
||||
elseif outpath then
|
||||
print(lang.script('CLI_CHECK_RESULTS_OUTPATH', count, outpath))
|
||||
else
|
||||
print(lang.script('CLI_CHECK_RESULTS_PRETTY', count))
|
||||
end
|
||||
end
|
||||
return count == 0 and 0 or 1
|
||||
end
|
||||
|
||||
return export
|
||||
@@ -0,0 +1,362 @@
|
||||
---@diagnostic disable: await-in-sync, param-type-mismatch
|
||||
local ws = require 'workspace'
|
||||
local vm = require 'vm'
|
||||
local guide = require 'parser.guide'
|
||||
|
||||
local getDesc = require 'core.hover.description'
|
||||
local getLabel = require 'core.hover.label'
|
||||
local jsonb = require 'json-beautify'
|
||||
local util = require 'utility'
|
||||
local markdown = require 'provider.markdown'
|
||||
local fs = require 'bee.filesystem'
|
||||
local furi = require 'file-uri'
|
||||
|
||||
---@alias doctype
|
||||
---| 'doc.alias'
|
||||
---| 'doc.class'
|
||||
---| 'doc.field'
|
||||
---| 'doc.field.name'
|
||||
---| 'doc.type.arg.name'
|
||||
---| 'doc.type.function'
|
||||
---| 'doc.type.table'
|
||||
---| 'funcargs'
|
||||
---| 'function'
|
||||
---| 'function.return'
|
||||
---| 'global.type'
|
||||
---| 'global.variable'
|
||||
---| 'local'
|
||||
---| 'luals.config'
|
||||
---| 'self'
|
||||
---| 'setfield'
|
||||
---| 'setglobal'
|
||||
---| 'setindex'
|
||||
---| 'setmethod'
|
||||
---| 'tableindex'
|
||||
---| 'type'
|
||||
|
||||
---@class docUnion broadest possible collection of exported docs, these are never all together.
|
||||
---@field [1] string in name when table, always the same as view
|
||||
---@field args docUnion[] list of argument docs passed to function
|
||||
---@field async boolean has @async tag
|
||||
---@field defines docUnion[] list of places where this is doc is defined and how its defined there
|
||||
---@field deprecated boolean has @deprecated tag
|
||||
---@field desc string code commentary
|
||||
---@field extends string | docUnion ? what type this 'is'. string:<Parent_Class> for type: 'type', docUnion for type: 'function', string<primative> for other type 's
|
||||
---@field fields docUnion[] class's fields
|
||||
---@field file string path to where this token is defined
|
||||
---@field finish [integer, integer] 0-indexed [line, column] position of end of token
|
||||
---@field name string canonical name
|
||||
---@field rawdesc string same as desc, but may have other things for types doc.retun andr doc.param (unused?)
|
||||
---@field returns docUnion | docUnion[] list of docs for return values. if singluar, then always {type: 'undefined'}? might be a bug.
|
||||
---@field start [integer, integer] 0-indexed [line, column] position of start of token
|
||||
---@field type doctype role that this token plays in documentation. different from the 'type'/'class' this token is
|
||||
---@field types docUnion[] type union? unclear. seems to be related to alias, maybe
|
||||
---@field view string full method name, class, basal type, or unknown. in name table same as [1]
|
||||
---@field visible 'package'|'private'|'protected'|'public' visibilty tag
|
||||
|
||||
local export = {}
|
||||
|
||||
function export.getLocalPath(uri)
|
||||
local file_canonical = fs.canonical(furi.decode(uri)):string()
|
||||
local doc_canonical = fs.canonical(DOC):string()
|
||||
local relativePath = fs.relative(file_canonical, doc_canonical):string()
|
||||
if relativePath == "" or relativePath:sub(1, 2) == '..' then
|
||||
-- not under project directory
|
||||
return '[FOREIGN] ' .. file_canonical
|
||||
end
|
||||
return relativePath
|
||||
end
|
||||
|
||||
function export.positionOf(rowcol)
|
||||
return type(rowcol) == 'table' and guide.positionOf(rowcol[1], rowcol[2]) or -1
|
||||
end
|
||||
|
||||
function export.sortDoc(a,b)
|
||||
if a.name ~= b.name then
|
||||
return a.name < b.name
|
||||
end
|
||||
|
||||
if a.file ~= b.file then
|
||||
return a.file < b.file
|
||||
end
|
||||
|
||||
return export.positionOf(a.start) < export.positionOf(b.start)
|
||||
end
|
||||
|
||||
|
||||
--- recursively generate documentation all parser objects downstream of `source`
|
||||
---@async
|
||||
---@param source parser.object | vm.global
|
||||
---@param has_seen table? keeps track of visited nodes in documentation tree
|
||||
---@return docUnion | [docUnion] | string | number | boolean | nil
|
||||
function export.documentObject(source, has_seen)
|
||||
--is this a primative type? then we dont need to process it.
|
||||
if type(source) ~= 'table' then return source end
|
||||
|
||||
--set up/check recursion
|
||||
if not has_seen then has_seen = {} end
|
||||
if has_seen[source] then
|
||||
return nil
|
||||
end
|
||||
has_seen[source] = true
|
||||
|
||||
--is this an array type? then process each array item and collect it
|
||||
if (#source > 0 and next(source, #source) == nil) then
|
||||
local objs = {} --make a pure numerical array
|
||||
for i, child in ipairs(source) do
|
||||
objs[i] = export.documentObject(child, has_seen)
|
||||
end
|
||||
return objs
|
||||
end
|
||||
|
||||
--if neither, then this is a singular docUnion
|
||||
local obj = export.makeDocObject['INIT'](source, has_seen)
|
||||
|
||||
--check if this source has a type (no type sources are usually autogen'd anon functions's return values that are not explicitly stated)
|
||||
if not obj.type then return obj end
|
||||
|
||||
local res = export.makeDocObject[obj.type](source, obj, has_seen)
|
||||
if res == false then
|
||||
return nil
|
||||
end
|
||||
return res or obj
|
||||
end
|
||||
|
||||
---Switch statement table. functions can be overriden by user file.
|
||||
---@table
|
||||
export.makeDocObject = setmetatable({}, {__index = function(t, k)
|
||||
return function()
|
||||
--print('DocError: no type "'..k..'"')
|
||||
end
|
||||
end})
|
||||
|
||||
export.makeDocObject['INIT'] = function(source, has_seen)
|
||||
---@as docUnion
|
||||
local ok, desc = pcall(getDesc, source)
|
||||
local rawok, rawdesc = pcall(getDesc, source, true)
|
||||
return {
|
||||
type = source.cate or source.type,
|
||||
name = export.documentObject((source.getCodeName and source:getCodeName()) or source.name, has_seen),
|
||||
start = source.start and {guide.rowColOf(source.start)},
|
||||
finish = source.finish and {guide.rowColOf(source.finish)},
|
||||
types = export.documentObject(source.types, has_seen),
|
||||
view = vm.getInfer(source):view(ws.rootUri),
|
||||
desc = ok and desc or nil,
|
||||
rawdesc = rawok and rawdesc or nil,
|
||||
}
|
||||
end
|
||||
|
||||
export.makeDocObject['doc.alias'] = function(source, obj, has_seen)
|
||||
|
||||
end
|
||||
|
||||
export.makeDocObject['doc.field'] = function(source, obj, has_seen)
|
||||
if source.field.type == 'doc.field.name' then
|
||||
obj.name = source.field[1]
|
||||
else
|
||||
obj.name = ('[%s]'):format(vm.getInfer(source.field):view(ws.rootUri))
|
||||
end
|
||||
obj.file = export.getLocalPath(guide.getUri(source))
|
||||
obj.extends = source.extends and export.documentObject(source.extends, has_seen) --check if bug?
|
||||
obj.async = vm.isAsync(source, true) and true or false --if vm.isAsync(set, true) then result.defines[#result.defines].extends['async'] = true end
|
||||
obj.deprecated = vm.getDeprecated(source) and true or false -- if (depr and not depr.versions) the result.defines[#result.defines].extends['deprecated'] = true end
|
||||
obj.visible = vm.getVisibleType(source)
|
||||
end
|
||||
|
||||
export.makeDocObject['doc.class'] = function(source, obj, has_seen)
|
||||
local extends = source.extends or source.value --doc.class or other
|
||||
local field = source.field or source.method
|
||||
obj.name = type(field) == 'table' and field[1] or nil
|
||||
obj.file = export.getLocalPath(guide.getUri(source))
|
||||
obj.extends = extends and export.documentObject(extends, has_seen)
|
||||
obj.async = vm.isAsync(source, true) and true or false
|
||||
obj.deprecated = vm.getDeprecated(source) and true or false
|
||||
obj.visible = vm.getVisibleType(source)
|
||||
end
|
||||
|
||||
export.makeDocObject['doc.field.name'] = function(source, obj, has_seen)
|
||||
obj['[1]'] = export.documentObject(source[1], has_seen)
|
||||
obj.view = source[1]
|
||||
end
|
||||
|
||||
export.makeDocObject['doc.type.arg.name'] = export.makeDocObject['doc.field.name']
|
||||
|
||||
export.makeDocObject['doc.type.function'] = function(source, obj, has_seen)
|
||||
obj.args = export.documentObject(source.args, has_seen)
|
||||
obj.returns = export.documentObject(source.returns, has_seen)
|
||||
end
|
||||
|
||||
export.makeDocObject['doc.type.table'] = function(source, obj, has_seen)
|
||||
obj.fields = export.documentObject(source.fields, has_seen)
|
||||
end
|
||||
|
||||
export.makeDocObject['funcargs'] = function(source, obj, has_seen)
|
||||
local objs = {} --make a pure numerical array
|
||||
for i, child in ipairs(source) do
|
||||
objs[i] = export.documentObject(child, has_seen)
|
||||
end
|
||||
return objs
|
||||
end
|
||||
|
||||
export.makeDocObject['function'] = function(source, obj, has_seen)
|
||||
obj.args = export.documentObject(source.args, has_seen)
|
||||
obj.view = getLabel(source, source.parent.type == 'setmethod', 1)
|
||||
local _, _, max = vm.countReturnsOfFunction(source)
|
||||
if max > 0 then obj.returns = {} end
|
||||
for i = 1, max do
|
||||
obj.returns[i] = export.documentObject(vm.getReturnOfFunction(source, i), has_seen) --check if bug?
|
||||
end
|
||||
end
|
||||
|
||||
export.makeDocObject['function.return'] = function(source, obj, has_seen)
|
||||
obj.desc = source.comment and getDesc(source.comment)
|
||||
obj.rawdesc = source.comment and getDesc(source.comment, true)
|
||||
end
|
||||
|
||||
export.makeDocObject['local'] = function(source, obj, has_seen)
|
||||
obj.name = source[1]
|
||||
end
|
||||
|
||||
export.makeDocObject['self'] = export.makeDocObject['local']
|
||||
|
||||
export.makeDocObject['setfield'] = export.makeDocObject['doc.class']
|
||||
|
||||
export.makeDocObject['setglobal'] = export.makeDocObject['doc.class']
|
||||
|
||||
export.makeDocObject['setindex'] = export.makeDocObject['doc.class']
|
||||
|
||||
export.makeDocObject['setmethod'] = export.makeDocObject['doc.class']
|
||||
|
||||
export.makeDocObject['tableindex'] = function(source, obj, has_seen)
|
||||
obj.name = source.index[1]
|
||||
end
|
||||
|
||||
export.makeDocObject['type'] = function(source, obj, has_seen)
|
||||
if export.makeDocObject['variable'](source, obj, has_seen) == false then
|
||||
return false
|
||||
end
|
||||
obj.fields = {}
|
||||
vm.getClassFields(ws.rootUri, source, vm.ANY, function (next_source, mark)
|
||||
if next_source.type == 'doc.field'
|
||||
or next_source.type == 'setfield'
|
||||
or next_source.type == 'setmethod'
|
||||
or next_source.type == 'tableindex'
|
||||
then
|
||||
table.insert(obj.fields, export.documentObject(next_source, has_seen))
|
||||
end
|
||||
end)
|
||||
table.sort(obj.fields, export.sortDoc)
|
||||
end
|
||||
|
||||
export.makeDocObject['variable'] = function(source, obj, has_seen)
|
||||
obj.defines = {}
|
||||
for _, set in ipairs(source:getSets(ws.rootUri)) do
|
||||
if set.type == 'setglobal'
|
||||
or set.type == 'setfield'
|
||||
or set.type == 'setmethod'
|
||||
or set.type == 'setindex'
|
||||
or set.type == 'doc.alias'
|
||||
or set.type == 'doc.class'
|
||||
then
|
||||
table.insert(obj.defines, export.documentObject(set, has_seen))
|
||||
end
|
||||
end
|
||||
if #obj.defines == 0 then return false end
|
||||
table.sort(obj.defines, export.sortDoc)
|
||||
end
|
||||
|
||||
---gathers the globals that are to be exported in documentation
|
||||
---@async
|
||||
---@return table globals
|
||||
function export.gatherGlobals()
|
||||
local all_globals = vm.getAllGlobals()
|
||||
local globals = {}
|
||||
for _, g in pairs(all_globals) do
|
||||
table.insert(globals, g)
|
||||
end
|
||||
return globals
|
||||
end
|
||||
|
||||
---builds a lua table of based on `globals` and their elements
|
||||
---@async
|
||||
---@param globals table
|
||||
---@param callback fun(i, max)
|
||||
function export.makeDocs(globals, callback)
|
||||
local docs = {}
|
||||
for i, global in ipairs(globals) do
|
||||
table.insert(docs, export.documentObject(global))
|
||||
callback(i, #globals)
|
||||
end
|
||||
docs[#docs+1] = export.getLualsConfig()
|
||||
table.sort(docs, export.sortDoc)
|
||||
return docs
|
||||
end
|
||||
|
||||
function export.getLualsConfig()
|
||||
return {
|
||||
name = 'LuaLS',
|
||||
type = 'luals.config',
|
||||
DOC = fs.canonical(fs.path(DOC)):string(),
|
||||
defines = {},
|
||||
fields = {}
|
||||
}
|
||||
end
|
||||
|
||||
---takes the table from `makeDocs`, serializes it, and exports it
|
||||
---@async
|
||||
---@param docs table
|
||||
---@param outputDir string
|
||||
---@return boolean ok, string[] outputPaths, (string|nil)[]? errs
|
||||
function export.serializeAndExport(docs, outputDir)
|
||||
local jsonPath = outputDir .. '/doc.json'
|
||||
local mdPath = outputDir .. '/doc.md'
|
||||
|
||||
--export to json
|
||||
local old_jsonb_supportSparseArray = jsonb.supportSparseArray
|
||||
jsonb.supportSparseArray = true
|
||||
local jsonOk, jsonErr = util.saveFile(jsonPath, jsonb.beautify(docs))
|
||||
jsonb.supportSparseArray = old_jsonb_supportSparseArray
|
||||
|
||||
|
||||
--export to markdown
|
||||
local md = markdown()
|
||||
for _, class in ipairs(docs) do
|
||||
md:add('md', '# ' .. class.name)
|
||||
md:emptyLine()
|
||||
md:add('md', class.desc)
|
||||
md:emptyLine()
|
||||
if class.defines then
|
||||
for _, define in ipairs(class.defines) do
|
||||
if define.extends then
|
||||
md:add('lua', define.extends.view)
|
||||
md:emptyLine()
|
||||
end
|
||||
end
|
||||
end
|
||||
if class.fields then
|
||||
local mark = {}
|
||||
for _, field in ipairs(class.fields) do
|
||||
if not mark[field.name] then
|
||||
mark[field.name] = true
|
||||
md:add('md', '## ' .. field.name)
|
||||
md:emptyLine()
|
||||
md:add('lua', field.extends.view)
|
||||
md:emptyLine()
|
||||
md:add('md', field.desc)
|
||||
md:emptyLine()
|
||||
end
|
||||
end
|
||||
end
|
||||
md:splitLine()
|
||||
end
|
||||
local mdOk, mdErr = util.saveFile(mdPath, md:string())
|
||||
|
||||
--error checking save file
|
||||
if( not (jsonOk and mdOk) ) then
|
||||
return false, {jsonPath, mdPath}, {jsonErr, mdErr}
|
||||
end
|
||||
|
||||
return true, {jsonPath, mdPath}
|
||||
end
|
||||
|
||||
return export
|
||||
@@ -0,0 +1,258 @@
|
||||
local lclient = require 'lclient'
|
||||
local furi = require 'file-uri'
|
||||
local ws = require 'workspace'
|
||||
local files = require 'files'
|
||||
local util = require 'utility'
|
||||
local lang = require 'language'
|
||||
local config = require 'config.config'
|
||||
local await = require 'await'
|
||||
local progress = require 'progress'
|
||||
local fs = require 'bee.filesystem'
|
||||
|
||||
local doc = {}
|
||||
|
||||
---Find file 'doc.json'.
|
||||
---@return fs.path
|
||||
local function findDocJson()
|
||||
local doc_json_path
|
||||
if type(DOC_UPDATE) == 'string' then
|
||||
doc_json_path = fs.canonical(fs.path(DOC_UPDATE)) .. '/doc.json'
|
||||
else
|
||||
doc_json_path = fs.current_path() .. '/doc.json'
|
||||
end
|
||||
if fs.exists(doc_json_path) then
|
||||
return doc_json_path
|
||||
else
|
||||
error(string.format('Error: File "%s" not found.', doc_json_path))
|
||||
end
|
||||
end
|
||||
|
||||
---@return string # path of 'doc.json'
|
||||
---@return string # path to be documented
|
||||
local function getPathDocUpdate()
|
||||
local doc_json_path = findDocJson()
|
||||
local ok, doc_path = pcall(
|
||||
function ()
|
||||
local json = require('json')
|
||||
local json_file = io.open(doc_json_path:string(), 'r'):read('*all')
|
||||
local json_data = json.decode(json_file)
|
||||
for _, section in ipairs(json_data) do
|
||||
if section.type == 'luals.config' then
|
||||
return section.DOC
|
||||
end
|
||||
end
|
||||
end)
|
||||
if ok then
|
||||
local doc_json_dir = doc_json_path:string():gsub('/doc.json', '')
|
||||
return doc_json_dir, doc_path
|
||||
else
|
||||
error(string.format('Error: Cannot update "%s".', doc_json_path))
|
||||
end
|
||||
end
|
||||
|
||||
---clones a module and assigns any internal upvalues pointing to the module to the new clone
|
||||
---useful for sandboxing
|
||||
---@param tbl any module to be cloned
|
||||
---@return any module_clone the cloned module
|
||||
local function reinstantiateModule(tbl, _new_module, _old_module, _has_seen)
|
||||
_old_module = _old_module or tbl --remember old module only at root
|
||||
_has_seen = _has_seen or {} --remember visited indecies
|
||||
if(type(tbl) == 'table') then
|
||||
if _has_seen[tbl] then return _has_seen[tbl] end
|
||||
local clone = {}
|
||||
_has_seen[tbl] = true
|
||||
for key, value in pairs(tbl) do
|
||||
clone[key] = reinstantiateModule(value, _new_module or clone, _old_module, _has_seen)
|
||||
end
|
||||
setmetatable(clone, getmetatable(tbl))
|
||||
return clone
|
||||
elseif(type(tbl) == 'function') then
|
||||
local func = tbl
|
||||
if _has_seen[func] then return _has_seen[func] end --copy function pointers instead of building clones
|
||||
local upvalues = {}
|
||||
local i = 1
|
||||
while true do
|
||||
local label, value = debug.getupvalue(func, i)
|
||||
if not value then break end
|
||||
upvalues[i] = value == _old_module and _new_module or value
|
||||
i = i + 1
|
||||
end
|
||||
local new_func = load(string.dump(func))--, 'function@reinstantiateModule()', 'b', _ENV)
|
||||
assert(new_func, 'could not load dumped function')
|
||||
for index, upvalue in ipairs(upvalues) do
|
||||
debug.setupvalue(new_func, index, upvalue)
|
||||
end
|
||||
_has_seen[func] = new_func
|
||||
return new_func
|
||||
else
|
||||
return tbl
|
||||
end
|
||||
end
|
||||
|
||||
--these modules need to be loaded by the time this function is created
|
||||
--im leaving them here since this is a pretty strange function that might get moved somewhere else later
|
||||
--so make sure to bring these with you!
|
||||
require 'workspace'
|
||||
require 'vm'
|
||||
require 'parser.guide'
|
||||
require 'core.hover.description'
|
||||
require 'core.hover.label'
|
||||
require 'json-beautify'
|
||||
require 'utility'
|
||||
require 'provider.markdown'
|
||||
|
||||
---Gets config file's doc gen overrides.
|
||||
---@return table dirty_module clone of the export module modified by user buildscript
|
||||
local function injectBuildScript()
|
||||
local sub_path = config.get(ws.rootUri, 'Lua.docScriptPath')
|
||||
local module = reinstantiateModule( ( require 'cli.doc.export' ) )
|
||||
--if default, then no build script modifications
|
||||
if sub_path == '' then
|
||||
return module
|
||||
end
|
||||
local resolved_path = fs.absolute(fs.path(DOC)):string() .. sub_path
|
||||
local f <close> = io.open(resolved_path, 'r')
|
||||
if not f then
|
||||
error('could not open config file at '..tostring(resolved_path))
|
||||
end
|
||||
--include all `require`s in script.cli.doc.export in enviroment
|
||||
--NOTE: allows access to the global enviroment!
|
||||
local data, err = loadfile(resolved_path, 't', setmetatable({
|
||||
export = module,
|
||||
|
||||
ws = require 'workspace',
|
||||
vm = require 'vm',
|
||||
guide = require 'parser.guide',
|
||||
getDesc = require 'core.hover.description',
|
||||
getLabel = require 'core.hover.label',
|
||||
jsonb = require 'json-beautify',
|
||||
util = require 'utility',
|
||||
markdown = require 'provider.markdown'
|
||||
},
|
||||
{__index = _G}))
|
||||
if err or not data then
|
||||
error(err, 0)
|
||||
end
|
||||
data()
|
||||
return module
|
||||
end
|
||||
|
||||
---runtime call for documentation exporting
|
||||
---@async
|
||||
---@param outputPath string
|
||||
function doc.makeDoc(outputPath)
|
||||
ws.awaitReady(ws.rootUri)
|
||||
|
||||
local expandAlias = config.get(ws.rootUri, 'Lua.hover.expandAlias')
|
||||
config.set(ws.rootUri, 'Lua.hover.expandAlias', false)
|
||||
local _ <close> = function ()
|
||||
config.set(ws.rootUri, 'Lua.hover.expandAlias', expandAlias)
|
||||
end
|
||||
|
||||
await.sleep(0.1)
|
||||
|
||||
-- ready --
|
||||
|
||||
local prog <close> = progress.create(ws.rootUri, lang.script('CLI_DOC_WORKING'), 0)
|
||||
|
||||
local dirty_export = injectBuildScript()
|
||||
|
||||
local globals = dirty_export.gatherGlobals()
|
||||
|
||||
local docs = dirty_export.makeDocs(globals, function (i, max)
|
||||
prog:setMessage(('%d/%d'):format(i, max))
|
||||
prog:setPercentage((i) / max * 100)
|
||||
end)
|
||||
|
||||
local ok, outPaths, err = dirty_export.serializeAndExport(docs, outputPath)
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
|
||||
return table.unpack(outPaths)
|
||||
end
|
||||
|
||||
---CLI call for documentation (parameter '--DOC=...' is passed to server)
|
||||
function doc.runCLI()
|
||||
lang(LOCALE)
|
||||
|
||||
if DOC_UPDATE then
|
||||
DOC_OUT_PATH, DOC = getPathDocUpdate()
|
||||
end
|
||||
|
||||
if type(DOC) ~= 'string' then
|
||||
print(lang.script('CLI_CHECK_ERROR_TYPE', type(DOC)))
|
||||
return
|
||||
end
|
||||
|
||||
local rootUri = furi.encode(fs.canonical(fs.path(DOC)):string())
|
||||
if not rootUri then
|
||||
print(lang.script('CLI_CHECK_ERROR_URI', DOC))
|
||||
return
|
||||
end
|
||||
|
||||
print('root uri = ' .. rootUri)
|
||||
|
||||
--- If '--configpath' is specified, get the folder path of the '.luarc.doc.json' configuration file (without the file name)
|
||||
--- 如果指定了'--configpath',则获取`.luarc.doc.json` 配置文件的文件夹路径(不包含文件名)
|
||||
--- This option is passed into the callback function of the initialized method in provide.
|
||||
--- 该选项会被传入到`provide`中的`initialized`方法的回调函数中
|
||||
local luarcParentUri
|
||||
if CONFIGPATH then
|
||||
luarcParentUri = furi.encode(fs.absolute(fs.path(CONFIGPATH)):parent_path():string())
|
||||
end
|
||||
|
||||
util.enableCloseFunction()
|
||||
|
||||
local lastClock = os.clock()
|
||||
|
||||
---@async
|
||||
lclient():start(function (client)
|
||||
client:registerFakers()
|
||||
|
||||
client:initialize {
|
||||
rootUri = rootUri,
|
||||
luarcParentUri = luarcParentUri,
|
||||
}
|
||||
io.write(lang.script('CLI_DOC_INITING'))
|
||||
|
||||
config.set(nil, 'Lua.diagnostics.enable', false)
|
||||
config.set(nil, 'Lua.hover.expandAlias', false)
|
||||
|
||||
ws.awaitReady(rootUri)
|
||||
await.sleep(0.1)
|
||||
|
||||
--ready--
|
||||
|
||||
local dirty_export = injectBuildScript()
|
||||
|
||||
local globals = dirty_export.gatherGlobals()
|
||||
|
||||
local docs = dirty_export.makeDocs(globals, function (i, max)
|
||||
if os.clock() - lastClock > 0.2 then
|
||||
lastClock = os.clock()
|
||||
local output = '\x0D'
|
||||
.. ('>'):rep(math.ceil(i / max * 20))
|
||||
.. ('='):rep(20 - math.ceil(i / max * 20))
|
||||
.. ' '
|
||||
.. ('0'):rep(#tostring(max) - #tostring(i))
|
||||
.. tostring(i) .. '/' .. tostring(max)
|
||||
io.write(output)
|
||||
end
|
||||
end)
|
||||
io.write('\x0D')
|
||||
|
||||
if not DOC_OUT_PATH then
|
||||
DOC_OUT_PATH = fs.current_path():string()
|
||||
end
|
||||
|
||||
local ok, outPaths, err = dirty_export.serializeAndExport(docs, DOC_OUT_PATH)
|
||||
print(lang.script('CLI_DOC_DONE'))
|
||||
for i, path in ipairs(outPaths) do
|
||||
local this_err = (type(err) == 'table') and err[i] or nil
|
||||
print(this_err or files.normalize(path))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return doc
|
||||
@@ -0,0 +1,170 @@
|
||||
local util = require 'utility'
|
||||
|
||||
--- @class cli.arg
|
||||
--- @field type? string|string[]
|
||||
--- @field description string Description of the argument in markdown format.
|
||||
--- @field example? string
|
||||
--- @field default? any
|
||||
|
||||
--- @type table<string, cli.arg>
|
||||
local args = {
|
||||
['--help'] = {
|
||||
description = [[
|
||||
Print this message.
|
||||
]],
|
||||
},
|
||||
['--check'] = {
|
||||
type = 'string',
|
||||
description = [[
|
||||
Perform a "diagnosis report" where the results of the diagnosis are written to the logpath.
|
||||
]],
|
||||
example = [[--check=C:\Users\Me\path\to\workspace]]
|
||||
},
|
||||
['--checklevel'] = {
|
||||
type = 'string',
|
||||
description = [[
|
||||
To be used with --check. The minimum level of diagnostic that should be logged.
|
||||
Items with lower priority than the one listed here will not be written to the file.
|
||||
Options include, in order of priority:
|
||||
|
||||
- Error
|
||||
- Warning
|
||||
- Information
|
||||
- Hint
|
||||
]],
|
||||
default = 'Warning',
|
||||
example = [[--checklevel=Information]]
|
||||
},
|
||||
['--check_format'] = {
|
||||
type = { 'json', 'pretty' },
|
||||
description = [[
|
||||
Output format for the check results.
|
||||
- 'pretty': results are displayed to stdout in a human-readable format.
|
||||
- 'json': results are written to a file in JSON format. See --check_out_path
|
||||
]],
|
||||
default = 'pretty'
|
||||
},
|
||||
['--version'] = {
|
||||
type = 'boolean',
|
||||
description = [[
|
||||
Get the version of the Lua language server.
|
||||
This will print it to the command line and immediately exit.
|
||||
]],
|
||||
},
|
||||
['--doc'] = {
|
||||
type = 'string',
|
||||
description = [[
|
||||
Generate documentation from a workspace.
|
||||
The files will be written to the documentation output path passed
|
||||
in --doc_out_path.
|
||||
]],
|
||||
example = [[--doc=C:/Users/Me/Documents/myLuaProject/]]
|
||||
},
|
||||
['--doc_out_path'] = {
|
||||
type = 'string',
|
||||
description = [[
|
||||
The path to output generated documentation at.
|
||||
If --doc_out_path is missing, the documentation will be written
|
||||
to the current directory.
|
||||
See --doc for more info.
|
||||
]],
|
||||
example = [[--doc_out_path=C:/Users/Me/Documents/myLuaProjectDocumentation]]
|
||||
},
|
||||
['--doc_update'] = {
|
||||
type = 'string',
|
||||
description = [[
|
||||
Update existing documentation files at the given path.
|
||||
]]
|
||||
},
|
||||
['--logpath'] = {
|
||||
type = 'string',
|
||||
description = [[
|
||||
Where the log should be written to.
|
||||
]],
|
||||
default = './log',
|
||||
example = [[--logpath=D:/luaServer/logs]]
|
||||
},
|
||||
['--loglevel'] = {
|
||||
type = 'string',
|
||||
description = [[
|
||||
The minimum level of logging that should appear in the logfile.
|
||||
Can be used to log more detailed info for debugging and error reporting.
|
||||
|
||||
Options:
|
||||
|
||||
- error
|
||||
- warn
|
||||
- info
|
||||
- debug
|
||||
- trace
|
||||
]],
|
||||
example = [[--loglevel=trace]]
|
||||
},
|
||||
['--metapath'] = {
|
||||
type = 'string',
|
||||
description = [[
|
||||
Where the standard Lua library definition files should be generated to.
|
||||
]],
|
||||
default = './meta',
|
||||
example = [[--metapath=D:/sumnekoLua/metaDefintions]]
|
||||
},
|
||||
['--locale'] = {
|
||||
type = 'string',
|
||||
description = [[
|
||||
The language to use. Defaults to en-us.
|
||||
Options can be found in locale/ .
|
||||
]],
|
||||
example = [[--locale=zh-cn]]
|
||||
},
|
||||
['--configpath'] = {
|
||||
type = 'string',
|
||||
description = [[
|
||||
The location of the configuration file that will be loaded.
|
||||
Can be relative to the workspace.
|
||||
When provided, config files from elsewhere (such as from VS Code) will no longer be loaded.
|
||||
]],
|
||||
example = [[--configpath=sumnekoLuaConfig.lua]]
|
||||
},
|
||||
['--force-accept-workspace'] = {
|
||||
type = 'boolean',
|
||||
description = [[
|
||||
Allows the use of root/home directory as the workspace.
|
||||
]]
|
||||
},
|
||||
['--socket'] = {
|
||||
type = 'number',
|
||||
description = [[
|
||||
Will communicate to a client over the specified TCP port instead of through stdio.
|
||||
]],
|
||||
example = [[--socket=5050]]
|
||||
},
|
||||
['--develop'] = {
|
||||
type = 'boolean',
|
||||
description = [[
|
||||
Enables development mode. This allows plugins to write to the logpath.
|
||||
]]
|
||||
}
|
||||
}
|
||||
|
||||
for nm, attrs in util.sortPairs(args) do
|
||||
if attrs.type == 'boolean' then
|
||||
print(nm)
|
||||
else
|
||||
print(nm .. "=<value>")
|
||||
end
|
||||
if attrs.description then
|
||||
local normalized_description = attrs.description:gsub("^%s+", ""):gsub("\n%s+", "\n"):gsub("%s+$", "")
|
||||
print("\n " .. normalized_description:gsub('\n', '\n '))
|
||||
end
|
||||
local attr_type = attrs.type
|
||||
if type(attr_type) == "table" then
|
||||
print("\n Values: " .. table.concat(attr_type, ', '))
|
||||
end
|
||||
if attrs.default then
|
||||
print("\n Default: " .. tostring(attrs.default))
|
||||
end
|
||||
if attrs.example then
|
||||
print("\n Example: " .. attrs.example)
|
||||
end
|
||||
print()
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
if _G['HELP'] then
|
||||
require 'cli.help'
|
||||
os.exit(0, true)
|
||||
end
|
||||
|
||||
if _G['VERSION'] then
|
||||
require 'cli.version'
|
||||
os.exit(0, true)
|
||||
end
|
||||
|
||||
if _G['CHECK'] then
|
||||
local ret = require 'cli.check'.runCLI()
|
||||
os.exit(ret, true)
|
||||
end
|
||||
|
||||
if _G['CHECK_WORKER'] then
|
||||
local ret = require 'cli.check_worker'.runCLI()
|
||||
os.exit(ret or 0, true)
|
||||
end
|
||||
|
||||
if _G['DOC_UPDATE'] then
|
||||
require 'cli.doc' .runCLI()
|
||||
os.exit(0, true)
|
||||
end
|
||||
|
||||
if _G['DOC'] then
|
||||
require 'cli.doc' .runCLI()
|
||||
os.exit(0, true)
|
||||
end
|
||||
|
||||
if _G['VISUALIZE'] then
|
||||
local ret = require 'cli.visualize' .runCLI()
|
||||
os.exit(ret or 0, true)
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
local version = require 'version'
|
||||
print(version.getVersion())
|
||||
@@ -0,0 +1,103 @@
|
||||
local lang = require 'language'
|
||||
local parser = require 'parser'
|
||||
local guide = require 'parser.guide'
|
||||
|
||||
local function nodeId(node)
|
||||
return node.type .. ':' .. node.start .. ':' .. node.finish
|
||||
end
|
||||
|
||||
local function shorten(str)
|
||||
if type(str) ~= 'string' then
|
||||
return str
|
||||
end
|
||||
str = str:gsub('\n', '\\\\n')
|
||||
if #str <= 20 then
|
||||
return str
|
||||
else
|
||||
return str:sub(1, 17) .. '...'
|
||||
end
|
||||
end
|
||||
|
||||
local function getTooltipLine(k, v)
|
||||
if type(v) == 'table' then
|
||||
if v.type then
|
||||
v = '<node ' .. v.type .. '>'
|
||||
else
|
||||
v = '<table>'
|
||||
end
|
||||
end
|
||||
v = tostring(v)
|
||||
v = v:gsub('"', '\\"')
|
||||
return k .. ': ' .. shorten(v) .. '\\n'
|
||||
end
|
||||
|
||||
local function getTooltip(node)
|
||||
local str = ''
|
||||
local skipNodes = {parent = true, start = true, finish = true, type = true}
|
||||
str = str .. getTooltipLine('start', node.start)
|
||||
str = str .. getTooltipLine('finish', node.finish)
|
||||
for k, v in pairs(node) do
|
||||
if type(k) ~= 'number' and not skipNodes[k] then
|
||||
str = str .. getTooltipLine(k, v)
|
||||
end
|
||||
end
|
||||
for i = 1, math.min(#node, 15) do
|
||||
str = str .. getTooltipLine(i, node[i])
|
||||
end
|
||||
if #node > 15 then
|
||||
str = str .. getTooltipLine('15..' .. #node, '(...)')
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
local nodeEntry = '\t"%s" [\n\t\tlabel="%s\\l%s\\l"\n\t\ttooltip="%s"\n\t]'
|
||||
local function getNodeLabel(node)
|
||||
local keyName = guide.getKeyName(node)
|
||||
if node.type == 'binary' or node.type == 'unary' then
|
||||
keyName = node.op.type
|
||||
elseif node.type == 'label' or node.type == 'goto' then
|
||||
keyName = node[1]
|
||||
end
|
||||
return nodeEntry:format(nodeId(node), node.type, shorten(keyName) or '', getTooltip(node))
|
||||
end
|
||||
|
||||
local function getVisualizeVisitor(writer)
|
||||
local function visitNode(node, parent)
|
||||
if node == nil then return end
|
||||
writer:write(getNodeLabel(node))
|
||||
writer:write('\n')
|
||||
if parent then
|
||||
writer:write(('\t"%s" -> "%s"'):format(nodeId(parent), nodeId(node)))
|
||||
writer:write('\n')
|
||||
end
|
||||
guide.eachChild(node, function(child)
|
||||
visitNode(child, node)
|
||||
end)
|
||||
end
|
||||
return visitNode
|
||||
end
|
||||
|
||||
|
||||
local export = {}
|
||||
|
||||
function export.visualizeAst(code, writer)
|
||||
local state = parser.compile(code, 'Lua', _G['LUA_VER'] or 'Lua 5.4')
|
||||
writer:write('digraph AST {\n')
|
||||
writer:write('\tnode [shape = rect]\n')
|
||||
getVisualizeVisitor(writer)(state.ast)
|
||||
writer:write('}\n')
|
||||
end
|
||||
|
||||
function export.runCLI()
|
||||
lang(LOCALE)
|
||||
local file = _G['VISUALIZE']
|
||||
local code, err = io.open(file)
|
||||
if not code then
|
||||
io.stderr:write('failed to open ' .. file .. ': ' .. err)
|
||||
return 1
|
||||
end
|
||||
code = code:read('a')
|
||||
return export.visualizeAst(code, io.stdout)
|
||||
end
|
||||
|
||||
return export
|
||||
Reference in New Issue
Block a user