🐳

Docker コンテナ上の RuboCop LSP を使って Ruby コードをフォーマットする

2024/10/18に公開

masaki です。
Ruby を使ったプロジェクトでは、フォーマッターとして RuboCop を使用していることが多いと思います。
また、RuboCop は LSP(Language Server Protocol)に対応しているため、エディタに組み込んで利用している開発者も多いのではないでしょうか。
本記事では RuboCop LSP を Docker コンテナ上で起動し、ホスト側のエディタで Ruby コードをフォーマットする設定について解説します。

この記事の対象者

  • RuboCop を日常的に使用している Ruby 開発者
  • LSP(Language Server Protocol)をエディタで活用している方
  • Docker を使って開発している方

動作環境

  • Ruby: 3.2 系
  • RuboCop: 1.67.0
  • MacOS: Sonoma 14.6.1
  • Docker: 27.2.0
  • Docker Compose: v2.29.2-desktop.2
  • Neovim: v0.10.2
  • VSCode: 1.94.2

Docker コンテナ上で起動した RuboCop LSP をホスト側のエディタで使う設定

まず、設定方法を紹介します。
後ほど、なぜこの方法を採用したのかの経緯について説明します。
Neovim(Vim)と VSCode の設定方法を解説しますが、LSP に対応しているエディタであれば、どんなエディタでも同様に動作するはずです。

Neovim(Vim)

Vim または Neovim で使用できる LSP クライアントの coc.nvim の設定と、Neovim のビルトイン LSP クライアントである nvim-lspconfig の設定を紹介します。

coc.nvim

プロジェクト配下で Ruby ファイル(任意のファイル)を開きます。
:CocLocalConfig を実行し、以下を設定します。

{
  "languageserver": {
    "rubocop": {
      "command": "docker",
      "args": [
        "exec",
        "-i",
        "sample-app-1",
        "rubocop",
        "--config",
        ".rubocop.yml",
        "--lsp"
      ],
      "filetypes": ["ruby"]
    }
  }
}

上記の設定で実際に実行されるコマンドは以下の通りです。

docker exec -i sample-app-1 rubocop --config .rubocop.yml --lsp

対象のコンテナのサービス名(sample-app-1)や、RuboCop 設定ファイル指定(--config .rubocop.yml)は適宜変更してください。
ポイントは :CocConfig(グローバル設定)ではなく、:CocLocalConfig(ローカル設定)で設定することです。
これは、プロジェクトによってコンテナのサービス名が異なったり、プロジェクト外ではコンテナの RuboCop LSP を使用しない場合があるためです。

また、Docker Compose を使用している場合は以下のように設定します。

{
  "languageserver": {
    "rubocop": {
      "command": "docker",
      "args": [
        "compose",
        "exec",
        "app",
        "rubocop",
        "--config",
        ".rubocop.yml",
        "--lsp"
      ],
      "filetypes": ["ruby"]
    }
  }
}
  • 動作デモ

RuboCop の違反検出およびフォーマットを行っています。

nvim-lspconfig(Neovim のみ)

nvim-lspconfig での設定例です。
プロジェクト配下に「/.nvim-lspconfig ファイルが存在するか」という判定は「sample-app というプロジェクト内でだけコンテナの RuboCop LSP を使いたい」という意図なので、他の方法でも問題ありません。

vim.opt.signcolumn = "yes"
vim.api.nvim_create_autocmd("FileType", {
  pattern = "ruby",
  callback = function()
    local filepath = vim.fn.expand("%:p")
    local project_root = vim.fn.getcwd()
    local config_file = project_root .. "/.nvim-lspconfig"

    if vim.fn.filereadable(config_file) == 1 then
      vim.lsp.start {
        name = "docker-lsp",
        cmd = {
          "docker",
          "exec",
          "-i",
          "sample-app-1",
          "rubocop",
          "--config",
          ".rubocop.yml",
          "--lsp"
        },
      }

      vim.api.nvim_buf_set_keymap(
        0, 'n', '<leader>f', '<cmd>lua vim.lsp.buf.format()<CR>',
        { noremap = true, silent = true }
      )
    end
  end,
})
  • 動作デモ

VSCode

VSCode の設定方法を紹介します。
まず RuboCop エクステンション を VSCode にインストールします。

設定を開きます。

「Workspace」のタブを選択し、「Command Path」に Vim の設定と同じように docker コマンドを入力して保存します。
「Workspace」タブ配下で設定することにより、プロジェクト内でのみ本設定が有効になります。

docker exec -i sample-app-1 rubocop --config .rubocop.yml

Vim の設定同様、対象のコンテナのサービス名(sample-app-1)や、RuboCop 設定ファイル指定(--config .rubocop.yml)は適宜変更してください。

また、Docker Compose を使用している場合の設定例は以下の通りです。

docker compose exec app rubocop --config .rubocop.yml
  • 動作デモ

なぜ設定しようと思ったのか

我々ソーシャルPLUSでは長年 RuboCop を使用しています。個人では RuboCop の LSP 対応が行われたときからエディタに導入して使用してきました。しかし、最近あるプロジェクトの開発中に RuboCop LSP が動かなくなりました。

そのプロジェクトでは Docker を使用してアプリの仮想環境を構築しており、RuboCop はそのアプリの development 環境(Gemfile の development group)にインストールされています。また、ホスト側で起動しているエディタで RuboCop LSP を使用するために、ホスト側でも環境構築(bundle install など)を行っていました。

なぜ動かなくなったというと、そのプロジェクトにホスト(Mac OS)でインストールできない特殊な gem を導入し、ホスト上で bundle install が失敗するようになったためです。

※ Docker コンテナ上では bundle install が成功するため、プロジェクトとしては問題ありません。

対策として以下を検討しました。

  1. 対象の gem を Gemfile からコメントアウトし、git update-index --assume-unchanged Gemfile* などで開発時だけ一時的に除外する
  2. 対象の gem を頑張って Mac に install する
  3. RuboCop 実行時、別に用意した .rubocop.yml を参照させて実行する

1 は簡単で手っ取り早いですが、毎回除外するのは少し手間です。また、除外後に元に戻すのを忘れて意図せず gem の更新に追従できなくなる可能性もあります。
2 は労力がかかるため見送りました。
3 は意図しないフォーマットルールが適用されると逆に手間がかかるため見送りました。

最初は 1 の方法で対応していましたが、そもそもホスト側で RuboCop LSP を動かすという運用自体が間違っていたように思います。
既に環境構築している Docker コンテナに加えて、ホスト側でも環境構築(bundle install など)を行うのは無駄だと感じました。
そこで、Docker コンテナ上で RuboCop LSP を起動し、ホスト側のエディタと接続してフォーマットを行う方法を採用しました。

なぜ動くのか

なぜこの設定で動くのでしょうか?
それは RuboCop LSP が標準入出力(stdin/stdout)を通じてメッセージ(JSON-RPC)のやり取りを行っているためです。

ホストで起動している Docker コンテナとは exec コマンドを通じて標準入出力で通信できます。
したがって、Docker コンテナ上で動いている RuboCop と、ホスト側のエディタとの間で通信が行われているわけです。

まとめ

LSP をコンテナで動かすことで、ホスト側の設定が不要になり、メンテナンスコストが下がります。
今回は Ruby と RuboCop について解説しましたが、他のプログラミング言語や LSP でも同様の方法が適用可能です。
同じような課題を抱えている方は、ぜひこの方法を試してみてください。

SocialPLUS Tech Blog

Discussion