Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

はじめに

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

ここでは ESLint の基本的な使い方は扱いません。
ESLint の基本についてはESLint 最初の一歩等を御覧ください。

no-literal-call

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

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

hello.js
"use strict";

var foo = "Hello custom rules"();

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

文字列は関数ではありません!

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

作成手順

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

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

ルール置き場を作る

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

フォルダ構成
📂 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こちらにあるものを使っています。

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

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

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

// ルールのオプション定義。今回は使わない。
module.exports.schema = [];
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)

テストを書く

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

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

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)のノードの種類です。
コードにその種類のノードが現れたらメソッドが呼び出されます。

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

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

hello.js
"use strict";

var foo = "Hello custom rules!"();
抽象構文木(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 というリポジトリで管理されています。
ESLint が渡してくれる抽象構文木(AST)もこの仕様に則ったものです。
ESLint でルールを書いていると、よくこの仕様書とにらめっこすることになります。

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

ロジックを考える

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

image

さて、関数呼び出しのノードを見て、呼び出し対象(callee)がリテラルかどうかをチェックするのは素直で良さそうです。

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)

いいですね :thumbsup:

設定を書く

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

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

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

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

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

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です。

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

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

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

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

まとめ

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

おわりに

お疲れ様でした。

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

ルール内から利用できる API について、より詳しくは公式ドキュメントを参照ください。
今後、この連載でも順次扱っていきます。

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

その2以降の予定

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


  1. 長くなるので位置情報などを省略しています。 

  2. 実は使わないルールもあります。 

  3. オプションがあればそれも。 

mysticatea
ESLint のメンテナ。Vue.js の開発チームメンバー。JavaScript 言語仕様書 ECMA-262 や JavaScript 構文解析器 Acorn のコントリビューター。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした