From bf4710d8c398b0634029f0a5be7622dfc216f50d Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 14 Oct 2025 20:43:25 -0400 Subject: [PATCH] fix(lsp): "attempt to index nil config" #36189 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: If a client doesn't have a config then an error may be thrown. Probably caused by: 2f78ff816b03661b5f74d0624e973eaca0d64ef1 Lua callback: …/lsp.lua:442: attempt to index local 'config' (a nil value) stack traceback: …/lsp.lua:442: in function 'can_start' …/lsp.lua:479: in function 'lsp_enable_callback' …/lsp.lua:566: in function <…/lsp.lua:565> Solution: Not all clients necessarily have configs. - Handle `config=nil` in `can_start`. - If user "enables" an invalid name that happens to match a *client* name, don't auto-detach the client. --- runtime/lua/vim/lsp.lua | 14 +++++++++++++- test/functional/plugin/lsp_spec.lua | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 3a9a6151d1..9f47044f1c 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -443,10 +443,16 @@ local function validate_config(config) validate('filetypes', config.filetypes, 'table', true) end +--- Returns true if: +--- 1. the config is managed by vim.lsp, +--- 2. it applies to the given buffer, and +--- 3. its config is valid (in particular: its `cmd` isn't broken). +--- --- @param bufnr integer --- @param config vim.lsp.Config --- @param logging boolean local function can_start(bufnr, config, logging) + assert(config) if type(config.filetypes) == 'table' and not vim.tbl_contains(config.filetypes, vim.bo[bufnr].filetype) @@ -485,7 +491,13 @@ local function lsp_enable_callback(bufnr) -- Stop any clients that no longer apply to this buffer. local clients = lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) for _, client in ipairs(clients) do - if lsp.is_enabled(client.name) and not can_start(bufnr, lsp.config[client.name], false) then + -- Don't index into lsp.config[…] unless is_enabled() is true. + if + lsp.is_enabled(client.name) + -- Check that the client is managed by vim.lsp.config before deciding to detach it! + and lsp.config[client.name] + and not can_start(bufnr, lsp.config[client.name], false) + then lsp.buf_detach_client(bufnr, client.id) end end diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 5a6754f225..0ca8b47586 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -6649,6 +6649,35 @@ describe('LSP', function() ) end) + it('handle nil config (some clients may not have a config!)', function() + exec_lua(create_server_definition) + exec_lua(function() + local server = _G._create_server() + vim.bo.filetype = 'lua' + -- Attach a client without defining a config. + local client_id = vim.lsp.start({ + name = 'test_ls', + cmd = function(dispatchers, config) + _G.test_resolved_root = config.root_dir --[[@type string]] + return server.cmd(dispatchers, config) + end, + }, { bufnr = 0 }) + + local bufnr = vim.api.nvim_get_current_buf() + local client = vim.lsp.get_client_by_id(client_id) + assert(client.attached_buffers[bufnr]) + + -- Exercise the codepath which had a regression: + vim.lsp.enable('test_ls') + vim.api.nvim_exec_autocmds('FileType', { buffer = bufnr }) + + -- enable() does _not_ detach the client since it doesn't actually have a config. + -- XXX: otoh, is it confusing to allow `enable("foo")` if there a "foo" _client_ without a "foo" _config_? + assert(client.attached_buffers[bufnr]) + assert(client_id == vim.lsp.get_client_by_id(bufnr).id) + end) + end) + it('attaches to buffers when they are opened', function() exec_lua(create_server_definition)