✍️

【実装編】手を動かしながら学ぶESLintルールの作り方

2022/05/16に公開
2

現代ではESLintを使って、チーム内でのコーディングルールの定義や自動修正をするのは当たり前になってきています。

しかし、ESLintのルールにないチーム独自のコーディングルールの場合はどうしていますか?

人力でやっていると

  • 毎回PRで個別に指摘していて、非効率
  • 人がチェックするので、漏れてしまうことがある

などの問題が起きてきます。

そんな時にESLintのルールを自分達で作ることができると一気にこれらの問題を解決することができます。

そこで本記事ではESLintルールの作り方をハンズオン形式で紹介します。
テンプレートも用意しているので、実際に手を動かしながら読んでみてください。

【実装編】の今回はルールの作成までを行います。
次の【導入編】でパッケージ化し、プロジェクトに導入するところをやっていく予定です。

ではいってみましょう!


今回作成するルール

あなたは架空の会社キャットテクノロジー株式会社の開発チームのエンジニアです。
キャットテクノロジーではみんな猫が好きなので、
🐈 変数の頭に必ずmyao のプレフィックスをつけなければいけない 🐈
という気の狂ったチーム独自のコーディングルールがあります。

今回はこのルール、var-prefix-myaoを作ります。

// var-prefix-myao
// Bad 😿
const user = new User()

// Good 😻
const myaoUser = new User()

プロジェクトの準備

ルールを作成するにはESLint Pluginとしてnpm パッケージにする必要があります。

Pluginの雛形を用意するのに、いくつかコマンドを実行したり、ファイルを編集したりする必要があり面倒なので、今回はTemplate Repositoryを用意しました。

以下の手順でレポジトリを作成してください。

  1. レポジトリにアクセスし、 Use this template を選択。
    1

  2. あとは普段通りレポジトリを作成してCloneしてきます。レポジトリの名前はtemplateを抜いたeslint-plugin-myaoにしておくとわかりやすいです。

  3. yarnを実行してパッケージをインストール

yarn
0から構築してみたい方向けに何をしたのかも記載しておきます。
  1. ディレクトリを作成
$ mkdir eslint-plugin-myao && cd eslint-plugin-myao
  1. 初期化
$ yarn init -y
  1. eslintの追加
$ yarn add -D eslint @types/eslint
  1. typescriptの追加
$ yarn add typescript
$ yarn run tsc --init
  1. tsconfig.jsonを修正
"outDir": "./lib",
"rootDir": "./src",
  1. srcディレクトリの作成
mkdir src
mkdir src/rules
  1. package.jsonを修正
"main": "dist/index.js
"scripts": {
  "test": "tsc && jest"
}
  1. jestの追加
$ yarn add -D jest @types/jest ts-jest
$ yarn ts-jest config:init
  1. jest.config.jsに追記
testRegex: "(src/.*\\.test)\\.ts$"

ここまでで開発する準備が整いました🎉

次項から実際にルールを実装していきます!


ルールを作成する

テストを書く

はじめに今回作りたいvar-prefix-myao のテストを書いてみましょう。
ESLintではRuleTesterrunメソッドを使って

  • valid: ルールをパスするコードパターン
  • invalid: ルールに違反しているコードパターン

を書くだけで、テストコードを書くことができます。

import { RuleTester } from "eslint"
import rule from "./rule"

const tester = new RuleTester({ parserOptions: { ecmaVersion: 6 } })
tester.run(
  "testName",
  rule,
  {
    valid: [
      // ルールをパスするコードパターン
    ],
    invalid: [
      // ルールに違反しているコードパターン
    ],
  }
)

今回作成するvar-prefix-myaoのテストを書いてみましょう。

src/rules/var-prefix-myao.test.ts
import { RuleTester } from "eslint"
import varPrefixMyao from "./var-prefix-myao"

const tester = new RuleTester({ parserOptions: { ecmaVersion: 6 } })
tester.run("var-prefixy-myao", varPrefixMyao, {
  valid: [{ code: "const myaoUser = new User()" }],
  invalid: [
    {
      code: "const user = new User()",
      errors: [{ message: "変数の頭には必ずmyaoをつけてね🐈" }],
    },
  ],
})

今はまだvar-prefix-myaoのルールを書いていないので空のルールを作成します。

src/rules/var-prefix-myao.ts
import { Rule } from "eslint"

const rule: Rule.RuleModule = {
  create: (context) => {
    return {
      // 後でここにルールを実装していく。
    }
  },
}

export = rule

さてこの時点でテストを実行して、失敗することを確認しておきましょう。
現状var-prefix-myaoはエラーを出さない空のルールなので、validは通りますが、invalidのテストが落ちていることが確認できればOKです👌

$ yarn run test
yarn run v1.22.18
$ tsc && jest
 FAIL  src/rules/var-prefix-myao.test.ts
  var-prefixy-myao
    valid
    // validは何のチェックもしてないので通る
      ✓ const myaoUser = new User(); (21 ms)
    invalid
      // invalidも何のチェックもしていないので通ってしまう
      ✕ const user = new User(); (10 ms)
...
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
...

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

さてここからいよいよESLintルールのコードを書いていくわけですが、その前にAST(抽象構文木)について説明します。
実はESLintがソースコードに対して行う操作は全てASTを通じて行われ、ルールを書くためにはASTについて理解しておく必要があるためです。

AST(Abstract Syntax Tree = 抽象構文木) は**プログラムをノードと呼ばれる単位に分割し、それらを組み合わせて木構造で表したデータ構造体です。**JavaScriptにおけるASTはESTreeに準拠します。

例えばconsole.log(’Hello world’) のASTをビジュアライズすると以下のようになります。
ast

実際のASTはノード内に様々なプロパティを持っており、JavaScriptにおいてはJSON形式で表現されることが一般的です。
ESLintはこのノードの情報を見てコードのチェックや修正を行います。

コードのASTに変換するにはastexplorerが便利です。
以下がconsole.log(’Hello world’) のASTのJSONです。

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

また、esqueryというツールを使って、ASTに対してクエリを書くことができます。

クエリの構文はcssセレクターの構文に非常に似ており(というか寄せており)、例えばメソッド呼び出し内での”Hello world” の文字列のLiteralノードを取り出したい場合は以下のように書くことができます。

CallExpression > Literal[value="Hello world"]

ESLintではこのesqueryをチェック対象のコードを見つけるために使用します。


ルールを実装する

ASTの概要が理解できたところで、いよいよ実際のルールを実装していきます。
ESLintのルールを作成するにはRule.RuleModuleインターフェースのcreateメソッドを実装します。

以下のように、戻り値にノードを取得するクエリをキーに書くことで対象のノードを取得し、エラーのレポートなどNodeに対する操作を値となる関数で行います。

const rule: Rule.RuleModule = {
  create: (context) => {
    return {
      '[ノードを取得するesquery]': (node: Node) => {
        [ノードに対する操作]
      },
    }
  },
}

つまりESLintのエラーを作るためには

  1. ノードを取得するesquery
  2. ノードに対する操作
    の2つを書けば良いことになります。

ノードを取得するesqueryを見つける

1. エラーとして検知したいコードのASTををastexplorerを使って調べる

astexplorerにアクセスし、今回エラーとして検知したいコードconst user = new User()を左側に貼り付けて、parserをespreeに変更(ESLintではparserにespreeが使用されているため)してください。右側にASTが表示されます。

ast

2. ASTを元にクエリを組む

ASTから今回ルールでチェックしたい変数宣言を取得するには
VariableDeclaration > VariableDeclarator > Identifierと辿っていけば良さそうです👌

さらに今回はmyaoで始まらない変数をNodeとして取得したいので、Identifierに対して正規表現を使って
VariableDeclaration > VariableDeclarator > Identifier[name=/^(?!myao).*$/]
とします。

3. 作成したクエリが正しいかをツールで確認する

作成したクエリが正しいかをesqueryのオンラインツールで確認します。

  • エラーになるコード(const user = new User())
  • パスするコード(const myaoUser = new User())
    の両方を貼り付けて、出力を確認してみてください。

エラーになるコード(const user = new User())のノードのみがクエリによって取得されていることが確認でき、作成したクエリVariableDeclaration > VariableDeclarator > Identifier[name=/^(?!myao).*$/]が正しいことが確認できました。

ノードに対する処理を書く

クエリがわかったので、今度はノードに対する処理を書いていきましょう!
と言っても今回はセレクターで既にmyaoがついていない変数名を検出できており、Nodeに対して操作することは何もないので、エラーをreportするだけでOKです👍

import { Rule } from "eslint"
import { Node } from "estree"

const rule: Rule.RuleModule = {
  create: (context) => {
    return {
      "VariableDeclaration > VariableDeclarator > Identifier[name=/^(?!myao).*$/]":
        (node: Node) => {
          // エラーを上げる
          context.report({
            message: "変数の頭には必ずmyaoをつけてね🐈",
            node,
          })
        },
    }
  },
}

export = rule

これでルールが完成しました!
テストが通っていればOKです🙆

$ yarn run test
yarn run v1.22.18
$ tsc && jest
 PASS  src/rules/var-prefix-myao.test.ts
  var-prefixy-myao
    valid
      ✓ const myaoUser = new User(); (22 ms)
    invalid
      ✓ const user = new User(); (4 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        3.909 s
Ran all test suites.
  Done in 11.41s.

自動修正に対応する

自動修正にも対応しておきましょう。

テストを修正する

自動修正をテストするにはinvalid.output に修正された後のコードを記載します。

src/rules/var-prefix-myao.test.ts
import { RuleTester } from "eslint"
import varPrefixMyao from "./var-prefix-myao"

const tester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } })
tester.run("var-prefixy-myao", varPrefixMyao, {
  valid: [{ code: `const myaoUser = new User();` }],
  invalid: [
    {
      code: `const user = new User();`,
      output: `const myaoUser = new User()`, // 追記
      errors: [{ message: "変数の頭には必ずmyaoをつけてね🐈" }],
    },
  ],
})

テストを実行してみてください。
outputが間違ってるよということでエラーが出力されています。

src/rules/var-prefix-myao.ts
$ yarn run test
...
Message:
    Output is incorrect.

  Difference:

  - Expected
  + Received

  - const myaoUser = new User()
  + const user = new User();

ルールを修正する

ではテストが通るようにルールを修正して、自動修正に対応させます。

自動修正に対応させるにはreport関数の引数にfix という修正用の関数を追加して、その中に具体的な修正処理を書いていきます。

また、meta.flexible: “code”も追記しておきます。

import { Rule } from "eslint"
import { Identifier } from "estree"

const rule: Rule.RuleModule = {
  create: (context) => {
    return {
      "VariableDeclaration > VariableDeclarator > Identifier[name=/^(?!myao).*$/]":
        (node: Identifier) => {
          context.report({
            message: "変数の頭には必ずmyaoをつけてね🐈",
            node,
+           fix: function (fixer) {
+             // ここに修正コードを書く
+           },
          })
        },
    }
  },
+  meta: {
+    fixable: "code",
+  },
}

fix関数にはfixer というコードの修正に便利なメソッド備えたオブジェクトが渡されます。
このfixerとセレクタが取得したnode を使って自動修正のコードを書きます。

今回の場合はnodeから変数名を取得してキャメルケースに書き換え、頭にmyaoをつける以下のようなコードを書けば良さそうです。

fix: function (fixer) {
  const { name } = node
  // 変数名をキャメルケースに変換
  // user → User
  const camelCase = `${name[0].toUpperCase()}${name.slice(1)}`
  // 頭にmyaoをつける
  // User → myaoUser
  return fixer.replaceText(node, `myao${camelCase}`)
}

再度テストを実行してみてください。
テストがパスすればOKです🙆

$ yarn run test
yarn run v1.22.18
$ tsc && jest
 PASS  src/rules/var-prefix-myao.test.ts
  var-prefixy-myao
    valid
      ✓ const myaoUser = new User(); (20 ms)
    invalid
      ✓ const user = new User(); (5 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.812 s, estimated 3 s
Ran all test suites.
  Done in 6.18s.

ここまででルールの作成が完了しました🎉
導入編では作成したルールをパッケージにして実際にプロジェクトに導入していきます。
https://zenn.dev/kazuwombat/articles/565305e4507a1c

参考にさせていただいた記事、本🙏
eslint-plugin-tutorial
Babel Plugin を作りながら AST と Babel を学ぶ
簡単JavaScript AST入門

Discussion

shingo.sasakishingo.sasaki

テンプレートリポジトリを元にコードを作成してテストコードを実行すると、以下のエラーが出ますが、これは eslintrc なりで、es6 を使用することの宣言がどこかで必要になりますか?

A fatal parsing error occurred: Parsing error: The keyword 'const' is reserved

かずうぉんばっとかずうぉんばっと

@shingo.sasaki
すみません、こちら記載に誤りがありました🙇‍♂️
おっしゃる通りRuleTesterでecmaVersionを以下のように変更すると、通るようになります。
参考

- new RuleTester()
+ new RuleTester({ parserOptions: { ecmaVersion: 6 } })

また、テンプレートの修正点もいくつか見つかったため更新しました🙏
テストスクリプトを追加
テスト対象を.tsファイルのみに限定

ご報告ありがとうございました🥰