ESLint&Prettierでまさかコードが壊れるなんて

この記事は[Goodpatch Advent Calendar 2020](https://qiita.com/advent-calendar/2020/goodpatch)の2日目となります。
最寄り駅のイチョウ並木にイルミネーションが灯り始めました。

# JSを整形したいだけだった

フロントエンド開発では定番の[eslint](https://github.com/eslint/eslint) と [prettier](https://github.com/prettier/prettier) 。
今回やりたいことはJavaScriptコードをeslintでコードチェックし、prettierでコード整形したいだけ。
それだけのはずでした。

## 使用ファイル
今回説明するコードサンプルは以下GitHubにまとめてあります。
こちらも合わせてご参照ください。
[https://github.com/ikezaworld/eslinttest](https://github.com/ikezaworld/eslinttest)

**ディレクトリ構成**

```
/
├─ src
│  ├─ index.js
│  └─ index_org.js
├─ .eslintrc.js
├─ .prettierrc.js
└─ package.json
```

## 何の変哲もないコードたち

対象コードは同じ内容の三項演算子が3つあるだけ。
見分けやすいよう変数名の連番（`test1` `test2` `test3`）のみ変えています。
名前以外は全くいっしょのコードたちです。

```javascript:src/index.js
const test1 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

const test2 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';


const test3 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';


```


## 整形を実行する

さっそく `eslint --fix ./src/index.js` を実行。
すると以下の結果になりました。

**▼ 実行結果**

```javascript:src/index.js
const test1 = isFoo
  `データfooを選択中です（${  1 } 個）` : '選択できるデータは存在しません';

const test2 = isFoo
  ? "データfooを選択中です（" + 1 + " 個）"
  : "選択できるデータは存在しません";

const test3 = isFoo
  `データfooを選択中です（${  1 } 個）` : '選択できるデータは存在しません';

```

## 結果がおかしい！？

### 消えた三項演算子？

`test1` `test3` は `isFoo` 後ろの三項演算子の `?` が消えています。 
これでは構文エラーとなりJavaScript実行ができない。
何だろうこれは。

### 無事だった2番目
全て壊れるのであれば、元々のコード側に問題があった可能性もあります。
しかし2番目の `test2` は正常に整形されています。
ただ、テンプレートリテラルは適用されていません。
どういうことだろう。


## 色んなパターンを試す

### 三項演算子?の直後に半角スペース
`test1` の三項演算子 `?` 直後に半角スペースを入れて実行してみる。

**▼ 実行前**

```javascript:src/index.js
const test1 = isFoo ? 
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

const test2 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';


const test3 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';


```



**▼ 実行結果**

```javascript:src/index.js
const test1 = isFoo
  ? "データfooを選択中です（" + 1 + " 個）"
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? "データfooを選択中です（" + 1 + " 個）"
  : "選択できるデータは存在しません";

const test3 = isFoo
  `データfooを選択中です（${  1 } 個）` : '選択できるデータは存在しません';

```

|カテゴリ|結果| 
|:---:|:---:|
|test1|△|   
|test2|△|
|test3|×|
※△は構文はOKだがテンプレートリテラルがされていないもの
実行結果は`test1` がOKになった。他はそのまま変化なし。


### 三項演算子?の直後の改行を消し1行にする

 `test1` の三項演算子 `?` 直後の改行を消し1行にしてみる。

**▼ 実行前**

```javascript:src/index.js
const test1 = isFoo ?  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

const test2 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';


const test3 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';


```

**▼ 実行結果**

```javascript:src/index.js
const test1 = isFoo
  ? "データfooを選択中です（" + 1 + " 個）"
  : "選択できるデータは存在しません";

const test2 = isFoo
  `データfooを選択中です（${  1 } 個）` : '選択できるデータは存在しません';

const test3 = isFoo
  `データfooを選択中です（${  1 } 個）` : '選択できるデータは存在しません';

```

|カテゴリ|結果| 
|:---:|:---:|
|test1|△|   
|test2|×|
|test3|×|

`test1` がOKになった。
しかし成功していたはずの `test2` が壊れてしまった。

### コード間の改行を1行に揃える

 `test1` `test2` `test3`  のコード間の改行を1行に揃えてみる。

**▼ 実行前**

```javascript:src/index.js
const test1 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

const test2 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

const test3 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

```

**▼ 実行結果**

```javascript:src/index.js
const test1 = isFoo
  `データfooを選択中です（${  1 } 個）` : '選択できるデータは存在しません';

const test2 = isFoo
  `データfooを選択中です（${  1 } 個）` : '選択できるデータは存在しません';

const test3 = isFoo
  `データfooを選択中です（${  1 } 個）` : '選択できるデータは存在しません';

```

|カテゴリ|結果| 
|:---:|:---:|
|test1|×|   
|test2|×|
|test3|×|

実行結果は、なんと全部失敗になってしまった。改行消しただけなのに。
これはひどい。


## 失敗から見えてくるもの

色んなパターンを試してみた。
結局どのパターンでも3つ全部成功はできなかった。
しかしこれまでのパターンから、

- 改行
- スペース

の有無にどうやら意味があるらしいことが見えてきた。


## 設定ファイル内容

### package.json

`package.json` は以下の通り。

```json:package.json
{
  "name": "eslinttest",
  "version": "1.0.0",
  "description": "test of eslint & prettier",
  "scripts": {
    "eslint:fix": "eslint --fix ./src/index.js",
    "prettier": "prettier --write ./src/index.js"
  },
  "devDependencies": {
    "eslint": "^7.14.0",
    "eslint-config-prettier": "^6.15.0",
    "eslint-plugin-prettier": "^3.1.4",
    "prettier": "^2.2.0"
  }
}
```

4つのモジュールをインストールしています。
versionは執筆時点で最新のものを使っています。

- eslint （eslint本体です）
- eslint-config-prettier （ESLint内でPrettierと競合するルールをオフにしてくれる）
- eslint-plugin-prettier （PrettierをESLintルールとして実行できるようにする）
- prettier （prettier本体です）

後は整形呼び出ししやすいようnpm scriptを書いているだけです。

### ESLint設定

ESLint設定を行っている`.eslintrc.js` 内容は以下の通り。

```javascript:.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es6: true,
  },
  extends: ["prettier"],
  plugins: ["prettier"],
  rules: {
    "prettier/prettier": "warn",
    "prefer-template": "warn",
  },
};
```

ESLintの設定はprettierが実行できるよう最低限の設定をしています。
文字列をテンプレートリテラルに整形してくれる`prefer-template`ルールも追加しています。
ESLintスタイルガイドで人気の高いairbnbの「[eslint-config-airbnb](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb)」 は、この`prefer-template`がデフォルトで適用されています。
参考：
prefer-template： https://eslint.org/docs/rules/prefer-template
Airbnb JavaScript Style Guide： https://github.com/airbnb/javascript#es6-template-literals

### prettier設定

prettier設定を行っている`.prettierrc` 内容は以下の通り。

```json:.prettierrc
{
  "printWidth": 80
}
```

`printWidth` は行内の折り返し文字数を指定するものです。
80文字折り返しを指定していますが、実はprettierのデフォルトも80です。
なので実質このファイルがあってもなくても変わりません。
一応明示的に設定しています。
参考： https://prettier.io/docs/en/options.html#print-width


## 原因

失敗時の整形処理を順を追って確認すると以下の通りです。

- prettier整形で三項演算子`?`部分について、改行位置整形のために一時削除される
- 複数行に跨がるprettier整形が終わる前に、割り込む形で `prefer-template` が入ってしまう
- prettierの1行の文字数折り返し整形を実行しようとするが`prefer-template`が対象コード内容を変えているため正しく処理されない

このために三項演算子が整形されず `?` が消えたままの現象が発生していました。

**▼ 失敗例分析**

- ①で`?`を削除。
- ②で`prefer-template`が挟まっている。
- ③でprettierが想定していた形が②で変更されており正しく処理できていない。
![code.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/866624/333a757c-f914-8153-f7f1-1dc47fbcdeb4.png)



**▼ 成功例分析**

成功時の場合は、prettier整形がきちんと終った後に`prefer-template`が実行されています。

- ①、②で三項演算子`?`や1行内折り返し文字数をprettier整形完了。
- prettier整形後の綺麗な状態で③`prefer-template`が正しく実行される。
![code2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/866624/e9200278-feef-e73b-6a7b-50c867939751.png)


## 原因検証

### prefer-templateなしで実行

`"prefer-template": "warn"` 部分を削除してみます。
要はテンプレートリテラル整形なしです。
この設定で`eslint --fix ./src/index.js` を実行。

```javascript:.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es6: true,
  },
  extends: ["prettier"],
  plugins: ["prettier"],
  rules: {
    "prettier/prettier": "warn",
  },
};
```

**▼ 実行結果**

```javascript:src/index.js
const test1 = isFoo
  ? "データfooを選択中です（" + 1 + " 個）"
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? "データfooを選択中です（" + 1 + " 個）"
  : "選択できるデータは存在しません";

const test3 = isFoo
  ? "データfooを選択中です（" + 1 + " 個）"
  : "選択できるデータは存在しません";
```

|カテゴリ|結果| 
|:---:|:---:|
|test1|○|
|test2|○|
|test3|○|
実行結果は全部成功。

### .prettierrc1行内折り返しを80→120にする

80文字折り返しだと今回のコードでは折り返しが発生します。
そのため120等大きめの数値に設定変更します。
これで`eslint --fix ./src/index.js` を実行。

```jason:.prettierrc
{
  "printWidth": 120
}
```

**▼ 実行結果**

```javascript:src/index.js
const test1 = isFoo ? `データfooを選択中です（${1} 個）` : "選択できるデータは存在しません";

const test2 = isFoo ? `データfooを選択中です（${1} 個）` : "選択できるデータは存在しません";

const test3 = isFoo ? `データfooを選択中です（${1} 個）` : "選択できるデータは存在しません";

```

|カテゴリ|結果| 
|:---:|:---:|
|test1|○|
|test2|○|
|test3|○|
実行結果は全部成功。

### 三項演算子の`?` と`true式`を同一行にする

三項演算子の`?`位置調整の整形がかからないよう全部1行にしてみます。
このESLint設定で`eslint --fix ./src/index.js` を実行。

**▼ 実行前**

```javascript:src/index.js
const test1 = isFoo ? 'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

const test2 = isFoo ? 'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';


const test3 = isFoo ? 'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';


```

**▼ 実行結果**

```javascript:src/index.js
const test1 = isFoo
  ? `データfooを選択中です（${1} 個）`
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? `データfooを選択中です（${1} 個）`
  : "選択できるデータは存在しません";

const test3 = isFoo
  ? `データfooを選択中です（${1} 個）`
  : "選択できるデータは存在しません";

```

|カテゴリ|結果| 
|:---:|:---:|
|test1|○|   
|test2|○|
|test3|○|
実行結果は全て成功。

### 三項演算子を2行に分けて `?`と`true式`を同一行にする

先ほどは三項演算子全部1行にしました。
今度は2行だけど、`?`と`true式`の間に余計な改行を挟まない状態で試してみます。
このESLint設定で`eslint --fix ./src/index.js` を実行。

**▼ 実行前**

```javascript:src/index.js
const test1 = isFoo 
? 'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

const test2 = isFoo 
? 'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';


const test3 = isFoo 
? 'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';


```

**▼ 実行結果**

```javascript:src/index.js
const test1 = isFoo
  ? `データfooを選択中です（${1} 個）`
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? `データfooを選択中です（${1} 個）`
  : "選択できるデータは存在しません";

const test3 = isFoo
  ? `データfooを選択中です（${1} 個）`
  : "選択できるデータは存在しません";

```

|カテゴリ|結果| 
|:---:|:---:|
|test1|○|   
|test2|○|
|test3|○|
実行結果は全て成功。

## 対策

### 1. 三項演算子の書き方に注意する

**JavaScriptにおいては三項演算子の`?`は `true式` と同じ行に書く**

`?` の直後に改行を入れても一応動きます。
ですが、大量のコードを一括でESLintした際は、今回の検証のように気づかずコード破損が起こる可能性があります。
そのためJavaScriptの三項演算子レギュレーションとしては、`?`は `true式` は同一行に書く方が安全です。

**例：1行での書き方**

```javascript
const test1 = isFoo ? 'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';
```

**例：複数行での書き方**

```javascript
const test1 = isFoo
  ? 'データfooを選択中です（' + 1 +' 個）'
  : '選択できるデータは存在しません';
```

### 2. ESLint未適用のコードにはprettierをしてからESLintを行う

一度prettier整形してしまえばそれ以降はESLint fixのみでも問題ない。
そのためprettier単独で整形した後に別ステップでESLint fixをかけること。
これなら整形で壊れることはないです。

例えば大量のESlint未適用コードが合った場合、そこに一括ESlintをかけるなら以下1.2.の順序で行う。
こうすれば以降はESlint+prettierを同時にかけても今回の問題は回避できます。

1. 最初にprettierのみ実行する。
2. その後別ステップとしてESLint fixをする。


**実際にprettilerとESLintを別々にかけてみた**

**▼ 実行前**

```javascript:src/index.js
const test1 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

const test2 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

const test3 = isFoo ?
  'データfooを選択中です（' + 1 +' 個）' : '選択できるデータは存在しません';

```

**▼ prettier実行**

`prettier --write ./src/index.js` を実行すると以下になります。

```javascript:src/index.js
const test1 = isFoo
  ? "データfooを選択中です（" + 1 + " 個）"
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? "データfooを選択中です（" + 1 + " 個）"
  : "選択できるデータは存在しません";

const test3 = isFoo
  ? "データfooを選択中です（" + 1 + " 個）"
  : "選択できるデータは存在しません";

```

三項演算子の`?`位置やインデントがきちんと整形された状態になりました。
prettier整形成功です。


**▼　ESLint実行**

prettierをかけたコードに対し、ESLint `eslint --fix ./src/index.js` を実行すると以下になります。

```javascript:src/index.js
const test1 = isFoo
  ? `データfooを選択中です（${1} 個）`
  : "選択できるデータは存在しません";

const test2 = isFoo
  ? `データfooを選択中です（${1} 個）`
  : "選択できるデータは存在しません";

const test3 = isFoo
  ? `データfooを選択中です（${1} 個）`
  : "選択できるデータは存在しません";

```

無事テンプレートリテラル適用されました。
ESLintも成功です。

このように、実行ステップを分ければ、三項演算子を維持したままインデントやスペース、テンプレートリテラルの整形が正しく行えます。



参考：PrettierとESLint fixを分けて実行するケース
https://github.com/prettier/eslint-config-prettier#special-rules


## まとめ

いかがでしたでしょうか。
明日の[Goodpatch Advent Calendar 2020](https://qiita.com/advent-calendar/2020/goodpatch)は、いつもスマイルなフロントエンド @zookeeper08 がお送りします。
皆様よい12月をお過ごしくださいませ。