LoginSignup
6

More than 1 year has passed since last update.

posted at

updated at

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

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

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
What you can do with signing up
6