fix(lsp): decode 'null' in server responses as vim.NIL #34849

Problem:
Previously, 'null' value in LSP responses were decoded as 'nil'.
This caused ambiguity for fields typed as '? | null' and led to
loss of explicit 'null' values, particularly in 'data' parameters.

Solution:
Decode all JSON 'null' values as 'vim.NIL' and adjust handling
where needed. This better aligns with the LSP specification,
where 'null' and absent fields are distinct, and 'null' should
not be used to represent missing values.

This also enables proper validation of response messages to
ensure that exactly one of 'result' or 'error' is present, as
required by the JSON-RPC specification.
This commit is contained in:
skewb1k
2025-08-03 17:42:44 +03:00
committed by GitHub
parent 2ef48fc65c
commit 40aef0d02e
9 changed files with 42 additions and 26 deletions

View File

@@ -323,7 +323,7 @@ end
--- @package
--- @param body string
function Client:handle_body(body)
local ok, decoded = pcall(vim.json.decode, body, { luanil = { object = true } })
local ok, decoded = pcall(vim.json.decode, body)
if not ok then
self:on_error(M.client_errors.INVALID_SERVER_JSON, decoded)
return
@@ -355,7 +355,6 @@ function Client:handle_body(body)
)
end
if err then
---@cast err lsp.ResponseError
assert(
type(err) == 'table',
'err must be a table. Use rpc_response_error to help format errors.'
@@ -374,8 +373,16 @@ function Client:handle_body(body)
end
self:send_response(decoded.id, err, result)
end))
-- This works because we are expecting vim.NIL here
elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then
-- Proceed only if exactly one of 'result' or 'error' is present, as required by the LSP spec:
-- - If 'error' is nil, then 'result' must be present.
-- - If 'result' is nil, then 'error' must be present (and not vim.NIL).
elseif
decoded.id
and (
(decoded.error == nil and decoded.result ~= nil)
or (decoded.result == nil and decoded.error ~= nil and decoded.error ~= vim.NIL)
)
then
-- We sent a number, so we expect a number.
local result_id = assert(tonumber(decoded.id), 'response id must be a number') --[[@as integer]]
@@ -415,7 +422,7 @@ function Client:handle_body(body)
M.client_errors.SERVER_RESULT_CALLBACK_ERROR,
callback,
decoded.error,
decoded.result
decoded.result ~= vim.NIL and decoded.result or nil
)
else
self:on_error(M.client_errors.NO_RESULT_CALLBACK_FOUND, decoded)