最近VimのLinterのプラグインをaleからすべてlanguage serverに移行した。

殆どのCLIツールはdiagnostic-languageserver経由で使うことでいい感じに動いてくれている。

ただ、golangci-lintだけはGoのスコープとの相性の関係ではうまく動作させることができなかった。

そこで、golangci-lint専用のlanguage server、golangci-lint-langserverを作成してみた。

LSPクライアントに読み込ませるとファイルを開いたときと保存したときに以下のようにgolangci-lintの結果を返してくれる。

自分が使うために作ったためWindows対応や細かいエラーハンドリング等はしていないが、個人的には割とちゃんと動いたので満足している。

制作動機 Link to heading

一応制作動機も書いておこうと思う。

今回aleの移行先として検討したdiagnostic-languageserverefm-langserverだが、基本的には開いているファイルにlintをかけてその結果を返す仕組みになっている。

しかし、Goはパッケージ単位(ディレクトリ単位)のスコープになっているためgolangci-lintはうまく動かない場合があった。

例えばunusedやdeadcodeのような使っていない変数や関数をチェックするようなものは、そのパッケージ内の別ファイルで使っていたとしてもそれがチェックされず、lintに引っかかってしまう。

また、別ファイルで定義されている関数や変数を使おうとした場合はそもそもビルド失敗扱いになりチェックすらできない。

かといって、ファイルが開かれるたびにパッケージ全体に対してgolangci-lintをかけると今度は関係ないファイルの結果も含まれてしまう。

この問題を解決するには、language server側で以下の流れのようにlinterが返した結果をLSPクライアントが送ってきた情報でフィルタをする必要があった。

  1. LSPクライアントのNotificationを受け取る
  2. golangci-lintを実行する
  3. 結果の中から1の情報を使って対象ファイルの結果だけをフィルタする
  4. LSPクライアントに結果を伝える

最初はdiagnostic-languageserverやefm-langserverにコントリビュートしようとも考えた。

しかし、そこそこ大きな機能を追加しないと実現できそうになく、かつgolangci-lintのためだけにその機能を追加するのもシンプルではないと思ったため、完全に独立したlanguage serverとして実装してしまうことにしてみた。

efm-langserverの実装を参考にして書いたため、かなり重複したコードにはなってはいる。 ただ、そのおかげで実装を大きくせずに書けたのでシンプルな状態は保つことができた。

個人的にこのlanguage serverは一時的な実装のつもりなので、goplsやdiagnostic-languageserver、efm-langserverが対応したらすぐにdeprecatedな状態にしようと思っている。

それまではgolangci-lint-langserverはある程度メンテするつもりなので、PRお待ちしています。