はじめて RuboCop Custom Cop を作って gem も公開してみた
はじめに
RuboCop の Extension/Custom Cop を初めて作って gem として公開してみました。
作ってて色々引っかかったりしたことがあったので、作り方などをまとめておきます。
目次
- はじめに
- 目次
- 大まかな手順
- rubocop-extension-generator をインストールしてテンプレートを生成する
- Cop の作成
- gem として公開してみる
- 余談: rubocop-extension-generator の在り処を見つけるまで
大まかな手順
RuboCop Extension/Custom Cop 作成
- rubocop-extension-generator をインストール
$ rubocop-extension-generator rubocop-your_own_extension_name
を実行して Rubocop Custom Cop gem のテンプレートを生成- ここで指定する
rubocop-your_own_extension_name
の中に複数の Cop が入る。 - 例:
rubocop-rails
であればyour_own_extension_name
はrails
で、 Rails に関する Cop をまとめられるイメージ。
- ここで指定する
$ rake 'new_cop[YourOwnExtensionName/YourOwnCopName]'
を実行して Cop を作成する- Cop を実装、
config/default.yml
を適宜修正
gem 公開
rubocop-extension-generator をインストールしてテンプレートを生成する
まずは rubocop-extension-generator
をインストールします。上記リポジトリの README の通りです。
$ gem install rubocop-extension-generator
インストールしたらテンプレートを生成します。
your_own_extension_name
の部分を作りたい Extension の名前に置き換えてください。
$ rubocop-extension-generator rubocop-your_own_extension_name
rubocop
と Extension 名の間の区切りは -
、Extension 名の単語間の区切りは _
を使います。
ここで生成している Extension は複数の Cop を束ねるような概念であることに気をつけましょう。
(具体的な Cop 名を入れてしまうと後で Cop 名と Extension 名が重複して悲しい感じになります)
Cop の作成
テンプレートができたら Cop を作成します。以下の Rake タスクを実行して Cop ファイルを作ります。
$ rake 'new_cop[YourOwnExtensionName/YourOwnCopName]'
実行すると
config/default.yml
lib/rubocop/cop/your_own_extension_name_cops.rb
lib/rubocop/cop/your_own_extension_name/your_own_cop_name.rb
spec/rubocop/cop/your_own_extension_name/your_own_cop_name_spec.rb
などのファイルが作成されます。
lib/rubocop/cop/your_own_extension_name/your_own_cop_name.rb
がメインになる、 Cop を実装していくファイルになります。
Cop の動作確認は単体だとやりにくいので、一緒に生成された spec/rubocop/cop/your_own_extension_name/your_own_cop_name_spec.rb
を使いながら挙動が意図通りになっているか確認すると
ここから先、実際の Cop の実装は RuboCop の公式ページの Development セクションを参考にしながら進めます。 AST を見ながら Cop を組んでいくので、 Node Pattern セクションなども参照すると良いと思います。
また、実際の Cop ファイルを見ると色々と書き方が学べるのでそれもおすすめです。私は Extensions マニュアル内の Custom Cops セクションにある他の Extension のコードを参考にしました。
lib/rubocop/your_extension_name.rb
でエラーが発生する
最初にちょっとハマりかけたところ。生成されたファイルそのままで bin/console
を実行しようとすると lib/rubocop/your_extension_name.rb
でエラーが発生して実行不能に陥りました。
デフォルトで生成される your_extension_name.rb
は以下のようなものになります。
require 'rubocop/google_ads/version' module RuboCop module GoogleAds class Error < StandardError; end # Your code goes here... PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze CONFIG = YAML.safe_load(CONFIG_DEFAULT.read).freeze private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT) end end
このファイル内で <module:GoogleAds>': uninitialized constant RuboCop::GoogleAds::YAML (NameError)
が発生してしまいました。
RuboCop::GoogleAds::YAML
なんて無いと言われていますが、 YAML
という定数が見つからなくて名前解決できてないのがいけないですね。 require 'yaml'
で直りました。
(PR 投げてもいいのかも)
基本的な Cop の実装方法
テンプレートが生成されているのでその中身を埋めていく感じで書けば良いです。
on_send
メソッドの中にルールを実装していくon_send
に渡されるnode
に解析対象のコードの情報が入ってくるので、その情報を使って Cop の検出対象かどうかを確認する- 問題がないコードだったら
return
する - 問題のあるコードだったら
add_offense
を呼び出して警告を追加する
- 問題がないコードだったら
- 定数
MSG
にエラーメッセージを定義しておくとそのメッセージが$ rubocop
の実行時に表示される
という感じ。
node
には RuboCop::AST::Node
、及びそれを継承した何らかのインスタンスが入ります。解析対象のコードの AST 情報が入っているので、
その中から必要な情報を取り出して検証したり、検出対象かどうかを AST レベルでマッチするかチェックするなどします。
node
を to_a
したり receiver, method_name = *node
add_offence
は最もシンプルにやるなら node
をそのまま渡すこともできますが、問題のある部分だけに絞ってカーソル(^^^
みたいな結果に出てくるやつ)を出すなど、細かい制御も可能です。
渡せる引数などの細かい仕様は add_offence
の rubydoc を参照。
gem として公開してみる
Cop ができたら gem として公開します。公開のために必要なファイル類は最初のテンプレート生成時に作成されているため、内容の編集をするだけで公開できます。 RubyGems.org への登録などの手順は割愛。以下のガイドに従って設定すれば公開できます。
公開前に MFA は設定しておきましょう。
余談: rubocop-extension-generator の在り処を見つけるまで
Rubocop の公式ガイドの Development
に Custom Cop の作成ガイドはありますが、 gem として公開するところまでは書いてありません。
gem に Cop を切り出して拡張しやすく…みたいな話を以前どこかで聞いた記憶があったので、できれば gem として作る前提のガイド無いかな…と探していたらありました。どこから見つけたのか記憶が飛んでますが
https://github.com/rubocop-hq/rubocop/blob/master/manual/extensions.md
この中にこの記事の最初に書いた rubocop-extension-generator があります。