今回はリファクタリングツール(≒ コード検索・置換ツール)であるSynvertについて紹介したいと思います。Synvertを使うことでリファクタリング作業をより効率化できると感じたため、まだ使ったことがない方はぜひ試してみてください。
\💎 #RubyKaigi 2023 LT会 with Matz💎/#メドピア の技術顧問でもある
Matzさん( @yukihiro_matz )にご参加いただき
RubyKaigi 2023にまつわる社内LTを開催!3名が発表し、Matzさんにその場で講評や
深掘りトークをしていただきました👏👏🎉#エンジニア
🟠ひろこ🟠 pic.twitter.com/78DjmI1CHK— メドピア採用担当みどり&ひろこ (@MedPeer_R) June 1, 2023
(前置き)RubyKaigiでの該当セッション

今回の資料は、RubyKaigi Day3 Richard HuangのFind and Replace Code based on ASTというセッションを元に作成しています。
Synvertとは

SynvertとはAST(Abstract Syntax Tree / 抽象構文木)ベースでコード検索や置換を行うことができるツールです。これにより、簡潔なコードへの変換や共通コードの抽象化など、様々なリファクタリングを実現することができます。
Synvertの特徴としては以下の3点が挙げられます。
- 正確な検索
- 複雑な置換
- 自動化
正確な検索
SynvertはRubyコードをパースしているため、正規表現等を使った検索よりも正確です。例えば、以下のように文字列リテラルの中の puts 'Hello'
は検索に含めないといったことが可能です。
# 検索に含める
puts 'Hello'
# 検索に含める
puts('Hello')
# 検索に含めない
TEXT<<
puts 'Hello'
TEXT
複雑な置換
Synvertは柔軟なカスタマイズが可能なため、Hashの古いrocket syntaxを置換するといった複雑な置換が可能です。(rocket syntaxを使っているところはないと信じたい)
# 置換前
:name => 'rocket'
# 置換後
name: 'rocket'
自動化
Synvertは設定ファイル(Snippet)に基づいてコードを自動的に変換するため、手作業で行う必要があったリファクタリング作業を自動化することができます。
# Snippet例
Synvert::Rewriter.new 'ruby', 'block_to_yield' do
configure(parser: Synvert::PARSER_PARSER)
description <<~EOS
It converts block to yield.
```ruby
def slow(&block)
block.call
end
```
=>
```ruby
def slow
yield
end
```
EOS
within_files Synvert::ALL_RUBY_FILES do
# def slow(&block)
# block.call
# end
# =>
# def slow
# yield
# end
find_node '.def[arguments INCLUDES &block]' do
if node.arguments.size > 1
delete 'arguments.last', and_comma: true
else
delete :arguments, :parentheses
end
find_node '.send[receiver=block][message=call]' do
delete :receiver, :dot
replace :message, with: 'yield'
end
end
end
end
Synvertを使う方法

Synvertは以下のような形で提供されており、用途に合わせた使い方ができます。
- デスクトップアプリ(MacとWindowsの両方に対応)
- PlayGround
- CLI(公式提供のSnippetあり)
- VSCode拡張
どういった場面で使えるか

主には以下のような場面でSynvertを使うことができます。
- コードベースのアップグレード
- コードスタイルの統一
- カスタムコードの変換
コードベースのアップグレード
RubyやRailsのバージョンアップなど、古いコードを最新の仕様に合わせる必要がある場合、Synvertを使用することで置き換え作業を効率化することができます。
コードスタイルの統一
プロジェクト内のコードスタイルや命名規則の統一を行いたい場合は、Synvertを使用してコードベース全体にわたるスタイルの変更や修正を自動化することができます。
カスタムコードの変換
Synvertは柔軟なカスタマイズが可能であり、独自のルールやパターンに基づいた修正やリファクタリングを定義することができます。プロジェクト固有のルールやコーディングガイドラインに合わせてSynvertをカスタマイズすることができます。
RuboCopでも良いのでは?

ここまででSynvertではなくRuboCopでも良いのでは?と思った方もいると思います。実際、RuboCopでもASTベースでコード検索や置換ができるので、独自Copを実装すればSynvertと同じようなことは実現できます。
ただSynvertの良いところとしては、エディタ等から通常のコード検索を行うような感覚で使えるので、単発のコード検索や置換を行う場面で活躍します。
リファクタリング例

実際にSynvertを使ったリファクタリング例を挙げてみます。
今回はRuby3.1以前のハッシュ構文から、Ruby3.1以降に追加されたハッシュリテラルのショートハンド構文への置き換えを試してみます。
# Ruby3.1以前
# ハッシュのキー名と値が同じ場合でも値を書かなければならない
a = 1
b = 2
C = { a: a, b: b }
#=> {:a=>1, :b=>2 }
c = { "a": a, "b": b }
#=> {:a=>1, :b=>2 }
# Ruby3.1以降
# ハッシュのキー名と値が同じ場合は値を書かずにキー名だけで識別できる
a = 1
b = 2
C = { a:, b: }
#=> {:a=>1, :b=>2 }
c = { "a":, "b": }
#=> Raises SyntaxError

ここでは簡単にリファクタリングを試したいので、PlayGroundを使用します。

PlayGroundにアクセスしたら、まずは上のメニューから「Generate Snippet」を選択し、スニペットを生成画面に遷移します。
左のInputsに置換前のサンプルコードを、右のOutputsに置換後のサンプルコードを記載します。その後、右下にある「Generate Snippet」をクリックします。
すると画面下にSnippetが自動で生成されます。

次に上のメニューから「Parse Snippet」を選択します。
画面上部の「Input Source Code:」にリファクタリングを実行したいコードを、画面中央部の「Synvert Snippet:」に先ほど生成したSnippetを貼り付けます。
すると画面下部にリファクタリングされたコードが自動生成されます。
このように、PlayGroundではSynvertを簡単に試すことができるので便利です。
既存アプリケーションに適用させる場合

とはいえ、部分的にリファクタリングするのではなく、既存のアプリケーション全体にリファクタリングを適用させたい場合の方が多いと思います。
その場合はSnippetが公式で用意されているためCLIの使用が便利になります。以下のコマンドをリファクタリングしたいリポジトリで実行することで実現可能です。
% gem install synvert
% synvert-ruby --run ruby/hash_short_syntax

今回はハッシュのショートハンド構文への置き換えのみ実行しましたが、公式では以下のようなSnippetが用意されているので、ドキュメントに目を通してみてください。
- ruby/gsub_to_tr
gsubで1文字の置換を行なっている箇所をtrに置き換える - factory_bot/convert_factory_girl_to_factory_bot
FactoryGirlからFactoryBotへの置き換え - rspec/stub_and_mock_to_double
stubとmockをdoubleメソッドに置き換える - rails/convert_rails_logger
RAILS_DEFAULT_LOGGERをRails.loggerに置き換える
まとめ

RubyやRailsのアップグレードやリファクタリングなど、ASTベースでコードの検索や置換を行いたいが、独自のCopを実装するまでもない場合はSyvertの使用がおすすめです。
参考
- xinminlabs/synvert-ruby
- Synvert
- Synvert PlayGround
- Synvert Ruby CLI
- Synvert Visual Studio Code
- RubyKaigi Find and Replace Code based on AST
今回は「Synvertでお手軽リファクタリング」というタイトルでSynvertについて紹介しました。RuboCopとSynvertを組み合わせることで、リファクタリング作業がより効率化できそうと感じたため、今後リファクタリングする機会があれば積極的に使っていきたいと思います。