📝

rfmtをつくっている, 高速なRuby code formatter

に公開

Kinesis Advantageを使い始めて、タイピングが捗っています。
fujitai soraです。

rfmtというRuby Gemを開発していて、その紹介記事になります。
アールフォーマットって読みます👀
Rustで作られたRubyのツールだからrfmt

GitHub(GitHub Star 押してくれると喜びます)
https://github.com/fs0414/rfmt

RubyGems
https://rubygems.org/gems/rfmt

DeepWiki
https://deepwiki.com/fs0414/rfmt

rfmt

bundler経由でinstallできるRuby専用のcode formatterです。

下記を主な特徴としています

  • 高速な処理
    • 後述しますが、既存エコシステムのformatterよりも高速であることを目指して開発しています
  • opinionatedな設計
    • 初期化時点で必要な設定が済んでおり、すぐに使い始められます
    • ユーザー側が設定変更できるパラメータは最小限とし、規約に従うことで簡単にformatを統一できます
  • その他、下記のケースに対応
    • Git HookやCI向けのcheckコマンド
    • 対象ファイルサイズに応じた並列処理の自動最適化、
    • dry runによる差分出力
    • format on save(現状はVimのみに対応、その他も順次対応予定)

技術構成

RubyGems公式のRust Extensionを使用して実装、build環境の構築を行っています。
Rubyの柔軟さやRubyコードから利用できるRfmt Classの実装、Rustによる実行速度と堅牢さの両立を目指してこのアーキテクチャを採用しました。
Rust Extensionに組み込まれているcrateであるmagnusを使用し、Ruby - Rust 間のFFIレイヤーを実装しています。

Ruby Frontend (CLI/Ruby interface) → Rust Core (Formatter) → Prism Parser (AST)

入力ソースコードを構文解析するParserにはPrismを使用しています。
デファクトスタンダードなParserが変わっても簡単に対応できるよう、formatを実行するEmmiter moduleは直接Prismとは依存せず、Bridgeレイヤーを介して依存を分離する設計を選択しています。
この辺りの技術詳細については、別記事で個別にまとめていこうと思います。

DeepWikiが生成したアーキテクチャ図、大体合ってる

ベンチマーク

下記はrfmtとRucoCopによるformatの速度比較です。
どちらもファイルサイズが増えても安定した速度で処理を完了しますが、rfmtが約6倍高速です。

自分の環境では基本的には100ms~350ms, それなりの規模のRails repositoryでも500msほどでformatが完了しています。

Files Total Lines rfmt RuboCop Ratio
9 ~1,000 122ms 798ms 6.54x
9 ~1,700 120ms 797ms 6.62x
35 ~4,200 122ms 798ms 6.57x

ベンチマーク比較は下記のfileにまとめられています。
rfmtが平均ケースで約6倍高速であり、メモリ効率は1/6ほどであるという結果です。
https://github.com/fs0414/rfmt/blob/main/docs/benchmark.md

Usege

より詳細な解説はREADMEに記載されていますので、細かなオプションはそちらを参照されてください。

install

bundler経由でinstall可能です

sh
gem install rfmt

or

Gemfile
gem 'rfmt'

設定ファイルの初期化

rfmtに関する設定はすべて .rfmt.ymlに集約され、下記のコマンドで生成します。
init時点でconfig fileを生成するため、追加の設定なしで即座に使い始められます。
生成するディレクトリを指定するoptionも用意しており、必要に応じたカスタマイズは可能です。

sh
% rfmt init
Created .rfmt.yml

format対象ファイルや除外ファイル、最小限のformatオプション設定が可能です。

.rfmt.yml
version: "1.0"

formatting:
  line_length: 100        # Maximum line length (40-500)
  indent_width: 2         # Spaces/tabs per indent (1-8)
  indent_style: "spaces"  # "spaces" or "tabs"
  quote_style: "double"   # "double", "single", or "consistent"

include:
  - "**/*.rb"
  - "**/*.rake"
  - "**/Rakefile"
  - "**/Gemfile"

exclude:
  - "vendor/**/*"
  - "tmp/**/*"
  - "node_modules/**/*"
  - "db/schema.rb"

format

シンプルなCLI設計にしています。
rfmtを実行するだけで必要なformatが完了する

sh
% rfmt

Git HookやCI時のStepなどで「formatが必要なファイルがあるかどうか」を判定したいケースでは、checkコマンドを利用できます。

sh
% rfmt check

format結果を適用せずに差分を確認したいケースは、diffオプションでdry runの結果を確認できます。

sh
% rfmt ./**.rb --diff

format on save

この機能は、現在はVimのみを対応しています。
VSCode, RubyMine, Zedへの対応も開発計画に入っており、順次対応予定です。

https://github.com/fs0414/rfmt?tab=readme-ov-file#editor-integration

個人的にはAI Codingも相まってformat on saveの重要度は少し下がっているように感じており、自分が使用しているVimで実験的に対応し、それ以外の機能を優先対応している状況です。

既存エコシステムとのIntegration

シンプルなRubyスクリプトの場合は、bundler経由でrfmtをすぐに使えます。
それ以外の依存はありません。

linterが必要な環境ではRuboCopなどのツールが欲しくなると思います。
formatterとしてrfmtを使う場合、競合しないための設定が必要です。
Rubocopの場合は簡単で、Layout Copをfalseにすればformat機能はオフになり、別ツールとの競合は無くなります。

.rubocop.yml
# フォーマット系Copを無効化
Layout:
Enabled: false

https://docs.rubocop.org/rubocop/cops_layout.html

これから

機能追加やメンテナンス、Public Issueへの対応を進めていきます。

また、GitHub IssueやPullRequestにはテンプレートを用意していますので、気軽にcontributionしていただければと思います。
バグの対応などは優先的に行います。

rfmtがRubyエコシステムの選択肢の一つになり、高速なtoolchainの構築に自分も貢献していければ嬉しいです😚

have fun

Discussion