Como usar o sonarqube no neovim

Este é um artigo que eu queria ter lido muitos anos atrás, mas ninguém nunca escreveu nada parecido. Eu levei diversos anos para configurar o SonarQube no Neovim e agora que tenho ele funcionando, nada mais justo do que ensinar o passo a passo e todos os detalhes que precisam ser ditos.

Só para constar, o SonarQube é um programa mal documentado e o que salva ele é o fato de ele ser de código aberto. Eu mesmo já tive que ler o código-fonte para descobrir como ele funciona sendo que tudo isso poderia estar documentado, estou falando de modo conectado, configurações iniciais e extensões de cliente.

O modo conectado foi a última coisa que consegui configurar antes de sair do Soluções Digitais e agora nada mais justo do que passar esse conhecimento oculto adiante.

Instalação

Antes de tudo é preciso instalar o SonarQube no seu sistema operacional, você pode instalar como um programa local em /home/user/.local ou como um programa global em /usr/local/bin, a preferência é sua.

Clone o projeto sonarqube-makefile para a sua máquina e faça a instalação do programa.

git clone https://codeberg.org/cizordj/sonarqube-makefile.git
cd sonarqube-makefile
make

Para instalar o SonarQube como um programa local rode o comando:

env PREFIX="$HOME/.local" DESTDIR="$HOME/.local/share/sonarqube" make install

Para desinstalar rode o comando:

env PREFIX="$HOME/.local" DESTDIR="$HOME/.local/share/sonarqube" make uninstall

Para instalar o SonarQube como um programa global rode o comando:

sudo make install

Para desinstalar rode o comando:

sudo make uninstall

Configuração

É aqui que vem o pulo do gato! O SonarQube é um programa que exige MUITAS extensões por parte do cliente e é por isso que ele não funciona fora da caixa. Essas extensões você mesmo pode implementar ou você pode usar plugins prontos que já fazem isso por você, caso você queira usar algum plugin eu recomendo o Sonarlint.nvim que é muito bem implementado. Este artigo, no entanto, não ensina a trabalhar com o plugin supracitado, por isso usaremos uma configuração mais manual.

Instale o plugin nvim-lspconfig e coloque o seguinte no seu arquivo init.lua.

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
  },
})

É uma configuração bem grande, não? Isso acontece porque o SonarQube exige muita implementação no lado do cliente e por isso muita coisa precisa ser programada. A primeira coisa que o servidor de linguagem exige é a configuração de inicialização (init_options), nela você passa algumas informações sobre a IDE que você está usando e o nome do espaço de trabalho (workspaceName), o SonarQube usa isso para criar cache para o projeto e alimentar sua telemetria.

Por isso é importante que bastante gente habilite a telemetria, pois assim a SonarSource pode oferecer melhor suporte ao Neovim.

A segunda parte mais importante reside na chave “handlers”. Perceba que o Neovim precisa lidar com muitos eventos específicos como, por exemplo, sonarlint/isOpenInEditor. Se você não implementar essas respostas, o SonarQube simplesmente não funciona. Há inúmeras requisições que o Sonar pode fazer para o seu editor, porém nem todas estão documentadas e você só consegue descobrir elas ao rodar em modo verboso enquanto lê os logs.

Modo conectado

Para ativar o modo conectado do SonarQube você precisa preencher o seguinte dentro da chave settings.

local settings = {
  sonarlint = {
    connectedMode = {
      project = {
        projectKey = "",
        connectionId = ""
      },
      connections = {
        sonarqube = {
          {
            serverUrl = "",
            token = "",
            connectionId = ""
          },
        }
      }
    }
  }
}

As informações como projectKey, serverUrl e token você obtém direto do site onde está hospedado a instância do SonarQube Cloud. Já o connectionId é um valor único que serve para identificar essa conexão e é você quem decide o que vai nele.

Lembrando também que você precisa implementar uma resposta para o evento sonarlint/getTokenForServer.

local handlers = {
    ["sonarlint/getTokenForServer"] = function(_, params)
        -- Retorne o seu token aqui
        return "1234"
    end
}

Apesar do token já estar na configuração estática, você ainda precisa retorná-lo durante a execução do SonarQube. O token da configuração estática não possui efeito, mas é exigido pelo Sonar.

Conclusão

Essa configuração não é perfeita e abre espaço para muitas melhorias, no entanto, você já consegue trabalhar no dia-a-dia com ela e entregar suas demandas. É possível criar um ecossistema inteiro ao redor do SonarQube, como, por exemplo, um sistema de armazenamento de credenciais, um assistente para configurar o modo conectado e um instalador automático. No entanto, isso demanda muito tempo e disposição; por isso, existem projetos como o do Marc que procura implementar todos os eventos possíveis e deixar a configuração do SonarQube mais suave.