- RuboCop作者がRubyコードフォーマッタを比較してみた: 前編(翻訳)
- RuboCop作者がRubyコードフォーマッタを比較してみた: 中編(翻訳)
- RuboCop作者がRubyコードフォーマッタを比較してみた: 後編(翻訳)
概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: The Missing Ruby Code Formatter | Meta Redux
- 原文公開日: 2019/03/30
- 著者: Bozhidar — RuboCopの作者です
- サイト: Meta Redux
記事が長いので3分割いたしました。また、タイトルは内容に即したものにしました。
RuboCop作者がRubyコードフォーマッタを比較してみた: 前編(翻訳)
ある特定の言語のコードのフォーマットを1種類の正式な方法に絞り込むというアイデアがGo言語で登場して以来(Goの場合はgofmt
がバンドルされている)、あらゆるコミュニティのプログラマーがその方法論を取り込もうとしてきました(成功の度合いは言語によってまちまちですが)。その中でもJavaScriptのPrettierは大ヒットし、きわめて広範囲に用いられています。Elixir 1.6では標準のフォーマッタがすぐ使えるようになっていますし、同様の試みが多数進行中です。
Rubyコミュニティの場合、かなり長い間「唯一かつ真のフォーマッタ」が追求されており、現時点でも既に以下のような複数のプロジェクトの中から選択できるようになっています。
- RuFo
- prettier-ruby
- rubyfmt(現時点では開発が止まっている様子)
- rubyfmt(上と名前は同じだが無関係の新プロジェクト)
そして言うまでもなく我らがRuboCopを忘れてはいけません(願わくば)。RuboCopは私が今を去ること2012年に始めたプロジェクトです。RuboCopを単なるLintツールだと思っている方が多いようですが、実際にはコードレイアウト(すなわちコードのフォーマッティング)の検査と修正専用のあらゆる種類のcop(=RuboCopのコードチェック)があるのです。
選択肢があるというのは一般にはよいことですが、見方を変えれば選ぶためには多少なりとも頭を使わなければなりませんし、その選択が正しいことを裏付ける調査が必要になることもあるでしょう1。ではどのコードフォーマッティングツールを使うべきなのでしょうか?Rubyに不在だったコードフォーマッターの問題は解決されたのでしょうか?
何しろ私はRuboCopの作者という立場ですから、私の意見に偏りがあっても不思議はありません。私がRuboCopを褒めちぎって、本記事で後述する他のライバルたちと比べてRuboCopがいかに優れているかを力説すると思われても仕方がないでしょう。しかし私は多少なりとも皆さんの予想を覆す方向にベストを尽くしたいと思います。
出場選手紹介
正しいツールを選ぶには、それぞれのツールが採用するアプローチにどんなトレードオフがあるかを最初に理解しておくべきでしょう。私はRuFoやその他のツールの専門家ではありませんが、基本的な部分は外していないと考えています。
- どのツールもRuby組み込みパーサーである
Ripper
を用いている(古い方のrubyfmt
を除く) - ツールは
Ripper
のAST(抽象構文木)を元に、ソースコードを望ましい方法で(正しいフォーマットで)完全に再生成する - ツールは1種類の正規なコード表現に集約させることを目指している(つまり設定がサポートされていないかきわめて限定されている)
- ほとんどのツールがpretty-printデータ構造を用いている(訳注:
PrettyPrint
と思われます) - ツールは高速にpretty化することを目指している(エディタでファイルを保存するたびに実行することが想定されている)
- ツールの動作は「透過的ではない」のが普通(何が変更されるかは見ただけではよくわからず、ツールが正しいと仮定するしかない)
RuboCopは、いくつかの点でこうしたツールと異なります。
parser
というサードパーティ製パーサーを用いている2- レイアウトが改変されたときにソースコードを再生成しない(変更の必要なファイルの該当箇所を単に更新する)
- Ruby誕生以来25年の歳月を経て、Rubyのコーディング標準が1つに収束しそうにないという事実を踏まえている(だからこそ設定項目が異様に多い: 詳しくは後ほど)
- pretty-printデータ構造は(今のところ)使っていない
- 十分高速だが、Ripperを使うツールほどではなく、コードに差分編集を適用することもない
- ユーザーに多くのフィードバックを返す(エディターでレイアウトが「壊れている」あらゆる箇所にメッセージを表示し、RuboCopが更新した箇所についても明示的なフィードバックを表示する)
- 単なるフォーマッタではない(静的コード解析の一般的なフレームワークであり、コードレイアウトは可能な分析のうちただ1種類に落ち着くことになる)
これらの特性が実用上どのような意味を持つのでしょうか?皆さんに代わって解説してみたいと思います。
一般的なアプローチ
コードをフォーマットするときの一般的なアプローチは、基本的に次の2つから選べます。
- コードのASTをビルドして、そのASTから素直にフォーマット済みコードを再生成する(RuboCop以外のすべてのツールはこのアプローチ)
- ASTとソースファイルの対応付けを用いて、既存のファイルの特定の箇所を更新する
第1のアプローチの方が間違いなく実装がシンプルかつ短期で済むでしょう。唯一のささやかなデメリットは「コメント」の扱いです。コメントはコードのAST表現には含まれないので、コメントを保護したいのであれば何らかの手を加えなければいけなくなるかもしれません(lexer向けに取得されたデータを分解するなど)。
RuboCopのアプローチはトリッキーです。修正するファイルへの変更をマーシャリング(シリアライズして1つずつ適用)する必要があるため、適用しようとする変更同士のコンフリクトは発生しません。この戦略には、一定のパフォーマンスコストも伴います。言うまでもなく、更新するコードの境目をきわめて注意深く扱わなければなりませんし、変更内容を分解することでさらなるフォーマット上の問題が発生する可能性もあります。やりたいことがコードのフォーマットだけであれば明らかにこのアプローチは理想からかけ離れてるのですが、RuboCopの場合はlintの対象は広範囲に渡っているので、このアプローチがとてもよく合っています。
仮に私が専用フォーマッタをこしらえるのであれば、きっと第1のアプローチを採用したことでしょう。
pretty-print
pretty-printはなかなかよくできています。私はどんなコードフォーマッタツールであってもpretty-printは重要な機能だと考えています。pretty-printの定義はさまざまなもの考えられますが、普通は次のように、配列やハッシュなどマルチラインで構成されるリテラルをきれいにしてくれます。
# pretty-printなし
{first_name: "Bruce", last_name: "Wayne", address: {city: "Gotham", street: "Wayne Drive"}, secret_identity: "Batman"}
# pretty-printあり
{first_name: "Bruce",
last_name: "Wayne",
address: {city: "Gotham",
street: "Wayne Drive"},
secret_identity: "Batman"}
上は一例ですが、pretty-printについて一般的な動作はおわかりいただけるかと思います。Ruby組み込みのpp
やap
ライブラリはpretty-printのよい例です。
pretty-printが有用なのは、一般にソースコードよりもREPLで使う場合です(REPLの実行時に大きなデータ構造を扱いたいことが多いでしょうから)が、コード内の見苦しい1行ハッシュリテラルや1行配列リテラル(またはそれに関する何らかのメソッドシグネチャ)を再フォーマットできる点は実に素晴らしいと言えます。
RuboCopは確かにpretty-printについては他に比べて遅れています(実装したとしてもそれほど複雑にならないはずですが)。しかし最初に、私たちはレイアウト部門でどの程度のことをやりたいのかを決めなければなりません3。
追記(2019-04-04)
RuboCop 0.67では限定的ながらも配列やハッシュやメソッド引数に対してpretty-print機能が搭載されました。詳しくは#6824をご覧ください。ただし、これに関連するcopはデフォルトでは無効になっていることをお忘れなく。