とある理由でassociationでクラス名を明示的に指定することをルール付けしたいなんてことがありました。今回は上記ルールを推奨するカスタムCopを作ってみました。
まずは成果物から。
以下のassociationがあったとします。
has_one :sample_association, dependent: :destroy
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を実行すると以下の警告が出てくれば成功です。
おわりに
思ったよりも簡単にカスタムCopを実装できました。口頭によるルールの伝承というのは時の流れと同時に陳腐化していくものなので、積極的に仕組み化していきたいものです。