RAKSUL TechBlog

RAKSULグループのエンジニアが技術トピックを発信するブログです

レビュー漏れ防止のためにRuboCopカスタムCopを作った

この記事はノバセル テクノ場 出張版2025 Advent Calendar 2025の23日目の記事です。

はじめに

ノバセルでエンジニアをしている原です。

本記事では、マイグレーションファイルで外部キー制約が使われていないかをチェックするRuboCopカスタムCopの実装方法を紹介します。

なぜ作ったのか

今開発しているプロダクトでは外部キー制約をつけないルールにしています(外部キー制約の是非についてはここでは語りません)。

コードレビューの際に気をつけてはいるものの、うっかり見逃してしまうケースがありました。RuboCopでチェックできないかと探してみたのですが、ちょうどいいCopが見つかりませんでした(実はあるかもしれませんが)。

ないなら作ってしまえ、ということで自作しました。

やりたいこと

プロジェクトのルールとして、マイグレーションファイルで以下を禁止したいケースがあります。

  • add_foreign_key メソッドの使用
  • referencesadd_referenceforeign_key: true オプション

これをコードレビューで毎回チェックするのは手間なので、RuboCopで自動検出できるようにします。

RuboCop Copの仕組み

実装に入る前に、RuboCopのカスタムCopがどう動くか簡単に説明します。

ASTとノード

RuboCopはRubyのソースコードをAST(抽象構文木)に変換して解析します。例えば以下のコード:

t.references :user, foreign_key: true

は、こんなASTになります:

(send
  (lvar :t) :references
  (sym :user)
  (hash
    (pair
      (sym :foreign_key)
      (true))))

sendはメソッド呼び出しを表すノードタイプです。

on_sendコールバック

on_sendメソッドを定義すると、RuboCopがASTを走査する際に、sendノード(メソッド呼び出し)を見つけるたびに呼び出されます。

def on_send(node)
  # nodeにはメソッド呼び出しの情報が入っている
  node.method_name  # => :references
  node.arguments    # => 引数のノード配列
end

他にも on_classon_defon_hash など、ノードタイプごとにコールバックがあります。

def_node_matcher

RuboCopには def_node_matcher というDSLがあり、ASTのパターンマッチングを簡潔に書けます。

def_node_matcher :add_foreign_key?, '(send nil? :add_foreign_key ...)'

このパターンの意味:

部分 意味
send メソッド呼び出しノード
nil? レシーバがない(add_foreign_key :table のような形式)
:add_foreign_key メソッド名が add_foreign_key
... 引数は何でもOK

定義すると add_foreign_key?(node) というメソッドが使えるようになり、マッチすれば truthy、しなければ nil を返します。

これを踏まえて実装を見ていきます。

実装

# frozen_string_literal: true

module RuboCop
  module Cop
    module Custom
      class MigrationForeignKey < ::RuboCop::Cop::Base
        # Node Patternの定義
        # nil? はレシーバなし(add_foreign_key :table)、... は引数不問
        def_node_matcher :add_foreign_key?, '(send nil? :add_foreign_key ...)'

        def on_send(node)
          # .rubocop.yml の Include で対象を絞っているなら
          # processed_source のチェックは削除してOKです。

          if add_foreign_key?(node)
            add_offense(node, message: 'add_foreign_keyメソッドの使用は禁止されています。')
            return
          end

          check_foreign_key_option(node)
        end

        private

        def check_foreign_key_option(node)
          # references や add_reference などのメソッドの引数を走査
          node.arguments.each do |arg|
            next unless arg.hash_type?

            arg.pairs.each do |pair|
              # key が :foreign_key かつ value が false 以外なら違反
              next unless pair.key.sym_type? && pair.key.value == :foreign_key
              next if pair.value.false_type?

              add_offense(pair, message: 'foreign_keyオプションはfalseにする必要があります。')
            end
          end
        end
      end
    end
  end
end

設定

ファイル配置

lib/rubocop/cop/custom/migration_foreign_key.rb に配置します。

.rubocop.yml

require:
  - ./lib/rubocop/cop/custom/migration_foreign_key

Custom/MigrationForeignKey:
  Enabled: true
  Include:
    - db/migrate/**/*

検出例

以下のようなマイグレーションファイルがあった場合:

class CreateOrders < ActiveRecord::Migration[7.0]
  def change
    create_table :orders do |t|
      t.references :user, foreign_key: true  # ← 検出される
      t.timestamps
    end

    add_foreign_key :orders, :shops  # ← 検出される
  end
end

rubocop を実行すると警告が出ます。

db/migrate/20241223000000_create_orders.rb:4:29: C: Custom/MigrationForeignKey: foreign_keyオプションはfalseにしてください。
      t.references :user, foreign_key: true
                          ^^^^^^^^^^^^^^^^^
db/migrate/20241223000000_create_orders.rb:8:5: C: Custom/MigrationForeignKey: add_foreign_keyメソッドの使用は禁止されています。
    add_foreign_key :orders, :shops
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

まとめ

RuboCopを動かすたびに内部でどうやってチェックしているのか疑問でしたが、思いのほか簡単に自作できました。ASTとコールバックの仕組みさえ理解すれば、プロジェクト固有のルールもすぐに実装できます。

他にもレビュー漏れが発生しやすいものについては、同様にカスタムCopを作ってチェックするようにしています。

最近はAIに「こういうチェックをするRuboCop Copを作って」と頼めばサクッと作ってくれるので、欲しいCopが見つからなければ自分で作ってみるのもおすすめです。