- RuboCop作者がRubyコードフォーマッタを比較してみた: 前編(翻訳) -- 本記事
- RuboCop作者がRubyコードフォーマッタを比較してみた: 中編(翻訳)
- RuboCop作者がRubyコードフォーマッタを比較してみた: 後編(翻訳)
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データ構造を用いているデータ構造を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はデフォルトでは無効になっていることをお忘れなく4。
関連記事
- 見事なまでの皮肉です。これらのツールは本来特定の決定を下さなくて済むようにするためのものなのですが。 ↩
- はいはい、ネーミングは難しいですよね。 ↩
- ダジャレを狙ってます(訳注: department layoutがオフィスの物理的な机の配置などを表す「部門レイアウト」でもあることにかけているようです)。 ↩
-
訳注: 2023/03/22時点でも、masterブランチのconfig/default.ymlでは、これに関連する
MultilineArrayLineBreaks
、MultilineMethodArgumentLineBreaks
、MultilineMethodParameterLineBreaks
、MultilineHashKeyLineBreaks
はfalseになっています。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。記事が長いので3分割いたしました。参考までに、元記事の後にtestdouble社のstandard(standardrb)というgemも登場しています↓。