既存プロジェクトのコーディングスタイルを無理なく整えていく【ESLint / CircleCI】

ZOZOテクノロジーズのむーさん[@murs313](https://twitter.com/murs313)です。
WEARが大好きで入社し、今はWEARのwebアプリの開発をしています。

WEARは今年の[ハロウィン](https://wear.jp/event/halloween/)で７才になりました。
歳を重ね、様々な人がコードを書き、コーディング規約や自動チェックがなく、WEARのコードはこのような状態になっていました…。
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309128/f1238f08-fefe-0205-a83d-0e9ea3cc4d56.png">

**ほげえええええええ！！** インデント！！！インデントォォ！！！
ファイルによって改行コードも違うやないか…！！なんてこった＼(^o^)／
このままでは気になってコードが書けない…。私はコーディングスタイルを統一する決意をしたのでした。

しかし、弊プロジェクトは手動デプロイで、機械的にスタイルを揃えたとしてもまとめてリリースすることは難しい状況でした。
なので、編集・新規作成したファイルのみを対象にして、コーディングスタイルを無理なく整えていくことにしました。

まずは書き方の振れ幅が大きいJavaScriptを対象にして、ESLintを導入することにしました。

# ゴール
- CircleCIでESLintによるJSファイルのコーディングスタイルのチェックを行う。
    - そのプルリクエストで触ったファイルだけをチェックする。
    - 運用に慣れてもらうため、ひとまず簡単に直せるようなルールのみをerrorにする。
- 差分のみをfixできるshellを念の為作っておく。
    - メンバーのエディタが多様なので、ESLintのチェックがエディタ上で表示されない場合があった。
    - 編集したファイルを都度都度fixするという手間をなくし、fix忘れを防ぐため。

# 手順
### 1. コーディング規約を決める
コーディング規約がない場合、まずはチームでコーディング規約を決めることから始めます。
細かいところはまずはスタンダードに乗っかると楽なので、「みんなが常日頃気になっているところ」だけ話しておくのが良いと思います。

- インデント（ソフトインデント / ハードインデント）
- 改行コード（LF / CRLF）
- 括弧の位置 等々…

話し合いの段階でESLintの推奨設定やJavaScript Standard Styleを見ておくと、「推奨や標準はこうなんだ」と参考になります。

- ESLintの推奨設定：https://eslint.org/docs/rules/ (左にチェックマークが付いているものが推奨設定)
- JavaScript Standard Style：https://standardjs.com/rules.html

### 2. ESLintの導入
淡々と導入手順を記していきますが、よく分からなかったら公式サイトを参照してみてください。

npmが動く環境であることを前提とします。
`package.json`がないプロジェクトなら、下記コマンドで`package.json`を作ります。

```bash
$ npm init -y
```

ESLintをインストールします。

```bash
$ npm install --save-dev eslint
```

ESLintを簡単に実行できるように、スクリプトを定義しておきます。`package.json`の`scripts`に下記のようなscriptを定義します。

```package.json
  "scripts": {
    "lint": "eslint",
    "fix": "eslint --fix"
  }
```

### 3. ESLintのルールを整備する
コーディングルールを設定していきます。
プロジェクトフォルダの直下に`.eslintrc.json`というファイルを作ります。

```json:.eslintrc.json
{
  "extends": "eslint:recommended"
}
```
`"eslint:recommended"`はESLintの推奨設定に則ることを意味します。
ESLintのコーディングルールは恐ろしいほどたくさんあるので、推奨設定から調整していくのがおすすめです。
先ほどスクリプト定義したので、下記のようにコマンドを打つとESLintが動きます。

```bash
$ npm run lint [jsファイルのパス]
```

errorやwarnがたくさん出るかもしれませんが、jQueryのプロジェクトなら`"env"`に`"jquery"`を追加したり、`"globals"`に変数を設定したり、みんなで決めたルールに沿って`"rules"`を追加したりして調整していきます。

```.eslintrc.json
{
  "env": {
    "browser": true,
    "jquery": true
  },
  "extends": "eslint:recommended",
  "rules": {
    "no-redeclare": "warn",
    "no-unused-vars": "warn",
    "camelcase": "warn",
    "linebreak-style": ["error", "unix"],
    "indent": ["error", "tab", { "SwitchCase": 1 }],
    "eol-last": ["error", "always"],
    "no-trailing-spaces": "error",
    "no-multiple-empty-lines": ["error", { "max": 1 }],
    "space-before-blocks": ["error", "always"],
    "quotes": ["error", "single"],
    "semi": ["error", "always"],
    "comma-style": ["error", "last"],
    "space-before-function-paren": ["error", "never"],
    "func-call-spacing": ["error", "never"],
    "block-spacing": ["error", "always"],
    "array-bracket-spacing": ["error", "never"],
    "no-array-constructor": "error",
    "spaced-comment": ["error", "always"]
  }
}
```
今回はESLintでfixできるようなものは基本的にerrorにし、簡単にfixできないものはwarnにしました。CircleCIが落ちる、直す、通る、という一連の流れをメンバーに体感してもらいたかったためです。

### 4. 差分ファイルだけlintとfixができるshellを書く
CircleCIで動かす用のshellと、ローカルで差分だけfixするようなshellを作ります。

- lint.sh  
    - masterとの差分があるjsファイルだけlintします。  
    - CircleCIに載せる用。ローカルでも動きます。
- lint-fix.sh  
    - masterとの差分があるjsファイルだけfixします。  
    - ローカルでESLintが直せるerrorを直します。

これらのファイルをプロジェクトディレクトリ直下に置いた前提で話を進めます。

```lint.sh
#!/bin/bash

echo 'CIRCLE_BRANCH: ' ${CIRCLE_BRANCH}
TARGET_BRANCH=${CIRCLE_BRANCH}

# masterブランチへのmerge時はチェックしない
if [ "$TARGET_BRANCH" = 'master' ]; then
  echo 'SKIP on merge into master'
  exit 0
fi

# ローカルでの実行用にカレントブランチをセットする
if [ "$TARGET_BRANCH" = '' ]; then
  TARGET_BRANCH=$(git rev-parse --abbrev-ref HEAD)
fi
echo 'TARGET_BRANCH: ' $TARGET_BRANCH

BASE_BRANCH=origin/master
echo 'BASE_BRANCH: ' $BASE_BRANCH

files=$(git diff --name-only $TARGET_BRANCH $BASE_BRANCH | grep -E '.js$')

error=false
for file in ${files}; do
  npm run lint -s -- ${file}
  result=$?
  if [ $result -ne 0 ]; then
    error=true
  fi
done

if $error; then
  exit 1
fi

exit 0
```

```lint-fix.sh
#!/bin/bash

# origin/masterをベースブランチにセット
BASE_BRANCH=origin/master
echo 'BASE_BRANCH: ' $BASE_BRANCH

files=$(git diff --name-only $BASE_BRANCH | grep -E '.js$')

error=false
for file in ${files}; do
  npm run fix -s -- ${file}
  result=$?
  if [ $result -ne 0 ]; then
    error=true
  fi
done

if $error; then
  exit 1
fi

exit 0
```

ファイルができたら、適当なjsファイルに差分を作って、下記のように実行して試してみます。

```bash
$ ./lint.sh
$ ./lint-fix.sh
```

### 5. CircleCIに導入
CircleCI側でリポジトリの追加ができている前提で、`.circleci/config.yml`に下記を記述します。

```yml:.circleci/config.yml
version: 2
jobs:
  build:
    docker:
      - image: circleci/node:10.16.0
    steps:
      - checkout
      - run: npm install
      - run: ./lint.sh
```
nodeが使えるimageを用意して、`./lint.sh`しています。
ここでは当時最新の[LTS(long term support)](https://github.com/nodejs/Release)だったnode v10系を使っています。

これをプッシュすれば、CircleCIによるlintが走るはずです！

プルリクの最後の方にこんな感じで表示されると思います。
- CircleCIが落ちたバージョン
    <img width="700" alt="スクリーンショット 2019-12-15 16.49.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309128/70db554d-ec0a-7453-b3ca-52ccb30b4c93.png">
- CircleCIが通ったバージョン
    <img width="700" alt="スクリーンショット 2019-12-15 16.50.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309128/1c984fff-13b4-48bf-babd-aba9ddb06442.png">

### 6. チームメンバーに説明・ドキュメント整備・各々の環境やエディタで使えるように設定
継続的に取り組むために1番大事なことです。

下記のようなことを伝えました。
- プルリクにこんなのが付くようになるよ
- エラーメッセージはこうやって見るよ
- CIが落ちたら直す。レビュワーはマージせずに一報入れる。

また、チームメンバー各々の環境やエディタで使えるように、nodeの環境構築やプラグインを入れる時間を取りました。

- Atom：[AtomにESLint導入した](https://qiita.com/HisakoIsaka/items/40966a0c78846f4053c9)
- VS Code：https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint

# 最後に
今回の開発の部分は、[今年7月に行われた開発合宿](https://techcorp.zozo.com/entry/20190711__Development_camp)で行いました。足湯コーディングめっちゃ良かった〜〜〜！
足湯コーディングしたい方は[JOIN US](https://tech.zozo.com/recruit/mid-career/detail59/)!!!

# スペシャルサンクス
- 弊社の素敵文化・フロントエンド共有会で相談に乗ってくれた@AmatsukiKuさん
- 合宿で罵詈雑言と共にCircleCIのことを教えてくれた[@inductor](https://twitter.com/_inductor_)(呼び捨て)