ESLintのカスタムルールをTypeScriptで書いてみた
はじめに
paiza株式会社の小野です。
直近ESLintやRuboCopなどのlinter設定に触れる機会が増えてきたのですが、
linterの設定を整えていくと気持ちよくコードが書けますし、ルール調査を通して言語そのものについて理解度も向上するのでlinter触るのいいなあと思っています。
せっかくなのでESLintについてさらに理解しておきたい、TypeScriptを書く機会もかなり増えてきたので今後役に立つかもしれない、ということでESLintのカスタムルールをTypeScriptで書く方法を調べてみました。
ゴール
- 簡単なカスタムルールを作ってみて、ローカルプロジェクト内でESLintに指摘させる
- TypeScriptのコードをチェックするカスタムルールをTypeScriptで記述する[1]
カスタムルールを作る
今回は例として、TypeScriptのユーティリティ型であるOmit
を使っている箇所を指摘するルールを作ってみましょう。[2][3]
例えば以下のコードではPersonName
型を作るためにOmit
を使っていますが、これを指摘させることにします。
type Person = { name: string; age: number }
type PersonAge = Pick<Person, "age">; // ok
type PersonName = Omit<Person, "age">; // error
const Omit = () => { console.log('Omitをomitしたい'); } // ok
準備
ESLintが入ったTypeScript環境については、ESLint公式ドキュメントやサバイバルTypeScriptを参考にしつつ構築しました。
後者は導入方法や使い方だけでなく背景知識も丁寧に解説されており、linterやESLintを使ってみたい・理解を深めたいという方は一度目を通してみることをおすすめします。
今回はeslint-no-omit-utility-type
というディレクトリを作成し、必要なパッケージをインストールします。[4]
mkdir eslint-no-omit-utility-type
cd eslint-no-omit-utility-type
yarn init -y
yarn add -D 'eslint@^8' 'typescript@^4.6' '@types/node@^16' '@typescript-eslint/eslint-plugin@^6' '@typescript-eslint/parser@^6' '@typescript-eslint/utils@^6'
yarn tsc --init
touch .eslintrc.js
touch Person.ts
mkdir src
tsconfig.json
と .eslintrc.js
はそれぞれ以下を記述しておきます。
{
"compilerOptions": {
"target": "es2021",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
},
"include": ["src", "*.ts"]
}
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
};
Person.ts
には先ほどの例のコードを記述しておきます。
type Person = { name: string; age: number }
type PersonAge = Pick<Person, "age">; // ok
type PersonName = Omit<Person, "age">; // error
const Omit = () => { console.log('Omitをomitしたい'); } // ok
試しにyarn eslint Person.ts
を実行して、指摘が発生しないことを確認しておきましょう。
yarn eslint Person.ts
yarn run v1.22.19
$ /Users/onoshi/eslint-no-omit-utility-type/node_modules/.bin/eslint Person.ts
✨ Done in 0.63s.
ルールを記述する
src
ディレクトリ配下にno-omit-utility-type.ts
を作成し、ルールを記述していきます。
touch src/no-omit-utility-type.ts
@typescript-eslint/utils
が提供しているESLintUtils.RuleCreator()
[5]を利用して記述していきます。以下が大枠です。
import { ESLintUtils } from '@typescript-eslint/utils';
const createRule = ESLintUtils.RuleCreator(
name => `https://example.com/rule/${name}` // ルールのドキュメントURL
);
export const rule = createRule({
create(context) {
return {
// ここにルールの実装を書く
};
},
// ここにルールのオプションを記述する e.g. name, meta, defaultOptionsなど
});
これを踏まえて、まずはルールの実装部分を記述していきます。方針としては以下を実装すればOKですので、順に見ていきます。
- 特定のコード表現(今回は
Omit
)が存在するかどうかをチェックする -
context.report()
メソッドを呼び出す
特定のコード表現が存在するかどうかをチェックする
ここで登場するのが、AST(Abstract Syntax Tree, 抽象構文木)です。ASTについての詳細な解説はこの記事では割愛しますが、ASTはコードを構成する要素を木構造で表現したもので、ESLintのようなlinterはASTを利用してコードを解析しています。
例えばPerson.ts
のASTをTypeScript AST Viewerで確認してみると、このように構成されていることがわかります。
Omit
をクリックすると、どのノードが対応しているのかが分かります
typescript-eslint
には各ノードに対応するTSInterfaceDeclaration
やTSTypeAnnotation
のようなTS
で始まる名前のメソッドがあり、ノードについての処理を書くことができます。
上の画像を見ると、Omit
はTypeReference
配下のIdentifier
として表現されているようですので、TSTypeReference
について条件を記述してあげれば良さそうです。
入力補完やコンパイラの支援を受けることで、各ノードやそれに関連する要素についての調査コストを大きく削減することができます。TypeScriptでカスタムルールを書くメリットですね。
src/no-omit-utility-type.ts
に追記したものが以下です。
import { ESLintUtils } from '@typescript-eslint/utils';
const createRule = ESLintUtils.RuleCreator(
name => `https://example.com/rule/${name}` // ルールのドキュメントURL
);
export const rule = createRule({
create(context) {
return {
// ここにルールの実装を書く
TSTypeReference(node) {
if (node.typeName.type === 'Identifier' && node.typeName.name === 'Omit') {
// context.report()メソッドを呼び出す
}
}
};
},
// ここにルールのオプションを記述する e.g. name, meta, defaultOptionsなど
});
context.report()
メソッドを呼び出す
指摘させたいコード表現についての条件が記述できたら、context.report()
メソッドを呼び出して指摘させます。
型曰く最低限node
とmessageId
をプロパティに持つオブジェクトを渡してあげれば良さそうなので、追記します。
import { ESLintUtils } from '@typescript-eslint/utils';
const createRule = ESLintUtils.RuleCreator(
name => `https://example.com/rule/${name}` // ルールのドキュメントURL
);
export const rule = createRule({
create(context) {
return {
TSTypeReference(node) {
if (node.typeName.type === 'Identifier' && node.typeName.name === 'Omit') {
// context.report()メソッドを呼び出す
context.report({
node,
messageId: 'no-omit-utility-type',
});
}
}
};
},
// ここにルールのオプションを記述する e.g. name, meta, defaultOptionsなど
});
ルールのオプションを記述する
引き続き型の支援に感謝しつつルールのオプションを記述してあげれば完成です。
import { ESLintUtils } from '@typescript-eslint/utils';
const createRule = ESLintUtils.RuleCreator(
name => `https://example.com/rule/${name}` // ルールのドキュメントURL
);
export const rule = createRule({
create(context) {
return {
TSTypeReference(node) {
if (node.typeName.type === 'Identifier' && node.typeName.name === 'Omit') {
context.report({
node,
messageId: 'no-omit-utility-type',
});
}
}
};
},
meta: {
type: 'problem',
docs: {
description: 'disallow the use of the `Omit` utility type',
},
messages: {
'no-omit-utility-type': 'The `Omit` utility type is forbidden',
},
schema: [],
},
name: 'no-omit-utility-type',
defaultOptions: [],
});
ルールの記述や各プロパティの内容については、必要に応じてESLintおよびtypescript-eslintのドキュメントを参照いただければと思います。
カスタムルールを使う
作ったカスタムルールを使って、意図通りの指摘が発生することを確認してみましょう。今回はeslint-plugin-local-rules
を利用してローカルプロジェクト内で試してみます。
eslint-plugin-local-rules
をインストールしておきましょう。eslint-local-rules.js
も作成しておきます。
yarn add -D eslint-plugin-local-rules
touch eslint-local-rules.js
eslint-local-rules.js
には以下を記述します。
module.exports = {
"no-omit-utility-type": require("./src/no-omit-utility-type").rule,
};
.eslintrc.js
のplugins
とrules
に以下を追記します。この記述により、eslint-plugin-local-rules
を通してno-omit-utility-type
をESLintに認識させることができます。
module.exports = {
// ...
"plugins": [
"@typescript-eslint",
"local-rules"
],
"rules": {
"local-rules/no-omit-utility-type": "error"
}
};
src/no-omit-utility-type.ts
をコンパイルして、yarn eslint Person.ts
を実行してみます。
yarn tsc # src/no-omit-utility-type.jsが生成される
yarn run v1.22.19
$ /Users/onoshi/eslint-no-omit-utility-type/node_modules/.bin/tsc
✨ Done in 0.65s.
yarn eslint Person.ts
yarn run v1.22.19
$ /Users/onoshi/eslint-no-omit-utility-type/node_modules/.bin/eslint Person.ts
/Users/onoshi/eslint-no-omit-utility-type/Person.ts
3:19 error The `Omit` utility type is forbidden local-rules/no-omit-utility-type
✖ 1 problem (1 error, 0 warnings)
error Command failed with exit code 1.
無事ESLintに指摘してもらうことができました!
VS Code上でも指摘が入っています
まとめ
この記事ではESLintのカスタムルールをTypeScriptで記述し、eslint-plugin-local-rules
を利用してローカルプロジェクト内でESLintの指摘を受けられることを確認しました。
周辺知識に関しては各パッケージの公式ドキュメントや文中および後述の参考文献に委ねる方針として、この記事内では多くを割愛させていただきました。
ESLintのカスタムルールを書いてみたいという方はぜひそちらにも目を通していただけたらと思います。
補記
TypeScriptの特定の型を禁止したい場合は、@typescript-eslint/ban-types
を利用できます。
module.exports = {
// ...
"rules": {
// "local-rules/no-omit-utility-type": "error",
"@typescript-eslint/ban-types": [
"error",
{
"types": {
"Omit": "",
},
"extendDefaults": true
}
]
}
};
yarn eslint Person.ts
yarn run v1.22.19
$ /Users/onoshi/eslint-no-omit-utility-type/node_modules/.bin/eslint Person.ts
/Users/onoshi/eslint-no-omit-utility-type/Person.ts
3:19 error Don't use `Omit` as a type. @typescript-eslint/ban-types
✖ 1 problem (1 error, 0 warnings)
error Command failed with exit code 1.
参考文献
文中に掲載していないものを中心に、参考にさせていただいた記事を以下にまとめておきます。
-
素のESLintではTypeScriptのコードをチェックすることはできません。また、カスタムルールはJavaScriptで記述します。https://eslint.org/docs/latest/extend/custom-rule-tutorial ↩︎
-
Omit
はオブジェクトの型から特定のプロパティを除外した型を作ることができますが、Pick
と異なり存在しないプロパティキーを指定してもTypeScriptコンパイラは指摘しない仕様となっています。これは歴史的経緯によるものとのことです。 ↩︎ -
「
Omit
の利用を避けましょう」という意図ではなく、あくまで例として。Omit
の利用シーンについてはこちらの記事が参考になりました。 ↩︎ -
typescript-eslint
について、サバイバルTypeScriptではバージョン5系を利用していますが、今回は執筆時点で最新のバージョン6系を利用しました。
6系ではtsconfig.json
のmodule
とmoduleResolution
の内容によってはimport時にエラーが発生します。ハマって2時間くらい溶かしました。 ↩︎ -
代わりに
ESLintUtils.RuleCreator.withoutDocs()
を利用することでドキュメントなしのカスタムルールを作成することもできますが、非推奨とされているためここでは仮のURLを指定しておくことにします。 ↩︎
Discussion