🌟

チームの決まり事を RuboCop のカスタムルールにしてみた

2024/10/24に公開

自分が所属している開発チームでは、データベースへ新しいカラムを追加する際に「そのカラムが何を意味するかをコメントで残す」というルールが設けてあります。
Rails でカラム作成時にコメントを追加するには、:column オプションを利用することになりますが、やはり人間ですので忘れてしまいますし、新しく入ってきたメンバーはそもそもルール自体を知りません。。

そこで、今回はタイトルにもあるようにカスタムルールを作成し、メンバー全員がルールを忘れていても、ルールが自動的に守られるような仕組みを導入してみました🎉

実装したコード

カスタムルールを作成するにあたって、以下の要件をもとに実装しました。

  • add_column メソッドを利用してカラムを追加しようとしているコード
  • create_table ブロック内でカラムを追加しようとしているコード(ただし、timestamps, references は例外として許容します)
# 例
class Test < ActiveRecord::Migration[7.2]
  def change
    add_column :users, :name, :string # => 🙅‍♂️ コメントがないためNG
    add_column :users, :age,  :number, comment: '年齢' # => 🙆‍♂️ コメントがあるためOK

    create_table :blogs do |t|
      t.references :user, null: false, foreign_key: true # => 🙆‍♂️ referencesはOK
      t.string :title, null: false # => 🙅‍♂️ コメントがないためNG
      t.text   :content, null: false, comment: 'ブログの内容' # => 🙆‍♂️ コメントがあるためOK

      t.timestamps # => 🙆‍♂️ timestampsはOK
    end
  end
end

そして、 Rails プロダクト内の lib ディレクトリへカスタムルールの実装を追加しました!

lib/rubocop/cop/style/column_comment_checker.rb
module Rubocop
  module Cop
    module Style
      class ColumnCommentChecker < RuboCop::Cop::Base
        MSG = 'カラムを追加する際は :comment オプションを使用して、コメントを必ず記述してください。'

        def_node_matcher :create_table_block?, <<~PATTERN
          (block
            (send nil? :create_table ...)  # メソッド呼び出し部分
            (args (arg $_))                # ブロック引数部分 (引数名をキャプチャ 例 t)
            _                              # ブロック本体(無視)
          )
        PATTERN

        def_node_matcher :add_column_definition?, <<~PATTERN
          (send nil? :add_column _ _ _ ...)
        PATTERN

        def_node_matcher :migration_column_definition?, <<~PATTERN
          (send (lvar %1) !{:references :timestamps} _ ...) # references と timestamps はコメント不要
        PATTERN

        def_node_matcher :has_comment_option?, <<~PATTERN
          (send ... (hash <(pair (sym :comment) (str _)) ...>))
        PATTERN

        def on_block(node)
          create_table_block?(node) do |table_variable|
            node.each_descendant(:send) do |send_node|
              migration_column_definition?(send_node, table_variable) do
                unless has_comment_option?(send_node)
                  add_offense(send_node, message: MSG)
                end
              end
            end
          end
        end

        def on_send(node)
          add_column_definition?(node) do
            unless has_comment_option?(node)
              add_offense(node, message: MSG)
            end
          end
        end
      end
    end
  end
end
テストコード
require 'rails_helper'

RSpec.describe Rubocop::Cop::Style::ColumnCommentChecker do
  subject(:cop) { described_class.new }

  context 'add_column 使用時に :comment オプションが指定されていない場合' do
    it '違反を検出すること' do
      expect_offense(<<~RUBY)
        add_column :users, :name, :string
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Style/ColumnCommentChecker: カラムを追加する際は :comment オプションを使用して、コメントを必ず記述してください。
      RUBY
    end
  end

  context 'add_column 使用時に :comment オプションが指定されている場合' do
    it '違反を検出しないこと' do
      expect_no_offenses(<<~RUBY)
        add_column :users, :name, :string, comment: 'User name', null: false
      RUBY
    end
  end

  context 'create_table 使用時に :comment オプションが指定されていない場合' do
    it '違反を検出すること' do
      expect_offense(<<~RUBY)
        create_table :users do |t|
          t.string :name
          ^^^^^^^^^^^^^^ Style/ColumnCommentChecker: カラムを追加する際は :comment オプションを使用して、コメントを必ず記述してください。
        end
      RUBY
    end

    context ':references と :timestamps が指定されている場合' do
      it '違反を検出しないこと' do
        expect_no_offenses(<<~RUBY)
          create_table :users do |tb|
            tb.references :user, null: false
            tb.timestamps
          end
        RUBY
      end
    end
  end

  context 'create_table 使用時に :comment オプションが指定されている場合' do
    it '違反を検出しないこと' do
      expect_no_offenses(<<~RUBY)
        create_table :users do |t|
          t.string :name, comment: 'User name'
        end
      RUBY
    end
  end
end

最後に rubocop.yml へ設定を追加してカスタムルールが適用されるようにします。

rubocop.yml
require:
 ...
  - './lib/rubocop/cop/style/column_comment_checker'

Style/ColumnCommentChecker:
  Exclude:
    - 'db/schema.rb' # => schema.rb は除外

記載してあった例題に対して実際に RuboCop を実行すると期待通りに動いていることが分かります🎉

実装するにあたって参考にした資料

https://sinsoku.hatenablog.com/entry/2018/04/24/022911
https://moneyforward-dev.jp/entry/2021/09/02/rubocop/
https://docs.rubocop.org/rubocop/development.html

そして実装するにあたって、大変助かったのが NodePattern Debugger という解析ツールです!!
https://nodepattern.herokuapp.com/

カスタムルールを作成していくにあたって、基本的には対象の Ruby コードを RuboCop::ProcessedSource を使用して AST に変換、その AST に対して NodePattern を利用して指定した正規表現に対して期待通りにマッチするかを確認していく形になるかと思われます。
しかし、このツールを使用することで GUI 上で簡単に確認が行えるので実装が捗りました!

最後に

今回、RuboCop のルールを初めて自作したことで、少しですが RuboCop の仕組みを知ることができて大変勉強になりました。

しくみのテックブログ

Discussion