Ruby

Synvertでお手軽リファクタリング

 

今回はリファクタリングツール(≒ コード検索・置換ツール)であるSynvertについて紹介したいと思います。Synvertを使うことでリファクタリング作業をより効率化できると感じたため、まだ使ったことがない方はぜひ試してみてください。

この記事は先日社内LTで発表した資料を元に作成しています。

(前置き)RubyKaigiでの該当セッション

RubyKaigiセッション

 

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

Synvertとは

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を使う方法

 

Synvertは以下のような形で提供されており、用途に合わせた使い方ができます。

  1. デスクトップアプリ(MacとWindowsの両方に対応)
  2. PlayGround
  3. CLI(公式提供のSnippetあり)
  4. VSCode拡張

どういった場面で使えるか

どういった場面で使えるか

 

主には以下のような場面でSynvertを使うことができます。

  1. コードベースのアップグレード
  2. コードスタイルの統一
  3. カスタムコードの変換

コードベースのアップグレード

RubyやRailsのバージョンアップなど、古いコードを最新の仕様に合わせる必要がある場合、Synvertを使用することで置き換え作業を効率化することができます。

Synvertの開発者はRailsのアップグレード作業を楽にするため、このツールを作成しました。

コードスタイルの統一

プロジェクト内のコードスタイルや命名規則の統一を行いたい場合は、Synvertを使用してコードベース全体にわたるスタイルの変更や修正を自動化することができます。

カスタムコードの変換

Synvertは柔軟なカスタマイズが可能であり、独自のルールやパターンに基づいた修正やリファクタリングを定義することができます。プロジェクト固有のルールやコーディングガイドラインに合わせてSynvertをカスタマイズすることができます。

RuboCopでも良いのでは?

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

 

PlayGroundにアクセスしたら、まずは上のメニューから「Generate Snippet」を選択し、スニペットを生成画面に遷移します。

左のInputsに置換前のサンプルコードを、右のOutputsに置換後のサンプルコードを記載します。その後、右下にある「Generate Snippet」をクリックします。

すると画面下にSnippetが自動で生成されます。

 

Parse Snippet

 

次に上のメニューから「Parse Snippet」を選択します。

画面上部の「Input Source Code:」にリファクタリングを実行したいコードを、画面中央部の「Synvert Snippet:」に先ほど生成したSnippetを貼り付けます。

すると画面下部にリファクタリングされたコードが自動生成されます。

このように、PlayGroundではSynvertを簡単に試すことができるので便利です。

既存アプリケーションに適用させる場合

既存アプリケーションに適用させる場合

 

とはいえ、部分的にリファクタリングするのではなく、既存のアプリケーション全体にリファクタリングを適用させたい場合の方が多いと思います。

その場合はSnippetが公式で用意されているためCLIの使用が便利になります。以下のコマンドをリファクタリングしたいリポジトリで実行することで実現可能です。

% gem install synvert
% synvert-ruby --run ruby/hash_short_syntax

 

公式提供Snippet

 

今回はハッシュのショートハンド構文への置き換えのみ実行しましたが、公式では以下のようなSnippetが用意されているので、ドキュメントに目を通してみてください。

  1. ruby/gsub_to_tr
    gsubで1文字の置換を行なっている箇所をtrに置き換える
  2. factory_bot/convert_factory_girl_to_factory_bot
    FactoryGirlからFactoryBotへの置き換え
  3. rspec/stub_and_mock_to_double
    stubとmockをdoubleメソッドに置き換える
  4. rails/convert_rails_logger
    RAILS_DEFAULT_LOGGERをRails.loggerに置き換える

まとめ

まとめ

 

RubyやRailsのアップグレードやリファクタリングなど、ASTベースでコードの検索や置換を行いたいが、独自のCopを実装するまでもない場合はSyvertの使用がおすすめです。

参考

 

今回は「Synvertでお手軽リファクタリング」というタイトルでSynvertについて紹介しました。RuboCopとSynvertを組み合わせることで、リファクタリング作業がより効率化できそうと感じたため、今後リファクタリングする機会があれば積極的に使っていきたいと思います。