LoginSignup
14
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-01

この記事はGoodpatch Advent Calendar 2020の2日目となります。
最寄り駅のイチョウ並木にイルミネーションが灯り始めました。

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

フロントエンド開発では定番のeslintprettier
今回やりたいことは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)のみ変えています。
名前以外は全くいっしょのコードたちです。

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

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


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


整形を実行する

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

▼ 実行結果

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

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

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

結果がおかしい!?

消えた三項演算子?

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

無事だった2番目

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

色んなパターンを試す

三項演算子?の直後に半角スペース

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

▼ 実行前

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

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


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


▼ 実行結果

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行にしてみる。

▼ 実行前

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

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


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


▼ 実行結果

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行に揃えてみる。

▼ 実行前

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

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

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

▼ 実行結果

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 は以下の通り。

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 内容は以下の通り。

.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 内容は以下の通り。

.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

▼ 成功例分析

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

  • ①、②で三項演算子?や1行内折り返し文字数をprettier整形完了。
  • prettier整形後の綺麗な状態で③prefer-templateが正しく実行される。 code2.png

原因検証

prefer-templateなしで実行

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

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

▼ 実行結果

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 を実行。

.prettierrc
{
  "printWidth": 120
}

▼ 実行結果

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 を実行。

▼ 実行前

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

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


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


▼ 実行結果

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 を実行。

▼ 実行前

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

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


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


▼ 実行結果

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行での書き方

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を同時にかけても今回の問題は回避できます。

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

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

▼ 実行前

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

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

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

▼ prettier実行

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

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 を実行すると以下になります。

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月をお過ごしくださいませ。

14
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
6