僕がrubocopに送ったPRが v0.47.0 に取り込まれました。
個人的に便利機能だと思うのでこの場を借りて軽く紹介したいと思います。
Rails/ReversibleMigrationについて
Railsのmigrationファイルで change
メソッドの中に書いたmigrationコマンドがreversible *1かどうかをチェックするためのcopです
github.com
具体例
https://github.com/bbatsov/rails-style-guide#reversible-migration より抜粋
これだと drop_table :users
だけだと逆方向のmigration(create_table
)でどのようなカラムでテーブルを作ればいいか分からないのでreversibleではありません。rake db:rollback
や rake db:migrate:down
した時にエラーになります。
class DropUsers < ActiveRecord::Migration
def change
drop_table :users
end
end
こういう場合、upメソッドとdownメソッドでそれぞれmigrationを書いてあげないと rake db:rollback
や rake db:migrate:down
が正しく実行されません
class DropUsers < ActiveRecord::Migration
def up
drop_table :users
end
def down
create_table :users do |t|
t.string :name
end
end
end
http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table にも書いていますが、 drop_table
にブロックを渡すとchangeで逆方向のmigrationを補完する時に create_table
の引数扱いになりカラムが作られます。
class DropUsers < ActiveRecord::Migration
def change
drop_table :users do |t|
t.string :name
end
end
end
どうして作ったか?
慣れてくればreversibleなコマンドかどうかは Railsの気持ちになって考えれば だいたい推測はつきます。
例えば
add_column :users, :name, :string
の逆は
remove_column :users, :name
ですが、
remove_column :users, :name
の逆方向を考えた時にどんな型でカラムを作ればいいかの情報がこれだけでは推測できないのでreversibleじゃないといった感じです。
微妙に迷った時も全部 http://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html に書いてあるのでこれを読めば分かります。
しかしコードレビューの度にそれをいちいち指摘するのは面倒なので自動化するために Rails/ReversibleMigration を作成しました。
検出精度について
さっきのリファレンスを全部読んだ上でchangeメソッド内の下記を検出するようにしています
- 絶対にreversibleじゃないmigrationコマンド
- change_table
- change_table_comment
- execute
- remove_belongs_to
- 特定の条件を満たさないとreversibleじゃないmigrationコマンド
- drop_table
- change_column_default
- remove_column
- remove_foreign_key
ただし特例として reversibleブロックがある場合は reversibleであるとみなして検出対象外にしています。
詳細は下記参照
railsguides.jp
auto correctについて
rubocopのcopでは autocorrect
メソッドを定義すればauto correct時の実装を定義することができますが、down(逆方向)のmigrationは過去のmigrationファイルを探して人間が書く必要がありそうな気がしたのであえて実装していません。
Cop開発Tips
次回以降自分がrubocop触りやすくするために開発手順とか参照すべきソースをメモっときます。
前提知識
日本語だとここが参考になりました。
koic.hatenablog.com
tech.sideci.com
rubocop静的解析したいソースをruby-parseでS式を出す
ruby_parser というRubyのソースコードを解析するためのgemを使います。rubocopのソースをcloneしていればbundle installで入るので別途gem installする必要はありません。
例えばこんなファイルを作っといて
class ExampleMigration < ActiveRecord::Migration
def change
drop_table :users do |t|
t.string :name
end
drop_table :users
end
end
ruby-parseに食わせてS式を出します。
bundle exec ruby-parse /tmp/migration.rb
(class
(const nil :ExampleMigration)
(const
(const nil :ActiveRecord) :Migration)
(def :change
(args)
(begin
(block
(send nil :drop_table
(sym :users))
(args
(procarg0 :t))
(send
(lvar :t) :string
(sym :name)))
(send nil :drop_table
(sym :users)))))
rubocopのcopに on_def
や on_send
などのメソッドを定義することによって、rubocopで解析中にS式のその要素と対応するメソッドが実行されます。
今回の場合はメソッド呼び出し drop_table
や change_table
などのメソッド呼び出しをフックしたかったので on_sendメソッドを作りました。
binding.pryなどでとめてnodeの中身を確認
on系のメソッドに binding.pry
や binding.irb
を書いて
def on_send(node)
binding.pry
任意のspecを実行
$ bundle exec rspec -- spec/rubocop/cop/rails/reversible_migration_spec.rb:85
Run options:
include {:focus=>true, :locations=>{"./spec/rubocop/cop/rails/reversible_migration_spec.rb"=>[85]}}
exclude {:broken=>
Randomized with seed 63344
From: /Users/sue445/workspace/github.com/bbatsov/rubocop/lib/rubocop/cop/rails/reversible_migration.rb @ line 120 RuboCop::Cop::Rails::ReversibleMigration
119: def on_send(node)
=> 120: binding.pry
121: return unless within_change_method?(node)
122: return if within_reversible_block?(node)
123:
124: check_irreversible_schema_statement_node(node)
125: check_drop_table_node(node)
126: check_change_column_default_node(node)
127: check_remove_column_node(node)
128: check_remove_foreign_key_node(node)
129: end
[1] pry(
=> s(:send, nil, :drop_table,
s(:sym, :users))
[2] pry(
慣れていないうちは1回1回 binding.pry
で止めてnodeの中身を確認しつつデバッグしていくのがいいと思います。余談ですがデバッグに慣れていなさすぎて1日 1migrationコマンド対応が限界でした('A`)
S式にマッチさせるmatcherを書く
cop内に
def_node_matcher :drop_table_call, <<-END
(send nil :drop_table ...)
END
のようなmatcherを書くことによって、
drop_table_call(node) do
end
のように処理を挟むことができます。
matcherの書き方は https://github.com/bbatsov/rubocop/blob/master/lib/rubocop/node_pattern.rb のコメントを要熟読
matcherに $_
や $...
を書くことにより、マッチした文字列を変数として取得することもできます。
$_
は単一nodeを取得
def_node_matcher :remove_foreign_key_call, <<-END
(send nil :remove_foreign_key _ $_)
END
remove_foreign_key_call(node) do |arg|
if arg.hash_type?
add_offense(
node, :expression,
format(MSG, 'remove_foreign_key(without table)')
)
end
end
$...
は以降のnodeを全部取得
def_node_matcher :remove_column_call, <<-END
(send nil :remove_column $...)
END
remove_column_call(node) do |args|
if args.to_a.size < 3
add_offense(
node, :expression,
format(MSG, 'remove_column(without type)')
)
end
end