2015年12月 4日

JavaScript

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

  • このエントリーをはてなブックマークに追加

Yahoo! JAPAN Tech Advent Calendar 2015の4日目の記事です。一覧はこちら

12月から福岡勤務の穴井(@pirosikick)です。

私が所属しているリッチラボ株式会社(以下、リッチラボ)では、スマートフォン向けのリッチ広告の企画・開発と、ヤフー・ソニーと協業のクラウドファンディングサイト「First Flight」の開発・運用を行っています。

今回は、リッチ広告の開発で利用しているESLintのルールを自作する方法について書きたいと思います。

サンプルの動作環境について

本記事に登場するサンプルは、下記の環境で実行しましたので、適宜ご自身の環境に置き換えてご覧ください。

  • Mac OS
  • Node.js 4系(4.2.1)
    • npm 3.3.9

サンプルのJavaScriptは一部ES6で書いておりますので、先日伊藤が書きました「ES6時代のNode.js」も併せてご覧ください。

ESLintとは

ESLint - Pluggable JavaScript linter

ESLintは、JavaScriptの静的検証ツールで、バグの原因になりそうな問題のあるコードを見つけたり、
インデント数や括弧前後のスペースなどのコーディングスタイルを統一するのに役立ちます。

類似のツールとしては、古くからあるものだとJSLintJSHint、最近のものだとJSCSがあります。

ESLintはルールを自作できる!

ESLintの最大の特徴は、ルールを自分で開発できることです。

リッチラボでも独自のルールを開発し、ESLintのプラグインとして公開しています。(プラグインについては後述)

https://www.npmjs.com/package/eslint-plugin-richlab

まだまだルールの種類は少ないですが、iOS9のviewportのバグを回避するためにwindow.innerHeight, window.innerWidthを禁止するルールなどがあります。

ESLintを動かす

ルールの開発に入る前に、ESLintを動かしてみましょう。

まず、npmでESLintをインストールします。

# ESLintのCLIをグローバルにインストール
$ npm install -g eslint-cli

# 今回のサンプル用にディレクトリを用意
$ mkdir eslint-playground && cd eslint-playground

# 空のpackage.jsonを用意し、
# --save-devでeslintをインストール
$ echo "{}" > package.json
$ npm install --save-dev eslint

ESLintの設定は、/*eslint ルール名:引数 */のようなコメントを直接ソースコードに書くか、
設定ファイル(.eslintrcか、package.jsoneslintConfigフィールド)を用意するか、
2種類の方法があります。

今回は.eslintrcを用意します。とりあえず、ESLintの推奨設定を継承するように、下記を.eslintrcに書きます。

// .eslintrc
// JSONだがコメントが書ける!
{
    // 推奨設定を継承する
    "extends": "eslint:recommended"
}

これで動かす準備が出来ました。見るからに悪そうなコードでファイルを作り、ESLintで検査を実行してみましょう。

# 見るからに悪そうなコード
$ echo "notGoodVariable = 100;;;;;" > hoge.js

# 実行
$ eslint hoge.js

/Users/xxxxx/xxxxx/hoge.js
  1:1   error  "notGoodVariable" is not defined  no-undef
  1:23  error  Unnecessary semicolon             no-extra-semi
  1:24  error  Unnecessary semicolon             no-extra-semi
  1:25  error  Unnecessary semicolon             no-extra-semi
  1:26  error  Unnecessary semicolon             no-extra-semi

✖ 5 problems (5 errors, 0 warnings)

無事(?)悪そうな部分が炙りだされました!

ESLintはルールが100個以上あり、一つ一つルールのドキュメントを確認しながら設定していくはかなり辛い作業です。
なので、新しく設定ファイルを作る際は、上記のように推奨設定を継承し、使いながらルールを増やしていくのがおすすめです。
また、GUIでポチポチしながら設定ファイルを作る.eslintrc editorというツールを作りましたので、そちらもぜひ使ってみてください!

ルールの仕組み

ESLintを動かす環境が整ったので、ルールがどのように動いているのか、軽く説明したいと思います。

まず、ESLintは、ソースコードをJavaScript ASTに変換します。
ASTとは、abstract syntax treeの略で日本語にすると抽象構文木です。抽象構文木とは、ソースコードを文法的に解析し、それを木構造で表現したものです。(下図)

AST

JavaScript ASTはJSONで表現されるので、図のconsole.log("Hello! AST");からは下記のようなJSONが出力されます。
また、出力されるJSONのフォーマットはASTを生成するパーサーによって違いますが、ESLintはespreeを利用しているので、ESTreeの仕様に沿ったフォーマットでJSONを出力します。

{
  "type": "Program",
  "body": [{
    "type": "ExpressionStatement",
    "expression": {
    "type": "CallExpression",
    "callee": {
        "type": "MemberExpression",
        "computed": false,
        "object": {
          "type": "Identifier",
          "name": "console",
          "range": [0, 7]
        },
        "property": {
          "type": "Identifier",
          "name": "log",
          "range": [8, 11]
        },
        "range": [0, 11]
    },
    "arguments": [{
      "type": "Literal",
      "value": "Hello! AST",
      "raw": "\"Hello! AST\"",
      "range": [12, 24]
    }],
    "range": [0, 25]
    },
    "range": [0, 26]
  }],
  "sourceType": "module",
  "range": [0, 26],
  "errors": []
}

ESLintは生成されたASTをまるごとルールに渡すのではなく、ルールが必要としているノード(CallExpressionなど)だけ渡します。
下記のようにASTの木構造をルート(Program)から巡回し、ルールが必要としているノードに到達した時にルール内に定義されているノードに対応した関数を実行します。

ASTの巡回

また、関数の実行タイミングは、巡回時に「ノードに入る(enter)」時か「ノードから出る(leave)」時の2種類があります。
実行タイミングの使い分けについては後述します。

ルール関数の実行タイミング

ルールの開発

ルールの開発方法を段階的に説明したいと思います。
最初は、hogeという変数名の定義・参照を禁止する「no-hoge」ルールを作ってみたいと思います。

まず、ルールを置くディレクトリを作ります。

# ルール用にディレクトリを作成
$ mkdir rules

そのディレクトリの中に「ルール名.js」でJSファイルを作ります。no-hogeなので「no-hoge.js」です。

# お好きなエディターでどうぞ
$ vim rules/no-hoge.js

JSファイルの中では、contextを引数に取る関数を定義し、module.exportsで外部から参照可能にします。
その関数は、キーが必要なノード名、値がそのノードが出現した時に実行する関数になっているObjectを返します。
引数のcontextには、検査結果の通知に使うreport関数や、ルール実行時の設定の参照、検査対象のファイル名の取得などのための変数・関数が定義されています。(contextの機能について

下記がno-hogeルールの実装です。
Identifierが出現した時にそのノードのnameが”hoge”の場合は、
hoge変数がソースコード上に現れたとみなし、context.reportでエラーメッセージと対象のノードを通知します。

"use strict";

// rules/no-hoge.js

// contextを引数に取る関数を外に晒す
module.exports = context => {

  // キーがノードタイプ、値が関数のObjectを返す
  return {
    // AST巡回時にIdetifierが出現されたら実行される
    // nodeはノード
    Identifier: node => {
      if (node.name === "hoge") {
        // 検査の結果をリポートする場合はcontext.reportを使う
        context.report({ node, message: "You MUST NOT USE hoge variable." });
      }
    }
  }
};

.eslintrcのrulesフィールドにno-hogeを追加し、ルールを有効にしましょう。

// .eslintrc
{
    "extends": "eslint:recommended"

    // 以下を追記
    "rules": {
        "no-hoge": 2 // 0は無効、1は警告、2はエラー
    }
}

では、作ったルールを実行しましょう。
ルールを置いたディレクトリを—rulesdirオプションで指定し、実行します。

# hoge変数に代入しているコードを作成
$ echo "var hoge = 'hoge';" > hoge.js


# そのまま実行すると「no-hogeなんてルールは無いよ」とエラーが出る
# (no-unused-varsも出ていますが。。。)
$ eslint hoge.js

/Users/hanai/src/github.com/pirosikick/eslint-playground/hoge.js
  1:1  error  Definition for rule 'no-hoge' was not found  no-hoge
  1:5  error  "hoge" is defined but never used             no-unused-vars

✖ 2 problems (2 errors, 0 warnings)# --rulesdirオプションにルールが置いてあるディレクトリを指定して実行


# --rulesdirオプションでルールを置いたディレクトリを指定
# ルールが認識され、エラーメッセージが表示されている
$ eslint --rulesdir rules hoge.js

/Users/hanai/src/github.com/pirosikick/eslint-playground/hoge.js
  1:5  error  You MUST NOT USE hoge variable    no-hoge
  1:5  error  "hoge" is defined but never used  no-unused-vars

✖ 2 problems (2 errors, 0 warnings)

ルールのテスト

ESLintのルールは単体テストが可能です。 上記のno-hogeの例ではエラーになりそうなコードでJSファイルを作っていますが、毎回そんなことをしていたら辛いので、単体テストを書いて想定通りに動いているかのチェックをしましょう。

まず、単体テスト用のディレクトリを用意し、単体テストを実行するためにmochaをインストールします。

# 単体テスト用のディレクトリを用意
$ mkdir tests

# mochaをインストール
$ npm install --save-dev mocha

ESLintのRuleTesterを使うと簡単に単体テストが書けます。
問題のないコードとエラーになるコードをインスタンス化したRuleTesterのrun関数に渡すだけです。
no-hogeの単体テストを下記のように書きました。(tests/no-hoge.js)

"use strict";

// テストしたいルールをrequire
const rule = require("../rules/no-hoge");

// ルールをテストするためのクラス
// インスタンス化して使う
const RuleTester = require("eslint").RuleTester;
const ruleTester = new RuleTester();

// no-hogeルールでは問題のないコード
const valid = [{
  code: 'var fuga = "fuga";'
}];

// no-hogeルールではエラーになるコード
const invalid = [{
  code: 'var hoge = "hoge";',
  // 想定されるエラー内容
  errors: [{ message: "You MUST NOT USE hoge variable.", type: "Identifier" }]
}];

// 単体テスト実行
ruleTester.run("no-hoge", rule, { valid, invalid });

mochaで単体テストを実行します。

# mocha実行
$ ./node_modules/.bin/mocha tests/no-hoge.js


  no-hoge
    ✓ var fuga = "fuga";
    ✓ var hoge = "hoge";


  2 passing (48ms)

テスト通ってますね!

ルールのオプション

単体テストができる環境が整いましたので、ちょっとルールを複雑にします。
ルールには設定ファイルからオプションを渡すことができますので、その機能を活用したルールを作ってみましょう。

no-hogeをちょっと修正して、任意の変数名の参照を禁止するtaboo-varsルールを作ります。

まず、設定ファイルからオプションを渡します。 配列の先頭はルールの無効/警告/エラーのフラグで、2番目以降がオプションとしてルールに渡されます。

// .eslintrc
{
    "extends": "eslint:recommended"

    "rules: {
        // オプションを渡す場合は値を配列にする
        // 2番目以降がオプションとしてルールに渡される
        "taboo-vars": [2, ["hoge", "fuga", "duga"]]
    }
}

ルール側では、オプションはcontext.optionsで受け取れます。

// rules/taboo-vars.js

module.exports = context => {
    console.log(context.options[0]); // ["hoge", "fuga", "duga"]

    return { ... };
};

また、JSON Schemaでオプションのフォーマットを指定することができ、
そのフォーマットと異なる値が渡された場合にエラーにすることができます。
JSON Schemaを指定しなくてもルールは実行できますが、 指定することで余計なバリデーション処理が不要となり、
コードがすっきりしますのでぜひ書きましょう。
(公式のルールは全てJSON Schemaが定義してあります!)

下記がtaboo-varsの実装です。

// rules/taboo-vars.js

"use strict";

const rule = context => {
  let vars = [];

  // context.optionsでオプションを受け取る
  if (Array.isArray(context.options[0])) {
    vars = context.options[0];
  }

  return {
    Identifier: node => {
      // オプションで受け取った値を使って検査
      if (vars.indexOf(node.name) !== -1) {
        context.report({
          node,
          message: `You MUST NOT USE taboo variables.`
        });
      }
    }
  }
};

// オプションのJSON Schema
rule.schema = [
  // 文字列の配列で、要素はユニーク
  {
    type: "array",
    items: { type: "string" },
    uniqueItems: true
  }
]

module.exports = rule;

テストはno-hogeとほとんど一緒ですが、optionsでオプションを渡してテストしています。

// tests/taboo-vars.js

"use strict";
var rule = require("../rules/taboo-vars");
var RuleTester = require("eslint").RuleTester;
var ruleTester = new RuleTester();

var valid = [{
  code: 'var fuga = "fuga";'
}];

var invalid = [{
  code: 'var fuga = "fuga";',
  // オプションを渡してテスト
  options: [["fuga"]],
  errors: [{
    message: "You MUST NOT USE taboo variables.",
    type: "Identifier"
  }]
}];

ruleTester.run("taboo-vars", rule, { valid, invalid });

実行タイミングの使い分け

「ルールの仕組み」のところで、ノードに対応する関数の実行タイミングに2種類(入る時・出る時)あると書きました。
これまでのサンプルは全てノードに「入る時」に実行されており、
ノードから「出る時」に実行したい場合はノード名に:exitを付けます。

module.exports = context => {
  return {
    // Idetifierから出るときに実行
    "Identifier:exit": node => {
      ...
    }
  };
};

:exitの用途ですが、例えばmax-nested-callbacksルールの場合だと、FunctionExpressionに入る時にカウンタに+1、FucntionExpressionから出る時にカウンタから-1し、関数のネスト数をカウントして閾値に達した時にエラーを出したりしています。
特定のノードだけを見ても分からないようなルールの場合に:exitを使っているケースが多いようです。

ルールの共有

これまでのサンプルでは、ルールを置いたディレクトリを—rulesdirオプションで指定し、
そのルールを実行してきました。
しかし、その方法ではリポジトリ内にルールのJSファイル(と単体テスト)を含める必要があり、
複数のプロジェクトでそのルールを共有したい場合に、やや不便です。

特定のプロジェクト内でしか使わないルールはその方法でもよいですが、
複数のプロジェクトをまたいで利用するルールは、
プラグイン化してnpmパッケージにすると便利です。

例えば、冒頭で紹介したリッチラボが公開しているルールを使用したい場合、
まずプラグインをインストールします。

# リッチラボが公開しているESLintプラグインをインストール
# 注)プラグインのパッケージ名は「eslint-plugin-*」でないといけない
$ npm install --save-dev eslint-plugin-richlab

設定ファイルのpluginsフィールドにプラグイン名(eslint-plugin-を除いた部分)を追加します。
プラグインに含まれるルールは「プラグイン名/ルール名」で参照できます。

// .eslintrc
{
    "plugins": ["richlab"]

    ...

    "rules": {
        // 「プラグイン名/ルール名」で指定
        "richlab/avoid-ios9-viewport-bugs": 2
    }
}

ESLintはYeoman Generatorを公開しており、
簡単にプラグインを作ることができますので、
npmに公開しても問題ないルールでしたらプラグイン化を検討しましょう。

おまけ:JS AST Explorer

ルールを書く際に、コードがどのようなASTになるのか確認する機会が多いと思いますが、
espreeなどのパーサーを使ってASTを生成し、
それをconsole.logなどで出力して確認するのはかなり面倒です。

そんな時は、JS AST Explorerを使いましょう。ブラウザでインタラクティブにASTを確認できます。

JS AST Explorer

おわり

いかがだったでしょうか?
ESLintを最初に触った時の印象は「なんかとっつきにくい」という感じでしたが、
最近では利用者も増え、日本語のドキュメントも多くあります。
また、ルールの仕組みも一度理解するととてもシンプルで、
未だ誰も考えついていないような素晴らしいルールを作れるのでは!と、とてもワクワクします。

みなさまもコードレビュー時に毎回指摘していることや、
チーム内で暗黙的に広まっているルールなどございましたら、
ESLintでルールを自作して、楽をしましょうっ!!

参考資料

ESLintはドキュメントが充実していますので、今回の記事で興味が湧いた方は公式ドキュメントに一通り目を通すとよいと思います。

ASTの仕様等については、jser.infoのazuさんの資料がとてもわかりやすいです。

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加