ESLint ルールを TypeScript で書く
備忘録。ESLint のルールを含むプラグインを TypeScript で書く時のポイントについて。
プラグインの仕組み自体は公式ドキュメントに従いつつ、TypeScript でどう対応させるかについて述べる。
依存パッケージ
最低限、以下を入れる:
- dependencies
eslint
- devDependencies
typescript@types/eslint@types/estree
今回はテストランナーに Jest を使うため、以下も devDependencies に追加する:
jestts-jest
@types/jest は不要であることに注意(理由は後述)。
なお tsconfig.json 及び jest.config.[tj]s の内容は通常のプロジェクトと比べて特に違いはないので、説明は省略する。
ルール定義
ここでは文字列リテラル、もしくはテンプレートリテラルの文字列片が hello である場合に警告するルール no-hello を題材とする。
no-hello.ts
import { Rule } from "eslint" import { Literal, TemplateElement } from "estree" export const noHello: Rule.RuleModule = { meta: { type: "problem" }, create(context: Rule.RuleContext): Rule.RuleListener { return { Literal(node: Literal & Rule.NodeParentExtension) { if (node.value === "hello") { context.report({ node, message: '"hello" is not allowed' }) } }, TemplateElement(node: TemplateElement & Rule.NodeParentExtension) { if (node.value.cooked === "hello") { context.report({ node, message: '"hello" is not allowed' }) } }, } } }
各ルールの定義の型 eslint.Rule.RuleModule、create 関数が受け取るコンテキストの型 eslint.Rule.RuleContext となる。
create が返す visitor の各関数の引数は、estree の対応するノードの型と、親ノードを参照できるようするための eslint.Rule.NodeParentExtension の交差型となる(この交差型によって親ノードを .parent で参照することができる)。
プラグイン定義
index.ts
import { noHello } from "./no-hello" export = { rules: { 'no-hello': noHello }, }
プラグイン定義を表す型は現時点ではないようなので、プラグインのドキュメントを見ながら typo しないように実装する。
tsconfig.json の module が "commonjs" なら、 module.exports = の代わりに export = が使える。
ルールのテスト
前述の通りテストランナーには Jest を使う。が、Jest の describe/it や expect は使わない(これが @types/jest が不要な理由)。ESLint がルールのテスト用に mocha をベースにしたテストユーティリティを提供している。テストはそのユーティリティを使って記述し、Jest はエントリポイントとしてのみ使う。((このテストのやり方は typescript-eslint を参考したものである。))
no-hello.test.ts
import { noHello } from "../src/no-hello" import { RuleTester } from "eslint" const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } }) ruleTester.run("no-hello", noHello, { valid: [ { code: 'const x = "goodbye"' }, { code: 'const x = `goodbye`' }, ], invalid: [ { code: 'const x = "hello"', errors: [{ message: '"hello" is not allowed' }] }, { code: 'const x = `hello${process.env.MESSAGE}`', errors: [{ message: '"hello" is not allowed' }] }, ] })
まず eslint.RuleTester のインスタンスを作成する。コンストラクタの parserOptions には、.eslintrc の parserOptions と同様に内部で使うパーサが受け取るオプションを渡すことができる。今回はテストコード内で const を使いたいため、ecmaVersion: 2015 を渡している。
デフォルトのパーサは espree である。パーサを変えたい時は parser にパーサのパスを設定する(これも .eslintrc の parser と同様の形式)。
RuleTester.run() にルール名とルール定義のオブジェクト(noHello)、そして valid/invalid それぞれのケースを渡すことでテストが実行される。上記の例ではエラーメッセージのみをチェックしているが、追加でエラーとなるべき箇所をアサートすることもできる。詳細は RuleTester.ValidTestCase と RuleTester.InvalidTestCase の型定義を確認すること。
まとめ
ESLint のルール・プラグインを TypeScript で実装 & テストする方法についてまとめた。
注意点として @types/eslint の型定義では estree を拡張した AST を扱えない(eslint.Rule.RuleListener 参照)。その場合、型定義を置き換えるなり any でごまかすなりの工夫が必要になる。