この記事はGoodpatch Advent Calendar 2020の2日目となります。
最寄り駅のイチョウ並木にイルミネーションが灯り始めました。
JSを整形したいだけだった
フロントエンド開発では定番のeslint と prettier 。
今回やりたいことはJavaScriptコードをeslintでコードチェックし、prettierでコード整形したいだけ。
それだけのはずでした。
使用ファイル
今回説明するコードサンプルは以下GitHubにまとめてあります。
こちらも合わせてご参照ください。
https://github.com/ikezaworld/eslinttest
ディレクトリ構成
/
├─ src
│ ├─ index.js
│ └─ index_org.js
├─ .eslintrc.js
├─ .prettierrc.js
└─ package.json
何の変哲もないコードたち
対象コードは同じ内容の三項演算子が3つあるだけ。
見分けやすいよう変数名の連番(test1
test2
test3
)のみ変えています。
名前以外は全くいっしょのコードたちです。
const test1 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test2 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test3 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
整形を実行する
さっそく eslint --fix ./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
の三項演算子 ?
直後に半角スペースを入れて実行してみる。
▼ 実行前
const test1 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test2 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test3 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
▼ 実行結果
const test1 = isFoo
? "データfooを選択中です(" + 1 + " 個)"
: "選択できるデータは存在しません";
const test2 = isFoo
? "データfooを選択中です(" + 1 + " 個)"
: "選択できるデータは存在しません";
const test3 = isFoo
`データfooを選択中です(${ 1 } 個)` : '選択できるデータは存在しません';
カテゴリ | 結果 |
---|---|
test1 | △ |
test2 | △ |
test3 | × |
※△は構文はOKだがテンプレートリテラルがされていないもの | |
実行結果はtest1 がOKになった。他はそのまま変化なし。 |
三項演算子?の直後の改行を消し1行にする
test1
の三項演算子 ?
直後の改行を消し1行にしてみる。
▼ 実行前
const test1 = isFoo ? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test2 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test3 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
▼ 実行結果
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行に揃えてみる。
▼ 実行前
const test1 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test2 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test3 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
▼ 実行結果
const test1 = isFoo
`データfooを選択中です(${ 1 } 個)` : '選択できるデータは存在しません';
const test2 = isFoo
`データfooを選択中です(${ 1 } 個)` : '選択できるデータは存在しません';
const test3 = isFoo
`データfooを選択中です(${ 1 } 個)` : '選択できるデータは存在しません';
カテゴリ | 結果 |
---|---|
test1 | × |
test2 | × |
test3 | × |
実行結果は、なんと全部失敗になってしまった。改行消しただけなのに。
これはひどい。
失敗から見えてくるもの
色んなパターンを試してみた。
結局どのパターンでも3つ全部成功はできなかった。
しかしこれまでのパターンから、
- 改行
- スペース
の有無にどうやら意味があるらしいことが見えてきた。
設定ファイル内容
package.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
内容は以下の通り。
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」 は、この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
内容は以下の通り。
{
"printWidth": 80
}
printWidth
は行内の折り返し文字数を指定するものです。
80文字折り返しを指定していますが、実はprettierのデフォルトも80です。
なので実質このファイルがあってもなくても変わりません。
一応明示的に設定しています。
参考: https://prettier.io/docs/en/options.html#print-width
原因
失敗時の整形処理を順を追って確認すると以下の通りです。
- prettier整形で三項演算子
?
部分について、改行位置整形のために一時削除される - 複数行に跨がるprettier整形が終わる前に、割り込む形で
prefer-template
が入ってしまう - prettierの1行の文字数折り返し整形を実行しようとするが
prefer-template
が対象コード内容を変えているため正しく処理されない
このために三項演算子が整形されず ?
が消えたままの現象が発生していました。
▼ 失敗例分析
▼ 成功例分析
成功時の場合は、prettier整形がきちんと終った後にprefer-template
が実行されています。
原因検証
prefer-templateなしで実行
"prefer-template": "warn"
部分を削除してみます。
要はテンプレートリテラル整形なしです。
この設定でeslint --fix ./src/index.js
を実行。
module.exports = {
env: {
browser: true,
es6: true,
},
extends: ["prettier"],
plugins: ["prettier"],
rules: {
"prettier/prettier": "warn",
},
};
▼ 実行結果
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
を実行。
{
"printWidth": 120
}
▼ 実行結果
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
を実行。
▼ 実行前
const test1 = isFoo ? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test2 = isFoo ? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test3 = isFoo ? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
▼ 実行結果
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
を実行。
▼ 実行前
const test1 = isFoo
? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test2 = isFoo
? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test3 = isFoo
? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
▼ 実行結果
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行での書き方
const test1 = isFoo ? 'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
例:複数行での書き方
const test1 = isFoo
? 'データfooを選択中です(' + 1 +' 個)'
: '選択できるデータは存在しません';
2. ESLint未適用のコードにはprettierをしてからESLintを行う
一度prettier整形してしまえばそれ以降はESLint fixのみでも問題ない。
そのためprettier単独で整形した後に別ステップでESLint fixをかけること。
これなら整形で壊れることはないです。
例えば大量のESlint未適用コードが合った場合、そこに一括ESlintをかけるなら以下1.2.の順序で行う。
こうすれば以降はESlint+prettierを同時にかけても今回の問題は回避できます。
- 最初にprettierのみ実行する。
- その後別ステップとしてESLint fixをする。
実際にprettilerとESLintを別々にかけてみた
▼ 実行前
const test1 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test2 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
const test3 = isFoo ?
'データfooを選択中です(' + 1 +' 個)' : '選択できるデータは存在しません';
▼ prettier実行
prettier --write ./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
を実行すると以下になります。
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は、いつもスマイルなフロントエンド @zookeeper08 がお送りします。
皆様よい12月をお過ごしくださいませ。