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"
---| "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
---@field cmd string[]
---@field stdin? boolean
---@field stdout? boolean
---@field stderr? boolean
---@field pattern string
---@field groups Group[]
---@field severity_map table<string, vim.diagnostic.Severity>
---@field pattern? string
---@field groups? Group[]
---@field severity_map? table<string, vim.diagnostic.Severity>
---@field source? string
---@field debounce? number
---@field json? JsonConfig
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)
local ok, resp = pcall(vim.validate, {
name = { name, "string" },
@@ -49,12 +174,13 @@ function M.validate(name, config)
stdin = { config.stdin, "boolean", true },
stdout = { config.stdout, "boolean", true },
stderr = { config.stderr, "boolean", true },
pattern = { config.pattern, "string" },
pattern = { config.pattern, "string", true },
groups = {
config.groups,
function(t)
return utils.is_list(t, "string")
end,
true,
"list of strings",
},
severity_map = {
@@ -62,10 +188,12 @@ function M.validate(name, config)
function(t)
return utils.is_map(t, "string", "number")
end,
true,
"map of string and number",
},
debounce = { config.debounce, "number", true },
source = { config.source, "string", true },
json = { config.json, "table", true },
})
end
@@ -74,11 +202,17 @@ function M.validate(name, config)
return false
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
end
function M:run(bufnr)
local input
-- TODO: add placeholder variables for when not using stdin
if self.config.stdin then
input = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
@@ -88,13 +222,37 @@ function M:run(bufnr)
vim.system,
self.config.cmd,
{ stdin = input },
---@param out vim.SystemCompleted
vim.schedule_wrap(function(out)
local output
if self.config.stdout then
output = out.stdout or ""
end
if self.config.stderr then
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
local output_lines = vim.fn.split(output, "\n", false)
@@ -107,11 +265,24 @@ function M:run(bufnr)
self.config.groups,
self.config.severity_map
)
if not ok then
utils.err(tostring(resp))
return
elseif resp then
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)
end
end