RuboCopの新しいルールを追加する方法(Custom Copの作り方)

何度もレビューで指摘することはRuboCopのルールとして追加して、自動化しておくと便利です。

この記事では、RuboCopで新しいルールを作る方法を紹介します。

参考にしたページ

これがとても参考になった。

github.com

Copの書き方は独特なので、RuboCopリポジトリlib/rubocop/cop ディレクトリにある他Copをいくつか読むと参考になります。

github.com

Custom Cop の作り方

ここでは例としてMatzの名前を "Hiroyuki Matsumoto" と間違って書いたとき、検知できるCopを作ってみます。

まずは .rubocop.ymlrequire の行を追加します。

require: './lib/custom_cops/matz_name'

AllCops:
  # 以下略...

次に、 lib/custom_cops/matz_name.rb に新しいルールを書きます。

# frozen_string_literal: true

module CustomCops
  # @example
  #   # bad
  #   MATZ_NAME = "Hiroyuki Matsumoto"
  #
  #   # good
  #   MATZ_NAME = "Yukihiro Matsumoto"
  class MatzName < RuboCop::Cop::Cop
    BAD_NAME = "Hiroyuki Matsumoto"
    GOOD_NAME = "Yukihiro Matsumoto"
    MSG = "Use '#{GOOD_NAME}' instead of '#{BAD_NAME}'."

    def on_str(node)
      add_offense(node) if node.source.include?(BAD_NAME)
    end

    def autocorrect(node)
      ->(corrector) {
        new_code = node.source.gsub(BAD_NAME, GOOD_NAME)
        corrector.replace(node.source_range, new_code)
      }
    end
  end
end

いくつか独特なルールがあるので、簡単に説明します。

on_xxx メソッドはRubyのコード内で該当するコードが見つかった時に呼ばれるメソッドです。 たくさんあるので興味ある人はRuboCopリポジトリlib/rubocop/ast/traversal.rbを読むと良いです。 基本的には「それっぽいメソッドを定義して、テスト書いてみて試す」とか「他Copの実装をパクる」で頑張ります。

add_offenseはルール違反を検知した時に呼び出します。 引数に渡している node に対して、MSG 定数の文字列をメッセージとして表示します。 メッセージを動的に変えたい場合は message オプションで変える事もできます。

add_offense(node, message: "foo")

autocorrect メソッドは実装すると --auto-correct に対応することができます。 コード例のように proc を返すようにする必要があります。

テストの書き方

spec/rails_helper.rb で下記のようにCustom Copを読み込むコード、RSpecのヘルパーメソッドの読み込みを行います。

require "rubocop"
Dir[Rails.root.join("lib/custom_cops/**/*.rb")].each { |f| require f }
require "rubocop/rspec/support"

RSpec.configure do |config|
  config.include(RuboCop::RSpec::ExpectOffense)
end

RuboCopリポジトリのテストコードを参考にしながら、コードを書きます。

# frozen_string_literal: true

require "rails_helper"

RSpec.describe CustomCops::MatzName do
  subject(:cop) { described_class.new }

  it "registers an offense" do
    expect_offense(<<-RUBY.strip_indent)
      MATZ_NAME = "Hiroyuki Matsumoto"
                  ^^^^^^^^^^^^^^^^^^^^ Use 'Yukihiro Matsumoto' instead of 'Hiroyuki Matsumoto'.
    RUBY
  end

  it "autocorrects" do
    new_source = autocorrect_source('MATZ_NAME = "Hiroyuki Matsumoto"')
    expect(new_source).to eq 'MATZ_NAME = "Yukihiro Matsumoto"'
  end
end

できればRuboCopにプルリクを投げよう

もし有用なCopを実装できたら、RuboCopにプルリクを投げると良いです。

  • RuboCopのメンバーがコードレビューしてくれる
  • 世界中のRuboCopユーザーがIssue挙げたり、メンテしてくれる

などのメリットがあります。

普段からRuboCopを便利に使わせて頂いているので、たまには恩返しとしてプルリクを送りたいですね。

おわり

「人間が何度もレビューで指摘する」は真面目で良いことだとは思いますが、怠惰力が足りないです。

機械的にレビューできる箇所は機械に任せ、我々は人間にしかできない「自動化の難しいところのレビュー」や「ビールを飲みながらアニメを観る」などを頑張りたい。