diff --git a/changelog.md b/changelog.md index 10339536c..f19b377b5 100644 --- a/changelog.md +++ b/changelog.md @@ -31,6 +31,7 @@ cfgs[2] = {} -- only warns missing `b` ``` This enables the previous missing field check behavior before [#2970](https://github.com/LuaLS/lua-language-server/issues/2970) +* `NEW` Added `--check_format=json|pretty` for use with `--check` to output diagnostics in a human readable format. ## 3.13.5 `2024-12-20` diff --git a/locale/en-us/script.lua b/locale/en-us/script.lua index 9c9163ae4..ee96900c7 100644 --- a/locale/en-us/script.lua +++ b/locale/en-us/script.lua @@ -648,8 +648,10 @@ CLI_CHECK_SUCCESS = 'Diagnosis completed, no problems found' CLI_CHECK_PROGRESS = 'Found {} problems in {} files' -CLI_CHECK_RESULTS = +CLI_CHECK_RESULTS_OUTPATH = 'Diagnosis complete, {} problems found, see {}' +CLI_CHECK_RESULTS_PRETTY = +'Diagnosis complete, {} problems found' CLI_CHECK_MULTIPLE_WORKERS = 'Starting {} worker tasks, progress output will be disabled. This may take a few minutes.' CLI_DOC_INITING = diff --git a/locale/ja-jp/script.lua b/locale/ja-jp/script.lua index 3c0cd48d6..6e1819d98 100644 --- a/locale/ja-jp/script.lua +++ b/locale/ja-jp/script.lua @@ -649,8 +649,10 @@ CLI_CHECK_SUCCESS = '診断が完了しました。問題は見つかりませんでした' CLI_CHECK_PROGRESS = '{} ファイルに渡り、{} 個の問題が発見されました' -CLI_CHECK_RESULTS = +CLI_CHECK_RESULTS_OUTPATH = '診断が完了しました。{} 個の問題が発見されました。詳しくは {} をご確認ください' +CLI_CHECK_RESULTS_PRETTY = +'診断が完了しました。{} 個の問題が発見されました' CLI_CHECK_MULTIPLE_WORKERS = '{} 個のワーカータスクを開始しているため、進行状況の出力が無効になります。完了まで数分かかることがあります。' CLI_DOC_INITING = diff --git a/locale/pt-br/script.lua b/locale/pt-br/script.lua index e763fb6c4..73d459d75 100644 --- a/locale/pt-br/script.lua +++ b/locale/pt-br/script.lua @@ -648,8 +648,10 @@ CLI_CHECK_SUCCESS = 'Diagnóstico completo, nenhum problema encontrado' CLI_CHECK_PROGRESS = -- TODO: need translate! 'Found {} problems in {} files' -CLI_CHECK_RESULTS = +CLI_CHECK_RESULTS_OUTPATH = 'Diagnóstico completo, {} problemas encontrados, veja {}' +CLI_CHECK_RESULTS_PRETTY = +'Diagnóstico completo, {} problemas encontrados' CLI_CHECK_MULTIPLE_WORKERS = -- TODO: need translate! 'Starting {} worker tasks, progress output will be disabled. This may take a few minutes.' CLI_DOC_INITING = -- TODO: need translate! diff --git a/locale/zh-cn/script.lua b/locale/zh-cn/script.lua index 0b748d84f..22f16eaac 100644 --- a/locale/zh-cn/script.lua +++ b/locale/zh-cn/script.lua @@ -648,8 +648,10 @@ CLI_CHECK_SUCCESS = '诊断完成,没有发现问题' CLI_CHECK_PROGRESS = '检测到问题 {} 在文件 {} 中' -CLI_CHECK_RESULTS = +CLI_CHECK_RESULTS_OUTPATH = '诊断完成,共有 {} 个问题,请查看 {}' +CLI_CHECK_RESULTS_PRETTY = +'诊断完成,共有 {} 个问题' CLI_CHECK_MULTIPLE_WORKERS = '开启 {} 个工作任务,进度输出将会被禁用。这可能会花费几分钟。' CLI_DOC_INITING = diff --git a/locale/zh-tw/script.lua b/locale/zh-tw/script.lua index 31e885208..090fb7ff0 100644 --- a/locale/zh-tw/script.lua +++ b/locale/zh-tw/script.lua @@ -648,8 +648,10 @@ CLI_CHECK_SUCCESS = '診斷完成,沒有發現問題' CLI_CHECK_PROGRESS = -- TODO: need translate! 'Found {} problems in {} files' -CLI_CHECK_RESULTS = +CLI_CHECK_RESULTS_OUTPATH = '診斷完成,共有 {} 個問題,請查看 {}' +CLI_CHECK_RESULTS_PRETTY = +'診斷完成,共有 {} 個問題' CLI_CHECK_MULTIPLE_WORKERS = -- TODO: need translate! 'Starting {} worker tasks, progress output will be disabled. This may take a few minutes.' CLI_DOC_INITING = -- TODO: need translate! diff --git a/script/cli/check.lua b/script/cli/check.lua index c9a23b02c..7f38dfea4 100644 --- a/script/cli/check.lua +++ b/script/cli/check.lua @@ -7,6 +7,46 @@ 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) @@ -21,48 +61,13 @@ function export.runCLI() exe = exe..'.exe' end - local function logFileForThread(threadId) - return LOGPATH .. '/check-partial-' .. threadId .. '.json' - end - - local function buildArgs(threadId) - 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 - args[#args + 1] = '--quiet' - args[#args + 1] = '--check_out_path' - args[#args + 1] = logFileForThread(threadId) - end - return args - end - - if numThreads > 1 then + 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(i)}) + local process, err = subprocess.spawn({buildArgs(exe, numThreads, i, CHECK_FORMAT, QUIET)}) if err then print(err) end @@ -76,11 +81,6 @@ function export.runCLI() checkPassed = process:wait() == 0 and checkPassed end - local outpath = CHECK_OUT_PATH - if outpath == nil then - outpath = LOGPATH .. '/check.json' - end - if numThreads > 1 then local mergedResults = {} local count = 0 @@ -95,11 +95,22 @@ function export.runCLI() end end end - util.saveFile(outpath, jsonb.beautify(mergedResults)) - if count == 0 then - print(lang.script('CLI_CHECK_SUCCESS')) - else - print(lang.script('CLI_CHECK_RESULTS', count, outpath)) + + 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 diff --git a/script/cli/check_worker.lua b/script/cli/check_worker.lua index a2e0bff11..353926395 100644 --- a/script/cli/check_worker.lua +++ b/script/cli/check_worker.lua @@ -17,11 +17,170 @@ 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 +local severity_colors = { + Error = colors.red, + Warning = colors.yellow, + Information = colors.white, + Hint = colors.white, +} + +local severity_str = {} --- @type table +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 +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))) @@ -36,18 +195,16 @@ function export.runCLI() end rootUri = rootUri:gsub("/$", "") - if CHECKLEVEL then - if not define.DiagnosticSeverity[CHECKLEVEL] then - print(lang.script('CLI_CHECK_ERROR_LEVEL', 'Error, Warning, Information, Hint')) - return - end + 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 = {} + local results = {} --- @type table local function errorhandler(err) print(err) @@ -65,9 +222,13 @@ function export.runCLI() 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 + if not quiet then io.write(lang.script('CLI_CHECK_INITING')) end @@ -75,31 +236,12 @@ function export.runCLI() ws.awaitReady(rootUri) - local disables = util.arrayToHash(config.get(rootUri, 'Lua.diagnostics.disable')) - for name, serverity in pairs(define.DiagnosticDefaultSeverity) do - serverity = config.get(rootUri, 'Lua.diagnostics.severity')[name] or serverity - if serverity:sub(-1) == '!' then - serverity = serverity:sub(1, -2) - end - if define.DiagnosticSeverity[serverity] > checkLevel then - disables[name] = true - end - end - config.set(rootUri, 'Lua.diagnostics.disable', util.getTableKeys(disables, true)) + -- 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 - local diagStatus = config.get(rootUri, 'Lua.diagnostics.neededFileStatus') - for diag, status in pairs(diagStatus) do - if status == 'Any' or status == 'Any!' then - diagStatus[diag] = 'Opened!' - end - end - for diag, status in pairs(protoDiag.getDefaultStatus()) do - if status == 'Any' or status == 'Any!' then - diagStatus[diag] = 'Opened!' - end - end - config.set(rootUri, 'Lua.diagnostics.neededFileStatus', diagStatus) + -- 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 @@ -108,33 +250,17 @@ function export.runCLI() if (i % numThreads + 1) == threadId 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 (os.clock() - lastClock > 0.2 or i == #uris) and not QUIET then + -- 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() - 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) - local filesWithErrors = 0 - local errors = 0 - for _, diags in pairs(results) do - filesWithErrors = filesWithErrors + 1 - errors = errors + #diags - end - if errors > 0 then - local errorDetails = ' [' .. lang.script('CLI_CHECK_PROGRESS', errors, filesWithErrors) .. ']' - io.write(errorDetails) - end - io.flush() + report_progress(i, max, results) end end end - if not QUIET then - io.write('\x0D') + if not quiet then + clear_line() end end) @@ -146,18 +272,21 @@ function export.runCLI() end end - local outpath = CHECK_OUT_PATH - if outpath == nil then - outpath = LOGPATH .. '/check.json' + 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 - -- 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)) - if not QUIET then + 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', count, outpath)) + print(lang.script('CLI_CHECK_RESULTS_PRETTY', count)) end end return count == 0 and 0 or 1 diff --git a/script/global.d.lua b/script/global.d.lua index daac5f6c8..f4e4b336b 100644 --- a/script/global.d.lua +++ b/script/global.d.lua @@ -69,6 +69,9 @@ CHECKLEVEL = 'Warning' ---@type string|nil CHECK_OUT_PATH = '' +---@type string | 'json' | 'pretty' +CHECK_FORMAT = 'pretty' + ---@type 'trace' | 'debug' | 'info' | 'warn' | 'error' LOGLEVEL = 'warn'