なんて美しい夕陽だ🌇

プログラミング、日常、忘れないように書いていきます

Railsでassociationにクラス名が指定されていないときに警告を出すカスタムCopを作ってみた

とある理由でassociationでクラス名を明示的に指定することをルール付けしたいなんてことがありました。今回は上記ルールを推奨するカスタムCopを作ってみました。

まずは成果物から。

以下のassociationがあったとします。

has_one :sample_association, dependent: :destroy

RuboCopを実行すると以下の警告が出てくるというものです。

rubocopの実行例

クラス名を明示的に指定することで警告は無くなります。

has_one :sample_association, class_name: "Sample", dependent: :destroy

今回はカスタムCopを作る方法を書き残していこうと思います。

目次

1. カスタムCopファイルを作成する

今回はコップ名を EnforceAssociationClassName とし 、lib/custom_cops/ 配下に enforce_association_class_name.rb のファイルを作成しました。

カスタムCopのテンプレートを作成するGemとして、rubocop-extension-generatorも用意されていますが、今回のように1つのルールを追加するような軽いものであれば、手動でファイルを作ってしまっても問題なさそうです。

2. 処理を書く

カスタムCopの処理を書いていきます。実際に書いたコードを以下に掲載します。

# frozen_string_literal: true

module CustomCops
  # This cop ensures `class_name` is specified in ActiveRecord associations.
  #
  # @example
  #   # bad
  #   has_many :items
  #
  #   # good
  #   has_many :items, class_name: 'Item'
  #
  class EnforceAssociationClassName < RuboCop::Cop::Base
    MSG = 'Explicitly specify `class_name` for `%<association>s` associations.'

    def_node_search :association_without_class_name, <<~PATTERN
      (send nil? {:has_one :has_many :belongs_to} (sym _) ...)
    PATTERN

    def on_send(node)
      association_without_class_name(node) do |assoc_node|
        association_method = assoc_node.method_name
        unless class_name_option?(assoc_node)
          add_offense(assoc_node, message: format(MSG, association: association_method))
        end
      end
    end

    private

    def class_name_option?(node)
      node.arguments.each do |arg|
        if arg.hash_type?
          pairs = arg.pairs
          return true if pairs.any? { |pair| pair.key.value == :class_name }
        end
      end
      false
    end
  end
end

いくつかポイントがあります。

  • RuboCop::Cop::Base を継承する
  • MSG
    • 警告時に出力されるメッセージ
  • def_node_search
    • マッチさせるノードパターンを書く
  • on_send
    • sendノードのコールバック関数
    • メソッド呼び出しのたびに実行される

3. Rubocopを実行してみる

以下のようにassociationを設定します。

has_one :sample_association, dependent: :destroy

RuboCopを実行すると以下の警告が出てくれば成功です。

rubocopの実行例

おわりに

思ったよりも簡単にカスタムCopを実装できました。口頭によるルールの伝承というのは時の流れと同時に陳腐化していくものなので、積極的に仕組み化していきたいものです。

参考

docs.rubocop.org

developers.bookwalker.jp