開発チームがお届けするブログリレーです!既に公開されている記事もありますので、こちらから他のメンバーの投稿もぜひチェックしてみてください!

はじめに

JavascriptやTypeScriptを採用しているプロジェクトにおいて、静的解析にESLintを導入しているプロジェクトは多いと思いますが、公式から用意されているルールに追加し、独自のコーディング規約を策定したいこともあると思います。
今回はESLintのカスタムルールを定義し、プロジェクトに好みのルールを追加する方法を解説しようと思います。

※Typescriptを使用してルールを記載することも可能ですが、ESLintとは別のライブラリが追加で必要になり導入コストが少し高いので、今回は生のJavascriptで進めていきます。
公式の資料はこちら。
https://eslint.org/docs/latest/extend/custom-rules

カスタムルールの構造

ESLintのルールを決めている基本的な構造は以下のようになっています。
記載のないオプションが存在しますが、最低限以下の設定値が記述されていれば問題ないかと思います。

module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "Description of the rule",
    },
    fixable: "code",
    schema: [], // no options
  },
  create: function (context) {
    return {
      // callback functions
    };
  },
};

  • meta: 文字通りルールのメタ情報が格納される
    • type: エラータイプ
    • problem: エラーを引き起こす可能性や、混乱を招く可能性のあるコードに対応するルール
    • suggestion: エラーを引き起こす要因にはなりずらいが、より良い記述方法がある場合に対応するルール
    • layout: コードの見た目(インデントや空白など)に対応するルール。これはソースコードの処理そのものに作用しない
    • docs: ドキュメントやツールの作成時に、解説などの文書を反映するオプション
    • description: ルールの簡単な説明
    • required: コアルールにおいて有効になっているかどうかの判定
    • url: ルールのドキュメントにアクセスできる URL
    • fixable: eslint のコマンドで修正されるか否か
    • hasSuggestions: 修正内容を提案できるか否か
    • schema: オプションの定義
    • deprecated: ルールが非推奨か否か
    • replacedBy: ルールが非推奨の場合に、代替ルールを指定する
  • create: ESLintがコードに対して何らかの処理を行うメソッドの格納先

実際に書いてみる

今回は以下のようなコードに対し、スネークケースを許容しないルールをESLintで作ってみたいと思います。

/* eslint-disable no-unused-vars */
const correctVariableName = "foo";
const not_correct_variable_name = "bar";

function correctFunctionName() {
  return true;
}

function not_correct_function_name() {
  return true;
}

1. インストール

まずはESLintが未導入であれば、インストールしましょう。公式のGetting Started通りで問題ないです。

npm init @eslint/config@latest

インストール後、eslint.config.js(またはそれと同等のconfigファイル)に、カスタムルールの設定を追加します。

import js from "@eslint/js";
import globals from "globals";
import { defineConfig } from "eslint/config";
import eslintPluginCustomRules from "./eslint-plugin-custom-rules.mjs";

export default defineConfig([
...
  {
    files: ["**/*.{js,mjs,cjs}"],
    plugins: { js, customRules: eslintPluginCustomRules },
    extends: ["js/recommended"],
    rules: {
      "customRules/not-allow-snakecase": "error",
    },
  },
]);

2. ルールのベースを定義する

以下2つのファイルを用意します。
前者がカスタムルールを ESLintのプラグインとしてexportする役割、後者がカスタムルールの定義を担っています。
metaの内容は必要に応じ変えてもらって構いません。基本以下のテンプレート通りでいいかと思います。

eslint-plugin-custom-rules.mjs

import notAllowSnakeCaseRule from "./src/rules/not-allow-snakecase.mjs";
const plugin = { rules: { "not-allow-snakecase": notAllowSnakeCaseRule } };
export default plugin;

src/rules/not-allow-snakecase.js

export default {
  meta: {
    type: "problem",
    messages: {
      error:
        "Variable or Function name '{{name}}' should not be in snake_case.",
      fix: "Replace snake_case with camelCase.",
    },
    docs: {
      description:
        "This rule is to disallow snake_case variable names in JavaScript files.",
    },
    fixable: "code",
    hasSuggestions: true,
    schema: [],
  },
  create(context) {
    // TODO: ここにルールを記述していく
  },
};

3. ルール実装

準備ができたらルールを書いていきます。
ESLintのルールを定義する際、コード内の変数や関数など判別するためにASTという構文を用いることになります。
実際にASTを用いてESLintのルールに落とし込む際は、以下のサイトが便利なので活用すると良いでしょう。
https://astexplorer.net/

今回ルールを適用したいのは変数になるので、ツールにサンプルコードを入力すると、VariableDeclaratortypeが該当することがわかります。

そこから渡ってきた引数に対しバリデーションを書いていきましょう。

src/rules/not-allow-snakecase.js

export default {
  meta: {
    type: "problem",
    messages: {
      error:
        "Variable or Function name '{{name}}' should not be in snake_case.",
      fix: "Replace snake_case with camelCase.",
    },
    docs: {
      description:
        "This rule is to disallow snake_case variable names in JavaScript files.",
    },
    fixable: "code",
    hasSuggestions: true,
    schema: [],
  },
  create(context) {
    function reportSnakeCase(node, variableName) {
      const newName = variableName.replace(
        /([a-z]+)_([a-z])/g,
        (_, p1, p2) => p1 + p2.toUpperCase(),
      );
      context.report({
        node,
        messageId: "error",
        data: { name: variableName },
        suggest: [
          {
            messageId: "fix",
            data: { name: variableName },
            fix(fixer) {
              return fixer.replaceText(node.id, newName);
            },
          },
        ],
      });
    }

    return {
      VariableDeclarator(node) {
        if (node.id.type === "Identifier") {
          const variableName = node.id.name;
          if (/[a-z]+_[a-z]+/.test(variableName)) {
            reportSnakeCase(node, variableName);
          }
        }
      },
      FunctionDeclaration(node) {
        if (node.id.type === "Identifier") {
          const variableName = node.id.name;
          if (/[a-z]+_[a-z]+/.test(variableName)) {
            reportSnakeCase(node, variableName);
          }
        }
      },
    };
  },
};

ここまで完了するとコードのエラーを指摘してくれるようになると思います。
(VSCodeのシンタックスに反映されない時は、コマンドパレットからエディタを再起動してみてください)
上の例のようにcontext.report()に追記すると、VSCodeのQuickFixが働くようになり、自動でソースコードを修正してくれるようになります。

4. テストコード追加

ESLintにはRule Testerという機能が盛り込まれているので、そちらを使います。
こちらは単体でもテスト実行可能ですが、Jestを導入した方がログがわかりやすい上、複数のテスト実行が用意なので、ついでに導入してみたいと思います。
Vistestでも同様のことが可能だと思うので、お好みで。

まずは以下の通りテストを記述します。
test/not-allow-snakecase.test.js

import { RuleTester } from "eslint";
import rule from "../src/rules/not-allow-snakecase.mjs";

const ruleTester = new RuleTester({
  languageOptions: { ecmaVersion: 2015 },
});

ruleTester.run("not-allow-snake-case", rule, {
  valid: [
    {
      code: "const correctVariableName = 'foo';",
    },
    {
      code: "function correctFunctionName() { return true; }",
    },
  ],
  invalid: [
    {
      code: "const not_correct_variable_name = 'bar';",
      errors: [
        {
          messageId: "error",
          data: { name: "not_correct_variable_name" },
          suggestions: [
            {
              messageId: "fix",
              data: { name: "notCorrectVariableName" },
              output: "const notCorrectVariableName = 'bar';",
            },
          ],
        },
      ],
    },
    {
      code: "function not_correct_function_name() { return true; }",
      errors: [
        {
          messageId: "error",
          data: { name: "not_correct_function_name" },
          suggestions: [
            {
              messageId: "fix",
              data: { name: "notCorrectFunctionName" },
              output: "function notCorrectFunctionName() { return true; }",
            },
          ],
        },
      ],
    },
  ],
});

テストファイルが用意できたらJestを導入しましょう。

npm install --save-dev jest

今回ルールおよびテストはESM形式で書いているので、nodeおよびJestのバージョン次第ではCannot use import statement outside a moduleエラーが発生することがあります。
その場合は、jestのコンフィグの修正と--experimental-vm-modulesのオプションを設定しておきましょう。

jest.config.js

export default {
  testMatch: [
    "**/__tests__/**/*.?(m)[jt]s?(x)",
    "**/?(*.)+(spec|test).?(m)[tj]s?(x)",
  ],
};

package.json

{
  // ...
  "type": "module",
  "scripts": {
    "test": "export NODE_OPTIONS=--experimental-vm-modules ; jest"
  },
  "devDependencies": {
    // ...
    "@eslint/js": "^9.25.1",
    "eslint": "^9.25.1",
    "globals": "^16.0.0",
    "jest": "^29.7.0"
  }
}

ここまでできればJestからESLintのテスターを起動できるようになっていると思います。
そのままテストコマンドを実行すれば、リンターのルールに対してテストが走るようになります。

> npm run test
> sample-eslint-custom-rules-for-blog@1.0.0 test
> export NODE_OPTIONS=--experimental-vm-modules ; jest

(node:24259) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
 PASS  test/notAllowSnakeCase.test.mjs
  not-allow-snake-case
    valid
      ✓ const correctVariableName = 'foo'; (44 ms)
      ✓ function correctFunctionName() { return true; } (5 ms)
    invalid
      ✓ const not_correct_variable_name = 'bar'; (12 ms)
      ✓ function not_correct_function_name() { return true; } (6 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        1.069 s
Ran all test suites.

いかがだったでしょうか。
ESLintのカスタムルールはすでに多くのプラグインが存在しますが、自身でリンターのルールを制御したいときもあるかと思いますので、そうした際は今回の内容を参考に、カスタムルールを作成してみてください!

以上です。