ESLint ルールを TypeScript で書く
備忘録。ESLint のルールを含むプラグインを TypeScript で書く時のポイントについて。
プラグインの仕組み自体は公式ドキュメントに従いつつ、TypeScript でどう対応させるかについて述べる。
依存パッケージ
最低限、以下を入れる:
- dependencies
eslint
- devDependencies
typescript
@types/eslint
@types/estree
今回はテストランナーに Jest を使うため、以下も devDependencies に追加する:
jest
ts-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
でごまかすなりの工夫が必要になる。