Node.js のための ESLint プラグイン紹介

こんにちは。
[ESLint] のメンテナ[^1]や [npm-run-all] 等 npm-scripts 向け CLI ツールの開発をしている @mysticatea です。
今回は Node.js アドベント カレンダーということで、[eslint-plugin-node] を紹介したいと思います。

[eslint-plugin-node] は、[Node.js] 向けに特化した [ESLint] の追加ルールを定義しているプラグインです。
[ESLint] 本体に付属しているルール群 (通称コアルール) にも [Node.js] に特化したものがありますが、このプラグインのルールは諸事情[^2]によりコアに入れなかったルールたちです。

[ESLint] 自体についてはこちらの記事をご覧ください。

- [ESLint 最初の一歩](http://qiita.com/mysticatea/items/f523dab04a25f617c87d)


# :cd: インストール

[ESLint] プラグインですので、[npm] を利用してインストールします。
[ESLint] をグローバル (`--global`) にインストールしている場合は、同じくグローバルへ。[ESLint] をローカル (`--save-dev`) にインストールしている場合は、同じくローカルへインストールしてください。

```bash:Shell
$ npm install --save-dev eslint eslint-plugin-node
```

- Node.js 4.0.0 以降が必要です。
- ESLint 3.1.0 以降が必要です。

なお、[推奨はローカル インストール](http://qiita.com/mysticatea/items/6bd56ff691d3a1577321)です。


# :wrench: 設定の書き方

`.eslintrc.json` 等の設定ファイルにプラグインの情報を書く必要があります。

```json:.eslintrc.json_の例
{
    "extends": ["eslint:recommended"],
    "plugins": ["node"],
    "env": {
        "es6": true,
        "node": true
    },
    "rules": {
        "node/exports-style": "error",           //`module.exports`と`exports.*`を混ぜて使うと警告します。
        "node/no-deprecated-api": "error",       //Node.jsの非推奨APIを使用すると警告します。
        "node/no-missing-import": "error",       //`import`構文で存在しないファイルを読もうとすると警告します。
        "node/no-missing-require": "error",      //`require()`で存在しないファイルを読もうとすると警告します。
        "node/no-unpublished-bin": "error",      //CLIのエントリポイントが無視リストに入っていた場合に警告します。
        "node/no-unpublished-import": "error",   //`import`構文で公開後に読めなくなるモジュールを読もうとすると警告します。
        "node/no-unpublished-require": "error",  //`require()`で公開後に読めなくなるモジュールを読もうとすると警告します。
        "node/no-unsupported-features": "error", //指定したNode.jsのバージョンでサポートされていない構文を使おうとすると警告します。
        "node/process-exit-as-throw": "error",   //`process.exit()`の実行パスを修正します。
        "node/shebang": "error",                 //シバンの誤りを指摘します。
    }
}
```

# :bulb: ルール説明

## [node/exports-style]

[Node.js] でクラスや関数をファイル外にエクスポートするとき、`module.exports` と `exports` を混在させて使おうとすると、予期せぬ事態になる可能性があります。

```js:例
module.exports = function foo() {
    // do something.
}

exports.foo = 1
```

この時、`exports.foo`はエクスポートされず消えてしまいます。
こういった事態を抑止するために、`module.exports` と `exports` のうち、指定した方だけを利用するように設定することができます。

```json:設定例
{
    "node/exports-style": ["error", "module.exports"],
    //または
    "node/exports-style": ["error", "exports"]
}
```

```js:例(module.exports)
//✔ GOOD
module.exports = {
    foo: 1,
    bar: 2
}
module.exports.baz = 3

//✘ BAD
exports.qiz = 4
```

```js:例(exports)
//✔ GOOD
exports.foo = 1
exports.bar = 2
exports.baz = 3

//✘ BAD
module.exports = {}
```

## [node/no-deprecated-api]

[Node.js] の非推奨 API を利用しようとすると警告します。

```json:設定例
{
    "node/no-deprecated-api": "error",
}
```

```js:例
//✘ BAD
fs.exists(fileName, (b) => {})
const b = new Buffer(text)
```

非推奨 API には、将来的に削除される予定のものや、セキュリティ リスクのあるもの等が存在します。
ぜひ避けましょう。

警告される API の一覧は[こちら](https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-deprecated-api.md)。

## [node/no-missing-require]

`require()`式で存在しないファイル/モジュールをインポートしようとすると警告します。
モジュール名やファイルパスの打ち間違えに素早く気がつくことができます。

```json:設定例
{
    "node/no-missing-require": "error",
}
```

```js:例
//✘ BAD
const typoFile = require("./typo-file");
const typoModule = require("typo-module");
```

## [node/no-missing-import]

`node/no-missing-require`と同じですが、`require()`式の代わりに`import`構文をチェックします。

> :warning: ECMAScript 2015 (ES6) ではファイルの検索ロジックは定義されておらず、[Node.js] はまだ ES Modules 構文をサポートしていません。そのため、このルールは将来的に変更される可能性があります。

## [node/no-unpublished-bin]

CLI のエントリポイントが無視リストに入っていた場合に警告します。

- `npm publish`でモジュールを公開する時、不要なファイルは含めないように設定しておくことが望ましいです。`package.json`の [files] フィールドや [.npmignore] ファイルを利用して、モジュールに含めるファイル・含めないファイルを指定することができます。
- 各モジュールは `package.json`の [bin] フィールドを使って CLI コマンドを公開することができます (例えば `eslint` コマンドはこの機能で公開されています)。

さて、npm は `package.json`の [bin] フィールドで指定されているファイルを自動的にモジュールに含めてくれません。そのファイルが [files] フィールドに含まれていなかったり、[.npmignore] ファイルに含まれていたりすると除外され、利用者がモジュールをインストールしようとしたときにエラーになります。

```json:package.json_例
{
    "name": "my-module",
    "version": "1.0.0",
    "bin": "bin/index.js",
    "files": ["lib"]        //※binディレクトリを含めていない
}
```

このようなエラーを事前に見つけて報告してくれます。

## [node/no-unpublished-require]

モジュール公開後に読めなくなるファイルを`require()`式でインポートしようとすると警告します。
このルールは、次のようなシチュエーションを防ぎます。

- 開発環境では動くが、モジュール公開後に読み込もうとしたファイル・モジュールが存在せずエラーになる
- あなたのローカル環境では動くが、チームメンバーが `clone` すると読み込もうとしたファイル・モジュールが存在せずエラーになる

これらを回避するために、次のような`require()`式を警告します。

- `require()`式が**公開されるファイル**[^3]に書かれている場合
    - インポート対象が**公開されないファイル**[^4]である
    - インポート対象が`package.json`の`dependencies`フィールドに含まれないモジュールである
- `require()`式が**公開されないファイル**に書かれている場合
    - インポート対象が`package.json`の`dependencies`/`devDependencies`両フィールドに含まれないモジュールである

[babel] 等のトランスパイラを利用している場合、ソースコードの位置とトランスパイル後のファイルの位置が異なる可能性があります。この場合は [convertPath](https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unpublished-require.md#convertpath) オプションを使うと適切な警告を生成できます。



## [node/no-unpublished-import]

`node/no-unpublished-require`と同じですが、`require()`式の代わりに`import`構文をチェックします。

> :warning: ECMAScript 2015 (ES6) ではファイルの検索ロジックは定義されておらず、[Node.js] はまだ ES Modules 構文をサポートしていません。そのため、このルールは将来的に変更される可能性があります。

## [node/no-unsupported-features]

**指定した [Node.js] のバージョンでサポートされていない ECMAScript 構文**を使おうとすると警告します。

例えば、[Node.js] 4 は (strict モードの外では) `let`,`const`,`class`をサポートしていません。一方で、`"use strict"`が適切に書かれていれば`let`,`const`,`class`を使うことができます。

- より詳しくは [node.green](http://node.green/) をチェックしてください。

このルールを使うと、未サポートの構文を使おうとしてしまった場合に素早く気がつくことができます。

どのバージョンを対象にするか? は、`package.json`の [engines] フィールドによって指定します。

```json:package.json_例
{
    "name": "my-module",
    "version": "1.0.0",
    "engines": {
        "node": ">=4.0.0"
    }
}
```

## [node/process-exit-as-throw]

`process.exit()`の実行パスを修正します。

実行パス解析を利用した静的検証を改善するメタルールです。
このルール自体は何も警告しませんが、`process.exit()`式が`throw`文と同等に扱われるように実行パス解析を修正することにより、実行パス解析を利用するルールの誤検出を軽減します。

例えば、次のコードにこのメタルールを有効にすることで [consistent-return] ルールの誤検出が修正されます。

```js
function foo(a) { //※ error 値を返さない実行パスがあります (Expected to return a value at the end of this function.)
    if (a) {
        return "ok"
    } else {
        process.exit(1)
    }
}
```

## [node/shebang]

シバンの誤りを指摘します。

各モジュールは `package.json`の [bin] フィールドを使って CLI コマンドを公開することができます。
この時、CLI コマンドとして実行される `.js` ファイルにはシバンを書くことになります。

```js:シバンの例
#!/usr/bin/env node

console.log("hello")
```

このようにすると、[npm] はこのファイルを Linux でも Windows でも実行できるように適切にインストールしてくれます。

このルールは、`eslint --fix` による自動修正でシバンを自動的に追加します。


# :pencil: あとがき

エディタに連携させてリアルタイムで警告が表示されるようにしたら、作業がより捗るのではないでしょうか。私見ですが、[ESLint] と相性が良いエディタはずばり [Visual Studio Code] です。
[ESLint] は起動が遅いのですが、[Visual Studio Code] は [Node.js] を内蔵しているために [ESLint] をエディタ内で実行できます。毎回起動しなくて良いので驚くほど軽快です。おすすめ。

それでは、良き開発ライフを :wave:。

----

[^1]: https://github.com/eslint/eslint/graphs/contributors
[^2]: ファイルシステムを使う必要があったり、メンテナンス コストが高かったり
[^3]: `package.json`の [files] フィールドに含まれていて、かつ [.npmignore] ファイルに含まれていないファイル
[^4]: `package.json`の [files] フィールドに含まれていないか、または [.npmignore] ファイルに含まれているファイル


[.npmignore]: https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package
[ESLint]: http://eslint.org/
[Node.js]: https://nodejs.org/
[Visual Studio Code]: http://code.visualstudio.com/
[babel]: https://babeljs.io/
[bin]: https://docs.npmjs.com/files/package.json#bin
[consistent-return]: http://eslint.org/docs/rules/consistent-return
[engines]: https://docs.npmjs.com/files/package.json#engines
[eslint-plugin-node]: https://www.npmjs.com/package/eslint-plugin-node
[files]: https://docs.npmjs.com/files/package.json#files
[npm]: https://www.npmjs.com/
[npm-run-all]: https://www.npmjs.com/package/npm-run-all
[node/exports-style]: https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/exports-style.md
[node/no-deprecated-api]: https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-deprecated-api.md
[node/no-missing-import]: https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-missing-import.md
[node/no-missing-require]: https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-missing-require.md
[node/no-unpublished-bin]: https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unpublished-bin.md
[node/no-unpublished-import]: https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unpublished-import.md
[node/no-unpublished-require]: https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unpublished-require.md
[node/no-unsupported-features]: https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features.md
[node/process-exit-as-throw]: https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/process-exit-as-throw.md
[node/shebang]: https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/shebang.md