ESLint独自ルールを作ってみたときのメモ
全体の手順
- 独自ルールを作成して、
rules
などの決まったディレクトリに置く - npm scriptsでは
"lint": "eslint --rulesdir rules"
のように、rulesdir
を指定する
独自ルールの作り方
- 基本的には本家ESLintの各ルールを読んだり、巷にあふれる独自ルールを参照するとよい
- 公式ドキュメントもあるが、ものすごく長く理解に時間がかかるので、日本語の文献や実コードを読みつつ進めるのが良いと思う。ある程度理解すると、公式ドキュメントが手にとるようにわかる。
独自ルールの構造
-
meta
とcreate
から構成される大きなオブジェクトをexportするJS(or TS)ファイルが実態 - ファイル名がルール名とイコールになる
たとえば、変数名の先頭アンダースコアを禁止したい場合は
rules/no-underscore-prefix.js
を作る。
/**
* @author meijin
*/
"use strict";
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "hoge",
recommended: false,
url: "https://eslint.org"
},
schema: [],
messages: {
unexpected: "Unexpected {{name}}."
}
},
create(context) {
return {
VariableDeclaration(node) {
context.getDeclaredVariables(node).forEach(variable => {
const name = variable.name;
if (name.charAt(0) === "_") {
context.report({
node,
messageId: "unexpected",
data: { name }
});
}
});
}
};
}
};
metaキーには、eslint v8からhasSuggestionsが必要になったらしい(Suggestionは、Lintエラー時に治したら良い候補をだしてくれる機能。IDEと連携している事が多い)
上記のように、metaキーには、このルールがどういうもので、どういう機能を備えているかというメタ情報が付与される。こちらの公式仕様は以下のセクションで確認できる。
createキーでは、最終的にASTの各Nodeに対してどういうLintを行うかをリスト化したオブジェクトを返す。
今回は変数宣言を見つけて、アンダースコアから始まっているか調べる例なので、VariableDeclaration
キーに関数を入れて返す。
これらのキーの命名は、厳密にはESTreeというプロジェクトで定義されており、以下のようなMarkdownで一覧で見ることができる。
そのことは以下のセクションで明示されており、ここからESTreeの定義リポジトリに飛ぶことができる。
今回のルールでは、変数宣言のノードを見つけても、そのノードの中からさらに個別の変数名を切り出す必要がある。これを実現するために、僕は当初Tokenを順にたどっていく実装を行い、オペレータ(演算子)が現れたらその左のTokenを使うといった実装をしようとしたが、変数を複数宣言する際に失敗した。
よほど込み入ったケースでなければ、Contextオブジェクトに便利な関数getDeclaredVariables
が定義されている。
getDeclaredVariables
は変数宣言なので、関数の宣言やImport文などにも適用できる便利なヘルパである。
getDeclaredVariables(node) - returns a list of variables declared by the given node. This information can be used to track references to variables.
If the node is a VariableDeclaration, all variables declared in the declaration are returned.
If the node is a VariableDeclarator, all variables declared in the declarator are returned.
If the node is a FunctionDeclaration or FunctionExpression, the variable for the function name is returned, in addition to variables for the function parameters.
If the node is an ArrowFunctionExpression, variables for the parameters are returned.
If the node is a ClassDeclaration or a ClassExpression, the variable for the class name is returned.
If the node is a CatchClause, the variable for the exception is returned.
If the node is an ImportDeclaration, variables for all of its specifiers are returned.
If the node is an ImportSpecifier, ImportDefaultSpecifier, or ImportNamespaceSpecifier, the declared variable is returned.
最終的に、Lintエラーないし警告として報告すべきと判断した場合は、以下のようにcontext.report
関数でNodeとともにメッセージやデータを投げることができる。
if (name.charAt(0) === "_") {
context.report({
node,
messageId: "unexpected",
data: { name }
});
}
テストのルールを書く。今回はESLint公式リポジトリをクローンしてその中に直接テストとルールを書き足すことでしか検証しなかったが、実際はプロダクトコード側でもテストは書けると思う。
以下のようにテストケースを書く。
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const rule = require("../../../lib/rules/no-underscore-prefix"),
RuleTester = require("eslint").RuleTester;
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
const ruleTester = new RuleTester();
ruleTester.run("no-underscore-prefix", rule, {
valid: [
"'hoge'",
"var hoge = 'hoge';",
"// var hoge = 'hoge';",
{ code: "let x,y = 'hoge';", env: { es6: true } },
{ code: "const hoge = 'hoge';", env: { es6: true } },
{ code: "const obj = { _name: 'hoge' };", env: { es6: true } },
"if (true) { var hoge = 'hoge' }"
],
invalid: [
{
code: "var _hoge = 'hoge';",
errors: [{ messageId: "unexpected", data: { name: "_hoge" }, type: "VariableDeclaration", line: 1, column: 1 }]
},
{
code: "const _hoge = 'hoge';",
env: { es6: true },
errors: [{ messageId: "unexpected", data: { name: "_hoge" }, type: "VariableDeclaration", line: 1, column: 1 }]
},
{
code: `const hoge = "OK";
const _hoge = "NG";
const hoge2 = "OK";`,
env: { es6: true },
errors: [{ messageId: "unexpected", data: { name: "_hoge" }, type: "VariableDeclaration", line: 3, column: 1 }]
},
{ code: "let _x,y = 'hoge';", env: { es6: true }, errors: [{ messageId: "unexpected", data: { name: "_x" }, type: "VariableDeclaration", line: 1, column: 1 }] },
{ code: "let x,_y = 'hoge';", env: { es6: true }, errors: [{ messageId: "unexpected", data: { name: "_y" }, type: "VariableDeclaration", line: 1, column: 1 }] },
{
code: "let _x,_y = 'hoge';",
env: { es6: true },
errors: [
{ messageId: "unexpected", data: { name: "_x" }, type: "VariableDeclaration", line: 1, column: 1 },
{ messageId: "unexpected", data: { name: "_y" }, type: "VariableDeclaration", line: 1, column: 1 }
]
},
{ code: "const { _name } = { _name: 'hoge' };", env: { es6: true }, errors: [{ messageId: "unexpected", data: { name: "_name" }, type: "VariableDeclaration", line: 1, column: 1 }] },
{
code: "let _x, _y;",
env: { es6: true },
errors: [
{ messageId: "unexpected", data: { name: "_x" }, type: "VariableDeclaration", line: 1, column: 1 },
{ messageId: "unexpected", data: { name: "_y" }, type: "VariableDeclaration", line: 1, column: 1 }
]
}
]
});
ポイントはes6以降をオプションで指定しないとエラーになることが多々あったこと。
ASTについて補足。
こちらのマークダウンも理解を助ける。
ASTエクスプローラーを使ってJavaScriptコードがいかにESTreeに展開される(言い回しがわからない)かを理解できる。
ASTは抽象構文木と略され、単にプログラムを木構造で表現した構文木から、無駄なカッコなど言語の意味を損なわない情報を落とした木を指す。
この記事は全体感を理解するのに役立つ
ESTreeはES6までの定義のため、現実的にはacornなどの別プロジェクトを使うことが多い。Babelも独自のASTを拡張して使っているとのこと。
Babel uses an AST modified from ESTree, with the core spec located here.
TypeScriptでESLintルールを書きたいときは以下の記事が参考になる
参考
本当はこっちで適用するほうがよさげ
contextの型定義
interface NodeListener {
ArrayExpression?: ((node: ESTree.ArrayExpression & NodeParentExtension) => void) | undefined;
ArrayPattern?: ((node: ESTree.ArrayPattern & NodeParentExtension) => void) | undefined;
ArrowFunctionExpression?: ((node: ESTree.ArrowFunctionExpression & NodeParentExtension) => void) | undefined;
AssignmentExpression?: ((node: ESTree.AssignmentExpression & NodeParentExtension) => void) | undefined;
AssignmentPattern?: ((node: ESTree.AssignmentPattern & NodeParentExtension) => void) | undefined;
AwaitExpression?: ((node: ESTree.AwaitExpression & NodeParentExtension) => void) | undefined;
BinaryExpression?: ((node: ESTree.BinaryExpression & NodeParentExtension) => void) | undefined;
BlockStatement?: ((node: ESTree.BlockStatement & NodeParentExtension) => void) | undefined;
BreakStatement?: ((node: ESTree.BreakStatement & NodeParentExtension) => void) | undefined;
CallExpression?: ((node: ESTree.CallExpression & NodeParentExtension) => void) | undefined;
CatchClause?: ((node: ESTree.CatchClause & NodeParentExtension) => void) | undefined;
ChainExpression?: ((node: ESTree.ChainExpression & NodeParentExtension) => void) | undefined;
ClassBody?: ((node: ESTree.ClassBody & NodeParentExtension) => void) | undefined;
ClassDeclaration?: ((node: ESTree.ClassDeclaration & NodeParentExtension) => void) | undefined;
ClassExpression?: ((node: ESTree.ClassExpression & NodeParentExtension) => void) | undefined;
ConditionalExpression?: ((node: ESTree.ConditionalExpression & NodeParentExtension) => void) | undefined;