feat(linter): add json parsing

This commit is contained in:
2025-03-28 17:46:51 +01:00
parent 5a77a45f6a
commit a92b2ef503
+175 -4
View File
@@ -19,18 +19,143 @@ M.__index = M
---| "source" ---| "source"
---| "code" ---| "code"
---@class JsonConfig
---@field diagnostics_root? string
---@field lnum? string
---@field end_lnum? string
---@field col? string
---@field end_col? string
---@field severity? string
---@field message? string
---@field source? string
---@field code? string
---@field zero_idx? boolean
---@field callback? fun(diag: vim.Diagnostic)
---@class LinterConfig ---@class LinterConfig
---@field cmd string[] ---@field cmd string[]
---@field stdin? boolean ---@field stdin? boolean
---@field stdout? boolean ---@field stdout? boolean
---@field stderr? boolean ---@field stderr? boolean
---@field pattern string ---@field pattern? string
---@field groups Group[] ---@field groups? Group[]
---@field severity_map table<string, vim.diagnostic.Severity> ---@field severity_map? table<string, vim.diagnostic.Severity>
---@field source? string ---@field source? string
---@field debounce? number ---@field debounce? number
---@field json? JsonConfig
M.config = {} M.config = {}
-- Extract a value from a JSON object using a path
---@param obj table The JSON object
---@param path string Path to the value (dot notation string)
---@return any The value at the specified path, or nil if not found
function M.get_json_value(obj, path)
if not obj then
return nil
end
local current = obj
local parts = {}
for part in path:gmatch("[^%.]+") do
table.insert(parts, part)
end
for _, key in ipairs(parts) do
if tonumber(key) ~= nil then
key = tonumber(key)
end
if type(current) ~= "table" then
return nil
end
current = current[key]
if current == nil then
return nil
end
end
return current
end
function M:process_json_output(json, bufnr)
local diagnostics = {}
local items = json
if self.config.json.diagnostics_root then
items = M.get_json_value(json, self.config.json.diagnostics_root)
end
if type(items) ~= "table" then
utils.err("diagnostics root is not an array or object")
return
end
if not vim.islist(items) then
items = { items }
end
for _, item in ipairs(items) do
local diag = {}
for field, path in pairs(self.config.json) do
if field ~= "diagnostics_root" and field ~= "callback" then
diag[field] = M.get_json_value(item, path)
end
end
diag.source = diag.source or self.config.source
if
diag.severity
and self.config.severity_map
and self.config.severity_map[diag.severity]
then
diag.severity = self.config.severity_map[diag.severity]
end
if not self.config.json.zero_idx then
if diag.lnum then
diag.lnum = diag.lnum - 1
end
if diag.end_lnum then
diag.end_lnum = diag.end_lnum - 1
end
if diag.col then
diag.col = diag.col - 1
end
if diag.end_col then
diag.end_col = diag.end_col - 1
end
end
if diag.end_lnum and diag.end_col then
local lines = vim.api.nvim_buf_get_lines(
bufnr,
diag.end_lnum,
diag.end_lnum + 1,
false
)
if #lines > 0 and #lines[1] > 0 then
diag.end_col = math.min(diag.end_col, #lines[1] - 1)
end
end
if type(self.config.json.callback) == "function" then
self.config.json.callback(diag)
end
table.insert(diagnostics, diag)
end
return diagnostics
end
function M.validate(name, config) function M.validate(name, config)
local ok, resp = pcall(vim.validate, { local ok, resp = pcall(vim.validate, {
name = { name, "string" }, name = { name, "string" },
@@ -49,12 +174,13 @@ function M.validate(name, config)
stdin = { config.stdin, "boolean", true }, stdin = { config.stdin, "boolean", true },
stdout = { config.stdout, "boolean", true }, stdout = { config.stdout, "boolean", true },
stderr = { config.stderr, "boolean", true }, stderr = { config.stderr, "boolean", true },
pattern = { config.pattern, "string" }, pattern = { config.pattern, "string", true },
groups = { groups = {
config.groups, config.groups,
function(t) function(t)
return utils.is_list(t, "string") return utils.is_list(t, "string")
end, end,
true,
"list of strings", "list of strings",
}, },
severity_map = { severity_map = {
@@ -62,10 +188,12 @@ function M.validate(name, config)
function(t) function(t)
return utils.is_map(t, "string", "number") return utils.is_map(t, "string", "number")
end, end,
true,
"map of string and number", "map of string and number",
}, },
debounce = { config.debounce, "number", true }, debounce = { config.debounce, "number", true },
source = { config.source, "string", true }, source = { config.source, "string", true },
json = { config.json, "table", true },
}) })
end end
@@ -74,11 +202,17 @@ function M.validate(name, config)
return false return false
end end
if not config.json and (not config.pattern or not config.groups) then
utils.err("Either json or pattern and groups must be provided")
return false
end
return true return true
end end
function M:run(bufnr) function M:run(bufnr)
local input local input
-- TODO: add placeholder variables for when not using stdin -- TODO: add placeholder variables for when not using stdin
if self.config.stdin then if self.config.stdin then
input = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) input = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
@@ -88,13 +222,37 @@ function M:run(bufnr)
vim.system, vim.system,
self.config.cmd, self.config.cmd,
{ stdin = input }, { stdin = input },
---@param out vim.SystemCompleted
vim.schedule_wrap(function(out) vim.schedule_wrap(function(out)
local output local output
if self.config.stdout then if self.config.stdout then
output = out.stdout or "" output = out.stdout or ""
end end
if self.config.stderr then if self.config.stderr then
output = out.stderr or "" output = out.stderr or ""
elseif out.stderr and out.stderr ~= "" then
utils.err(out.stderr)
end
if self.config.json then
local ok, json = pcall(
vim.json.decode,
output,
{ luanil = { object = true, array = true } }
)
if not ok then
utils.err("Failed to parse JSON: " .. json)
return
end
local diagnostics = self:process_json_output(json, bufnr)
if diagnostics then
vim.diagnostic.set(self.namespace, bufnr, diagnostics)
end
return
end end
local output_lines = vim.fn.split(output, "\n", false) local output_lines = vim.fn.split(output, "\n", false)
@@ -107,11 +265,24 @@ function M:run(bufnr)
self.config.groups, self.config.groups,
self.config.severity_map self.config.severity_map
) )
if not ok then if not ok then
utils.err(tostring(resp)) utils.err(tostring(resp))
return return
elseif resp then elseif resp then
resp.source = resp.source or self.config.source resp.source = resp.source or self.config.source
local lines = vim.api.nvim_buf_get_lines(
bufnr,
resp.end_lnum,
resp.end_lnum + 1,
false
)
if #lines > 0 and #lines[1] > 0 then
resp.end_col = math.min(resp.end_col, #lines[1] - 1)
end
table.insert(diagnostics, resp) table.insert(diagnostics, resp)
end end
end end