Skip to content

Commit 65c4015

Browse files
committed
Merge pull request #29 from azu/MyLint
ESLintの実装サンプル
2 parents 4930977 + c3ec702 commit 65c4015

File tree

9 files changed

+168
-2
lines changed

9 files changed

+168
-2
lines changed

Diff for: .md.eslintrc

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

Diff for: SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
* [README](README.md)
44
* [Introduction](ja/introduction/README.md)
55
* [jQuery](ja/jQuery/README.md)
6+
* [ESLint](ja/ESLint/README.md)
67

Diff for: ja/ESLint/README.md

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# ESLint
2+
3+
[ESLint](http://eslint.org/ "ESLint")はJavaScriptのコードをJavaScriptで書かれたルールによって検証するLintツールです。
4+
5+
大まかな動作としては、検証したいJavaScriptのコードをパースしてできたAST(抽象構文木)を、ルールで検証し、エラーや警告を出力します。
6+
7+
このルールがプラグインとして書けるようになっていて、ESLintの全てのルールがプラグインとして実装されています。
8+
9+
> The pluggable linting utility for JavaScript and JSX
10+
11+
ESLintサイト上には、上記のように書かれていることからもわかりますが、プラグインに重きを置いた設計となっているので、
12+
今回はESLintのプラグインアーキテクチャについてを見て行きましょう
13+
14+
## どう書ける?
15+
16+
[import, no-console.js](../../src/ESLint/no-console.js)
17+
18+
## どういう仕組み?
19+
## どういう用途に向いている?
20+
## どういう用途に向いていない?
21+
## この仕組みを使ってるもの
22+
## 実装してみよう
23+
24+
今回は、ESLintのルールを解釈できるシンプルなLintの処理を書いてみます。
25+
26+
利用するルールは先ほども出てきた[no-console.js](#no-console.js)をそのまま使い、
27+
このルールを使って同じようにJavaScriptのコードを検証できる`MyLinter`を書いてみます。
28+
29+
### MyLinter
30+
31+
MyLinterは単純な2つのメソッドを持つクラスとして実装しました。
32+
33+
- `MyLinter#loadRule(rule): void`
34+
- 利用するルールを登録する処理
35+
- `rule`[no-console.js](#no-console.js)がexportしたもの
36+
- `MyLinter#lint(code): string[]`
37+
- `code`を受け取りルールによってLintした結果を返す
38+
- Lint結果はエラーメッセージの配列とする
39+
40+
実装したものが以下のようになっています。
41+
42+
[import, src/ESLint/MyLinter.js](../../src/ESLint/MyLinter.js)
43+
44+
このMyLinterを使って、`MyLinter#load`[no-console.js](#no-console.js)を読み込ませて、
45+
46+
```js
47+
function add(x, y){
48+
console.log(x, y);
49+
return x + y;
50+
}
51+
add(1, 3);
52+
```
53+
54+
というコードをLintしてみます。
55+
56+
[import, src/ESLint/MyLinter-example.js](../../src/ESLint/MyLinter-example.js)
57+
58+
コードには`console`という名前のオブジェクトが含まれているので、 _"Unexpected console statement."_ というエラーメッセージが取得出来ました。
59+
60+
### RuleContext
61+
62+
もう一度、[MyLinter.js](#MyLinter.js)を見てみると、`RuleContext`というシンプルなクラスがあることに気づくと思います。
63+
64+
この`RuleContext`はルールから使えるユーティリティメソッドをまとめたもので、
65+
今回は`RuleContext#report`というエラーメッセージをルールからMyLinterへ通知するものだけを実装しています。
66+
67+
ルールの実装の方を見てみると、直接オブジェクトをexportしてる訳ではなく、
68+
`context` つまり`RuleContext`のインスタンスを受け取っていることが分かると思います。
69+
70+
[import, no-console.js](../../src/ESLint/no-console.js)
71+
72+
このようにして、ルールは `context` という与えられたものだけを使うので、ルールができることを制御しやすくなり、
73+
ルールがMyLinter本体の実装の詳細を知らなくても良くなります。
74+
75+
## エコシステム

Diff for: package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
"eslint": "eslint src/**/*.js",
2525
"eslint:md": "eslint -c .md.eslintrc --ext .md ja/**/*.md",
2626
"textlint": "summary-to-path | xargs textlint --rule spellcheck-tech-word",
27-
"test": "mocha --recursive && npm run textlint && npm run eslint:md && npm run eslint && npm run build"
27+
"test:example": "find ./src -name '*-example.js' | xargs babel-node",
28+
"test": "mocha && npm run test:example && npm run textlint && npm run eslint:md && npm run eslint && npm run build"
2829
},
2930
"keywords": [
3031
"plugin",
3132
"extension"
3233
],
3334
"devDependencies": {
35+
"babel": "^5.8.23",
3436
"eslint": "^1.3.0",
3537
"eslint-plugin-markdown": "git://github.com/eslint/eslint-plugin-markdown.git",
3638
"espower-babel": "^3.3.0",
@@ -44,6 +46,8 @@
4446
"textlint-rule-spellcheck-tech-word": "^4.0.1"
4547
},
4648
"dependencies": {
49+
"esprima": "^2.5.0",
50+
"estraverse": "^4.1.0",
4751
"jquery": "^2.1.4"
4852
}
4953
}

Diff for: src/ESLint/MyLinter-example.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"use strict";
2+
import assert from "assert";
3+
import MyLinter from "./MyLinter";
4+
import noConsole from "./no-console";
5+
6+
let linter = new MyLinter();
7+
linter.loadRule(noConsole);
8+
var code = `
9+
function add(x, y){
10+
console.log(x, y);
11+
return x + y;
12+
}
13+
add(1, 3);
14+
`;
15+
var results = linter.lint(code);
16+
assert(results.length > 0);
17+
assert.equal(results[0], "Unexpected console statement.");

Diff for: src/ESLint/MyLinter.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use strict";
2+
import {parse} from "esprima";
3+
import {traverse} from "estraverse";
4+
import {EventEmitter} from "events";
5+
class RuleContext extends EventEmitter {
6+
report(node, message) {
7+
this.emit("report", message);
8+
}
9+
}
10+
export default class MyLinter {
11+
constructor() {
12+
this._emitter = new EventEmitter();
13+
this._ruleContext = new RuleContext();
14+
}
15+
16+
loadRule(rule) {
17+
var ruleExports = rule(this._ruleContext);
18+
// on(nodeType, nodeTypeCallback);
19+
Object.keys(ruleExports).forEach(nodeType => {
20+
this._emitter.on(nodeType, ruleExports[nodeType]);
21+
});
22+
}
23+
24+
25+
lint(code) {
26+
var messages = [];
27+
var addMessage = (message)=> {
28+
messages.push(message);
29+
};
30+
this._ruleContext.on("report", addMessage);
31+
var ast = parse(code);
32+
traverse(ast, {
33+
enter: (node) => {
34+
this._emitter.emit(node.type, node);
35+
},
36+
leave: (node) => {
37+
this._emitter.emit(`${node.type}:exit`, node);
38+
}
39+
});
40+
this._ruleContext.removeListener("report", addMessage);
41+
return messages;
42+
}
43+
}

Diff for: src/ESLint/no-console.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use strict";
2+
module.exports = function (context) {
3+
return {
4+
"MemberExpression": function (node) {
5+
if (node.object.name === "console") {
6+
context.report(node, "Unexpected console statement.");
7+
}
8+
}
9+
};
10+
};

Diff for: test/ESLint/MyLinter-test.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// LICENSE : MIT
2+
"use strict";
3+
import assert from "power-assert";
4+
import MyLinter from "../../src/ESLint/MyLinter";
5+
import noConsole from "../../src/ESLint/no-console";
6+
describe("MyLint", function () {
7+
it("should load and lint", function () {
8+
let linter = new MyLinter();
9+
linter.loadRule(noConsole);
10+
var results = linter.lint(`console.log("test")`);
11+
assert(results.length > 0);
12+
assert.equal(results[0], "Unexpected console statement.");
13+
});
14+
});

Diff for: test/mocha.opts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
--recursive
12
--compilers js:espower-babel/guess

0 commit comments

Comments
 (0)