[Rails] 最小限の労力でrubocopの恩恵を受ける

## はじめに

「railsプロジェクトやるときには、rubocop入れましょう」と呪文のように刷り込まれてきましたが、実は、rubocopで何ができるのか、どんなメリットが有るのかきちんと理解していなかったのでまとめました。

この手の「いろいろできるツール」の使う際の大事な心がけとしては、**提供される機能をくまなく把握して全部使いこなすことではなく、導入の目的を満たす８割ぐらいのことを、最小限の労力でできるようにすること**が大事なんじゃないかとつくづく思います。

この記事を読んで、rubocopを導入することだけでなく、労力を取られずに（＝本来書くべきロジックやテストに集中できるように）コードの品質を高めるという成果が得られてもらえれば、何よりです。

## rubocopとは？

一言でいうと、ruby(.rbファイル)のコードを検査して、定められた規約に違反している箇所を検出してくれるツールです。

「コードが長すぎる」「インデンが適切でない」とかコードの可読性を高めるものだけでなく、「明確にすべきオプションがされていない」「DBとmodelで整合性があっていない」などバグにつながるような規約違反も検出してくれます。そして多くの場合、自動的に修正もしてくれます。

また、railsを使用している場合には、rubocop-railsを同時利用することで、rails特有のファイル（ex. マイグレーションファイル、設定ファイル）も検査してくれます。

## メリット

rubocopを導入することで受けられるメリット（恩恵）は以下のものだと思います。

- バグにつながるような規約違反だったり、コードの読みやすさを阻害するような**規約違反を自動検出してくれる**
- チーム開発の場合は、チームで定めた同じ設定ファイルを使用することで、**各自が書いたコードを同一ルールに基づいた一定の品質に保つ**ことの助けになる
- 単純な規約違反（余計な余白がある、インデントが適切ではない）を**自動的に修正**してくれる
- 自動実行ツール(pre-commit 後述）と組み合わせることで、**意識せずにcommitのタイミングで自動実行**して、規約違反を検出してくれる。（そもそもの実行し忘れるということを防げる）
- コードレビューを機械的に行ってくれるので、規約違反の内容を理解し修正することで、**自分のコードの品質を高めることができる**

## rubocopの使い方

railsを使用している場合の導入〜使い方を記載してきます。

試した環境は以下です。

- OS : macOS Catalina(10.15.7)
- ruby : 2.6.6
- rails : 6.0.3.3

また、私の場合まずは作るの優先でやったrailsアプリがあり、model,contorllerともに5,6個程度作成した状態で導入しました。

なお、私は導入に当たり以下のことを心がけました。なので、他の方が記載された導入の仕方や、設定内容とは異なる部分、相容れない部分があると思います。

- いきなり100%を目指さない
- ツールがやってくれることは、ツールに任せ、自分がやらなきゃいけないことに注力する
- 継続的に使えるようにする

### インストール＆設定

今回はrubocopとともに、[railsの規約](https://rails.rubystyle.guide/)に則ったチェックを行ってくれる、`rubocop-rails`もあわせて導入します。
Gemfileに以下の記載を行い、bundle install

```ruby
group :development do
  gem 'rubocop', require: false
  gem 'rubocop-rails'
end
```

### とりあえずチェックする

```ruby
$ rubocop
Inspecting 57 files
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Offenses:
57 files inspected, 292 offenses detected, 260 offenses auto-correctable
```

292個って、、めちゃくちゃ出ましたね・・・ しかもスクロースしてもすべては表示しきれずorz

これを、一個一個確認して、修正。なんてことをやってくと。もうrubocopなんてしない！と投げ出してしまうので、継続的に使える有用なツールにしましょう。

というわけで、次の「設定ファイルの作成」と「自動修正」を行っています。

### 設定ファイルの作成

以下のコマンドを実行すると、設定ファイル(`.rubocop.yml`)を作成してくれ、かつ検出された規約違反を読み飛ばすための設定を(`.rubocop_todo.yml`)に記載してくれます。

```ruby
$ rubocop --auto-gen-config
```

適用する（しない）規約・範囲を設定ファイルに書いてあげます。

私は、とりあえず「チェック対象を自分で書いたコードに限定する」という考えで、以下の設定を入れてみました。

- 先程インストールしたrubocop-railsを適用させる
- コマンドで自動生成されたファイルや、初期ファイルはチェック対象外にする

```ruby
inherit_from: .rubocop_todo.yml
require:
  - rubocop-rails   # rubocop-railsを適用
 
AllCops:
  TargetRubyVersion: 2.6
  NewCops: enable    # ← 新しい規約が登録された場合に、適用するかどうかの判定
  Exclude:
    - 'bin/**'
    - 'node_modules/**/*'
    - 'config/**/*'
    - 'config.ru'
    - 'db/schema.rb'
    - 'db/seeds.rb'
    - 'Gemfile'
```

- 自分のコード記載方法にそぐわない、エラー検出が必要ない規約を無効or変更

```ruby
# 日本語でのコメントを許可
Style/AsciiComments:
  Enabled: false

# クラスのコメント必須を無視
Style/Documentation:
  Enabled: false

# 「frozen_string_literal: true」を追加しない
Style/FrozenStringLiteralComment:
  Enabled: false

# メソッドの行数が 10 行までは厳しすぎるので，20行までに変更
Metrics/MethodLength:
  Max: 20

# private/protected は一段深くインデントする
Layout/IndentationConsistency:
  EnforcedStyle: indented_internal_methods
```

これらの設定ファイルを記載したうえで、規約違反を退避したファイル(`.rubocop_todo.yml`)の中身をコメントアウトして、再度rubocopを実行します。私の場合50個くらいまで減りました。

ここまで来てもまだすべてを一つづつ直す気にはなれないので、次の自動修正を実行しします。

### 自動修正

冒頭にも記載したとおり、簡単な（かつ対処法が明確な）規約違反はrubocop -aコマンドで自動的に修正してくれます。

※ここは、一つ一つ規約違反の内容確認した上で、自動修正するべきという意見もありますが、私は「自動修正してくれるものは任せよう」と割り切って、規約違反はサーと見て自動修正しました。

実行すると、こんな感じで、自動修正された規約違反は`[Corrected]` が付与され、最後に、規約違反の総件数に対して、何件自動修正されたか表示されます。

```ruby
$ rubocop -a
.rubocop.yml: Style/IndentationConsistency has the wrong namespace - should be Layout
Inspecting 29 files
....................CC.CCCCCC

Offenses:

db/migrate/20200928124523_devise_create_users.rb:6:59: C: [Corrected] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
      t.string :nickname,           null: false, default: ""
                                                          ^^

~ 途中略 〜

29 files inspected, 24 offenses detected, 22 offenses corrected
```

### 手動修正

そして、自動修正しても残ってしまった規約違反が、自分で対応しなきゃいけない（＝**本来やりたかった、時間をかけて、内容理解し、あるべきコードに修正する作業**）ものです。

私の場合は、以下の２つが残りました。こちらはバグに繋がる可能性のある規約違反だと思うので、エラーの内容を確認して（ex. `Rails/HasManyOrHasOneDependent` というキーワードでググれば、公式サイトの内容or丁寧解説記事にありつけます）、修正方法を検討して修正し、再度rubocopを実行して、無事に規約違反なしとなりました。

```ruby
# has_manyアソシエーションにたいして、dependentオプション(親レコード削除時に、同時に消す？ 残す？ エラーにする？ 警告出す？）が未設定
app/models/category.rb:3:3: C: Rails/HasManyOrHasOneDependent: Specify a :dependent option.
  has_many :estimate_details
  ^^^^^^^^

# modelでuniqueバリデーションを定義しているのに、DB定義にはunique定義がされていない
app/models/category.rb:6:3: C: Rails/UniqueValidationWithoutIndex: Uniqueness validation should be with a unique index.
  validates :user_id, uniqueness: { scope: :category_name }
```

## rubocopを自動実行させる

rubocopは便利なツールですが、そもそも実行することを忘れないために、何かを契機に自動的に実行するようにしましょう。

### commit時に自動実行させる

一番良いのがcommitのタイミングだと思うので、pre-commitというgemを導入して、git commitコマンド発行したタイミングで自動実行するようにします。

- pre-commitのインストール

`gem pre-commit`をGemfileに設定して、`bundle install` したあとに、以下のコマンドでpre-commitのファイルを生成します。

```ruby
$ pre-commit install
Installed /Users/hiro/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/pre-commit-0.39.0/templates/hooks/automatic to .git/hooks/pre-commit
```

- pre-commitの設定

commit時にrubocopを自動実行するために、以下コマンドで設定を行います。

```ruby
# 設定前の状態を確認 
$ pre-commit list    
Available providers: default(0) git(10) git_old(11) yaml(20) env(30)
Available checks   : before_all ci coffeelint common console_log csslint debugger gemfile_path go go_build go_fmt jshint jslint json local merge_conflict migration nb_space pry rails rspec_focus rubocop ruby ruby_symbol_hashrockets scss_lint tabs whitespace yaml
Default   checks   : common rails
Enabled   checks   : common rails
Evaluated checks   : tabs nb_space whitespace merge_conflict debugger pry local jshint console_log migration
Default   warnings : 
Enabled   warnings : 
Evaluated warnings :

# git commitのタイミングで、rubocopを実行するように設定
$ git config pre-commit.checks rubocop

# 設定後の状態を確認
$ pre-commit list    
Available providers: default(0) git(10) git_old(11) yaml(20) env(30)
Available checks   : before_all ci coffeelint common console_log csslint debugger gemfile_path go go_build go_fmt jshint jslint json local merge_conflict migration nb_space pry rails rspec_focus rubocop ruby ruby_symbol_hashrockets scss_lint tabs whitespace yaml
Default   checks   : rubocop   # ← rubocopが設定された
Enabled   checks   : rubocop   # ← rubocopが設定された
Evaluated checks   : rubocop   # ← rubocopが設定された
Default   warnings : 
Enabled   warnings : 
Evaluated warnings :
```

また、bundle経由で、pre-commitを使用する場合には.git配下の設定ファイル(`.git/hooks/pre-commit`)を以下のように修正する必要があります。

```ruby
#!/usr/bin/env sh

〜 中略 〜

PATH=$PATH:/usr/local/bin:/usr/local/sbin

cmd=`git config pre-commit.ruby 2>/dev/null`
if   test -n "${cmd}"
then true
elif which rvm   >/dev/null 2>/dev/null
then cmd="rvm default do ruby"
elif which rbenv >/dev/null 2>/dev/null
then cmd="rbenv exec ruby"  #← 修正前
then cmd="rbenv exec bundle exec ruby"  #← 修正後
else cmd="ruby"
fi

〜 中略 〜
```

設定完了後に`git commit`コマンドを実行して、rubocopが自動実行されるか確認します。
（規約違反が出るように事前に行末余白を仕込んでおきました）

```ruby
$ git commit
pre-commit: Stopping commit because of errors.
Inspecting 1 file
C

Offenses:

app/controllers/home_controller.rb:4:1: C: Layout/TrailingWhitespace: Trailing whitespace detected.

1 file inspected, 1 offense detected, 1 offense auto-correctable
.rubocop.yml: Style/IndentationConsistency has the wrong namespace - should be Layout

pre-commit: You can bypass this check using `git commit -n`

```

commit時にrubocopが自動実行されることが確認できました。違反が検出されると、commit処理は中断されます。

検出後の流れとしては、「違反内容確認」→（修正すべき場合は）「手動修正 or 自動修正(`rubocop -a`)」→ 「ステージング環境へ登録」→再度「commit」の流れになるかと思います。

### 自動実行を回避する
「開発中のコードをとりあえずpushして他メンバーと共有したい」、「とりいそぎ規約違反は目をつむり、動くものをupしたい」、など、rubocopの規約チェックを回避したい場合があると思います。
そんなときには、以下のコマンドでpre-commitによるrubocopの自動実行を回避して、commitできます。

```
git commit -n
```

ただし、rubocop実行の回避が常態化しないよう、くれごれも「一時的」「特別な理由がある」場合のみに限るようにしてください。

## おわりに
今後、commitのために自動的にrubocopが走り、チェックが行われる設定ができ継続的に使えるようになりました。
コードを書いて、新たな規約違反が出たときに、設定ファイルを見直したり、内容を確認して書き方を改めましょう。そうすることで、より有用なツールに育てていけますし、自分のコード品質を高めることができると考えております。

## 参考にさせていただいた記事
使い方に関して、完全に無知でしたのでとても参考になりました。
記事の著者の方にはこの場を借りて、感謝のお礼をさせていただきます。

- rubocopの使い方について
https://kitsune.blog/rails-rubocop
https://qiita.com/tomohiii/items/1a17018b5a48b8284a8b

- pre-commitの導入
https://techblog.kyamanak.com/entry/2018/06/19/221910
https://dev.classmethod.jp/articles/pre-commit-rubocop/

## 更新履歴
### 2020/11/25
- commit時の自動実行を回避する方法を追記
- 誤字脱字を修正