Skip to content

ESLintの実装サンプル #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Sep 1, 2015
3 changes: 2 additions & 1 deletion .md.eslintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"rules": {
"no-undef": 0
"no-undef": 0,
"no-console": 0
},
"plugins": [
"markdown"
Expand Down
1 change: 1 addition & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
* [README](README.md)
* [Introduction](ja/introduction/README.md)
* [jQuery](ja/jQuery/README.md)
* [ESLint](ja/ESLint/README.md)

75 changes: 75 additions & 0 deletions ja/ESLint/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# ESLint

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

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

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

> The pluggable linting utility for JavaScript and JSX

ESLintサイト上には、上記のように書かれていることからもわかりますが、プラグインに重きを置いた設計となっているので、
今回はESLintのプラグインアーキテクチャについてを見て行きましょう

## どう書ける?

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

## どういう仕組み?
## どういう用途に向いている?
## どういう用途に向いていない?
## この仕組みを使ってるもの
## 実装してみよう

今回は、ESLintのルールを解釈できるシンプルなLintの処理を書いてみます。

利用するルールは先ほども出てきた[no-console.js](#no-console.js)をそのまま使い、
このルールを使って同じようにJavaScriptのコードを検証できる`MyLinter`を書いてみます。

### MyLinter

MyLinterは単純な2つのメソッドを持つクラスとして実装しました。

- `MyLinter#loadRule(rule): void`
- 利用するルールを登録する処理
- `rule`は[no-console.js](#no-console.js)がexportしたもの
- `MyLinter#lint(code): string[]`
- `code`を受け取りルールによってLintした結果を返す
- Lint結果はエラーメッセージの配列とする

実装したものが以下のようになっています。

[import, src/ESLint/MyLinter.js](../../src/ESLint/MyLinter.js)

このMyLinterを使って、`MyLinter#load`で[no-console.js](#no-console.js)を読み込ませて、

```js
function add(x, y){
console.log(x, y);
return x + y;
}
add(1, 3);
```

というコードをLintしてみます。

[import, src/ESLint/MyLinter-example.js](../../src/ESLint/MyLinter-example.js)

コードには`console`という名前のオブジェクトが含まれているので、 _"Unexpected console statement."_ というエラーメッセージが取得出来ました。

### RuleContext

もう一度、[MyLinter.js](#MyLinter.js)を見てみると、`RuleContext`というシンプルなクラスがあることに気づくと思います。

この`RuleContext`はルールから使えるユーティリティメソッドをまとめたもので、
今回は`RuleContext#report`というエラーメッセージをルールからMyLinterへ通知するものだけを実装しています。

ルールの実装の方を見てみると、直接オブジェクトをexportしてる訳ではなく、
`context` つまり`RuleContext`のインスタンスを受け取っていることが分かると思います。

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

このようにして、ルールは `context` という与えられたものだけを使うので、ルールができることを制御しやすくなり、
ルールがMyLinter本体の実装の詳細を知らなくても良くなります。

## エコシステム
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
"eslint": "eslint src/**/*.js",
"eslint:md": "eslint -c .md.eslintrc --ext .md ja/**/*.md",
"textlint": "summary-to-path | xargs textlint --rule spellcheck-tech-word",
"test": "mocha --recursive && npm run textlint && npm run eslint:md && npm run eslint && npm run build"
"test:example": "find ./src -name '*-example.js' | xargs babel-node",
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*-example.jsを実行するテストを追加

"test": "mocha && npm run test:example && npm run textlint && npm run eslint:md && npm run eslint && npm run build"
},
"keywords": [
"plugin",
"extension"
],
"devDependencies": {
"babel": "^5.8.23",
"eslint": "^1.3.0",
"eslint-plugin-markdown": "git://github.com/eslint/eslint-plugin-markdown.git",
"espower-babel": "^3.3.0",
Expand All @@ -44,6 +46,8 @@
"textlint-rule-spellcheck-tech-word": "^4.0.1"
},
"dependencies": {
"esprima": "^2.5.0",
"estraverse": "^4.1.0",
"jquery": "^2.1.4"
}
}
17 changes: 17 additions & 0 deletions src/ESLint/MyLinter-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use strict";
import assert from "assert";
import MyLinter from "./MyLinter";
import noConsole from "./no-console";

let linter = new MyLinter();
linter.loadRule(noConsole);
var code = `
function add(x, y){
console.log(x, y);
return x + y;
}
add(1, 3);
`;
var results = linter.lint(code);
assert(results.length > 0);
assert.equal(results[0], "Unexpected console statement.");
43 changes: 43 additions & 0 deletions src/ESLint/MyLinter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use strict";
import {parse} from "esprima";
import {traverse} from "estraverse";
import {EventEmitter} from "events";
class RuleContext extends EventEmitter {
report(node, message) {
this.emit("report", message);
}
}
export default class MyLinter {
constructor() {
this._emitter = new EventEmitter();
this._ruleContext = new RuleContext();
}

loadRule(rule) {
var ruleExports = rule(this._ruleContext);
// on(nodeType, nodeTypeCallback);
Object.keys(ruleExports).forEach(nodeType => {
this._emitter.on(nodeType, ruleExports[nodeType]);
});
}


lint(code) {
var messages = [];
var addMessage = (message)=> {
messages.push(message);
};
this._ruleContext.on("report", addMessage);
var ast = parse(code);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これパースしてからthis._ruleContext.on("report", addMessage);した方がよさそう。
面倒だからエラーハンドリングしてないけど、エラーが起きると浮遊するイベントができる

traverse(ast, {
enter: (node) => {
this._emitter.emit(node.type, node);
},
leave: (node) => {
this._emitter.emit(`${node.type}:exit`, node);
}
});
this._ruleContext.removeListener("report", addMessage);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

クラスにstate持つほどでもないと思ったので、lintし終わった開放してる。

return messages;
}
}
10 changes: 10 additions & 0 deletions src/ESLint/no-console.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use strict";
module.exports = function (context) {
return {
"MemberExpression": function (node) {
if (node.object.name === "console") {
context.report(node, "Unexpected console statement.");
}
}
};
};
14 changes: 14 additions & 0 deletions test/ESLint/MyLinter-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// LICENSE : MIT
"use strict";
import assert from "power-assert";
import MyLinter from "../../src/ESLint/MyLinter";
import noConsole from "../../src/ESLint/no-console";
describe("MyLint", function () {
it("should load and lint", function () {
let linter = new MyLinter();
linter.loadRule(noConsole);
var results = linter.lint(`console.log("test")`);
assert(results.length > 0);
assert.equal(results[0], "Unexpected console statement.");
});
});
1 change: 1 addition & 0 deletions test/mocha.opts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
--recursive
--compilers js:espower-babel/guess