How to Use SonarQube in Neovim
This is an article I wanted to read many years ago, but no one ever wrote anything similar. It took me several years to configure SonarQube in Neovim, and now that it works,I want to teach the step-by-step process and all the details that need to be taught.
To be clear, SonarQube is poorly documented, and what saves it is that it is open source. I myself had to read the source code to find out how it works when everything could be documented. I am talking about connected mode, initial configurations, and client extensions.
Connected mode was the last thing I managed to configure before leaving Soluções Digitais and now I want to pass down this hidden knowledge forward.
Installation
Before anything else, you need to install SonarQube on your operating system;
you can install it as a local program in /home/user/.local or as a global
program in /usr/local/bin; the preference is yours.
Clone the sonarqube-makefile project to your machine and build the program.
git clone https://codeberg.org/cizordj/sonarqube-makefile.git
cd sonarqube-makefile
make
To install SonarQube as a local program, run the command:
env PREFIX="$HOME/.local" DESTDIR="$HOME/.local/share/sonarqube" make install
To uninstall, run the command:
env PREFIX="$HOME/.local" DESTDIR="$HOME/.local/share/sonarqube" make uninstall
To install SonarQube as a global program, run the command:
sudo make install
To uninstall, run the command:
sudo make uninstall
Configuration
This is where things get tricky! SonarQube is a program that requires MANY client extensions, which is why it doesn’t work out of the box. You can implement these extensions yourself or you can use ready-made plugins that do this for you; if you want to use any plugin, I recommend Sonarlint.nvim as it is very well implemented. However, this article does not teach working with the aforementioned plugin, so we will use a more manual configuration.
Install the plugin
nvim-lspconfig and put the
following in your init.lua file.
local function get_snake_case_current_folder()
local cwd = vim.fn.getcwd()
local folder_name = cwd:match("^.+/(.+)$") or cwd
local snake_case = folder_name:gsub("%s+", "_"):gsub("[^%w_]", ""):lower()
return snake_case
end
local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')
--- @class lspconfig.Config
configs.sonarqube = {
default_config = {
cmd = { "sonarqube-lsp" },
autostart = true,
handlers = {
["window/logMessage"] = function(_, params)
vim.print(params.message)
end,
["sonarlint/readyForTests"] = function()
vim.notify("Sonarqube has started", vim.log.levels.INFO)
end,
["sonarlint/reportConnectionCheckResult"] = function(_, params)
local connectionId = params.connectionId
local reason = params.reason
local success = params.success
if success then
vim.notify(
string.format(
"Connected successfully to %s!",
connectionId
),
vim.log.levels.INFO
)
else
vim.notify(
string.format(
"Connection to %s failed! Reason: %s",
connectionId,
reason
),
vim.log.levels.ERROR
)
end
end,
["sonarlint/isOpenInEditor"] = function(_, params)
local file_path = vim.uri_to_fname(params[1])
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(buf) and vim.api.nvim_buf_get_name(buf) == file_path then
return true
end
end
return false
end,
["sonarlint/filterOutExcludedFiles"] = function(_, params)
return params
end,
["sonarlint/getTokenForServer"] = function(_, params)
local serverUrl = params[1]
-- Return the token based on the serverUrl
return serverUrl
end,
["sonarlint/listFilesInFolder"] = function(_, params)
local folderUri = vim.uri_to_fname(params.folderUri)
local files = {
foundFiles = {}
}
local uv = vim.loop
-- Open the folder
local handle = uv.fs_scandir(folderUri)
if not handle then
vim.notify("Cannot open folder: " .. folderUri, vim.log.levels.ERROR)
return files
end
while true do
local name, type = uv.fs_scandir_next(handle)
if not name then
break
end
if type == "file" then
table.insert(files.foundFiles,
{
fileName = name,
filePath = folderUri
}
)
end
end
return files
end
},
detached = false,
filetypes = {
'java',
'javascript',
'javascriptreact',
'typescript',
'typescriptreact',
'css',
'html',
'python',
'cpp',
'c',
'php',
'dockerfile'
},
root_dir = function(fname)
if vim.version()['minor'] == 9
then
return lspconfig.util.find_git_ancestor(fname)
else
return vim.fs.root(0, '.git')
end
end,
settings = {
sonarlint = {
-- connectedMode = {
-- project = {
-- projectKey = "",
-- connectionId = ""
-- },
-- connections = {
-- sonarqube = {
-- {
-- serverUrl = "",
-- token = "",
-- connectionId = ""
-- },
-- }
-- }
-- },
showAnalyzerLogs = false,
showVerboseLogs = false,
disableTelemetry = false,
["files.exclude"] = {
["**/.git"] = true,
["**/node_modules"] = true,
},
rules = vim.empty_dict()
}
},
init_options = {
productKey = "neovim",
productName = "Neovim",
productVersion = tostring(vim.version()),
workspaceName = get_snake_case_current_folder(),
firstSecretDetected = false,
showVerboseLogs = false,
platform = vim.loop.os_uname().sysname,
architecture = vim.loop.os_uname().machine,
},
single_file_support = false
},
docs = {
description = 'An advanced linter in your IDE for Clean Code',
}
}
lspconfig.sonarqube.setup({
on_attach = function(client, bufnr)
common.setKeymaps(bufnr, client.name)
end,
flags = {
debounce_text_changes = 3000
},
})
It’s a pretty big configuration, isn’t it? This happens because SonarQube requires a lot of client-side implementation and thus a lot of programming is needed. The first thing the language server demands is the initialization configuration (init_options); in it, you pass some information about the IDE you are using and the workspace name (workspaceName); SonarQube uses this to create cache for the project and feed its telemetry.
This is why it’s important that quite a few people enable telemetry, as SonarSource can offer better support for Neovim.
The second most important part lies in the “handlers” key. Notice that Neovim needs to handle many specific events like, for example, sonarlint/isOpenInEditor. If you don’t implement these responses, SonarQube simply won’t work. There are countless requests that Sonar can make to your editor, but not all of them are documented, and you only find out about them by running it in verbose mode while reading the logs.
Connected Mode
To activate the connected mode for SonarQube, you need to fill out the following within the settings key.
local settings = {
sonarlint = {
connectedMode = {
project = {
projectKey = "",
connectionId = ""
},
connections = {
sonarqube = {
{
serverUrl = "",
token = "",
connectionId = ""
},
}
}
}
}
}
For information like projectKey, serverUrl, and token, you get it directly from the site where your SonarQube Cloud instance is hosted. As for the connectionId, it is a unique value used to identify this connection, and it is up to you what goes in it.
Also remember that you need to implement a response for the sonarlint/getTokenForServer event.
local handlers = {
["sonarlint/getTokenForServer"] = function(_, params)
-- Return your token here
return "1234"
end
}
Although the token is already in the static configuration, you still need to return it during SonarQube execution. The token from the static configuration has no effect, but it is required by Sonar.
Conclusion
This configuration is not perfect and leaves room for many improvements, but with it, you can already work on a daily basis and deliver your requirements. It is possible to create an entire ecosystem around SonarQube, such as a credential storage system, an assistant for configuring connected mode, and an automatic installer. However, this demands a lot of time and willingness; hence the existence of projects like Marc which aim to implement all possible events and make the SonarQube configuration smoother.