はじめに
今となっては Node.js での開発などでは必須となったツール、 ESLint。
インポートの順番を特定の順番にしたり未使用の変数に Warning を出したり、プロジェクトに必要なルールを設定することでコードの品質を一定に保つことができるスグれたツールです。
プラグインの機構も備えており、多くの開発者が様々なプラグインを組み合わせてルールを設定していることと思います。
そんな ESLint ですがあらゆるユースケースに対応できるかというと必ずしもそうではなく、中には ESLint でカバーできていないプロジェクト独自のルールがあったりするのではないでしょうか。
この手の独自のルールというのは機械的にチェックすることができないと、レビューの時に余計なやり取りが生じたりプロジェクト新規参画者にとってもどういうルールがあるのかをいちいち読みに行かないといけなかったりとあまりいい状態とは言えません。
そこで今回は、そんな ESLint でカバーできないルールをプラグインを自作することでカバーできるようにしてみようと思います。
必要なもの
ESLint や Typescript など基本的なものを除くと必要なものは @typescript-eslint/utils 一つだけです。
モノレポ前提で話を進めます。
プラグインを作っていく
今回は、 import { foo as bar } from 'baz'
の as bar
のように、名前付きモジュールをリネームしてインポートさせないようなルールを含むプラグインを作ってみます。
仕様としては以下の感じです。
- OK
import { foo } from 'bar'
- NG
import { foo as bar } from 'baz'
Do not rename imported identifier. (no-renamed-import) - NG
import { foo as foo } from 'bar'
Do not use unnecessary local specifier. (no-unnecessary-local-specifier)
NG パターンでは OK パターンに修正可能にもしてみます。
1. 自作プラグイン用のワークスペースを作る
ここでは local-rules
というディレクトリに @project/eslint-plugin-local-rules
というワークスペースを作成します。
{
"workspaces": [
"local-rules"
]
}
エントリポイントは dist/index.js
とします。
{
"name": "@project/eslint-plugin-local-rules",
"main": "dist/index.js",
"scripts": {
"build": "rm -rf dist && tsc src/index.ts --target es2015 --module commonjs --outDir dist"
}
}
typescript-eslint/utils
をインストールします。
$ yarn workspace @project/eslint-plugin-local-rules add -D @typescript-eslint/utils
このあたりの設定は適宜書き換えてください。
2. ルールを書く
ESLint はコードを AST にして解釈するので、ルールもそれに従った形で書く必要があります。
AST がよくわからなくても大丈夫です。 AST explorer というサイトでコードを貼り付けるとどういうものに変換されるのかを確認することができます。
例えば import { foo as bar } from 'baz'
というコードは次のように変換されます。
変換結果
{
"type": "Program",
"start": 0,
"end": 34,
"body": [
{
"type": "ImportDeclaration",
"start": 0,
"end": 33,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 9,
"end": 19,
"imported": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "foo"
},
"local": {
"type": "Identifier",
"start": 16,
"end": 19,
"name": "bar"
}
}
],
"source": {
"type": "Literal",
"start": 28,
"end": 33,
"value": "baz",
"raw": "'baz'"
}
}
],
"sourceType": "module"
}
今回の例だと ImportSpecifier
のあたりをごにょごにょすれば良さそうです。
型でいうと TSESLint.RuleModule
を使って書いていくことになります。詳細はドキュメントに!
import { TSESLint } from '@typescript-eslint/utils'
export const NoRenamedImportRule: TSESLint.RuleModule<
'no-renamed-import' | 'no-unnecessary-local-specifier'
> = {
meta: {
type: 'suggestion',
schema: [],
messages: {
'no-renamed-import': 'Do not rename imported identifier.',
'no-unnecessary-local-specifier': 'Do not use unnecessary local specifier.',
},
hasSuggestions: true,
fixable: 'code',
},
create: context => ({
ImportSpecifier: node => {
if (node.imported.name !== node.local.name)
context.report({
node,
messageId: 'no-renamed-import',
fix: fixer => fixer.replaceText(node, node.imported.name),
})
else if (node.imported.range.some((v, i) => v !== node.local.range[i]))
context.report({
node,
messageId: 'no-unnecessary-local-specifier',
fix: fixer => fixer.replaceText(node, node.imported.name),
})
},
}),
}
キモは context.report()
です。これにより ESLint にエラーを伝えることができます。
3. ルールをテストする
これも TSESLint.RuleTester
のおかげで簡単に書くことができます。
import { TSESLint } from '@typescript-eslint/utils'
import { NoRenamedImportRule } from './no-renamed-import'
const tester = new TSESLint.RuleTester({
parser: require.resolve('espree'),
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
})
test('no-renamed-import のテスト', () => {
tester.run('no-renamed-import', NoRenamedImportRule, {
valid: [
{
code: `import { foo } from 'baz'`,
},
],
invalid: [
{
code: `import { foo as bar } from 'baz'`,
errors: [
{
messageId: 'no-renamed-import',
},
],
output: `import { foo } from 'baz'`,
},
{
code: `import { foo as foo } from 'baz'`,
errors: [
{
messageId: 'no-unnecessary-local-specifier',
},
],
output: `import { foo } from 'baz'`,
},
],
})
})
valid
には OK パターンを、 invalid
には NG パターン(修正可能なものには output
を添えて)を書いておけば OK です。
注意点としては複数のエラーが期待される NG パターンの場合、それら全ての messageId
を errors
に書く必要があることですかね。
4. ESLint に適用する
あとはビルドして ESLint の設定ファイルに書き込むだけです。
一般的なプラグイン同様に eslint-plugin-
は省略することができます。
$ yarn workspace @project/eslint-plugin-local-rules build
{
"plugins": ["@project/local-rules"],
"rules": {
"@project/local-rules/no-renamed-import": "error"
}
}
どちらのルールも正常に動いてますね!
画像は VSCode のものですが、読み込まれてない?ってときは ESLint Server を再起動したり Output タブから確認するといいかもです。
参考にさせていただいた記事
TypeScript で eslint-plugin を作成する
プロジェクト内で完結するESLintのローカルルールを作りたい