Smart Communication Design Company
ホーム > ナレッジ > Blog > フロントエンドBlog > 2017年9月 > ESLintのルールを自作しよう!

フロントエンドBlog

Webのフロントエンドを構成する技術、特にHTMLやCSS、JavaScript、またそれらに関連する話題を扱うBlogです。

ESLintのルールを自作しよう!

UI開発者 加藤

ESLintとは指定したルールに沿ってJavaScirptコードをチェックしてくれるツールです。明らかなバグを減らしたり、複数人で作業したときでも書き方を統一させるのに役立ちます。

主な特長として、使用したいルールをピックアップして自分だけのルールセットを作ったり、各種ライブラリが公開している設定ファイルを使ったりなど柔軟なカスタマイズが可能であることがあげられます。ECMAScript 6用のルールも既に定義されており、合計で約250項目ものルールを提供してくれています。ESLint公式サイトのList of available rulesページに定義済みのルールが一覧されていますので、興味がある方はご覧ください。

今回はまだ定義されていないルールを自作する方法をご紹介したいと思います。なお、ご紹介する方法はESLintのバージョン4.6.1時点でのものです。バージョンアップにより記法が変わる場合がありますのでご注意ください。

テーマ

今回のサンプルとして作成するルールは「jQueryのセレクタには子孫セレクタは使わず、メソッドチェーンを使おう」です。既にjQueryのプラグインは提供されておりますが引数などの細かいルールは定義されていないようです。

フォルダ構成

実際にコードをチェックする「rules/no-descendant-combinator.js」と、そのチェック自体に問題がないかをテストする「test/no-descendant-combinator.js」を作成します。自作ルールが増えたときに互いに対応するファイルがわかりやすいようにファイル名を同じにします。今回は下記のようなフォルダ構成を想定しています。

lib/
 ├ rules
 │   └ no-descendant-combinator.js
 └ test
      └ no-descendant-combinator.js
ルールの記述方法

Working with Rulesページに、ESLintにおけるルール定義の基本構造が示されています。

module.exports = {
    meta: {
        docs: {
            description: "disallow unnecessary semicolons",
            category: "Possible Errors",
            recommended: true
        },
        fixable: "code",
        schema: [] // no options
    },
    create: function(context) {
        return {
            // callback functions
        };
    }
};

metaにはルール自体の情報を定義します。自作ルールの場合はdocsは省略可能です。fixableも自動修正できないルールに関してはキーごと削除して問題ありません。schemaは特にオプションの指定がない場合は空の配列を指定します。

最低限ではありますが、今回のテーマに合わせて記述した「rules/no-descendant-combinator.js」が下記です。

// rules/no-descendant-combinator.js
const ERROR_MESSAGE = '子孫セレクタの使用は非推奨です。';
module.exports = {
    meta: {
        docs: {
            description: "disallow descendant-combinator"
        },
        schema: [] // no options
    },
    create: function(context) {
        function checkString (node) {
            if ((node.value.indexOf(' ') !== -1)) {
                context.report({
                    node: node,
                    message: ERROR_MESSAGE
                });
            }
        }
        return {
            Literal: function(node) {
                if (typeof node.value === "string") {
                    checkString(node);
                }
            }
        };
    }
};

実際にコードをチェックするロジックを書いていく部分がcreate関数の中になります。この部分はASTの仕様に沿って記述します。

ASTとは

AST構造はコードから言語の文法的な意味だけを切り取り、「この部分は値を定義している部分」、「この関数の範囲は何行目~何行目」といった情報を持ちます。

ESLintでは実際のコードをAST構造に変換してからチェックを行っています。JavaScriptのAST構造には様々な仕様がありますが、今回はESTreeという団体が策定している仕様で進めていきます。コードをASTに変換するにはESLintプロジェクトで使用されているespreeというパーサーが便利です。

実際にどのような変換が行われるかを見てみましょう。たとえば下記のような変数を定義する一文があるとします。

var message = "Hello world!";

このコードをespreeを使ってAST構造に変換すると、下記のような木構造が出来上がります。

{
    "body": [{
        "declarations": [{
            "id": { "type": "Identifier", "name": "message" },
            "init": { "type": "Literal", "value": "Hello World!" }
            ...
        }],
        "type": "VariableDeclaration",
        ...
    }]
}

一部だけ抜粋しましたが、変数定義の一文は「typeVariableDeclarationで、変数messageHello Wolrd!という値を代入している」という構造に変換されました。ここで重要なのがtypeの値です。先ほどの「rules/no-descendant-combinator.js」のcreate関数の中で、Literalという名前の関数を定義しています。このLiteralは実はASTのtypeの値と対応しています。AST構造を上から順に下っていき、typeLiteralの記述が見つかったら実際にその部分に関してチェックを行います。実際にルールを書く際はこのASTの仕様と見比べながら書いていくようになります。

テストファイルの作成

続いてはルールのロジックにバグがないかをテストするファイルを作ります。ESLintプロジェクトで使用されているRuleTesterモジュールを使用します。

// test/no-descendant-combinator.js
let rule = require("../rules/no-descendant-combinator");
let RuleTester = require('eslint').RuleTester;
let ruleTester = new RuleTester();

const ERROR_MESSAGE = '子孫セレクタの使用は非推奨です。';

ruleTester.run("no-descendant-combinator", rule, {
    valid: [
        '$(".parent").find(".child")'
    ],
    invalid: [
        {
            code: 'var $child = $(".parent .child")',
            errors:[{message: ERROR_MESSAGE, type: "Literal"}]
        }
    ]
}

validにはテストを通過することが期待されるケースを書いていきます。invalidにはテストに失敗することが期待されるケース、ASTのタイプ、エラーメッセージをオブジェクト型で書いていきます。その他にもオプションが多数ありますので、Rule Unit Testsの項目をご覧ください。

先のコードでは合計2回のテストが行われ、その内1回が通過、もう1回が失敗することが期待されています。実際にテストを実行してみます。テストフレームワークとしてmochaを使用します。

$ mocha lib/test/no-descendant-combinator.js

no-descendant-combinator
  valid
    √ $(".parent").find(".child") (61ms)
  invalid
    √ var $child = $(".parent .child")
2 passing (80ms)

片方が通過、片方が失敗し、テスト全体としては成功していることがわかります。実際は意図しないバグを減らせるようvalid,invalidのケースをさらに増やす必要があります。ちなみにESLintが定義している「セミコロン必須」のルールに対するテストケースはvalid,invalid合わせて130パターンほど定義されています。

また現状のロジックでは全てのLiteralタイプに対してチェックを行ってしまいます。たとえば、ただ変数に文字列を代入するだけでも値にスペースが含まれているとエラーになってしまうでしょう。今回は割愛しますが、他のタイプをうまく使いながら条件を絞っていく必要があります。

自作したルールを実際に使用する場合は、eslint実行時に--rulesdirオプションでルールファイルのあるディレクトリを指定するだけでその階層にあるルールを全て適用してくれます。

$ eslint hoge.js --rulesdir 'lib/rules/'

以上でルールの作成は完了です。長い道のりではありますが、一度ルールを作ってしまえば以降のコーディング作業の効率をアップさせ、品質を上げることができると思います。より良いコーディングライフのための自分なりのルールを作ってみてはいかがでしょうか。