Skip to content

Commit 196257c

Browse files
committed
Merge pull request #31 from azu/how-to-eslint
feat(ESLint):どう書ける?
2 parents 243f669 + de4bfaf commit 196257c

File tree

4 files changed

+232
-7
lines changed

4 files changed

+232
-7
lines changed

Diff for: .md.eslintrc

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"rules": {
33
"no-undef": 0,
4+
"no-unused-vars": 0,
45
"no-console": 0
56
},
67
"plugins": [

Diff for: ja/ESLint/README.md

+231-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[ESLint](http://eslint.org/ "ESLint")はJavaScriptのコードをJavaScriptで書かれたルールによって検証するLintツールです。
44

5-
大まかな動作としては、検証したいJavaScriptのコードをパースしてできたAST(抽象構文木)を、ルールで検証し、エラーや警告を出力します。
5+
大まかな動作としては、検証したいJavaScriptのコードをパースしてできたAST(抽象構文木)をルールで検証し、エラーや警告を出力します。
66

77
このルールがプラグインとして書けるようになっていて、ESLintの全てのルールがプラグインとして実装されています。
88

@@ -13,12 +13,179 @@ ESLintサイト上には、上記のように書かれていることからも
1313

1414
## どう書ける?
1515

16+
ESLintでは`.eslintrc`という設定ファイルに利用するルールの設定をして使うため、
17+
実行方法についてはドキュメントを参照して下さい。
18+
19+
- [Documentation - ESLint - Pluggable JavaScript linter](http://eslint.org/docs/user-guide/configuring "Documentation - ESLint - Pluggable JavaScript linter")
20+
21+
ESLintにおけるルールとは、以下のような一つのオブジェクトを返す関数をexportしたモジュールのことを言います。
22+
1623
[import, no-console.js](../../src/ESLint/no-console.js)
1724

25+
ESLintではコードを文字列ではなくASTを元にしてチェックしていきます。
26+
ASTについてはここでは詳細を省きますが、コードをJavaScriptのオブジェクトで表現した木構造のデータだと思えば問題ないと思います。
27+
28+
例えば、
29+
30+
```js
31+
console.log("Hello!");
32+
```
33+
34+
というコードをパースしてASTにすると以下のようなオブジェクトとして取得できます。
35+
36+
```json
37+
{
38+
"type": "Program",
39+
"body": [
40+
{
41+
"type": "ExpressionStatement",
42+
"expression": {
43+
"type": "CallExpression",
44+
"callee": {
45+
"type": "MemberExpression",
46+
"computed": false,
47+
"object": {
48+
"type": "Identifier",
49+
"name": "console"
50+
},
51+
"property": {
52+
"type": "Identifier",
53+
"name": "log"
54+
}
55+
},
56+
"arguments": [
57+
{
58+
"type": "Literal",
59+
"value": "Hello!",
60+
"raw": "\"Hello!\""
61+
}
62+
]
63+
}
64+
}
65+
],
66+
"sourceType": "script"
67+
}
68+
```
69+
70+
- [JavaScript AST explorer](http://felix-kling.de/esprima_ast_explorer/#/FNrLHi8ngW "JavaScript AST explorer")
71+
72+
ESLintではこのASTを使って、変数が未使用であるとか[no-console.js](#no-console.js)のように`console.log`などがコードに残ってないか
73+
といったことをルールを元にチェックすることができます。
74+
75+
ルールをどう書けるかという話に戻すと、`context`というオブジェクトはただのユーティリティ関数と思ってもらって問題なくて、
76+
returnしてるメソッドをもったオブジェクトがルールの本体と言えます。
77+
78+
ESLintではルールをどうやって使っているかというと、ASTは木構造のとなってるので、
79+
そのASTを深さ優先で探索していきながらそれぞれ登録したルールに対して、
80+
「今`"MemberExpression"` typeのNodeに到達した」といったことを通知することを繰り返しています。
81+
82+
先ほどの`console.log`のASTにおける`MemberExpression` typeのNodeとは以下のオブジェクトをことを言います。
83+
84+
```json
85+
{
86+
"type": "MemberExpression",
87+
"computed": false,
88+
"object": {
89+
"type": "Identifier",
90+
"name": "console"
91+
},
92+
"property": {
93+
"type": "Identifier",
94+
"name": "log"
95+
}
96+
}
97+
```
98+
99+
[no-console.js](#no-console.js)のルールを見ると`MemberExpression` typeのNodeが `node.object.name === "console"` であるなら
100+
`console`が残ってると判断してエラーレポートすると読めてくると思います。
101+
102+
ASTの探索がイメージしにくい場合は以下のルールで探索の動作を見てみると分かりやすいかもしれません。
103+
104+
- [azu.github.io/visualize_estraverse/](http://azu.github.io/visualize_estraverse/ "visualize estraverse step")
105+
106+
```js
107+
function debug(string){
108+
console.log(string);
109+
}
110+
debug("Hello");
111+
```
112+
113+
<video controls>
114+
<source src='./movie/traverse.webm' type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
115+
<source src='./movie/traverse.mp4' type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
116+
<p>動画を再生するには、webmまたはmp4をサポートしたブラウザが必要です。</p>
117+
</video>
118+
119+
その他、ESLintのルールの書き方についてはドキュメントや以下の記事を見てみるといいでしょう。
120+
121+
- [Documentation - ESLint - Pluggable JavaScript linter](http://eslint.org/docs/developer-guide/working-with-rules "Documentation - ESLint - Pluggable JavaScript linter")
122+
- [コードのバグはコードで見つけよう!|サイバーエージェント 公式エンジニアブログ](http://ameblo.jp/principia-ca/entry-11837554210.html "コードのバグはコードで見つけよう!|サイバーエージェント 公式エンジニアブログ")
123+
18124
## どういう仕組み?
19-
## どういう用途に向いている?
20-
## どういう用途に向いていない?
21-
## この仕組みを使ってるもの
125+
126+
ESLintはコードをパースしてASTにして、そのASTをJavaScriptで書いたルールでチェックしてレポートする
127+
という大まかな仕組みは分かりました。
128+
129+
次に、このルールをプラグインとする仕組みがどのようにして動いているのか見て行きましょう。
130+
131+
ESLintのLintは次のような3つの手順で行われています。
132+
133+
1. ルール毎に使っている`Node.type`をイベント登録する
134+
2. ASTをtraverseしながら、`Node.type`のイベントを発火する
135+
3. ルールから`context.report()`された内容を集めて表示する
136+
137+
このイベントの登録と発火にはEventEmitterを使っていて、
138+
ESLint本体に対してルールは複数あるので、典型的なPub/Subパターンとなっています。
139+
140+
擬似的なコードで表現すると以下のような流れでLintの処理が行われています。
141+
142+
```js
143+
import {parse} from "esprima";
144+
import {traverse} from "estraverse";
145+
import {EventEmitter} from "events";
146+
147+
function lint(code){
148+
// コードをパースしてASTにする
149+
var ast = parse(code);
150+
// イベントの登録場所
151+
var emitter = new EventEmitter();
152+
var results = [];
153+
emitter.on("report", message => {
154+
// 3. のためのreportされた内容を集める
155+
results.push(message);
156+
});
157+
// 利用するルール一覧
158+
var ruleList = getAllRules();
159+
// 1. ルール毎に使っている`Node.type`をイベント登録する
160+
ruleList.forEach(rule => {
161+
// それぞれのルールに定義されているメソッド一覧を取得
162+
// e.g) MemberExpression(node){}
163+
// => {"MemberExpression" : function(node){}, ... } というオブジェクト
164+
var methodObject = getDefinedMethod(rule);
165+
Object.keys(methodObject).forEach(nodeType => {
166+
emitter.on(nodeType, methodList[nodeType]);
167+
});
168+
});
169+
// 2. ASTをtraverseしながら、`Node.type`のイベントを発火する
170+
traverse(ast, {
171+
// 1.で登録したNode.typeがあるならここで呼ばれる
172+
enter: (node) => {
173+
emitter.emit(node.type, node);
174+
},
175+
leave: (node) => {
176+
emitter.emit(`${node.type}:exit`, node);
177+
}
178+
});
179+
// 3. ルールから`context.report()`された内容を集めて表示する
180+
console.log(results.join("\n"));
181+
}
182+
```
183+
184+
Pub/Subパターンを上手く使うことで、ASTをtraverseするのが一巡のみでそれぞれのルールに対して
185+
どういうコードであるかという情報が`emit`で通知できていることがわかります。
186+
187+
もう少し具体的にするため、実装して動かせるようなものを作ってこの仕組みについて見ていきます。
188+
22189
## 実装してみよう
23190

24191
今回は、ESLintのルールを解釈できるシンプルなLintの処理を書いてみます。
@@ -69,7 +236,64 @@ add(1, 3);
69236

70237
[import, no-console.js](../../src/ESLint/no-console.js)
71238

72-
このようにして、ルールは `context` という与えられたものだけを使うので、ルールができることを制御しやすくなり、
73-
ルールがMyLinter本体の実装の詳細を知らなくても良くなります。
239+
このようにして、ルールは `context` という与えられたものだけを使うので、ルールがMyLinter本体の実装の詳細を知らなくても良くなります。
240+
241+
## どういう用途に向いている?
242+
243+
このプラグインアーキテクチャはPub/Subパターンを上手くつかっていて、
244+
ESLintのように与えられたコードを読み取ってチェックするような使い方に向いています。
245+
246+
つまり、read-onlyなプラグインのアーキテクチャとしてはパフォーマンスも期待できると思います。
247+
248+
また、ルールは `context` という与えられたものだけを使うようになっているため、
249+
ルールと本体を密結合にはなりにくいです。
250+
また`context`に何を与えるかを決める事で、ルールが行える範囲を制御しやすいと言えます。
251+
252+
## どういう用途に向いていない?
253+
254+
逆に与えられたコード(AST)を書き換えするようなことをする場合には、
255+
ルールを同時に処理を行うためルール間で競合するような変更がある場合に破綻してしまいます。
256+
257+
そのため、この仕組みに加えてもう一つ抽象レイヤーを設けないと対応は難しいと思います。
258+
259+
つまり、read-writeなプラグインのアーキテクチャとしては単純にこのパターンだけでは難しい部分が出てくるでしょう。
260+
261+
> **NOTE** ESLint 2.0でautofixing、つまり書き換えの機能の導入が予定されています。
262+
> これは`SourceCode`抽象オブジェクトをルールに、書き換えのコマンドを蓄積して最後に実際に書き換えを行うという抽象レイヤーを設けています。
263+
> - [Implement autofixing · Issue #3134 · eslint/eslint](https://github.com/eslint/eslint/issues/3134 "Implement autofixing · Issue #3134 · eslint/eslint")
264+
265+
## この仕組みを使ってるもの
266+
267+
- [azu/textlint](https://github.com/azu/textlint "azu/textlint")
268+
- テキストやMarkdownをパースしてASTにしてLintするツール
269+
270+
## エコシステム
271+
272+
ESLintのルールはただのJavaScriptファイルであり、またESLintは主に開発時に使うツールとなっています。
273+
274+
ルール自体を[npm](https://www.npmjs.com/ "npm")で公開したり、ルールや設定をまとめたものをESLintでは"Plugin"と呼び、
275+
こちらもnpmで公開して利用するのが一般的な使い方になっています。
276+
277+
また、ESLintはデフォルトで有効なルールがないので、設定ファイルを作るか、
278+
[sindresorhus/xo](https://github.com/sindresorhus/xo "sindresorhus/xo")といったESLintのラッパーを利用する形となります。
279+
280+
ESLint公式の設定として`eslint:recommended`が用意されていて、これを`extends`することで推奨の設定を継承できます。
281+
282+
```json
283+
{
284+
"extends": "eslint:recommended"
285+
}
286+
```
287+
288+
これらの設定自体もJavaScriptで表現できるため、設定もnpmで公開して利用できるようになっています。
289+
290+
- [Shareable Configs - ESLint - Pluggable JavaScript linter](http://eslint.org/docs/developer-guide/shareable-configs "Documentation - ESLint - Pluggable JavaScript linter")
291+
292+
これはコーディングルールが多種多様なように、ESLintで必要なルールも個人差があるので、
293+
柔軟に対応できるようにするためでもあり、_The pluggable linting utility_を表現している仕組みとなってます。
294+
295+
このように、ESLintルールや設定といった殆どの部分をJavaScriptのモジュールの仕組みで利用できるようにし、
296+
それらを[npm](https://www.npmjs.com/ "npm")を使って公開しやすいような形を取っています。
74297

75-
## エコシステム
298+
設定なしで使えるのが一番楽ですが、そこが現実として難しいため、
299+
柔軟な設定のしくみと設定を共有しやすい形を持っていると言えます。

Diff for: ja/ESLint/movie/traverse.mp4

325 KB
Binary file not shown.

Diff for: ja/ESLint/movie/traverse.webm

122 KB
Binary file not shown.

0 commit comments

Comments
 (0)