feat(linter): add json parsing
This commit is contained in:
+175
-4
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user