RuboCopのCustom Copを作る


## はじめに

[rubocop-rspec](https://github.com/nevir/rubocop-rspec)を参考にCustom Copを作る方法です

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

```ruby
# 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を作成します

```bash
$ 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に変更した関係でバージョンを指定しているみたいです

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

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

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


### lib/rubocop-sample.rb

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

```ruby
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`を更新する事で対応していますのでそのまま実装します

```ruby
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`）にしておきます

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

### lib/rubocop/sample.rb

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

```ruby
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::Processor](https://github.com/whitequark/parser/blob/master/lib/parser/ast/processor.rb)の`on_xxx`を呼び出します

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

```ruby
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しておきます

```ruby
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を書いていきます

```ruby
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です

```bash
$ 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をローカルにインストールします

```bash
$ 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

```ruby
class Sample
  def aaa
    1
  end
end
```

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

```bash
$ 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

- [rubocop](https://github.com/bbatsov/rubocop)
- [rubocop-rspec](https://github.com/nevir/rubocop-rspec)