Ruby
RuboCop

RuboCopのCustom Copを作る

More than 1 year has passed since last update.

はじめに

rubocop-rspecを参考にCustom Copを作る方法です

今回はメソッド名がaaaであれば違反になるサンプルCopを作成します

# NG
def aaa
    1
end

Copの名前はBadMethodNameにして、gem化してみます

試したバージョン

  • Ruby - 2.1.1p76
  • RuboCop - 0.23.0
  • Bundler - 1.5.3

rubocop-sample

rubocop-sampleというGemを作成します

$ bundle gem rubocop-sample -t
      create  rubocop-sample/Gemfile
      create  rubocop-sample/Rakefile
      create  rubocop-sample/LICENSE.txt
      create  rubocop-sample/README.md
      create  rubocop-sample/.gitignore
      create  rubocop-sample/rubocop-sample.gemspec
      create  rubocop-sample/lib/rubocop/sample.rb
      create  rubocop-sample/lib/rubocop/sample/version.rb
      create  rubocop-sample/.rspec
      create  rubocop-sample/spec/spec_helper.rb
      create  rubocop-sample/spec/rubocop/sample_spec.rb
      create  rubocop-sample/.travis.yml
Initializing git repo in /Users/xxx/rubocop-sample

rubocop-sample.gemspec

RuboCopを追加しておきます

※ 0.19の時点でRubocopからRuboCopに変更した関係でバージョンを指定しているみたいです

spec.add_runtime_dependency('rubocop', '~> 0.19', '>= 0.19')

そのほかは適宜編集してください

編集後にbundle installで依存ライブラリをインストールしておきます

lib/rubocop-sample.rb

lib/rubocop/sample.rbをrequireするlib/rubocop-sample.rbを作成します

require 'rubocop/sample'

rubocop-rspecは実行時に--requireオプションを使って外部Copをrequireさせてます

その時にrubocop --require rubocop-sampleと実行できる為に作成しています、別にrubocop --require rubocop/sampleでもいい人ならいらないのかもしれません

※ Ruby歴数ヶ月だとこれ以上の意味がわかりませんでした

lib/rubocop/sample/inject.rb

RuboCopは外部Copを取り入れる仕組みがない?みたいなのでそれに対応するlib/rubocop/sample/inject.rbを作成します

RuboCopはCopリスト(rubocop/config/default.yml)をRuboCop::ConfigLoaderクラスの@default_configurationに保持しています

rubocop-rspecでは以下のような実装で@default_configurationを更新する事で対応していますのでそのまま実装します

module RuboCop
  module Sample
    module Inject
      DEFAULT_FILE = File.expand_path('../../../../config/default.yml'), __FILE__)

      def self.defaults!
        hash = YAML.load_file(DEFAULT_FILE)
        config = ConfigLoader.merge_with_default(hash, DEFAULT_FILE)
        ConfigLoader.instance_variable_set(:@default_configuration, config)
      end
    end
  end
end

config/default.yml

作成するCop名と説明(Description)と有効(Enabled)にしておきます

Style/BadMethodName:
  Description: 'aaaというメソッド名は利用してはいけない'
  Enabled: true

lib/rubocop/sample.rb

ruboco/sample.rbをrequireされた時点で作成したRuboCop::Sample::Inject.default!が実行されるようにします

require 'rubocop'

require 'rubocop/sample/version'
require 'rubocop/sample/inject'

RuboCop::Sample::Inject.defaults!

# 作成するCop
require 'rubocop/sample/cop/style/bad_method_name'

lib/rubocop/sample/cop/style/bad_method_name.rb

RuboCopはソースをASTに変換した後にParser::AST::Processoron_xxxを呼び出します

今回はメソッド名のチェックなのでon_def(インスタンスメソッド)を実装して名前をチェックします

module RuboCop
  module Cop
    module Style
      class BadMethodName < Cop
        def on_def(node)
          method_name, _other = *node
          if method_name.to_s == 'aaa'
            add_offense(node, :name, 'aaaは利用してはいけません!', :fatal)
          end
        end
      end
    end
  end
end

add_offenseに違反を追加します

※ 今回はサンプルの為そのまま実装しましたが、メソッドの違反チェック用にRuboCop::Cop::CheckMethodsが用意されているのでそれを利用した方がよいです

spec/spec_helper.rb

作成したgemとRuboCopのspec_helperをrequireしておきます

require File.join(Gem::Specification.find_by_name('rubocop').gem_dir, 'spec', 'spec_helper.rb')

spec/rubocop-sample.rb

そのままじゃエラーになるので修正しておきます(環境によって違うかも)

spec/rubocop/cop/style/bad_method_name_spec.rb

RuboCopのspec_helperをrequireした事でinspect_sourceが利用できるようになりますのでspecを書いていきます

require 'spec_helper'

describe RuboCop::Cop::Style::BadMethodName do
  subject(:cop) { RuboCop::Cop::Style::BadMethodName.new }

  it 'インスタンスメソッド名がaaaは利用できない' do
    inspect_source(cop, ['def aaa; 1;end'])
    expect(cop.offenses.size).to eq(1)
    expect(cop.messages).to eq(['aaaは利用してはいけません!'])
  end
end

rake spec

エラーが出なければOKです

$ rake spec
ruby -S rspec ./spec/rubocop/cop/style/bad_method_name_spec.rb ./spec/rubocop/sample_spec.rb

...
...

RuboCop::Cop::Style::BadMethodName
  インスタンスメソッド名がaaaは利用できない

Rubocop::Sample
  should have a version number

Finished in 0.00237 seconds
2 examples, 0 failures

rake install

作成したgemをローカルにインストールします

$ rake install
rubocop-sample 0.0.1 built to pkg/rubocop-sample-0.0.1.gem.
rubocop-sample (0.0.1) installed.

$ gem list rubocop-sample

*** LOCAL GEMS ***

rubocop-sample (0.0.1)

試す

src/sample.rb

class Sample
  def aaa
    1
  end
end

実行時には必ず--require rubocop-sampleオプションをつけて実行します

$ rubocop --require rubocop-sample --only BadMethodName sample.rb
Inspecting 1 file
F

Offenses:

test.rb:2:7: F: aaaは利用してはいけません!
  def aaa
      ^^^

1 file inspected, 1 offense detected

Links