TypeScript で eslint-plugin を作成する
いきなりですが、以下のようなコンポーネントを作成したとします。
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 を作成しました。
本題に入ります。
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"
ならセミコロンを付けないことを強制
といったように、各 rules
はコーディングに際してのルールを定義しています。
semi
以外のルールに関しては以下を参考にしてください。
しかし、ESLint が全てを網羅しているわけではありません。
- imgタグにはalt属性を付与することを必須にしたい
- buttonタグにtype属性を付与することを必須にしたい
などをルールとして定めたい場合は eslint-plugin を使用する必要があります。
ESLint がプラグインの機構を設けているからこそ様々なルールを適用できる。とも言い換えれますね。
eslint の rules を作成する
rules
を作成する前に、TypeScriptで開発するために必要なライブラリをインストールします。
$ yarn add -D typescript @typescript-eslint/experimental-utils
tsconfig.json
の作成も行いましょう。
細かな説明は省略しますが、迷ったら以下を参考にしてください。
rules
作成にあたって、以下のフォルダ構成で説明します。
ここでは ruleName
という名前でファイルやルールを記述しますが、作成したいルールに合わせて適宜変更してください。
src
├── __tests__
│ └── ruleName.ts
├── index.ts
└── 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()
という記述が出てきましたが、RuleContext
は report
メソッドを持っており、ルールに違反するコードに対して該当する箇所やエラーの内容を記述します。
僕が作成した eslint-plugin-jsx-dollar
であれば JSXElement
の children の中から JSXText
を探して $
が末尾に含まれていればエラーとして表示するようにしています。
テストを追加する
ルールが完成したら正しく動作するかのテストを追加しましょう。
実際はテストを予め記述しておくと良さそうです。
作成した各ルールを読み込み、valid
, invalid
の各パターンのテストケースを記述します。
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 の名前などを設定します。
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
tsc
でビルドした上で rules
を読み込んであげます。
module.exports = require("./").rules;
ここまでくれば、検証用のファイルなどを作成して eslint
を実行してみましょう!
付録: npm publish
@hiro08 さんが書かれた以下の記事が参考になります。
ざっくりとした流れは以下の通りです。
-
package.json
に必要な設定(name
,version
, ...)を記述する - npm への ACCESS TOKEN を取得する
- Github Actions や CircleCI を用いてリリースを自動化する
Discussion
とても参考になりました^^ ありがとうございます!
一つお伺いしたいのですが以下のrequireはどこを見ているのでしょうか? なぜこれでrulesが取得できるのか悩んでいます。
質問ありがとうございます!
package.json の
main
が示すファイルを参照しています!ですので
yarn build
してdist
ディレクトリが作られた後にyarn lint
すると正常に動作します!お返事ありがとうございます! package.jsonのmainに記入すればそのような書き方ができたんですね😳 知らなかったです。
レポジトリ名を eslint-plugin ではじめないと動かないことを知らなくて、少し格闘しましたが無事に初めて自分のeslint pluginを作成できることができました!
感謝です^^
詳しくは npm のドキュメントが参考になります👍
おめでとうございます🎉