ESLint(v9, Flat config) でカスタムルールを追加する(JavaScript)
最初に
先日ESlintのv9がリリースされました。
ESLint v9.0.0 released - ESLint - Pluggable JavaScript Linter
v9からはデフォルトが Flat config になっています。
NodeのバージョンもNode.js < v18.18.0, v19 no longer supported
となりました。
ただしtypescript-eslintは現在(2024/04/14)v9対応中のようです。
ESLint v9 Support · Issue #8211 · typescript-eslint/typescript-eslint
なのでTypeScriptでやりたいところですが、とりあえずJavaScriptでv9の Flat config におけるカスタムルールを追加するやり方のメモです。
全体のファイル構成
eslint-custom-rules-tutorial
┣ eslint-rules-plugin
┃ ┣ enforce-foo-bar.js
┃ ┣ enforce-foo-bar.test.js
┃ ┣ index.js
┃ ┗ vitest.config.js
┣ src
┃ ┗ example.js
┣ .prettierrc.json
┣ eslint.config.js
┣ package.json
┗ pnpm-lock.yaml
package.json
ESM形式なので"type":"module"
を指定しています。
{
"name": "eslint-custom-rules-tutorial",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"lint": "eslint",
"lint:test": "vitest --config eslint-rules-plugin/vitest.config.js run",
"lint:fix": "eslint --fix",
"lint:i": "eslint --inspect-config",
"format": "prettier {src,eslint-rules-plugin}/**/*.js --write"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^9.0.0",
"prettier": "3.2.5",
"vitest": "^1.4.0"
},
"packageManager": "pnpm@8.15.6+sha256.01c01eeb990e379b31ef19c03e9d06a14afa5250b82e81303f88721c99ff2e6f"
}
リント対象のコード
function correctFooBar() {
const foo = 'bar'
}
function incorrectFoo() {
const foo = 'baz' // Problem!
}
ルールの作成部分
プラグインにして読み込むためのルールを作成しています。
export const enforceFooBarRule = {
meta: {
type: 'problem',
docs: {
description: "Enforce that a variable named `foo` can only be assigned a value of 'bar'.",
},
fixable: 'code',
schema: [],
messages: {
fooBarMessageId: 'Value other than "bar" assigned to `const foo`. Unexpected value: {{ notBar }}.',
},
},
create(context) {
return {
VariableDeclarator(node) {
if (node.parent.kind === 'const') {
if (node.id.type === 'Identifier' && node.id.name === 'foo') {
if (node.init && node.init.type === 'Literal' && node.init.value !== 'bar') {
context.report({
node,
messageId: 'fooBarMessageId',
data: {
notBar: node.init.value,
},
fix(fixer) {
return fixer.replaceText(node.init, '"bar"')
},
})
}
}
}
},
}
},
}
ESM形式で書いている点以外はほぼ公式のサンプルのままです。
Rule Structureを参考に定義していきます。
ルールオブジェクトにmetaプロパティでメタ情報(ルールのタイプ、説明、自動修正、エラーメッセージなど)を追加します。
それからルールを記述するためのcreate
関数を定義します。
custom-rules#rule-structureに書いてあるcreate
関数の説明を抜粋すると以下のように書いてあります。
create():JavaScriptコードの抽象構文木(ESTreeで定義されたAST)を走査する際に、ESLintがノードを "訪問 "するために呼び出すメソッドを持つオブジェクトを返します:
create
関数で返すオブジェクトのキーにはESTreeのノードタイプかセレクターを指定することができ、それに対して定義したビジター関数は対象のノードを拾って返してくれます。
サンプル中でいうとVariableDeclaratorをキーに指定しているので、その関数はVariableDeclarator
のノードを拾って返してくれます。
ちなみにconsole.log({node})
を書いて中身を見るとこんな感じです。
console.log({node})の表示結果
{
node: <ref *1> Node {
type: 'VariableDeclarator',
start: 35,
end: 46,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 35, 46 ],
id: Node {
type: 'Identifier',
start: 35,
end: 38,
loc: [SourceLocation],
range: [Array],
name: 'foo',
parent: [Circular *1]
},
init: Node {
type: 'Literal',
start: 41,
end: 46,
loc: [SourceLocation],
range: [Array],
value: 'bar',
raw: "'bar'",
parent: [Circular *1]
},
parent: Node {
type: 'VariableDeclaration',
start: 29,
end: 46,
loc: [SourceLocation],
range: [Array],
declarations: [Array],
kind: 'const',
parent: [Node]
}
}
}
ASTの解析結果はAST explorerのサイトでも確認できます。
サンプルコードのAST全体
foo = 'baz'
のVariableDeclarator
を選択した場合
あとはVariableDeclarator
で取得したノードの中を調べていきます。
constで宣言したfoo変数にbar
という値が代入された場合、ルールは何もしません。逆にbar
が代入されていない場合、context.report()
でESLintにエラーを報告します。messageId: 'fooBarMessageId',
でメタ情報のエラーメッセージと紐付けます。data
属性で指定した値はエラーメッセージ内で参照することできます。 fix()
関数では--fix
をつけて実行したときの修正内容を定義します。
ルールをプラグインとして追加できる形にしてエクスポートする
import { enforceFooBarRule } from './enforce-foo-bar.js'
const plugin = {
meta: {
name: 'eslint-rules-plugin',
version: '1.0.0',
},
rules: { 'enforce-foo-bar': enforceFooBarRule },
}
export default plugin
※参考
eslint.config.js
フラットコンフィグなのでeslint.config.js
に配列形式で設定を書いてエクスポートします。
'use strict'
import eslintRulesPlugin from './eslint-rules-plugin/index.js'
export default [
{
files: ['src/**/*.js'],
plugins: { localRules: eslintRulesPlugin },
rules: {
'no-unused-vars': 'error',
'localRules/enforce-foo-bar': 'error',
},
},
]
作成したプラグインをインポートして、pluginsプロパティにオブジェクトで追加しています。
plugins: { 任意のキー名: 作成したプラグイン }
の形式で記述します。
これでプラグインに追加されます。
plugins: { localRules: eslintRulesPlugin }
追加したプラグインのルールを有効化します。
rulesプロパティに{rules: 'プラグインのキー名/ルール名': '深刻度'}
の形式で記述します。
rules: {
...,
'localRules/enforce-foo-bar': 'error',
},
※参考:
- The new config file: eslint.config.js - ESLint's new config system, Part 2: Introduction to flat config - ESLint
- #configuration-file - Configuration Files - ESLint
- #configuring-rules - Configuration Files - ESLint
ルールのテスト部分
RuleTesterを使ってルールのテストを書くことができます。
valid
にルールに当てはまる内容を書きます。invalid
にルールに当てはまらない内容を書きます。
invalid
のoutput
には自動修正されたときの期待値を書きます。
import { RuleTester } from 'eslint'
import { enforceFooBarRule } from './enforce-foo-bar.js'
const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 'latest' } })
// Throws error if the tests in ruleTester.run() do not pass
ruleTester.run(
'enforce-foo-bar', // rule name
enforceFooBarRule, // rule code
{
valid: [
{
name: '変数名がfooで、内容がbarの場合',
code: "const foo = 'bar';",
},
{
name: '変数名がfoo以外で、内容がbar以外の場合',
code: "const huga = 'bar';",
},
],
invalid: [
{
name: '変数名がfooで、内容がbar以外の場合はbarに修正される',
code: "const foo = 'baz';",
output: 'const foo = "bar";',
errors: [
{
messageId: 'fooBarMessageId',
},
],
},
],
},
)
Vitestの追加
RuleTesterのデフォルトのままだとテストの実行結果が表示されずわかりにくいのでVitestを追加しています。
pnpm add -D vitest
eslint-rules-plugin
ディレクトリにvitest.config.js
も追加しています。
globals
をtrue
にしてやると自動でRuleTesterのアサーションにvitestが組み込まれます。
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
root: './',
include: ['**.test.js'],
},
})
もしくはglobals:true
を設定せずにテストファイル内でRuleTesterにvitestを設定するやり方でも実行できます。
import { RuleTester } from 'eslint'
import { enforceFooBarRule } from './enforce-foo-bar.js'
import * as vitest from 'vitest'
// vitestの設定 -------------------------
RuleTester.afterAll = vitest.afterAll
RuleTester.it = vitest.it
RuleTester.only = vitest.it.only
RuleTester.describe = vitest.describe
// -------------------------------------
const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 'latest' } })
...略
※参考
- Docs: document how to setup RuleTester.afterAll · Issue #7275 #issuecomment-1643242066
- @typescript-eslint/rule-tester #vitest
テストの実行
package.jsonにvitestを実行するスクリプトを追加しています。
{
"scripts": {
"lint:test": "vitest --config eslint-rules-plugin/vitest.config.js run",
},
}
テストを実行します。
eslint-custom-rules-tutorial $ pnpm lint:test
...中略
RUN v1.4.0
...中略
✓ eslint-rules-plugin/enforce-foo-bar.test.js (3)
✓ enforce-foo-bar (3)
✓ valid (2)
✓ 変数名がfooで、内容がbarの場合
✓ 変数名がfoo以外で、内容がbar以外の場合
✓ invalid (1)
✓ 変数名がfooで、内容がbar以外の場合はbarに修正される
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 19:17:02
Duration 1.61s (transform 39ms, setup 0ms, collect 457ms, tests 32ms, environment 0ms, prepare 526ms)
リントの実行
ESlintを実行してみます。
rulesに指定したビルトインのルールno-unused-vars
とオリジナルで作成したlocalRules/enforce-foo-bar
が実行されています。
eslint-custom-rules-tutorial $ pnpm lint
...中略
eslint-custom-rules-tutorial/src/example.js
1:10 error 'correctFooBar' is defined but never used no-unused-vars
2:9 error 'foo' is assigned a value but never used no-unused-vars
5:10 error 'incorrectFoo' is defined but never used no-unused-vars
6:9 error Value other than "bar" assigned to `const foo`. Unexpected value: baz localRules/enforce-foo-bar
6:9 error 'foo' is assigned a value but never used no-unused-vars
✖ 5 problems (5 errors, 0 warnings)
1 error and 0 warnings potentially fixable with the `--fix` option.
ELIFECYCLE Command failed with exit code 1.
eslint-custom-rules-tutorial $
自動修正の実行
--fix
をつけて実行すると自動で修正されます。
eslint-custom-rules-tutorial $ pnpm lint:fix
...中略
1:10 error 'correctFooBar' is defined but never used no-unused-vars
2:9 error 'foo' is assigned a value but never used no-unused-vars
5:10 error 'incorrectFoo' is defined but never used no-unused-vars
6:9 error 'foo' is assigned a value but never used no-unused-vars
✖ 4 problems (4 errors, 0 warnings)
ELIFECYCLE Command failed with exit code 1.
function incorrectFoo() {
- const foo = 'baz' // Problem!
+ const foo = "bar" // Problem!
}
インスペクターの起動
v9でコンフィグのインスペクターツールが導入されました。
Introducing ESLint Config Inspector - ESLint - Pluggable JavaScript Linter
eslint --inspect-config
で起動できます。
スクリプトで起動コマンドを実行します。
pnpm lint:i
127.0.0.1:7777
で立ち上がります。
Discussion