├── .github └── workflows │ └── build.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.yml ├── LICENSE ├── README.md ├── guide ├── 10_your_first_rule │ ├── README.ja.md │ └── README.md ├── 20_dive_into_ast │ ├── README.ja.md │ ├── README.md │ ├── ast_diagram.png │ ├── astexplorer.png │ └── esquery.png ├── 30_other_parsers │ ├── README.ja.md │ ├── README.md │ └── switch_parser.png ├── README.ja.md └── README.md ├── package.json ├── renovate.json ├── src ├── index.ts └── rules │ ├── no-function-apply.test.ts │ ├── no-function-apply.ts │ ├── no-jsx-button.test.ts │ ├── no-jsx-button.ts │ ├── no-literal.test.ts │ └── no-literal.ts ├── tsconfig.json └── yarn.lock /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [16.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: Install dependencies 20 | run: yarn --frozen-lockfile 21 | - name: Format check 22 | run: yarn format:check 23 | - name: Unit Testing 24 | run: yarn test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | .env.test 65 | 66 | # parcel-bundler cache (https://parceljs.org/) 67 | .cache 68 | 69 | # next.js build output 70 | .next 71 | 72 | # nuxt.js build output 73 | .nuxt 74 | 75 | # vuepress build output 76 | .vuepress/dist 77 | 78 | # Serverless directories 79 | .serverless/ 80 | 81 | # FuseBox cache 82 | .fusebox/ 83 | 84 | # DynamoDB Local files 85 | .dynamodb/ 86 | 87 | # End of https://www.gitignore.io/api/node 88 | 89 | lib/ 90 | .DS_Store 91 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | __snapshots__/ 3 | coverage/ 4 | node_modules/ 5 | *.png 6 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: all 2 | tabWidth: 2 3 | semi: true 4 | singleQuote: false 5 | bracketSpacing: true 6 | printWidth: 120 7 | arrowParens: avoid 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2019 Quramy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESLint plugin tutorial 2 | 3 | ## What's this? 4 | 5 | This is an example repository to explain how to create your ESLint rules. 6 | 7 | ## Why should we learn how to create custom ESLint rules? 8 | 9 | Lint rules help to keep our codes' quality constant. Automatic code checking brings time for more productive activities, and also eliminates indivisual effects from code review. 10 | 11 | Creating ESLint rules is a good subject to learn AST(Abstract Syntax Tree) analysis. Today, analysis of AST is the foundation of the JavaScript build ecosystem. There are many libraries using AST, such as Babel plugins, custom TypeScript transformers, prettier, webpack and so on. Your team's JavaScript gets improved significantly if you can control AST freely! 12 | 13 | ## Tutorial 14 | 15 | [See guides](./guide/README.md). 16 | 17 | ## Getting started 18 | 19 | This repository is also designed to work as a project template for custom ESLint rules. 20 | 21 | If you want to start quickly, follow the procedure below: 22 | 23 | - Clone this repository 24 | - Remove `.git` and `guide` dirs 25 | - Change pkg name via edit `package.json` 26 | - Change and test rule codes under `src/rules` dir 27 | 28 | This repository includes: 29 | 30 | - TypeScript setting 31 | - Jest 32 | - GitHub actions configuration 33 | 34 | ## LICENSE 35 | 36 | MIT 37 | -------------------------------------------------------------------------------- /guide/10_your_first_rule/README.ja.md: -------------------------------------------------------------------------------- 1 | # Your first rule 2 | 3 | 本章では ESLint プラグインの作成方法を学びます。 4 | 5 | ## Create a rule module 6 | 7 | まず`src/rules/no-literal.ts` という名前で新しいファイルを作成して以下のように編集してください。 8 | 9 | ```ts 10 | import { Rule } from "eslint"; 11 | 12 | const rule: Rule.RuleModule = { 13 | create: context => { 14 | return { 15 | Literal: node => { 16 | context.report({ 17 | message: "😿", 18 | node, 19 | }); 20 | }, 21 | }; 22 | }, 23 | }; 24 | 25 | export = rule; 26 | ``` 27 | 28 | おめでとうございます!はじめての ESLint ルールができました。 29 | 30 | これは非常にバカバカしいルールです。何かしらのリテラル(例: `1`, `'hoge'`, ...)を見つけると泣いている猫の絵文字を出力するルールです。 31 | しかし、このルールは様々なことを教えてくれます。 32 | 33 | - ESLint ルールは `RuleModule` インターフェイスを実装する必要がある 34 | - ルールは `create` 関数をもち、この関数の引数は `context` である 35 | - `create` 関数はオブジェクトを返却しなくてはならない 36 | - このオブジェクトのキーは私達の興味がある AST ノードの種類を表している(AST ノードタイプとキーの関係は後ほど学んでいきます :smile: ) 37 | - その値は関数であり、エラーメッセージがこの関数で出力される 38 | 39 | ## Test the rule 40 | 41 | つづいて、このルールが動作することをテストしてみましょう。 42 | 43 | `src/rules/no-literal.test.ts` という名前でファイルをもう 1 つ作成して編集しましょう: 44 | 45 | ```ts 46 | import { RuleTester } from "eslint"; 47 | 48 | import rule from "./no-literal"; 49 | 50 | const tester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } }); 51 | 52 | tester.run("no-literal", rule, { 53 | valid: [{ code: `let x` }], 54 | invalid: [ 55 | { 56 | code: `const x = 1;`, 57 | errors: [{ message: "😿" }], 58 | }, 59 | ], 60 | }); 61 | ``` 62 | 63 | 以下によりこのテストコードを走らせます: 64 | 65 | ```sh 66 | $ npm test 67 | ``` 68 | 69 | 次のようなコンソール出力が得られましたか? 70 | 71 | ```text 72 | PASS src/rules/no-literal.test.ts 73 | no-literal 74 | valid 75 | ✓ let x (29ms) 76 | invalid 77 | ✓ const x = 1; (5ms) 78 | 79 | Test Suites: 1 passed, 1 total 80 | Tests: 2 passed, 2 total 81 | Snapshots: 0 total 82 | Time: 1.506s, estimated 2s 83 | ``` 84 | 85 | The test code tests 2 assertions: 86 | 87 | このコードは 2 つのアサーションをおこなっています。 88 | 89 | 1. 正しいコードが与えられた場合、ルールが何もエラーを出力しない 90 | 1. 間違ったコードが与えられた場合、 ルールがエラーを出力する(:crying_cat_face:) 91 | 92 | ## Create plugin 93 | 94 | それでは、ルールを ESLint プラグインとして配布する準備をしましょう。 95 | 96 | プラグインはルールモジュールの名前を ESLint に伝えるための index ファイルを必要とします。 97 | 98 | `src/index.ts` を作成して次のように編集してください: 99 | 100 | ```ts 101 | import noLiteral from "./rules/no-literal"; 102 | 103 | export = { 104 | rules: { 105 | "no-literal": noLiteral, 106 | }, 107 | }; 108 | ``` 109 | 110 | `npm publish` を実行する前に、初めてのプラグインが NPM プロジェクトの中で動作することを確認します。 111 | 112 | 次のコマンドを eslint-plugin-tutorial ディレクトリ以下で実行してください(`npm` の代わりに `yarn` を使ってもかまいません)。 113 | 114 | ```sh 115 | $ npm link 116 | ``` 117 | 118 | これで、このパッケージを npm コマンドでインストールできます。 119 | 120 | 次に、プラグインを利用するサンプルプロジェクトを作成してください。 121 | 122 | ```sh 123 | $ cd .. 124 | $ mkdir example-prj 125 | $ cd example-prj 126 | $ npm init -f 127 | $ npm i eslint --dev 128 | ``` 129 | 130 | サンプルプロジェクトへプラグインを追加します。 131 | 132 | ```sh 133 | $ npm link @quramy/eslint-plugin-tutorial 134 | ``` 135 | 136 | 最後に .eslintrc を作成してプラグインを利用するように設定してください。 137 | 138 | ```json 139 | { 140 | "plugins": ["@quramy/tutorial"], 141 | "parserOptions": { 142 | "ecmaVersion": 2015 143 | }, 144 | "rules": { 145 | "@quramy/tutorial/no-literal": 2 146 | } 147 | } 148 | ``` 149 | 150 | ESLint プラグインパッケージは "eslint-plugin"というプレフィクスから始める必要があります。 151 | 今回は、プラグインパッケージは "@quramy/eslint-plugin-tutorial" という名前ですから、ESLint はこの命名規約によって "@quramy/tutorial" という名前で認識します。 152 | 153 | さあ、それでは実行してみましょう! 154 | 155 | ```sh 156 | $ echo "const x = 1;" | npx eslint --stdin 157 | ``` 158 | 159 | 次の出力が得られましたか? 160 | 161 | ```text 162 | 163 | 1:11 error 😿 @quramy/tutorial/no-literal 164 | 165 | ✖ 1 problem (1 error, 0 warnings) 166 | ``` 167 | 168 | ## Summary 169 | 170 | - ESLint ルールを作成するには、 `Rule.RuleModule` を実装する 171 | - ESLint プラグイン NPM パッケージには "eslint-plugin" プレフィクスが必要である 172 | 173 | [Next](../20_dive_into_ast/README.ja.md) 174 | -------------------------------------------------------------------------------- /guide/10_your_first_rule/README.md: -------------------------------------------------------------------------------- 1 | # Your first rule 2 | 3 | In this chapter, let's learn to how create ESLint plugin. 4 | 5 | ## Create a rule module 6 | 7 | First of all, put a new file as `src/rules/no-literal.ts` and edit it as the following: 8 | 9 | ```ts 10 | import { Rule } from "eslint"; 11 | 12 | const rule: Rule.RuleModule = { 13 | create: context => { 14 | return { 15 | Literal: node => { 16 | context.report({ 17 | message: "😿", 18 | node, 19 | }); 20 | }, 21 | }; 22 | }, 23 | }; 24 | 25 | export = rule; 26 | ``` 27 | 28 | Congrats! This is your first ESLint rule! 29 | 30 | This is a very silly rule, which says crying cat emoji when it finds some literals(e.g. `1`, `'hoge'`, ...). 31 | However, it tells us various things. 32 | 33 | - ESLint rule should implement `RuleModule` interface 34 | - It should have `create` function which has an argument, `context` 35 | - `create` method should return an object 36 | - This object's keys represents AST node type which we are interested in (We learn the relation between AST node type and the keys later :smile:) 37 | - It's value is a function and an error message is thrown in this function 38 | 39 | ## Test the rule 40 | 41 | Next, let's test that this rule works. 42 | 43 | Put another file, `src/rules/no-literal.test.ts` and edit: 44 | 45 | ```ts 46 | import { RuleTester } from "eslint"; 47 | 48 | import rule from "./no-literal"; 49 | 50 | const tester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } }); 51 | 52 | tester.run("no-literal", rule, { 53 | valid: [{ code: `let x` }], 54 | invalid: [ 55 | { 56 | code: `const x = 1;`, 57 | errors: [{ message: "😿" }], 58 | }, 59 | ], 60 | }); 61 | ``` 62 | 63 | And run this test code via: 64 | 65 | ```sh 66 | $ npm test 67 | ``` 68 | 69 | Can you see the following console output? 70 | 71 | ```text 72 | PASS src/rules/no-literal.test.ts 73 | no-literal 74 | valid 75 | ✓ let x (29ms) 76 | invalid 77 | ✓ const x = 1; (5ms) 78 | 79 | Test Suites: 1 passed, 1 total 80 | Tests: 2 passed, 2 total 81 | Snapshots: 0 total 82 | Time: 1.506s, estimated 2s 83 | ``` 84 | 85 | This code tests 2 assertions: 86 | 87 | 1. If given a valid source code, your rule says nothing 88 | 1. If given an invalid source code, your rules reports an error message(:crying_cat_face:) 89 | 90 | ## Create plugin 91 | 92 | So, let's prepare to publish our rule as an ESLint plugin. 93 | 94 | A plugin needs an index file which tells the name of the rule module to ESLint 95 | 96 | Put `src/index.ts` and edit as the following: 97 | 98 | ```ts 99 | import noLiteral from "./rules/no-literal"; 100 | 101 | export = { 102 | rules: { 103 | "no-literal": noLiteral, 104 | }, 105 | }; 106 | ``` 107 | 108 | Before executing `npm publish`, confirm our first plugin works within an NPM project. 109 | 110 | Execute the following command under eslint-plugin-tutorial directory(Use `yarn` instead of `npm` if you prefer): 111 | 112 | ```sh 113 | $ npm link 114 | ``` 115 | 116 | Then we can install this package via npm command. 117 | 118 | Next, create a sample project to use our plugin. 119 | 120 | ```sh 121 | $ cd .. 122 | $ mkdir example-prj 123 | $ cd example-prj 124 | $ npm init -f 125 | $ npm i eslint --dev 126 | ``` 127 | 128 | We add our plugin into the sample project: 129 | 130 | ```sh 131 | $ npm link @quramy/eslint-plugin-tutorial 132 | ``` 133 | 134 | Finally, put .eslintrc and configure to use our plugin. 135 | 136 | ```json 137 | { 138 | "plugins": ["@quramy/tutorial"], 139 | "parserOptions": { 140 | "ecmaVersion": 2015 141 | }, 142 | "rules": { 143 | "@quramy/tutorial/no-literal": 2 144 | } 145 | } 146 | ``` 147 | 148 | An ESLint plugin package should have "eslint-plugin" prefix. 149 | Now, our plugin's package is named as "@quramy/eslint-plugin-tutorial" so ESLint recognises it as "@quramy/tutorial" using this naming convention. 150 | 151 | Ok come on, run it! 152 | 153 | ```sh 154 | $ echo "const x = 1;" | npx eslint --stdin 155 | ``` 156 | 157 | Can you see the following ? 158 | 159 | ```text 160 | 161 | 1:11 error 😿 @quramy/tutorial/no-literal 162 | 163 | ✖ 1 problem (1 error, 0 warnings) 164 | ``` 165 | 166 | ## Summary 167 | 168 | - You implement `Rule.RuleModule` to create a ESLint rule 169 | - ESLint plugin NPM package should have "eslint-plugin" prefix 170 | 171 | [Next](../20_dive_into_ast/README.md) 172 | -------------------------------------------------------------------------------- /guide/20_dive_into_ast/README.ja.md: -------------------------------------------------------------------------------- 1 | # Dive into AST 2 | 3 | 私達はすでに ESLint ルールモジュールの書き方を学びました。 4 | この章では、ESLint と AST(抽象構文木)解析の関係を学びましょう。 5 | 禁止したいコードのパーツを見つける方法を学ぶ、と言い換えてもよいでしょう! 6 | 7 | ## お題 8 | 9 | 本章のゴールは、 関数の `apply` を呼び出しているソースコードを見つけるルールの作成です。 10 | 例えば: 11 | 12 | ```js 13 | const fn = x => console.log(x); 14 | fn.apply(this, ["hoge"]); // We want to ban it! 15 | ``` 16 | 17 | TDD の手法を使ってみましょう。 18 | 19 | まず、このルールのテストコードを書きます。 20 | `src/rules/no-function-apply.test.ts` という名前で新しいテストファイルを作成し、次のように編集してください: 21 | 22 | ```ts 23 | import { RuleTester } from "eslint"; 24 | 25 | import rule from "./no-function-apply"; 26 | 27 | const tester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } }); 28 | 29 | tester.run("no-function-apply", rule, { 30 | valid: [{ code: `fn('hoge')` }], 31 | invalid: [ 32 | { 33 | code: `fn.apply(this, ['hoge'])`, 34 | errors: [{ message: "Don't use 'apply'" }], 35 | }, 36 | ], 37 | }); 38 | ``` 39 | 40 | 対応するルールモジュールも作成します。 41 | 42 | ```ts 43 | /* src/rules/no-function-apply.ts */ 44 | 45 | import { Rule } from "eslint"; 46 | 47 | const rule: Rule.RuleModule = { 48 | create: context => { 49 | return { 50 | // To be implemented later 51 | }; 52 | }, 53 | }; 54 | 55 | export = rule; 56 | ``` 57 | 58 | 上記のルールはまだ空であり、どのようなコードが与えられても何も起きません。 59 | したがって、 `npm test` は必ず失敗します。 60 | 61 | ## AST の可視化 62 | 63 | ルールのコーディングをおこなう前に、私達が探し出して禁止したいソースコードのパターンについて考えてみてください。 64 | 65 | JavaScript プログラムコードは ESLint の世界では AST として認識されています。 66 | ですので、「ソースコードのパターン」は「AST のパターン」と言い換えられます。 67 | 68 | そこで、今回のルールの禁止コードである `fn.apply(this, ['hoge'])`について、AST の形を明らかにしていきましょう。 69 | 70 | 次の図は対応する AST を可視化したものです。 71 | 72 | ![ast_diagram](./ast_diagram.png) 73 | 74 | AST は木構造のデータ表現です。 75 | ソースコードの AST 検証には https://astexplorer.net を使うことができます。 76 | 77 | ![astexplorer](./astexplorer.png) 78 | 79 | https://astexplorer.net/#/gist/76acd406762b142f796a290efaba423e/f721eb98505736ec48892ab556517e30d2a24066 80 | 81 | ## ESLint における AST 82 | 83 | ESLint のパーサー(例: acorn, esprima, babylon, typescript-eslint-parser など)は JavaScript プログラムを構文木へパースします。 84 | 構文木の要素は「ノード」と呼ばれます。 85 | ノードは次のインターフェイスで定義されます。 86 | 87 | ```ts 88 | interface BaseNodeWithoutComments { 89 | // Every leaf interface that extends BaseNode must specify a type property. 90 | // The type property should be a string literal. For example, Identifier 91 | // has: `type: "Identifier"` 92 | type: string; 93 | loc?: SourceLocation | null; 94 | range?: [number, number]; 95 | } 96 | ``` 97 | 98 | 先述のように、私達は `fn.apply(this, ['hoge'])` の AST オブジェクトを AST explorer によって知っており、この構文木が "ExpressionStatement" というオブジェクトを持っていることを見てきました。 99 | そして、このノードの `type` は `"ExpressionStatement"` という文字列値となります。 100 | 101 | さて、ESLint ルールに話を戻しましょう。 102 | 103 | 前章にて、次のようなシンプルなルールを書きました: 104 | 105 | ```ts 106 | const rule: Rule.RuleModule = { 107 | create: context => { 108 | return { 109 | Literal: node => { 110 | // Do something 111 | }, 112 | }; 113 | }, 114 | }; 115 | ``` 116 | 117 | `create` 関数は `Literal` という名前をキーに持つオブジェクトを返却しています。 118 | "Literal" というキーはどこから来たのでしょう? 119 | これはリテラルノードのタイプ名であり、ESLint はこの名前に反応してハンドラ関数を呼ぶのです。 120 | 121 | 「セレクタ」と呼ばれるより複雑なキーをこのオブジェクトの中で利用可能です。 122 | セレクタは HTML における CSS クエリにとても似ています。 123 | 124 | 例えば、次のセレクタは関数呼び出しの中のリテラルノードを見つけます。 125 | 126 | ```text 127 | "CallExpression Literal" 128 | ``` 129 | 130 | これは子孫クエリの例です。 131 | セレクタ記法は、ほぼ CSS クエリと同様の記法です。 132 | 他にどのようなクエリ記法があるかを知りたい場合は、 https://github.com/estools/esquery を見てください。 133 | 134 | ## セレクタの作成 135 | 136 | それでは、 `fn.apply(this, ['hoge'])` のような、apply 関数の呼び出しを見つけるセレクタクエリを作成しましょう。 137 | 138 | これには esquery のデモアプリがとても便利です。 139 | 140 | - http://estools.github.io/esquery/ を開く 141 | - 一番上のテキストエリアに `fn.apply(this, ['hoge'])` と入力 142 | - つづいてテキスト入力欄に `CallExpression` と入力 143 | 144 | ![esquery](./esquery.png) 145 | 146 | このツールは入力したクエリが入力のソースコードの AST にヒットするかどうかを教えてくれます。 147 | 148 | いま、私達は `.apply` の呼び出しを探したいと考えており、これは以下のように分解できます: 149 | 150 | - CallExpression ノード 151 | - MemberExpression ノード 152 | - "apply" という名前を持つ Identifier ノード 153 | 154 | また、私達は AST explorer の結果によって `fn.apply(this, ['hoge'])` の AST 構造をすでに知っています。 155 | これらを使って、どのようなクエリがマッチするか考えてみてください。 156 | 157 | 答えがわかりましたか? 158 | 次のクエリがヒットするでしょう。 159 | 160 | ``` 161 | CallExpression > MemberExpression > Identifier.property[name='apply'] 162 | ``` 163 | 164 | "no-function-apply" ルールを完成させましょう。 165 | 166 | ```ts 167 | /* src/rules/no-function-apply.ts */ 168 | 169 | import { Rule } from "eslint"; 170 | import { Node } from "estree"; 171 | 172 | const rule: Rule.RuleModule = { 173 | create: context => { 174 | return { 175 | "CallExpression > MemberExpression > Identifier.property[name='apply']": (node: Node) => { 176 | context.report({ 177 | message: "Don't use 'apply'", 178 | node, 179 | }); 180 | }, 181 | }; 182 | }, 183 | }; 184 | 185 | export = rule; 186 | ``` 187 | 188 | 最後に、もう一度 `npm test` を実行してください。今度は成功するはずです :sunglasses: 189 | 190 | ## 付録: esquery の"field"記法 191 | 192 | ところで、 `Identifier[name='apply']` ではなく、 `Identifier.property[name='apply']` というクエリを使っていたことに気づきましたか? 193 | `.property` という部分は「フィールド」という esquery の文法です。 194 | `Identifier.property` は「親要素における `property` フィールドとして位置している Identifier ノード」を意味しています。 195 | 196 | なぜこの文法を使うのでしょう? 197 | `Identifier[name='apply']` では不十分なのでしょうか? 198 | 199 | 答えは yes です。 200 | 201 | `fn.apply` は MemberExpression ノードにパースされます。 202 | このノードは 2 つの子ノードを持ち、この子ノードは両方ともに同じ "Identifier" タイプです。 203 | 204 | ```js 205 | { 206 | type: "MemberExpression", 207 | object: { 208 | type: "Identifier", 209 | name: "fn" 210 | }, 211 | property: { 212 | type: "Identifier", 213 | name: "apply" 214 | } 215 | } 216 | ``` 217 | 218 | したがって、もし `Identifier[name='apply']` を使ってしまうと、正しいコードである `apply.hoge()` でもエラーが出力されてしまいます。 219 | 220 | `Identifier.property` セレクタによって、2 番目の Identifier ノードをピックアップできたのです。 221 | 222 | ## Summary 223 | 224 | - ESlint ルールにおけるオブジェクトキーには AST セレクタが利用可能 225 | - https://astexplorer.net で AST の検証が可能 226 | - http://estools.github.io/esquery でセレクタのチェックが可能 227 | 228 | [Previous](../10_your_first_rule/README.ja.md) 229 | [Next](../30_other_parsers/README.ja.md) 230 | -------------------------------------------------------------------------------- /guide/20_dive_into_ast/README.md: -------------------------------------------------------------------------------- 1 | # Dive into AST 2 | 3 | We already learned how to write ESLint rule modules. 4 | In this chapter, let's learn the relation ESLint rules and AST(Abstract Syntax Tree) analysis. 5 | In other words, we'll learn how to find the part you want to ban from the source code! 6 | 7 | ## Subject 8 | 9 | The goal of this chapter is creating a rule which finds a part of source code calling `apply` of some functions. 10 | For example: 11 | 12 | ```js 13 | const fn = x => console.log(x); 14 | fn.apply(this, ["hoge"]); // We want to ban it! 15 | ``` 16 | 17 | For now, let's adopt the TDD approach. 18 | 19 | First, we write a test for this rule. 20 | Put a new test file as `src/rules/no-function-apply.test.ts` and edit the following: 21 | 22 | ```ts 23 | import { RuleTester } from "eslint"; 24 | 25 | import rule from "./no-function-apply"; 26 | 27 | const tester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } }); 28 | 29 | tester.run("no-function-apply", rule, { 30 | valid: [{ code: `fn('hoge')` }], 31 | invalid: [ 32 | { 33 | code: `fn.apply(this, ['hoge'])`, 34 | errors: [{ message: "Don't use 'apply'" }], 35 | }, 36 | ], 37 | }); 38 | ``` 39 | 40 | And create a rule module corresponding it. 41 | 42 | ```ts 43 | /* src/rules/no-function-apply.ts */ 44 | 45 | import { Rule } from "eslint"; 46 | 47 | const rule: Rule.RuleModule = { 48 | create: context => { 49 | return { 50 | // To be implemented later 51 | }; 52 | }, 53 | }; 54 | 55 | export = rule; 56 | ``` 57 | 58 | The above rule is still empty and says nothing when given invalid source codes so `npm test` must fail. 59 | 60 | ## Visualize AST 61 | 62 | Before coding the rule, think about the pattern of the source code we want to find and ban. 63 | 64 | A JavaScript program source code is recognized as AST in ESLint world. 65 | Therefore "the pattern of source code" can be paraphrased as "the pattern of AST". 66 | 67 | So let's demystify the shape of AST for `fn.apply(this, ['hoge'])`, which is an invalid code example for the rule. 68 | 69 | The following figure is a visualization of the corresponding AST. 70 | 71 | ![ast_diagram](./ast_diagram.png) 72 | 73 | AST is tree structural data representation. 74 | You can see and inspect AST of your source code using https://astexplorer.net . 75 | 76 | ![astexplorer](./astexplorer.png) 77 | 78 | https://astexplorer.net/#/gist/76acd406762b142f796a290efaba423e/f721eb98505736ec48892ab556517e30d2a24066 79 | 80 | ## AST of ESLint 81 | 82 | A parser(e.g. acorn, esprima, babylon, typescript-eslint-parser, etc...) in ESLint parses JavaScript source program to a syntax tree and each element of this tree is called "Node". 83 | Node is defined as the following interface: 84 | 85 | ```ts 86 | interface BaseNodeWithoutComments { 87 | // Every leaf interface that extends BaseNode must specify a type property. 88 | // The type property should be a string literal. For example, Identifier 89 | // has: `type: "Identifier"` 90 | type: string; 91 | loc?: SourceLocation | null; 92 | range?: [number, number]; 93 | } 94 | ``` 95 | 96 | As mentioned above, we've got an AST object for `fn.apply(this, ['hoge'])` via AST explorer and found this tree has an "ExpressionStatement" object. 97 | This object is also one type of node. 98 | And the node's `type` is the string value `"ExpressionStatement"`. 99 | 100 | Well, let's return to the rules of ESLint. 101 | 102 | In the previous chapter, we wrote a simple rule such as: 103 | 104 | ```ts 105 | const rule: Rule.RuleModule = { 106 | create: context => { 107 | return { 108 | Literal: node => { 109 | // Do something 110 | }, 111 | }; 112 | }, 113 | }; 114 | ``` 115 | 116 | The `create` function returns an object whose key name is `Literal`. 117 | Where does the key "Literal" come from? 118 | It's type name of literal AST node and ESLint calls the handler function in response to this name. 119 | 120 | We can use more complex keys called "Selector" in the object. 121 | Selector is very similar to CSS query in HTML. 122 | 123 | For example, the following is a selector to find a literal node in a function calling expression. 124 | 125 | ```text 126 | "CallExpression Literal" 127 | ``` 128 | 129 | This is an example of descendant query. 130 | The selector notation is almost the same as the CSS query notation. 131 | See https://github.com/estools/esquery if you want other query syntax. 132 | 133 | ## Build selector 134 | 135 | So, let's build a selector query to find calling apply functions such as `fn.apply(this, ['hoge'])`. 136 | 137 | esquery demo app is very useful to do that. 138 | 139 | - Open http://estools.github.io/esquery/ 140 | - Type `fn.apply(this, ['hoge'])` at the top text area 141 | - Type `CallExpression` to the next text input 142 | 143 | ![esquery](./esquery.png) 144 | 145 | This tool tells us whether the input query hits the input source code AST. 146 | 147 | Now we want to find calling `.apply` and this can be factored as the following: 148 | 149 | - CallExpression node 150 | - MemberExpression node 151 | - Identifier node whose name "apply" 152 | 153 | And already we've know the AST structure of `fn.apply(this, ['hoge'])` by the AST explorer result. 154 | Think about which query matches using them. 155 | 156 | Do you reach the answer? 157 | The following query will hit. 158 | 159 | ``` 160 | CallExpression > MemberExpression > Identifier.property[name='apply'] 161 | ``` 162 | 163 | Let's complete our "no-function-apply" rule. 164 | 165 | ```ts 166 | /* src/rules/no-function-apply.ts */ 167 | 168 | import { Rule } from "eslint"; 169 | import { Node } from "estree"; 170 | 171 | const rule: Rule.RuleModule = { 172 | create: context => { 173 | return { 174 | "CallExpression > MemberExpression > Identifier.property[name='apply']": (node: Node) => { 175 | context.report({ 176 | message: "Don't use 'apply'", 177 | node, 178 | }); 179 | }, 180 | }; 181 | }, 182 | }; 183 | 184 | export = rule; 185 | ``` 186 | 187 | Finally, run `npm test` once again. It should exit successfully :sunglasses: 188 | 189 | ## Appendix: "field" syntax of esquery 190 | 191 | Did you notice that we used `Identifier.property[name='apply']` rather than `Identifier[name='apply']` ? 192 | The part `.property` is called "field" in esquery syntax. 193 | `Identifier.property` means "Identifier node which is located as `property` field at the parent node". 194 | 195 | Why do we use this syntax ? 196 | Does not `Identifier[name='apply']` satisfy for our rule ? 197 | 198 | No it doesn't. 199 | 200 | `fn.apply` is parsed to a MemberExpression node and this node 2 child nodes and both nodes have the same type "Identifier". 201 | 202 | ```js 203 | { 204 | type: "MemberExpression", 205 | object: { 206 | type: "Identifier", 207 | name: "fn" 208 | }, 209 | property: { 210 | type: "Identifier", 211 | name: "apply" 212 | } 213 | } 214 | ``` 215 | 216 | So our rule reports an error for valid code `apply.hoge()` if we use `Identifier[name='apply']`. 217 | 218 | We can pick up only the second Identifier node using `Identifier.property` selector. 219 | 220 | ## Summary 221 | 222 | - We can use AST selector as object keys in ESlint rule 223 | - We can inspect AST via https://astexplorer.net 224 | - We can check selector queries via http://estools.github.io/esquery 225 | 226 | [Previous](../10_your_first_rule/README.md) 227 | [Next](../30_other_parsers/README.md) 228 | -------------------------------------------------------------------------------- /guide/20_dive_into_ast/ast_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quramy/eslint-plugin-tutorial/8248eecff2e28edd61e9df3dcb880d2ec1934c50/guide/20_dive_into_ast/ast_diagram.png -------------------------------------------------------------------------------- /guide/20_dive_into_ast/astexplorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quramy/eslint-plugin-tutorial/8248eecff2e28edd61e9df3dcb880d2ec1934c50/guide/20_dive_into_ast/astexplorer.png -------------------------------------------------------------------------------- /guide/20_dive_into_ast/esquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quramy/eslint-plugin-tutorial/8248eecff2e28edd61e9df3dcb880d2ec1934c50/guide/20_dive_into_ast/esquery.png -------------------------------------------------------------------------------- /guide/30_other_parsers/README.ja.md: -------------------------------------------------------------------------------- 1 | # Other parsers 2 | 3 | もしかしたら、あなたのチームでは TypeScript のようなトランスパイラを利用しているかもしれません。 4 | このような場合、作成した ESLint ルールは同じトランスパイラでテストされるべきです。 5 | 6 | ## 別のパーサーを追加する 7 | 8 | この章を通して、 TypeScript / React JSX を扱えるようになりましょう。 9 | 10 | ```tsx 11 | type Props = { 12 | onClick: () => void; 13 | }; 14 | const MyComponent = ({ onClick }: Props) =>