diff --git a/.md.eslintrc b/.md.eslintrc index 1f24738..9589990 100644 --- a/.md.eslintrc +++ b/.md.eslintrc @@ -1,6 +1,7 @@ { "rules": { - "no-undef": 0 + "no-undef": 0, + "no-console": 0 }, "plugins": [ "markdown" diff --git a/SUMMARY.md b/SUMMARY.md index 6920d4b..e3a599e 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -3,4 +3,5 @@ * [README](README.md) * [Introduction](ja/introduction/README.md) * [jQuery](ja/jQuery/README.md) +* [ESLint](ja/ESLint/README.md) diff --git a/ja/ESLint/README.md b/ja/ESLint/README.md new file mode 100644 index 0000000..f935816 --- /dev/null +++ b/ja/ESLint/README.md @@ -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本体の実装の詳細を知らなくても良くなります。 + +## エコシステム \ No newline at end of file diff --git a/package.json b/package.json index 3a35dae..3dcf308 100644 --- a/package.json +++ b/package.json @@ -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", + "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", @@ -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" } } diff --git a/src/ESLint/MyLinter-example.js b/src/ESLint/MyLinter-example.js new file mode 100644 index 0000000..42754e5 --- /dev/null +++ b/src/ESLint/MyLinter-example.js @@ -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."); \ No newline at end of file diff --git a/src/ESLint/MyLinter.js b/src/ESLint/MyLinter.js new file mode 100644 index 0000000..92587a8 --- /dev/null +++ b/src/ESLint/MyLinter.js @@ -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); + traverse(ast, { + enter: (node) => { + this._emitter.emit(node.type, node); + }, + leave: (node) => { + this._emitter.emit(`${node.type}:exit`, node); + } + }); + this._ruleContext.removeListener("report", addMessage); + return messages; + } +} \ No newline at end of file diff --git a/src/ESLint/no-console.js b/src/ESLint/no-console.js new file mode 100644 index 0000000..495680c --- /dev/null +++ b/src/ESLint/no-console.js @@ -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."); + } + } + }; +}; \ No newline at end of file diff --git a/test/ESLint/MyLinter-test.js b/test/ESLint/MyLinter-test.js new file mode 100644 index 0000000..0adee75 --- /dev/null +++ b/test/ESLint/MyLinter-test.js @@ -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."); + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts index 8d0282d..f83e8df 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1 +1,2 @@ +--recursive --compilers js:espower-babel/guess \ No newline at end of file