RuboCop
は、Rubocop::Cop::Cop
を継承することで独自のルールを追加することできるようで、
レビューでよくコメントもらう内容等、RuboCop
で検知できるようになったら便利だなと思い、
ルールを作成してみたので、そのへんの手順等をMEMOしておきます✍
公式の開発用のDocはこちら📝
今回作ったもの
今回は下記のようなことを検証するルールを作ってみました🤖
SQLの実行回数削減のため、pluck
を使っていたらselect
を使うことをサジェストする
# bad Foo.where(id: Bar.pluck(:foo_id)) # good Foo.where(id: Bar.select(:foo_id))
where
内でLIKE
を使っていたらsanitized_sql_like
を使うことをサジェストする
# bad Foo.where('title LIKE ?', "%#{title}%") # good Foo.where('title LIKE ?', "%#{sanitized_sql_like(title)}%")
作ったものは下記のリポジトリにアップしています。
新しいルールを作るのに必要なこと
新しいルール作ってrubocop
実行時に静的解析を行うようにするには下記が必要なようです👀
RuboCop::Cop::Cop
を継承したClassを作るRuboCop::Cop::Cop
を継承したClassのテストを書く- 作成したClassを
.rubocop.yml
でrequire
する
RuboCop::Cop::Cop
を継承したClassを作る
今回、私が実際に作ったClassは下記のような形です。
下記は、SQLの実行回数削減のため、pluck
を使っていたらselect
を使うことをサジェストするルールです。
require 'rubocop' module CustomCops # @example # # bad # Foo.where(id: Bar.pluck(:foo_id)) # # # good # Foo.where(id: Bar.select(:foo_id)) class UseSelectInWhere < RuboCop::Cop::Cop MSG = 'Use select instead of pluck in where to use subqueries to reduce SQL executions.' def_node_matcher :where_in_pluck_candidate?, <<~PATTERN (send _ :where (:hash (:pair (:sym _) (send _ :pluck (:sym _)) ...))) PATTERN def on_send(node) where_in_pluck_candidate?(node) do add_offense(node) end end def autocorrect(node) # TODO end end end
ちょっと解説すると
MSG
に設定された値が違反時にメッセージとして表示されるようです。def_node_matcher
でwhere
で{ sym: 任意のオブジェクト.pluck}
のような形で引数に渡しているか解析するmatherwhere_in_pluck_candidate?
を定義しています。※このmatherは抜け漏れがあるかも。。。where_in_pluck_candidate?
に合致するようなものがあったらadd_offense
で警告を出しています。- 今回は
TODO
にしてしまっていますが、autocorrect
に処理を書くとautocorrect
の処理を定義出来ます。
RuboCop::Cop::Cop
を継承したClassのテストを書く
先程作ったClassのテストは下記のような形で行うようにしました。
RuboCop::RSpec::ExpectOffense
をincludeしてexpect_offense
に、コードとメッセージを渡してあげるような形でテストするようです👀
※expect_offense
内の記載内容をいい感じに取得する方法がわからなかったので、とりあえずテストを実行して失敗したときの値が、それっぽかったらコピペするような形にしたが、もっといい方法がありそう。。。
require 'rubocop' require 'rubocop/rspec/support' RSpec.configure do |config| config.include(RuboCop::RSpec::ExpectOffense) end describe CustomCops::UseSelectInWhere do subject(:cop) { described_class.new } it 'raised offense when use `pluck` in `where`.' do expect_offense(<<-RUBY) Foo.where(id: Bar.pluck(:foo_id)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use select instead of pluck in where to use subqueries to reduce SQL executions. RUBY end end
作成したClassを.rubocop.yml
でrequire
する
.rubocop.yml
で先程作ったclassのファイルをrequire
することでルールを追加します。
require: './cops/use_select_in_where'
rubocop
実行時に静的解析されるようになりました🙌
$ be rubocop sample/ sample/use_select_in_where.rb:4:23: C: CustomCops/UseSelectInWhere: Use select instead of pluck in where to use subqueries to reduce SQL executions. scope :recent, -> { Foo.where(id: Bar.pluck(:foo_id)) } ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
おわりに
レビューでよくコメントが入る内容や、プロジェクト独自で拡張したクラスを使う必要がある場合等、rubocopの独自ルールとして定義しておくと、レビュー時の抜け漏れ等が防げて良さそうですね✨
追記: RuboCop Version 1.0で作り方が変わるかも
Version 1.0に向けてCop周りの仕様の見直しが結構入っているようなので、これから作るときは下記を参考にしたほうが良さそう👀
rubocop/v1_upgrade_notes.adoc at master · rubocop-hq/rubocop · GitHub