プロジェクト固有の ESLint ルール追加 RTA

TypeScript 使用ルートです.

AI Coding Agent Enablement in TypeScriptTS特化Clineプログラミング で紹介されていたように独自の lint ルールを作りたいということがあるかと思いますが, それシュッとプロジェクト内に完結した形で作りたいといったときにご活用ください.

サンプルリポジトリは以下:

1. 依存ライブラリをインストール

TypeScript でルールを書くために, 以下の 2 つを devDependencies としてインストールしてください.

  • jiti: ESLint で TypeScript で書かれた設定ファイルを読み込むのに必要 (参考)
  • @typescript-eslint/utils: TypeScript で ESLint ルールを作るのに使うユーティリティ
# npm
npm i --save-dev jiti @typescript-eslint/utils
# yarn
yarn add -D jiti @typescript-eslint/utils
# pnpm
pnpm add -D jiti @typescript-eslint/utils

jiti の代わりに Node.js のネイティブの --experimental-strip-types を使うことも一応できます (参考) が, モジュール解決の方法を Node.js 用に合わせつつ, かつ既存のプロジェクトとも共存させる必要があるためちょっと手間が増えます.

また TypeScript を使わずに JavaScript でルールを書く場合もこれらは不要ですが, 当然のことながら TypeScript の方が型があって書きやすい & メンテナンスがしやすいと思うのでこの方法で進めます.

2. ルール作成用のユーティリティを用意

ルールを作るためのユーティリティを /rules/utils.ts に配置します. 以下のコードをコピペしてください (<owner><repo> の部分だけ書き換えてください).

// /rules/utls.ts

import { ESLintUtils } from "@typescript-eslint/utils";
import type { AnyRuleModule } from "@typescript-eslint/utils/ts-eslint";
import type { Rule } from "eslint";

export const createRule = ESLintUtils.RuleCreator(
  (ruleName) => `https://github.com/<owner>/<repo>/blob/main/rules/${ruleName}.ts`,
);

/** typescript-eslint の提供するルールの型と ESLint 本体のルールの型を合わせる */
export function compat(rule: AnyRuleModule): Rule.RuleModule {
  return rule as unknown as Rule.RuleModule;
}

compat() は typescript-eslint が提供するルールの型と ESLint 本体が提供するルールの型を合わせるために用意していますが, 設定ファイルを書くのに tseslint.config() を使っている (そもそも typescript-eslint 側の型を使うことにしている) 場合は不要です.

3. ルールを作成

作成したいルールはプロジェクトごとに異なると思いますが, ここでは例として no-explicit-any のカスタマイズ版を作ってみましょう.

先ほど作ったユーティリティを使って, /rules/no-explicit-any.ts にルールを作成します.

// /rules/no-explicit-any.ts

/*
 * https://typescript-eslint.io/rules/no-explicit-any/ のカスタマイズ版
 */

import { compat, createRule } from "./utils";

// オプション (今回はオプションなし)
type Options = [];

// メッセージ ID のユニオン型 (今回は 1 種類だけ)
type MessageIds = "noExplicitAny";

export default compat(
  createRule<Options, MessageIds>({
    name: "no-explicit-any",
    meta: {
      type: "suggestion",
      docs: {
        description: "明示的な any を禁止",
      },
      messages: {
        // 画像の出展: https://x.com/ten986/status/1926239460196319302
        noExplicitAny: "https://pbs.twimg.com/media/Grtgko8XsAAcKTT?format=jpg&name=large",
      },
      schema: [],
    },
    defaultOptions: [],
    create: (context) => ({
      // any キーワードに対して
      TSAnyKeyword: (node) => {
        // エラーを表示
        context.report({
          node,
          messageId: "noExplicitAny",
        });
      },
    }),
  }),
);

ルールの詳しい作り方や fix, suggestion の提供などは ESLint のドキュメントtypescript-eslint のルールの実装 を参考にしてください.

4. ルールを有効化

/rules/index.ts にルールをまとめるファイルを作り,

// /rules/index.ts

export { default as "no-explicit-any" } from "./no-explicit-any";

もし既存の設定ファイルが JavaScript (/eslint.config.js) なら, TypeScript で書いたルールを使えるように /eslint.config.ts にリネームしつつ, 作成したルールを有効化しましょう (参考).

// /eslint.config.ts

import { defineConfig } from "eslint/config";
import * as rules from "./rules";

export default defineConfig(
  // ...
  // プラグインとして登録
  {
    plugins: {
      local: { rules },
    },
  },
  // ルールを有効化
  {
    files: ["src/**/*.ts"],
    rules: {
      "local/no-explicit-any": "warn",
    },
  },
  // ...
);

5. 完成

コードの any の部分にカスタマイズした警告メッセージが表示されている

https://pbs.twimg.com/media/Grtgko8XsAAcKTT?format=jpg&name=large

(本当はエディタ上で画像が表示されるとよかったんですけどね)