Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby LSPアドオンシステムの概要(翻訳)

概要

CC BY-NC-SA 4.0に基づいて翻訳・公開いたします。


CC BY-NC-SA 4.0| 表示 - 非営利 - 継承 4.0 国際 | Creative Commons

Shopify/ruby-lsp - GitHub

Ruby LSPアドオンシステムの概要(翻訳)

カンファレンスニュース

Ruby LSPチームは、11月にChicagoで開催されるRubyConf 2024に参加します。Ruby LSPやRuby開発者の経験について幅広く話してみたい方は、ぜひご参加ください。

🔗 概要

本記事では、Ruby LSPアドオンシステムを紹介し、これによって解決される問題や、アドオンシステムのアーキテクチャについて解説するとともに、アドオンのサンプルもいくつか紹介し、今後のアドオンエコシステムでRuby開発エクスペリエンスを強化する構想についても共有します。

🔗 Ruby LSPの概要

Ruby LSPは、言語サーバー(language server)の実装であり、Rubyコードを効率よく書けるようにすることを目的として設計されています。Ruby LSPは、エディタに機能を提供するために静的コード解析を利用します。ただしRubyエコシステムでは動的プログラミングやDSLでRubyを拡張することが多く(特にRailsではこうした手法が広範囲に用いられています)、この点がエディタツールで問題になる可能性があります。

あらゆるgemは、メタプログラミングで記述することで独自のDSLを定義できるので、Ruby LSPの静的解析ツールキットに個別のDSL用の処理を追加するのは現実的ではありません。

さらに、依存関係のセットはプロジェクトごとに異なっているため、可能なDSLをすべて処理しようとすると、パフォーマンスと正確性が両方とも損なわれてしまう可能性もあります。

さまざまな他のgemが、そのgem固有の特殊な処理を言語サーバーに伝える方法を大規模にスケール可能にするには、伝達方法を拡張可能にする必要がありました。また、ランタイムコンポーネントを言語サーバーの知識に追加可能かどうかという検討課題もありました。これが可能になれば、静的分析における制約の一部を、開発中のアプリケーションと通信する形で克服できるようになります。

LSP(Language Server Protocol)を使うことで、エディタが複数の言語サーバーと同時に通信して、作者が独自のLSPを実装できるようになります。言語サーバーの基本部分を実装するのはさほど複雑ではありませんが、適切なエンコード処理や、広範囲に渡る仕様のサポート、十分なパフォーマンスの確保は困難です。この手法には以下のようないくつもの短所があります。

  • 作業の繰り返しを強いられる
    さまざまなRubyバージョンマネージャやプラットフォームごとに同じ機能を実装することになります。

  • 静的解析の再実装が必要
    言語サーバーを新しく作るたびに静的解析ツールを独自に実装することになります。

  • リソース使用量が増える
    コードベースのインデックス作成処理が重複し、ドキュメント表現のメンテナンスも重複します。

  • 編集の応答性が低下する
    解析済みのRubyコード表現を複数のLSPサーバーで共有できず、Rubyコードを個別のLSPサーバーが処理しなければならなくなる。

  • 設定項目が増える
    言語サーバーが複数になれば、言語サーバーごとに必要な設定項目も増えます。

  • 専用のVS Code拡張が必要
    他のエディタと異なり、VS Codeをサポートするには、対応するVS Code拡張がLSPサーバーごとに必要になります。

🔗 アドオンシステムのアーキテクチャ

これらの問題に対処するため、私たちは開発者がRuby LSPの振る舞いを拡張するためのアドオンシステムを提供しています。アドオン方式にすることで、特殊な機能を多数の言語サーバーに追加できるようになり、インデックス化機能の拡張にも有用なので、Ruby LSPが持つコードベースの宣言に関する知識を強化できるようになります。

アドオンは、以下のようにさまざまな方法で配布可能です。

  • 単独のアドオンgemとして配布する
  • 既存のライブラリに追加する
  • 既存のアプリやライブラリに含めることで、プロジェクト固有の振る舞いを追加する

アドオンはRubyで記述され、RubyLsp::Addonを継承するクラスとして実装されます。アドオンの命名がmy_gem/lib/ruby_lsp/my_gem/addon.rbという規約に沿っていれば、Ruby LSPが自動的にアドオンを検出して読み込みます。Ruby LSPと同様、アドオンも特定のエディタに縛られません。

Ruby LSPは、最新の高速パーサーであるPrismに大きく依存しています。Prismは、LSPがコードとやりとりする方法の基本部分となっているので、アドオンを構築するにはPrismに精通していることが重要です。

ruby/prism - GitHub

以下のサンプルコードでは、Ruby LSPが提供するglobal_stateというオブジェクトが、「インデックス」「設定」「ファイルエンコーディング」などの現在のコードベースに関する共有概念を提供します。
message_queueは、エディタにメッセージを送信する方法を提供します(監視するファイルの登録やトレースイベントのログ出力など)。

🔗 サンプル: ホバー

このサンプルコードは、開発者がclass定義にマウスをかざしたときにいくつかの情報を表示します。

# lib/ruby_lsp/my_gem/addon.rb
require "ruby_lsp/addon"
require_relative "my_listener"

module RubyLsp
  module MyAddonGem
    class Addon < ::RubyLsp::Addon
      def activate(global_state, message_queue)
        # オプション: 言語サーバー起動時に1度だけ実行する
        # 必要のあるアクティベーションコードをここに入力
      end

      def deactivate
        # オプション: サーバー終了時に必要なクリーンアップコードをここで入力
        # (サブプロセスの終了など)
      end

      def name
        "My Gem"
      end
    end
  end
end

このサーバー内ではObserverパターンを使っています。このパターンでは、リスナーをPrismのディスパッチャに登録することで、特定のノードイベント(on_class_node_enterなど)に対応するイベントを受信します。この方法では、AST(抽象構文木)を1回スキャンすればよく、アドオンごとにASTをスキャンする必要はありません。

以下のサンプルコードは、マウスをクラスにかざすとクラス名を表示します。

# lib/ruby_lsp/my_gem/my_listener.rb
class MyListener
  def initialize(dispatcher)
    # 登録することで`on_class_node_enter`イベントをリッスンする
    dispatcher.register(self, :on_class_node_enter)
  end

  # `on_class_node_enter`用のハンドラメソッドを定義する
  def on_class_node_enter(node)
    $stderr.puts "Hello, #{node.constant_path.slice}!"
  end
end

実際の動作を確認するために、以下のようにRubyコードのスニペットを渡します。

dispatcher = Prism::Dispatcher.new
MyListener.new(dispatcher)

parse_result = Prism.parse("class Foo; end")
dispatcher.dispatch(parse_result.value)

上を実行するとHello, Foo!が出力されます。

実際のLSP機能を実装するには、@response_builderにappendします。この抽象化は、さまざまな場所からの出力をエディタで期待される形式に組み立てるのに使われます。たとえば、以下のサンプルコードは、個別のclassHover用の情報を追加します。

  def on_class_node_enter(node)
    range = range_from_node(node)
    text = "This class is named **#{node.constant_path.slice}**"
    contents = Interface::MarkupContent.new(kind: "markdown", value: text)

    @response_builder << Interface::Hover.new(contents: contents)
  end

Interface::クラスは、Ruby LSPが依存しているlanguage-server-protocol gemから取得します)

🔗 アドオンの機能

アドオンには、以下のようなエディタ統合を実現する豊富なAPIが提供されます。

  • テスト: ヘルパーなどのユーティリティ。
  • ログ出力: Ruby LSPが行っていることを可視化。
  • フォーマッタやlinterの登録: 保存時フォーマットやクイックフィックスなどの機能をサポート。
  • クライアントに通知を送信する: 情報やエラーを表示する。
  • ファイル更新をリッスンする: 設定ファイルが変更されたら再読み込みする。
  • 静的解析(インデックス作成、型推論など): コードベース全体の構造を正確に表現する。

本記事執筆時点では、CodeLensCompletionDefinitionDocumentSymbolHover用のLSPリクエストをアドオンで強化できます。

🔗 インデックス作成の強化

Ruby LSPの機能を強化する方法は2通りあります。

1つ目は、呼び出し側で発生するDSLを処理し、プロジェクトに既に存在する宣言を変更しないようにすることです。例としては、Railsのvalidateメソッドがあります(動的に呼び出されるメソッド名を表すシンボルが渡される)。

2つ目は、宣言DSLを処理する方法です。宣言DSLは、宣言をメタプログラミングで作成します。Railsにおける例としては、belongs_toがあります。これは現在のクラスを改変して、belongs_toに渡されたものに応じてメソッドを追加します。

どちらの方法も、通常はRuby LSPから「見えない」ので、インデックスを拡張するためのAPIをアドオンで提供しています。すべての宣言がインデックス化されることで、「定義ジャンプ」「マウスオーバー」「コード補完」「シグネチャヘルプ」「ワークスペース内のシンボルへの移動」などの機能が自動的に有効になります。

🔗 制限事項

このアドオンシステムは言語サーバーを拡張するための方法ですが、他の言語のエコシステムでは一般的ではありません。私たちは、Ruby LSPサーバーに提供するアーキテクチャをリッチで拡張可能なものにするために、コミュニティの協力のもとでAPIで必要なものを理解するための取り組みを進めています。

現時点では、アドオンで拡張可能なのは言語サーバーの「サーバー部分のみ」である点にご注意ください。エディタのUI要素はこのAPIでは追加できません。また、エディタがUI要素を管理するためのAPIはエディタごとに異なるので、今後UI要素用のAPIが追加可能になる見込みは低いでしょう。

🔗 ケーススタディ: Ruby LSP Rails

Shopify/ruby-lsp-rails - GitHub

Shopifyが開発したRuby LSP Railsは最初のアドオンであり、私たちがAPIを設計するうえで役に立ちました。このアドオンは、以下のような要請を強化します。

  • マウスオーバー: データベーススキーマ情報を表示する
  • 定義ジャンプ: 関連付けやコールバック
  • CodeLens:  ActiveSupport::TestCaseから継承した「宣言型」テストを実行する

Railsアドオンは、開発中のRailsアプリと直接通信するので、コントローラのアクションから対応するルーティングへのジャンプのような、静的分析では構築が極めて難しいもしくは不可能な機能を提供できます。

Ruby LSP Railsのデモ

🔗 ケーススタディ: Standard

Logo for Standard

standardrb/standard - GitHub

Standard(Standard Ruby)は、RuboCopの上に構築されたlinter兼フォーマッタです。当初のStandardは、LSPサーバーとVS Code拡張を自前で持っていました

私たちは、Standardの作者であるJustin Searlsの協力を得て、Standardをアドオンとして利用可能にするために必要ないくつかの変更をRuby LSPに加えました。

詳しくは、Justin Searlsの以下の記事をご覧ください。

参考: Why I just uninstalled my own VS Code extension | justin․searls․co

私たちのビジョンを信頼し、Standard Rubyアドオンのリリースに力を貸してくれたJustinに感謝いたします。

🔗 その他のアドオン

現在、以下のようなサードパーティ製アドオンが既に利用可能です。

この他のアドオンを見つけるには、rubygems.orgruby-lsp-を検索することをおすすめします。

🔗 今後のビジョンについて

私たちは、Ruby LSPをフル機能の言語サーバーにするだけではなく、アドオンのエコシステムが充実しているトータルなエディタ統合を提供するときのプラットフォームにすることを構想しています。今後もっと多くのgemが、linterやフォーマッタ以外の専門的なツールもサポートするアドオンを組み込むことを期待しています。アドオンが自動的に検出され、ユーザーが一切設定する必要がなくなるのが理想です。私たちの目標は、Ruby LSP v1.0までにAPIを安定させることです。

次のステップ

アドオンについて詳しくは、Ruby LSPドキュメントAdd-onsページを参照してください。

SlackのRuby DXのワークスペースでアドオンに関する会話に参加することも可能です。私たちは、Ruby LSPアドオンが今後のRuby開発エクスペリエンスを強化する様子を目にするときを楽しみにしています。

関連記事

Ruby: 2024年までのPrismパーサーの長い歴史を振り返る(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。