RuboCop作者がRubyコードフォーマッタを比較してみた: 前編(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

記事が長いので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を除く)
  • ツールはRipperAST(抽象構文木)を元に、ソースコードを望ましい方法で(正しいフォーマットで)完全に再生成する
  • ツールは1種類の正規なコード表現に集約させることを目指している(つまり設定がサポートされていないかきわめて限定されている)
  • ほとんどのツールがpretty-printデータ構造を用いているデータ構造をpretty-printしている(訳注: PrettyPrintと思われます)
  • ツールは高速にpretty化することを目指している(エディタでファイルを保存するたびに実行することが想定されている)
  • ツールの動作は「透過的ではない」のが普通(何が変更されるかは見ただけではよくわからず、ツールが正しいと仮定するしかない)

RuboCopは、いくつかの点でこうしたツールと異なります。

  • parserというサードパーティ製パーサーを用いている2
  • レイアウトが改変されたときにソースコードを再生成しない(変更の必要なファイルの該当箇所を単に更新する)
  • Ruby誕生以来25年の歳月を経て、Rubyのコーディング標準が1つに収束しそうにないという事実を踏まえている(だからこそ設定項目が異様に多い: 詳しくは後ほど)
  • データ構造のpretty-printは(今のところ)使っていない
  • 十分高速だが、Ripperを使うツールほどではなく、コードに差分編集を適用することもない
  • ユーザーに多くのフィードバックを返す(エディターでレイアウトが「壊れている」あらゆる箇所にメッセージを表示し、RuboCopが更新した箇所についても明示的なフィードバックを表示する)
  • 単なるフォーマッタではない(静的コード解析の一般的なフレームワークであり、コードレイアウトは可能な分析のうちただ1種類に落ち着くことになる)

これらの特性が実用上どのような意味を持つのでしょうか?皆さんに代わって解説してみたいと思います。

一般的なアプローチ

コードをフォーマットするときの一般的なアプローチは、基本的に次の2つから選べます。

  1. コードのASTをビルドして、そのASTから素直にフォーマット済みコードを再生成する(RuboCop以外のすべてのツールはこのアプローチ)
  2. 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組み込みのppapライブラリはpretty-printのよい例です。

pretty-printが有用なのは、一般にソースコードよりもREPLで使う場合です(REPLの実行時に大きなデータ構造を扱いたいことが多いでしょうから)が、コード内の見苦しい1行ハッシュリテラルや1行配列リテラル(またはそれに関する何らかのメソッドシグネチャ)を再フォーマットできる点は実に素晴らしいと言えます。

RuboCopは確かにpretty-printについては他に比べて遅れています(実装したとしてもそれほど複雑にならないはずですが)。しかし最初に、私たちはレイアウト部門でどの程度のことをやりたいのかを決めなければなりません3

追記(2019-04-04)

RuboCop 0.67では限定的ながらも配列やハッシュやメソッド引数に対してpretty-print機能が搭載されました。詳しくは#6824をご覧ください。ただし、これに関連するcopはデフォルトでは無効になっていることをお忘れなく。



  1. 見事なまでの皮肉です。結局RuboCopには特定のフォーマットを選べる余地が必要なのです。 
  2. はいはい、名前がよくないのは先刻ご承知です! 
  3. ダジャレを狙ってます(訳注: department layoutがオフィスの物理的な机の配置などを表す「部門レイアウト」でもあることにかけているようです)。 
デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ