🌟
チームの決まり事を RuboCop のカスタムルールにしてみた
自分が所属している開発チームでは、データベースへ新しいカラムを追加する際に「そのカラムが何を意味するかをコメントで残す」というルールが設けてあります。
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 を実行すると期待通りに動いていることが分かります🎉
実装するにあたって参考にした資料
そして実装するにあたって、大変助かったのが NodePattern Debugger という解析ツールです!!
カスタムルールを作成していくにあたって、基本的には対象の Ruby コードを RuboCop::ProcessedSource を使用して AST に変換、その AST に対して NodePattern を利用して指定した正規表現に対して期待通りにマッチするかを確認していく形になるかと思われます。
しかし、このツールを使用することで GUI 上で簡単に確認が行えるので実装が捗りました!
最後に
今回、RuboCop のルールを初めて自作したことで、少しですが RuboCop の仕組みを知ることができて大変勉強になりました。
Discussion