ESLint のカスタムルールを作ろう! (その1)

# はじめに

ESLint の魅力の1つに、自分のプロダクトに合わせた独自ルールを作成することができるという点があります。
これから数回に分けて独自ルールの作り方・使い方を解析していきます。
この記事では、簡単なルールを作りながら、ESLint のルールの基本的な仕組みを解説します。

ここでは ESLint の基本的な使い方は扱いません。
ESLint の基本については[ESLint 最初の一歩](http://qiita.com/mysticatea/items/f523dab04a25f617c87d)等を御覧ください。

# `no-literal-call`

この記事では、最初の目標としてリテラルを関数呼び出ししている場合に警告するルールを作っていきます。
その過程で必要な知識を解説していくつもりです。

今回題材にするソースコードはこちら。

```js:hello.js
"use strict";

var foo = "Hello custom rules"();
```

これは有効な JavaScript コードですが、実行すると必ず `TypeError` になります。
文字列を関数呼び出ししているので、当然ですね。

![文字列は関数ではありません!](https://qiita-image-store.s3.amazonaws.com/0/72555/3e157241-f41d-fd65-c036-355e49ec404e.png)

しかし ESLint はこのコードを警告してくれないので、カスタムルールを作ろうというわけです。

# 作成手順

さて、カスタムルール作りを開始します。
必要な手順は、次の4つです。

1. ルール置き場を作る
2. テストを書く
3. ルールを書く
4. 設定ファイルを書く

## ルール置き場を作る

まずはルール置き場を作って、**ルール名と同じ名前の**ファイルを作ります。
ルール置き場の名前はなんでも良いのですが、この記事では次のように`eslint`フォルダを作り、その下に`rule`と`test`フォルダを作ります。

```txt:フォルダ構成
📂 eslint-custom-rule-tutorial/step1
├─ 📂 eslint
│  ├─ 📂 rule
│  │  └─ 📄 no-literal-call.js
│  └─ 📂 test
│     └─ 📄 no-literal-call.js
├─ 📄 .eslintrc
├─ 📄 hello.js
└─ 📄 package.json
```

> 上記の環境のうち、`.eslintrc`, `hello.js`, `package.json` は[こちら](https://github.com/mysticatea/eslint-custom-rule-tutorial/tree/master/step1)にあるものを使っています。

ルールとテストの最初のひな形は次のようになります。

```js:eslint/rule/no-literal-call.js
"use strict";

// ルール定義。
module.exports = function(context) {
    return {
    };
};

// ルールのオプション定義。今回は使わない。
module.exports.schema = [];
```

```js:eslint/test/no-literal-call.js
"use strict";

// テスターを読み込む
var RuleTester = require("eslint").RuleTester;

// テスターを作って実行する
// tester.run(ルール名, ルール定義, テストパターン);
var tester = new RuleTester();
tester.run("no-literal-call", require("../rule/no-literal-call"), {
    valid: [],
    invalid: []
});
```

どんなルールを作る場合でも、まずはこのひな形から始まります。

この状態でテストを実行すると、空っぽのテストが実行されます。
(テストには`mocha`を利用しています)

```
$ npm test

> eslint-custom-rule-tutorial@1.0.0 test eslint-custom-rule-tutorial/step1
> mocha eslint/test/*.js

  0 passing (1ms)
```

## テストを書く

さぁ、テストを書きましょう。

テストは、このルールが正しいと考えるコードと、問題と考えるコードをそれぞれ列挙します。
これを最初に書いておくと、目標がはっきりしていい感じです。

```js:eslint/test/no-literal-call.js
"use strict";

var RuleTester = require("eslint").RuleTester;
var tester = new RuleTester();

tester.run("no-literal-call", require("../rule/no-literal-call"), {
    valid: [
        // 変数や関数を呼び出すのは正しい!
        {code: "foo();"},
        {code: "obj.foo();"},
        {code: "(function() {})();"},
        {code: "(() => 0)();", env: {es6: true}}
    ],
    invalid: [
        // 関数以外のリテラルを呼び出すのは間違っている!
        {code: "true();", errors: ["This is not a function."]},
        {code: "false();", errors: ["This is not a function."]},
        {code: "null();", errors: ["This is not a function."]},
        {code: "100();", errors: ["This is not a function."]},
        {code: "\"hello\"();", errors: ["This is not a function."]},
        {code: "/abc/();", errors: ["This is not a function."]},
        {code: "[1,2,3]();", errors: ["This is not a function."]},
        {code: "({foo: 0})();", errors: ["This is not a function."]},
        {code: "`hello`();", env: {es6: true}, errors: ["This is not a function."]},
        {code: "(class A {})();", env: {es6: true}, errors: ["Class constructors cannot be invoked without 'new'"]}
    ]
});
```

何をしているのか、分かりやすいと思います。

`invalid`側には`errors`プロパティも指定する必要があります。
`errors`は「表示すべき警告メッセージ」です。配列で、もし複数の警告メッセージがある場合はすべて指定します。
また、ES2015(ES6) の新しい構文を使う場合はコードの他に`env: {es6: true}`を指定する必要があります。

この状態でテストを実行してみると、「問題と考えるべきコードが警告されていない」と怒られます。

```:shell
$ npm test

> eslint-custom-rule-tutorial@1.0.0 test eslint-custom-rule-tutorial/step1
> mocha eslint/test/*.js

  no-literal-call
    √ foo();
    √ obj.foo();
    √ (function() {})();
    √ (() => 0)();
    1) true();
    2) false();
    3) null();
    4) 100();
    5) "hello"();
    6) /abc/();
    7) [1,2,3]();
    8) ({foo: 0})();
    9) `hello`();
    10) (class A {})();

  4 passing (80ms)
  10 failing

...
```

ルールが空っぽですからね。

## ルールを書く

いよいよ本題、ルールを書きます。

ルールは、1つ以上のメソッドを持つオブジェクトです。
メソッド名は、抽象構文木(AST)のノードの種類です。
コードにその種類のノードが現れたらメソッドが呼び出されます。

百聞は一見にしかず。
例を見てみましょう。

```js:eslint/rule/no-literal-call.js
"use strict";

module.exports = function(context) {
    return {
        "ArrayExpression": function(node) {
            console.log("配列リテラルを発見!");
        }
    };
};

module.exports.schema = [];
```

`ArrayExpression`という名前のメソッドを持つオブジェクトを`return`しています。
このメソッドは、コード中に`ArrayExpression`、つまり配列が出現すると呼び出されます。
この状態でテストを実行すると、配列のテストの時だけ「配列リテラルを発見!」と印字されます。

```:shell
$ npm test

> eslint-custom-rule-tutorial@1.0.0 test eslint-custom-rule-tutorial/step1
> mocha eslint/test/*.js

  no-literal-call
    √ foo();
    √ obj.foo();
    √ (function() {})();
    √ (() => 0)();
    1) true();
    2) false();
    3) null();
    4) 100();
    5) "hello"();
    6) /abc/();
配列リテラルを発見!
    7) [1,2,3]();
    8) ({foo: 0})();
    9) `hello`();
    10) (class A {})();

  4 passing (80ms)
  10 failing

...
```

### 抽象構文木(AST)について

簡単に言うと、文字列データであるソースコードを解析してオブジェクトにしたものです。
ESLint は事前にソースコードを解析してオブジェクトにしてから、それを各ルールに渡してくれます。
渡されるオブジェクトは単純なJSONデータです。

例を見てみましょう。
今回用意したソースコード`hello.js`は、次のような抽象構文木になります[^1]。

```js:hello.js
"use strict";

var foo = "Hello custom rules!"();
```

```json:抽象構文木(AST)
{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "Literal",
        "value": "use strict"
      }
    },
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "foo"
          },
          "init": {
            "type": "CallExpression",
            "callee": {
              "type": "Literal",
              "value": "Hello custom rules!"
            },
            "arguments": []
          }
        }
      ],
      "kind": "var"
    }
  ]
}
```

JavaScript の抽象構文木(AST)の仕様は [ESTree](https://github.com/estree/estree) というリポジトリで管理されています。
ESLint が渡してくれる抽象構文木(AST)もこの仕様に則ったものです。
ESLint でルールを書いていると、よくこの仕様書とにらめっこすることになります。

また、実際にコードから作られる抽象構文木(AST)を確認したい場合は、[astexplorer.net](http://astexplorer.net/#/VeRiaJxlcX)を利用すると捗ります。
コードを入力するとリアルタイムで抽象構文木(AST)を表示してくれるため、便利です。

### ロジックを考える

では今回のルールでは、どの種類の抽象構文木(AST)ノードをチェックすればよいでしょうか?
私はルールを実装するとき、よく下の画像のようにテストとルール定義を並べます。
こうすると考えがまとまりやすい気がします (個人的に)。

![image](https://qiita-image-store.s3.amazonaws.com/0/72555/874a7f65-be98-4e11-fb8c-8ffcfa7315ef.png)

さて、[関数呼び出しのノード](https://github.com/estree/estree/blob/master/es5.md#callexpression)を見て、呼び出し対象(callee)がリテラルかどうかをチェックするのは素直で良さそうです。

```js:eslint/rule/no-literal-call.js
"use strict";

var LITERAL_TYPE = /^(?:Literal|ArrayExpression|ObjectExpression|TemplateLiteral)$/;

module.exports = function(context) {
    return {
        /**
         * この CallExpression の呼び出し対象が Literal, ArrayExpression, ObjectExpression,
         * TemplateLiteral, ClassExpression のいずれかだった場合、警告する。
         *
         * @param {ASTNode} node - チェックする CallExpression ノード。
         */
        "CallExpression": function(node) {
            var callee = node.callee;
            var message = null;

            // 呼び出し対象のtypeがリテラルかどうかチェックする。
            if (LITERAL_TYPE.test(callee.type)) {
                message = "This is not a function.";
            }
            if (callee.type === "ClassExpression") {
                message = "Class constructors cannot be invoked without 'new'.";
            }

            // リテラルだったら警告する
            if (message) {
                context.report({node: node, message: message});
            }
        }
    };
};

module.exports.schema = [];
```

こんな感じ。
最後に `context.report({node: node, message: message});` という文があります。
これは**問題を報告する**メソッドです。
ここに渡したノードを、渡したメッセージで警告します。
すべてのルールが必ず使うはず[^2]の重要なメソッドです。

それでは、ルールも書けたのでテストを実行してみます。

```:shell
$ npm test

> eslint-custom-rule-tutorial@1.0.0 test eslint-custom-rule-tutorial/step1
> mocha eslint/test/*.js

  no-literal-call
    √ foo();
    √ obj.foo();
    √ (function() {})();
    √ (() => 0)();
    √ true();
    √ false();
    √ null();
    √ 100();
    √ "hello"();
    √ /abc/();
    √ [1,2,3]();
    √ ({foo: 0})();
    √ `hello`();
    √ (class A {})();


  14 passing (81ms)
```

いいですね :+1: 

## 設定を書く

ルールが完成して、テストによって動作することを確認できました。
次はこれを本物のソースコードに適用してみます。

そのためには、2つの設定が必要です。

1. `eslint`コマンドの`--rulesdir`オプション
2. `.eslintrc`の警告レベル

### `eslint`コマンドの`--rulesdir`オプション

カスタムルールが配置されているディレクトリを指定するコマンドライン オプションです。
`package.json`を開いて、`npm run lint`コマンドを修正します。

```diff:package.json
    "scripts": {
-     "lint": "eslint .",
+     "lint": "eslint . --rulesdir eslint/rule",
      "watch": "npm test -- --watch --growl",
      "test": "mocha eslint/test/*.js"
    },
```

このディレクトリにテストは含めません。

### `.eslintrc`の警告レベル

`.eslintrc`に、カスタムルールの警告レベルを追加します。
キーはカスタムルールの**ファイル名**(拡張子除く)で、値は`0, 1, 2`のいずれか[^3]です。

```diff:.eslintrc
  {
      "extends": "eslint",
      "env": {
          "node": true
      },
      "rules": {
+         "no-literal-call": 2
      }
  }
```

以上でカスタムルール作成の全手順が完了しました。

改めて`npm run lint`を実行してみましょう。

```js:hello.js
"use strict";

var foo = "Hello custom rules"();
```

```:shell
$ npm run lint

> eslint-custom-rule-tutorial@1.0.0 lint eslint-custom-rule-tutorial/step1
> eslint . --rulesdir eslint/rule

eslint-custom-rule-tutorial/step1/hello.js
  3:5   error  "foo" is defined but never used  no-unused-vars
  3:11  error  This is not a function           no-literal-call

✖ 2 problems (2 errors, 0 warnings)

...
```

無事に文字列の関数呼び出しが警告されました :tada: 
もちろん、エディタの Linter プラグインを適切に設定していれば...

![image](https://qiita-image-store.s3.amazonaws.com/0/72555/1ac7637f-b8e1-4fba-581a-cc5886cf9f7e.png)

# まとめ

1. ルール置き場を作る
    - ルール置き場に **ルール名.js** のファイルを作って、そこにルールを書く
2. ルールのテストを書く
3. ルールを書く
4. 設定を書く
    - `--rulesdir` オプションを使って、ESLint にルール置き場を渡す
    - `.eslintrc` の `"rules"` セクションに作ったルールを書く

# おわりに

お疲れ様でした。

今回は ESLint カスタムルールの基本的な作り方・使い方を紹介しました。
ESLint では、比較的簡単にオリジナルの静的検証とそのテストを書くことができます。

ルール内から利用できる API について、より詳しくは[公式ドキュメント](http://eslint.org/docs/developer-guide/working-with-rules)を参照ください。
今後、この連載でも順次扱っていきます。

あなたのプロダクト・プロジェクトで活用して頂けると幸いです。

# その2以降の予定

- トークンや空白を扱うルールの作り方
- スコープ・変数・参照を扱うルールの作り方
- オプションを持つルールの作り方
- 状態を持つルールの作り方
- 実行パス解析を必要とするルールの作り方
- ESLint Plugin として公開する方法

----

[^1]: 長くなるので位置情報などを省略しています。
[^2]: 実は使わないルールもあります。
[^3]: オプションがあればそれも。