TypeScript で eslint-plugin を作成する

2021/02/15に公開
4

いきなりですが、以下のようなコンポーネントを作成したとします。

const Component = ({ text }) => {
  return (
    <span>${text}</span>
  );
};

テンプレートリテラルっぽいですが、JSX の記述のどこかを間違えています。
仮に text という props の値が "sample" という文字列だとすると、出力されるHTMLは以下のようになります。

<span>$sample</span>

余計な $ が混入しているので削除してあげる必要があります。

-   <span>${text}</span>
+   <span>{text}</span>

ちなみに(無理やり)テンプレートリテラルを使用して記述すると以下の形になります。

-   <span>${text}</span>
+   <span>{`${text}`}</span>

稀にしか発生しないですが余計な $ が混入していることを ESLint が教えてくれると良さそうだと思い eslint-plugin を作成しました。

https://www.npmjs.com/package/eslint-plugin-jsx-dollar

本題に入ります。
eslint-plugin の説明から入り、ルールの作り方やテスト、ローカル環境での検証について説明します。

eslint-plugin って何?

eslint-plugin-jsx-a11y, eslint-plugin-react, eslint-plugin-import...
といった様々な eslint-plugin があります。

eslint-plugin は ESLint 自体には含まれていないルール(rules)を作成して ESLint を拡張して使う役割を担っています。

rules についてですが、
例えば ESLint 自体に含まれている rules の末尾のセミコロン(semi)に関して

  • "always" ならセミコロンを付けること強制
  • "never" ならセミコロンを付けないことを強制

https://eslint.org/docs/rules/semi#options

といったように、各 rules はコーディングに際してのルールを定義しています。
semi 以外のルールに関しては以下を参考にしてください。

https://eslint.org/docs/rules/

しかし、ESLint が全てを網羅しているわけではありません。

  • imgタグにはalt属性を付与することを必須にしたい
  • buttonタグにtype属性を付与することを必須にしたい

などをルールとして定めたい場合は eslint-plugin を使用する必要があります。
ESLint がプラグインの機構を設けているからこそ様々なルールを適用できる。とも言い換えれますね。

eslint の rules を作成する

rules を作成する前に、TypeScriptで開発するために必要なライブラリをインストールします。

$ yarn add -D typescript @typescript-eslint/experimental-utils

tsconfig.json の作成も行いましょう。
細かな説明は省略しますが、迷ったら以下を参考にしてください。

https://github.com/kyoncy/eslint-plugin-jsx-dollar/blob/main/tsconfig.json

rules 作成にあたって、以下のフォルダ構成で説明します。
ここでは ruleName という名前でファイルやルールを記述しますが、作成したいルールに合わせて適宜変更してください。

src
├── __tests__
│   └── ruleName.ts
├── index.ts
└── rules
    └── ruleName.ts

細かい説明は後ほど行いますが、ざっくりと各ルールは以下のように構成されます。

src/rules/ruleName.ts
import { TSESLint } from "@typescript-eslint/experimental-utils";

export const ruleName: TSESLint.RuleModule<"messageId", []> = {
  meta: { ... }
  create: (context) => { ... }
}

meta には各々のルールに関するメタデータを設定します。
errorとして扱うのかwarningとして扱うのかであったり、エラーとして表示するメッセージや eslint --fix で修正可能であるかといった情報をもたせます。

create には実際に適用するルールを記述します。
interface だけ説明すると RuleContext を受け取って RuleListener を return するといった感じです。
return する RuleListener には AST として取得する要素に対して RuleFunction を記述する必要があります。
JavaScript の switch に関してなにかルールを適用させたい場合は、SwitchStatement というキーに対して RuleFunction を記述します。

例えば、コメントを書くことを許さないという適用させたい場合は以下のように記述してあげると良いでしょう。

  create: (context) => {
    return {
      Comment: (node) => {
        context.report({
	  node,
	  messageId: "messageId",
	})
      },
    };
  },

ここで、 context.report() という記述が出てきましたが、RuleContextreport メソッドを持っており、ルールに違反するコードに対して該当する箇所やエラーの内容を記述します。

僕が作成した eslint-plugin-jsx-dollar であれば JSXElement の children の中から JSXText を探して $ が末尾に含まれていればエラーとして表示するようにしています。

https://github.com/kyoncy/eslint-plugin-jsx-dollar/blob/main/src/rules/jsx-dollar.ts

テストを追加する

ルールが完成したら正しく動作するかのテストを追加しましょう。

実際はテストを予め記述しておくと良さそうです。
作成した各ルールを読み込み、valid, invalid の各パターンのテストケースを記述します。

src/__tests__/ruleName.ts
import { TSESLint } from "@typescript-eslint/experimental-utils";
import { ruleName } from "../rules/ruleName";

const tester = new TSESLint.RuleTester({
  parser: require.resolve("espree"),
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: "module",
  },
});

tester.run("ruleName", ruleName, {
  valid: [{ code: "describe valid code pattern" }],
  invalid: [{
    code: "describe invalid code pattern",
    errors: [{ messageId: "messageId" }]
  }]
})

eslint-plugin を作成する

作成した各ルールに対してテストも記述すれば残りは eslint-plugin として提供することだけです。
作成したルールをまとめたうえで、plugin の名前などを設定します。

src/index.ts
import { ruleName } from "./rules/ruleName";

export = {
  rules: {
    "ruleName": ruleName,
  },
  configs: {
    all: {
      plugins: ["plugin-name"],
      rules: {
        "ruleName": "error",
      },
    },
  },
};

これで eslint-plugin は完成です!

ローカル環境で eslint-plugin を試す

eslint-plugin-local-rules をインストールして README に沿ってローカル環境で作成したルールを適用しましょう。

$ yarn add -D eslint-plugin-local-rules

https://github.com/cletusw/eslint-plugin-local-rules#readme

tsc でビルドした上で rules を読み込んであげます。

./eslint-local-rules.js
module.exports = require("./").rules;

ここまでくれば、検証用のファイルなどを作成して eslint を実行してみましょう!

付録: npm publish

@hiro08 さんが書かれた以下の記事が参考になります。

https://zenn.dev/hiro08gh/articles/2aa74ead3b2248983f92

ざっくりとした流れは以下の通りです。

  1. package.json に必要な設定(name, version, ...)を記述する
  2. npm への ACCESS TOKEN を取得する
  3. Github Actions や CircleCI を用いてリリースを自動化する

Discussion

ハトすけハトすけ

とても参考になりました^^ ありがとうございます!
一つお伺いしたいのですが以下のrequireはどこを見ているのでしょうか? なぜこれでrulesが取得できるのか悩んでいます。

module.exports = require("./").rules;
ハトすけハトすけ

お返事ありがとうございます! package.jsonのmainに記入すればそのような書き方ができたんですね😳 知らなかったです。

レポジトリ名を eslint-plugin ではじめないと動かないことを知らなくて、少し格闘しましたが無事に初めて自分のeslint pluginを作成できることができました!

感謝です^^