この記事は「イエソド アウトプット筋 トレーニング Advent Calendar 2020」14日目の記事です。
ESLintで独自ルールを設定する
node projectの開発時、
- プロジェクト固有でlint掛けたいなー
- でも別にpackageにするほどではないなー(リポジトリのメンテ面倒だし)
って場合に、独自ルールを追加するやり方を紹介します。
環境
筆者の実行環境。多少違えど問題ないと思います。
- MacOS Catalina 10.15.7
- node 12.18.4
サンプル
何はともあれソースコードがないと話にならないと思うので、GitHubのリポジトリに挙げておきます。
前準備
(git cloneするなら意味ないので飛ばしちゃってください)
$ mkdir eslint-local-rule // プロジェクト名(ディクレトリ名)は適当に
$ cd eslint-local-rule
$ npm init -y
$ npm i -D eslint
$ mkdir src
依存
{
"name": "eslint-local-rule",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^7.15.0"
}
}
実装
lint対象となる、以下のようなindex.js
を用意します。
const unused = 'unused string';
eslintの設定ファイル
eslint公式docに従い、設定ファイルを追加します。
eslintの設定にはいくつか方法がありますが、ここでは.eslintrc
を、ファイル形式はjs
を、使用します。
module.exports = {
root: true,
env: {
node: true,
},
parserOptions: {
ecmaVersion: 2020
},
extends: [
'eslint:recommended'
],
};
parserOptions
docによれば、ecmaVersion
のデフォルト値は5
です。つまりes5のシンタックスしか認められません。
そうなるとconst
なんかは使用できないし、es6(2015)以降の構文が使用できないのは嫌なので、ここでは一旦最新の2020
にします。(サンプルコードでES2020で導入されたシンタックスは出てこないので6以上であれば何でもいいです)
さて、ここまで設定することで、eslint実行時にerrorが吐かれるのが期待値です。
$ npx eslint ./src
/Users/path-to-dir/eslint-local-rule/src/index.js
1:7 error 'unused' is assigned a value but never used no-unused-vars
✖ 1 problem (1 error, 0 warnings)
ではここから本題の独自ルールの設定の追加をしていきます。
ディレクトリ作成
独自ルールを記述したファイルを配置するため、プロジェクトルートに新たにディレクトリを作成します。ここではeslint
という名前にします。
$ mkdir eslint
$ mkdir eslint/rules eslint/tests
$ tree // 一部省略
.
├── eslint
│ ├── rules
│ └── tests // 今回は時間の都合上、test書かないです...
├── node_modules
├── package-lock.json
├── package.json
└── src
└── index.js
ルール実装
それでは、eslint/rules
ディレクトリ内にルール用のJavaScriptファイルを実装していきます。
ESLintが使用するruleの記述にはフォーマットが定義されていて、独自ルールを定義する際は、それを参考にしていきます。
簡単のため、ここでは特定の変数名を禁止するというルールを実装していきます。
module.exports = {
meta: {
// 一旦後回し。空Objectで。
},
create(context) {
return {
VariableDeclarator: (node) => {
console.log(node);
},
};
}
};
まずは一旦、eslintと連携して動作するか検証すべく、対象nodeをコンソール出力するだけのスクリプトを書きました。(詳細な説明は後ほど。)
ESLintと連携
さて、この状態でeslint
を実行してももちろん、上記のスクリプトは実行されることはありません。つまり、結果は前回実行時と同じハズです。
$ npx eslint ./src
/Users/path-to-dir/eslint-local-rule/src/index.js
1:7 error 'unused' is assigned a value but never used no-unused-vars
✖ 1 problem (1 error, 0 warnings)
eslint
に先ほど定義したrule(eslint/rules/ban-variable-name.js
)を実行してもらうには、以下のプラグインを利用します。
$ npm i -D eslint-plugin-rulesdir
そして、.eslintrc.js
を修正します。
const rulesDirPlugin = require('eslint-plugin-rulesdir');
rulesDirPlugin.RULES_DIR = 'eslint/rules';
module.exports = {
root: true,
env: {
node: true,
},
parserOptions: {
ecmaVersion: 2020
},
plugins: ['rulesdir'],
extends: [
'eslint:recommended'
],
rules: {
'rulesdir/ban-variable-name': 'error'
}
};
eslint-plugin-rulesdir
プラグインの利用を宣言し、追加ルールの配置されたディレクトリを指定してあげ、rules
プロパティに利用設定を追加します。
この状態で再度eslint
を実行すると、、
結果
$ npx eslint ./src
Node {
type: 'VariableDeclarator',
start: 6,
end: 30,
loc: SourceLocation {
start: Position { line: 1, column: 6 },
end: Position { line: 1, column: 30 }
},
range: [ 6, 30 ],
id: Node {
type: 'Identifier',
start: 6,
end: 12,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 6, 12 ],
name: 'unused',
parent: [Circular]
},
init: Node {
type: 'Literal',
start: 15,
end: 30,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 15, 30 ],
value: 'unused string',
raw: "'unused string'",
parent: [Circular]
},
parent: Node {
type: 'VariableDeclaration',
start: 0,
end: 30,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 0, 30 ],
declarations: [ [Circular] ],
kind: 'const',
parent: Node {
type: 'Program',
start: 0,
end: 31,
loc: [SourceLocation],
range: [Array],
body: [Array],
sourceType: 'script',
comments: [],
tokens: [Array],
parent: null
}
}
}
/Users/rikukobayashi/private/eslint-local-rule/src/index.js
1:7 error 'unused' is assigned a value but never used no-unused-vars
✖ 1 problem (1 error, 0 warnings)
無事Node
オブジェクトがコンソール出力されました
ルール実装に戻る
先ほどほったらかしたruleの説明に戻ります。
exportするObjectのcreate
プロパティ(メソッド)で返すObjectでは、keyにSelector
を、valueに実行する関数
を指定します。
Selector
というのが公式docにも述べられている通り、ASTのnodeを指定するための文字列です。CSSセレクタのように各属性を用いて指定できるようですが、大抵はnode typeを用いると思います。
VariableDeclarator
は変数宣言の構文のtypeです。
仮にindex.js
を以下のように修正しても該当するNodeは1つしかないので、コンソール出力されるNodeオブジェクトは1つのみです。
const unused = 'unused string';
function main() {
console.log(unused);
}
main();
コンソール出力
$ npx eslint ./src
Node {
type: 'VariableDeclarator',
start: 6,
end: 30,
loc: SourceLocation {
start: Position { line: 1, column: 6 },
end: Position { line: 1, column: 30 }
},
range: [ 6, 30 ],
id: Node {
type: 'Identifier',
start: 6,
end: 12,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 6, 12 ],
name: 'unused',
parent: [Circular]
},
init: Node {
type: 'Literal',
start: 15,
end: 30,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 15, 30 ],
value: 'unused string',
raw: "'unused string'",
parent: [Circular]
},
parent: Node {
type: 'VariableDeclaration',
start: 0,
end: 31,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 0, 31 ],
declarations: [ [Circular] ],
kind: 'const',
parent: Node {
type: 'Program',
start: 0,
end: 88,
loc: [SourceLocation],
range: [Array],
body: [Array],
sourceType: 'script',
comments: [],
tokens: [Array],
parent: null
}
}
}
一方で、
const unused = 'unused string';
const main = () => {
console.log(unused);
};
main();
のように関数宣言ではなく、関数式を用いた場合、該当Nodeは2つとなりコンソール出力は増えているでしょう。
コンソール出力
$ npx eslint ./src
Node {
type: 'VariableDeclarator',
start: 6,
end: 30,
loc: SourceLocation {
start: Position { line: 1, column: 6 },
end: Position { line: 1, column: 30 }
},
range: [ 6, 30 ],
id: Node {
type: 'Identifier',
start: 6,
end: 12,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 6, 12 ],
name: 'unused',
parent: [Circular]
},
init: Node {
type: 'Literal',
start: 15,
end: 30,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 15, 30 ],
value: 'unused string',
raw: "'unused string'",
parent: [Circular]
},
parent: Node {
type: 'VariableDeclaration',
start: 0,
end: 31,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 0, 31 ],
declarations: [ [Circular] ],
kind: 'const',
parent: Node {
type: 'Program',
start: 0,
end: 88,
loc: [SourceLocation],
range: [Array],
body: [Array],
sourceType: 'script',
comments: [],
tokens: [Array],
parent: null
}
}
}
Node {
type: 'VariableDeclarator',
start: 39,
end: 78,
loc: SourceLocation {
start: Position { line: 3, column: 6 },
end: Position { line: 5, column: 1 }
},
range: [ 39, 78 ],
id: Node {
type: 'Identifier',
start: 39,
end: 43,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 39, 43 ],
name: 'main',
parent: [Circular]
},
init: Node {
type: 'ArrowFunctionExpression',
start: 46,
end: 78,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 46, 78 ],
id: null,
expression: false,
generator: false,
async: false,
params: [],
body: Node {
type: 'BlockStatement',
start: 52,
end: 78,
loc: [SourceLocation],
range: [Array],
body: [Array],
parent: [Circular]
},
parent: [Circular]
},
parent: Node {
type: 'VariableDeclaration',
start: 33,
end: 78,
loc: SourceLocation { start: [Position], end: [Position] },
range: [ 33, 78 ],
declarations: [ [Circular] ],
kind: 'const',
parent: Node {
type: 'Program',
start: 0,
end: 88,
loc: [SourceLocation],
range: [Array],
body: [Array],
sourceType: 'script',
comments: [],
tokens: [Array],
parent: null
}
}
}
さて、先ほど提起した特定の変数名を禁止するというルールですが、例えば具体的にhoge
という言葉を禁止するとします。
その場合は以下のようなruleになるでしょう。
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Ban some keywords from being used as a variable name",
category: "Variables",
},
schema: [], // no options
},
create(context) {
return {
VariableDeclarator: (node) => {
if (node.id.name === "hoge") {
context.report({
node,
message: `The keyword ${node.id.name} is banned to be used as a variable name`,
});
}
},
};
},
};
index.js
を修正して
const unused = 'unused string';
const hoge = () => {
console.log(unused);
}
hoge();
eslintを実行すると
$ npx eslint ./src
/Users/path-to-dir/eslint-local-rule/src/index.js
3:7 error The keyword hoge is banned to be used as a variable name rulesdir/ban-variable-name
✖ 1 problem (1 error, 0 warnings)
無事怒られました
ただ、今のままhoge
というキーワードのみに固定されるのは芸がないので、eslintの設定オプションで指定できるようにしましょう。
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Ban some keywords from being used as a variable name",
category: "Variables",
},
schema: [
{
type: "object",
properties: {
words: {
type: "array",
items: {
type: "string",
},
},
},
additionalProperties: false,
},
],
},
create(context) {
const [option] = context.options;
const bannedWords = option ? option.words : [];
return {
VariableDeclarator: (node) => {
if (bannedWords.includes(node.id.name)) {
context.report({
node,
message: `The keyword ${node.id.name} is banned to be used as a variable name`,
});
}
},
};
},
};
schema
プロパティ
docにある通り、ruleのoptionに制約を付与することができます。json-shemaで定義されているプロパティなら何でも使えるので、好きに定義できます。
ここでは、words
プロパティ名を持つObjectをオプションに指定したので、利用(設定)する場合はこんな感じになります。
module.exports = {
...省略,
rules: {
'rulesdir/ban-variable-name': ['error', { words: ['hoge'] }]
}
};
この設定でeslintを実行してみると
$ npx eslint ./src
/Users/path-to-dir/eslint-local-rule/src/index.js
3:7 error The keyword hoge is banned to be used as a variable name rulesdir/ban-variable-name
✖ 1 problem (1 error, 0 warnings)
OK
ついでに試しにちょっと修正してみます。
const unused = 'unused string';
const hoge = () => {
console.log(unused);
}
hoge();
const fuga = 100;
module.exports = {
...省略,
rules: {
'rulesdir/ban-variable-name': ['error', { words: ['hoge', 'fuga'] }]
}
};
これで再度実行してみると、、
$ npx eslint ./src
/Users/path-to-dir/eslint-local-rule/src/index.js
3:7 error The keyword hoge is banned to be used as a variable name rulesdir/ban-variable-name
9:7 error The keyword fuga is banned to be used as a variable name rulesdir/ban-variable-name
9:7 error 'fuga' is assigned a value but never used no-unused-vars
✖ 3 problems (3 errors, 0 warnings)
ちゃんと正しくオプションが反映されていますね。めでたし
VSCodeとの連携
VSCode user向けの話になります。
上記までで、独自ルールをESLint cliと連携させることはできました。
普段、VSCodeで開発していてeslintを使用している大抵の人は、extensionを使ってリアルタイム(リアクティブ?)でeslintの警告が見れるようにしていると思います。
上記で追加した独自ルールも同様に、(vscodeの設定に手を加えることなく)警告が見れるようになります。
警告が表示されない場合はVSCodeを再起動してみてください。改めてeslint extensionが設定ファイルを読み込んで表示してくれるようになるハズです。
おわり
こんな感じで簡単にカスタムルールを追加できるよっていうのをお話しました。
本来であればちゃんとtestを書きましょうってところで、eslintが提供してくれているRuleTester
を使ったテストのことまで書こうと思いましたが、ちょっと時間的に厳しいのでまたの機会に。。