はのちゃ爆発

はのちゃが技術ネタとか日常のこととかを書いてます。

はじめて RuboCop Custom Cop を作って gem も公開してみた

はじめに

RuboCop の Extension/Custom Cop を初めて作って gem として公開してみました。

rubygems.org

作ってて色々引っかかったりしたことがあったので、作り方などをまとめておきます。

目次

大まかな手順

RuboCop Extension/Custom Cop 作成

  1. rubocop-extension-generator をインストール
  2. $ rubocop-extension-generator rubocop-your_own_extension_name を実行して Rubocop Custom Cop gem のテンプレートを生成
    • ここで指定する rubocop-your_own_extension_name の中に複数の Cop が入る。
    • 例: rubocop-rails であれば your_own_extension_namerails で、 Rails に関する Cop をまとめられるイメージ。
  3. $ rake 'new_cop[YourOwnExtensionName/YourOwnCopName]' を実行して Cop を作成する
  4. Cop を実装、 config/default.yml を適宜修正

gem 公開

  1. gemspec, README.md, CHANGELOG.md などを整備
  2. RubyGems.org の設定
  3. RubyGems.org に push

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 レベルでマッチするかチェックするなどします。 nodeto_a したり receiver, method_name = *node

add_offence は最もシンプルにやるなら node をそのまま渡すこともできますが、問題のある部分だけに絞ってカーソル(^^^ みたいな結果に出てくるやつ)を出すなど、細かい制御も可能です。 渡せる引数などの細かい仕様は add_offence の rubydoc を参照

gem として公開してみる

Cop ができたら gem として公開します。公開のために必要なファイル類は最初のテンプレート生成時に作成されているため、内容の編集をするだけで公開できます。 RubyGems.org への登録などの手順は割愛。以下のガイドに従って設定すれば公開できます。

guides.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 があります。