ESLint 設定を Glob ベースでがっつり書き直した

Prettier や TypeScript を使うようになって、3年前に構築してぼちぼち保守してきた自分用の共有設定 [eslint-config-mysticatea] をそのまま使えないことが増えてきました。また、1年ほど前に [Glob ベースで設定できるようになっている](https://qiita.com/mysticatea/items/e758c9a9a7dfb8b9cdb1#3611-glob-based-configuration) ので、この際がっつり書き直すことにしました。

ついでに、ESLint 共有設定を作る際のプラクティスのようなものがまとまっていないと感じたので、まとめてみます。改めて整理してみると、本当にバッドノウハウの塊だなぁ...。互換性に縛られずリライトしたいですね...。

# 新しい共有設定の紹介

新しい設定は [@mysticatea/eslint-plugin](https://github.com/mysticatea/eslint-plugin/tree/master/lib/configs) にあります ([eslint-config-mysticatea] ではない)。

```console:shell
$ npm install --save-dev eslint @mysticatea/eslint-plugin
```

```yml:.eslintrc.yml
extends:
  - plugin:@mysticatea/es2018 # es5, es2015, es2016, es2017, es2019 もある。目的に応じて。
```

中身は以下のようになっています。

- バグっぽいコードを見つけるために ESLint コアルールと [eslint-plugin-eslint-comments] のルールを有効にしています。
- 空白や改行については [eslint-plugin-prettier] 経由で Prettier を使っています。
- `.ts` ファイルでは [typescript-eslint-parser] と [eslint-plugin-typescript] のルールも有効にしています。
- `.vue` ファイルでは [vue-eslint-parser] と [eslint-plugin-vue] のルールも有効にしています。
- `test` ディレクトリの中は [mocha] のグローバル変数を有効にしています。
- `scripts` ディレクトリの中は Node.js 環境であるとします ([eslint-plugin-node] のルールも有効にして、`.js` ファイルはスクリプト、`.mjs` ファイルはモジュールとして扱う)。
- `.eslintrc.js` や `webpack.config.js` 等の一部ファイルは Node.js 環境であるとします。

また、開発目的に応じて追加の設定を行うことができます。

```yml:.eslintrc.yml
extends:
  - plugin:@mysticatea/es2018
  - plugin:@mysticatea/+browser # ブラウザ向けのグローバル変数を定義する。
                                # ただし name, event, top 等、よく使うような名前は定義しない。
```

```yml:.eslintrc.yml
extends:
  - plugin:@mysticatea/es2018
  - plugin:@mysticatea/+node # 全体を Node.js 環境とする。
```

```yml:.eslintrc.yml
extends:
  - plugin:@mysticatea/es2018
  - plugin:@mysticatea/+eslint-plugin # eslint-plugin 開発向けの設定を追加する。
                                      # eslint-plugin-node と eslint-plugin-eslint-plugin の
                                      # ルールを追加。
```

```yml:.eslintrc.yml
extends:
  - plugin:@mysticatea/es2018
  - plugin:@mysticatea/+modules # ES modules のオプションと追加のルールを有効にする。
  - plugin:@mysticatea/+browser
```

Glob-based な設定によって、いつも使っているディレクトリ構造について1行で設定できるようになりました。`.vue` `.ts` に関しても追加の設定なしで検証かけられて楽になりました。

# 共有設定のプラクティス

## 1. 共有設定機能を使わない

共有設定機能、つまり `eslint-config-xxx` のことです。この機能は ESLint のプラグイン機構が設計されるより前に作られたため、プラグインと共に使おうとすると不都合が多いです。

> 例えば、プラグインのパッケージは `eslint` が `require("eslint-plugin-xxx")` で読み込める位置にインストールする必要がありますが、共有設定の `dependencies` にプラグインを追加しても期待通りにインストールできない場合が多いです。人気のある [eslint-config-airbnb] の Usage に複雑なインストール手順が書かれているのはそのため。

そこで、プラグイン機構 (`eslint-plugin-xxx`) を共有設定機能の代わりに使うのがおすすめです。
プラグインは、以下のように `configs` プロパティで共有設定を公開することができます。現在も多くのプラグインでは、これを利用して `plugin:node/recommended` のような推奨設定を提供していますね。

```js:eslint-plugin-hoge/index.js
module.exports = {
    configs: {
        fuga: {
            // ここに設定を書く
        }
    },
    rules: {},
}
```

```yml:.eslintrc.yml
extends:
  - plugin:hoge/fuga
```

## 2. 他のプラグインを使うときはルールを再定義する

次の例は良くない例です。

```js:eslint-plugin-hoge/index.js
module.exports = {
    configs: {
        fuga: {
            plugins: ["node", "hoge"],
            rules: {
                "node/no-unsupported-features": "error",
            },
        },
    },
    rules: {},
}
```

[eslint-plugin-node] を使う設定が書かれているので、この設定を使う場合は、前項の [eslint-config-airbnb] のようにインストール手順で [eslint-plugin-node] もインストールするよう要請する必要があります。

代わりに、自身の中にルールを再定義するのがおすすめです。

```js:eslint-plugin-hoge/index.js
const nodePlugin = require("eslint-plugin-node")

module.exports = {
    configs: {
        fuga: {
            plugins: ["hoge"],
            rules: {
                "hoge/node/no-unsupported-features": "error",
            },
        },
    },
    rules: {
        "node/no-unsupported-features": nodePlugin.rules["no-unsupported-features"],
    },
}
```

こうすることで [eslint-plugin-node] プラグインを読み込む主体が ESLint から `eslint-plugin-hoge` に変わるので、`dependencies` に記述した他のプラグインを確実に利用できます。そのため、`eslint-plugin-hoge` の利用者は `eslint-plugin-hoge` (と `eslint` 本体) だけをインストールすればそれを使えるようになります。

> これは、共有設定とプラグインに関する議論の中で ESLint の作者が提案した方法です ([eslint/eslint#3458](https://github.com/eslint/eslint/issues/3458#issuecomment-257161846))。いわゆる[菱形継承問題](https://ja.wikipedia.org/wiki/%E8%8F%B1%E5%BD%A2%E7%B6%99%E6%89%BF%E5%95%8F%E9%A1%8C)があって自動的に処理するのが難しいため、このような提案になっています。バッドノウハウ。

## 3. 基本設定と環境別の設定に分離する

JavaScript は様々な用途で使われています。Web サイト、Web アプリに、Node.js アプリ (ESLint もそのうちの1つ)、Electron アプリ、Windows Script Host (WSH) や AppleScript 等のオートメーションツール、などなど。用途によって必要な設定は変わってきます。また、同じ環境でも使用するフレームワーク・ライブラリによって有用な検証ルールを提供してくれていたりします。

そこで、共有設定を作る場合には環境によらない基本的な設定と、環境ごとに特化した設定を分けておくと良いです。

私の場合は ECMAScript 言語仕様の範疇にある検証ルールを `plugin:@mysticatea/es2018` のような設定にまとめ、環境ごとに `plugin:@mysticatea/+browser`, `plugin:@mysticatea/+node`, `plugin:@mysticatea/+eslint-plugin` のような追加設定を書いています。

[eslint-config-airbnb] では基本設定と React 用の追加設定とに分かれていますね。

## 4. テストを書く

作成した共有設定を保守していくために CI テストを書くと良いです。

- `eslint.CLIEngine` オブジェクトを作ってプラグインを読み込み、正常動作することを確認する。
- [eslint-find-rules] のようなツールを使って足りないルールを確認する。

などすると良いでしょう。

> - プラグインを `CLIEngine` から読み込むためには `node_modules/eslint-plugin-xxx` なディレクトリが必要なので、[mysticatea/eslint-plugin/.eslintrc.js#L10-L19] のようにシンボリックリンクを作成してやると良いと思います。バッドノウハウ。
> - [mysticatea/eslint-plugin/tests/lib/configs](https://github.com/mysticatea/eslint-plugin/tree/26907926ddbf06e17e3fb18529e808c7f38872d6/tests/lib/configs) ではいろいろテストを書いたので、これを元に ConfigTester API を公開できたらいいな: [eslint/eslint#10289](https://github.com/eslint/eslint/issues/10289)

## 5. セルフホストする

プラグイン自身のコードの静的検証にそのプラグインを使えると格好いいです。

[mysticatea/eslint-plugin/.eslintrc.js#L10-L19] のように、`.eslintrc.js` ファイルでシンボリック リンクを作ると自分自身を使えたりします。


[eslint-config-airbnb]: https://www.npmjs.com/package/eslint-config-airbnb
[eslint-config-mysticatea]: https://www.npmjs.com/package/eslint-config-mysticatea
[eslint-find-rules]: https://www.npmjs.com/package/eslint-find-rules
[eslint-plugin-eslint-comments]: https://www.npmjs.com/package/eslint-plugin-eslint-comments
[eslint-plugin-eslint-plugin]: https://www.npmjs.com/package/eslint-plugin-plugin
[@mysticatea/eslint-plugin]: https://github.com/mysticatea/eslint-plugin
[eslint-plugin-node]: https://www.npmjs.com/package/eslint-plugin-node
[eslint-plugin-prettier]: https://www.npmjs.com/package/eslint-plugin-prettier
[eslint-plugin-typescript]: https://www.npmjs.com/package/eslint-plugin-typescript
[eslint-plugin-vue]: https://www.npmjs.com/package/eslint-plugin-vue
[mocha]: https://www.npmjs.com/package/mocha
[typescript-eslint-parser]: https://www.npmjs.com/package/typescript-eslint-parser
[vue-eslint-parser]: https://www.npmjs.com/package/vue-eslint-parser

[mysticatea/eslint-plugin/.eslintrc.js#L10-L19]: https://github.com/mysticatea/eslint-plugin/blob/26907926ddbf06e17e3fb18529e808c7f38872d6/.eslintrc.js#L10-L19