這篇文章目的是介紹如何建立一個ESLint插件和建立一個ESLint
rule
,用以幫助咱們更深刻的理解ESLint的運行原理,而且在有必要時能夠根據需求建立出一個完美知足本身需求的Lint規則。前端
禁止項目中setTimeout
的第二個參數是數字。vue
PS: 若是是數字的話,很容易就成爲魔鬼數字,沒有人知道爲何是這個數字, 這個數字有什麼含義。node
ESLint官方爲了方便開發者開發插件,提供了使用Yeoman模板(generator-eslint
)。react
對於Yeoman咱們只需知道它是一個腳手架工具,用於生成包含指定框架結構的工程化目錄結構。git
npm install -g yo generator-eslint
mkdir eslint-plugin-demo cd eslint-plugin-demo
yo eslint:plugin
下面進入命令行交互流程,流程結束後生成ESLint插件項目框架和文件。github
? What is your name? OBKoro1 ? What is the plugin ID? korolint // 這個插件的ID是什麼 ? Type a short description of this plugin: XX公司的定製ESLint rule // 輸入這個插件的描述 ? Does this plugin contain custom ESLint rules? Yes // 這個插件包含自定義ESLint規則嗎? ? Does this plugin contain one or more processors? No // 這個插件包含一個或多個處理器嗎 // 處理器用於處理js之外的文件 好比.vue文件 create package.json create lib/index.js create README.md
如今能夠看到在文件夾內生成了一些文件夾和文件,但咱們還須要建立規則具體細節的文件。web
上一個命令行生成的是ESLint插件的項目模板,這個命令行是生成ESLint插件具體規則的文件。
yo eslint:rule // 生成 eslint rule的模板文件
建立規則命令行交互:npm
? What is your name? OBKoro1 ? Where will this rule be published? (Use arrow keys) // 這個規則將在哪裏發佈? ❯ ESLint Core // 官方核心規則 (目前有200多個規則) ESLint Plugin // 選擇ESLint插件 ? What is the rule ID? settimeout-no-number // 規則的ID ? Type a short description of this rule: setTimeout 第二個參數禁止是數字 // 輸入該規則的描述 ? Type a short example of the code that will fail: 佔位 // 輸入一個失敗例子的代碼 create docs/rules/settimeout-no-number.md create lib/rules/settimeout-no-number.js create tests/lib/rules/settimeout-no-number.js
. ├── README.md ├── docs // 使用文檔 │ └── rules // 全部規則的文檔 │ └── settimeout-no-number.md // 具體規則文檔 ├── lib // eslint 規則開發 │ ├── index.js 引入+導出rules文件夾的規則 │ └── rules // 此目錄下能夠構建多個規則 │ └── settimeout-no-number.js // 規則細節 ├── package.json └── tests // 單元測試 └── lib └── rules └── settimeout-no-number.js // 測試該規則的文件
npm install
以上是開發ESLint插件具體規則的準備工做,下面先來看看AST和ESLint原理的相關知識,爲咱們開發ESLint rule
打一下基礎。json
AST是: Abstract Syntax Tree
的簡稱,中文叫作:抽象語法樹。數據結構
將代碼抽象成樹狀數據結構,方便後續分析檢測代碼。
astexplorer.net是一個工具網站:它能查看代碼被解析成AST的樣子。
以下圖:在右側選中一個值時,左側對應區域也變成高亮區域,這樣能夠在AST中很方便的選中對應的代碼。
下圖中被圈起來的部分,稱爲AST selectors(選擇器)。
AST 選擇器的做用:使用代碼經過選擇器來選中特定的代碼片斷,而後再對代碼進行靜態分析。
AST 選擇器不少,ESLint官方專門有一個倉庫列出了全部類型的選擇器: estree
下文中開發ESLint rule
就須要用到選擇器,等下用到了就懂了,如今知道一下就行了。
在開發規則以前,咱們須要ESLint是怎麼運行的,瞭解插件爲何須要這麼寫。
ESLint使用JavaScript解析器Espree把JS代碼解析成AST。
PS:解析器:是將代碼解析成AST的工具,ES六、react、vue都開發了對應的解析器因此ESLint能檢測它們的,ESLint也是所以一統前端Lint工具的。
在拿到AST以後,ESLint會以"從上至下"再"從下至上"的順序遍歷每一個選擇器兩次。
rule
回調在深度遍歷的過程當中,生效的每條規則都會對其中的某一個或多個選擇器進行監聽,每當匹配到選擇器,監聽該選擇器的rule,都會觸發對應的回調。
打開rule
生成的模板文件lib/rules/settimeout-no-number.js
, 清理一下文件,刪掉沒必要要的選項:
module.exports = { meta: { docs: { description: "setTimeout 第二個參數禁止是數字", }, fixable: null, // 修復函數 }, // rule 核心 create: function(context) { // 公共變量和函數應該在此定義 return { // 返回事件鉤子 }; } };
刪掉的配置項,有些是ESLint官方核心規則纔是用到的配置項,有些是暫時沒必要了解的配置,須要用到的時候,能夠自行查閱ESLint 文檔
上文ESLint原理第三部中提到的:在深度遍歷的過程當中,生效的每條規則都會對其中的某一個或多個選擇器進行監聽,每當匹配到選擇器,監聽該選擇器的rule,都會觸發對應的回調。
create
返回一個對象,對象的屬性設爲選擇器,ESLint會收集這些選擇器,在AST遍歷過程當中會執行全部監聽該選擇器的回調。
// rule 核心 create: function(context) { // 公共變量和函數應該在此定義 return { // 返回事件鉤子 Identifier: (node) => { // node是選中的內容,是咱們監聽的部分, 它的值參考AST } }; }
建立一個ESLint rule
須要觀察代碼解析成AST,選中你要檢測的代碼,而後進行一些判斷。
如下代碼都是經過astexplorer.net在線解析的。
setTimeout(()=>{ console.log('settimeout') }, 1000)
lib/rules/settimeout-no-number.js
:
module.exports = { meta: { docs: { description: "setTimeout 第二個參數禁止是數字", }, fixable: null, // 修復函數 }, // rule 核心 create: function (context) { // 公共變量和函數應該在此定義 return { // 返回事件鉤子 'CallExpression': (node) => { if (node.callee.name !== 'setTimeout') return // 不是定時器即過濾 const timeNode = node.arguments && node.arguments[1] // 獲取第二個參數 if (!timeNode) return // 沒有第二個參數 // 檢測報錯第二個參數是數字 報錯 if (timeNode.type === 'Literal' && typeof timeNode.value === 'number') { context.report({ node, message: 'setTimeout第二個參數禁止是數字' }) } } }; } };
context.report():這個方法是用來通知ESLint這段代碼是警告或錯誤的,用法如上。在這裏查看context
和context.report()
的文檔。
規則寫完了,原理就是依據AST
解析的結果,作針對性的檢測,過濾出咱們要選中的代碼,而後對代碼的值進行邏輯判斷。
可能如今會有點懵逼,可是沒關係,咱們來寫一下測試用例,而後用debugger
來看一下代碼是怎麼運行的。
測試文件tests/lib/rules/settimeout-no-number.js
:
/** * @fileoverview setTimeout 第二個參數禁止是數字 * @author OBKoro1 */ "use strict"; var rule = require("../../../lib/rules/settimeout-no-number"), // 引入rule RuleTester = require("eslint").RuleTester; var ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 7, // 默認支持語法爲es5 }, }); // 運行測試用例 ruleTester.run("settimeout-no-number", rule, { // 正確的測試用例 valid: [ { code: 'let someNumber = 1000; setTimeout(()=>{ console.log(11) },someNumber)' }, { code: 'setTimeout(()=>{ console.log(11) },someNumber)' } ], // 錯誤的測試用例 invalid: [ { code: 'setTimeout(()=>{ console.log(11) },1000)', errors: [{ message: "setTimeout第二個參數禁止是數字", // 與rule拋出的錯誤保持一致 type: "CallExpression" // rule監聽的對應鉤子 }] } ] });
下面來學習一下怎麼在VSCode中調試node文件,用於觀察rule
是怎麼運行的。
實際上打console
的形式,也是能夠的,可是在調試的時候打console實在是有點慢,對於node這種節點來講,信息也不全,因此我仍是比較推薦經過debugger
的方式來調試rule
。
launch.json
rule
文件中打debugger
或者在代碼行數那裏點一下小紅點。debugger
{ // 使用 IntelliSense 瞭解相關屬性。 // 懸停以查看現有屬性的描述。 // 欲瞭解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "啓動程序", // 調試界面的名稱 // 運行項目下的這個文件: "program": "${workspaceFolder}/tests/lib/rules/settimeout-no-number.js", "args": [] // node 文件的參數 }, // 下面是用於調試package.json的命令 以前能夠用,貌似vscode出了點bug致使如今用不了了 { "name": "Launch via NPM", "type": "node", "request": "launch", "runtimeExecutable": "npm", "runtimeArgs": [ "run-script", "dev" //這裏的dev就對應package.json中的scripts中的dev ], "port": 9229 //這個端口是調試的端口,不是項目啓動的端口 }, ] }
lib/rules/settimeout-no-number.js
中打一些debugger
tests/lib/rules/settimeout-no-number.js
rule
。eslint插件都是以npm
包的形式來引用的,因此須要把插件發佈一下:
npm login
npm
包: npm publish
便可,ESLint已經把package.json
弄好了。安裝npm
包:npm i eslint-plugin-korolint -D
引入插件一條條寫入規則
// .eslintrc.js module.exports = { plugins: [ 'korolint' ], rules: { "korolint/settimeout-no-number": "error" } }
extends
繼承插件配置:當規則比較多的時候,用戶一條條去寫,未免也太麻煩了,因此ESLint能夠繼承插件的配置:
修改一下lib/rules/index.js
文件:
'use strict'; var requireIndex = require('requireindex'); const output = { rules: requireIndex(__dirname + '/rules'), // 導出全部規則 configs: { // 導出自定義規則 在項目中直接引用 koroRule: { plugins: ['korolint'], // 引入插件 rules: { // 開啓規則 'korolint/settimeout-no-number': 'error' } } } }; module.exports = output;
使用方法:
使用extends
來繼承插件的配置,extends
不止這種繼承方式,即便你傳入一個npm包,一個文件的相對路徑地址,eslint也能繼承其中的配置。
// .eslintrc.js module.exports = { extends: [ 'plugin:korolint/koroRule' ] // 繼承插件導出的配置 }
PS : 這種使用方式, npm的包名不能爲eslint-plugin-xx-xx
,只能爲eslint-plugin-xx
不然會有報錯,被這個問題搞得頭疼o(╥﹏╥)o
以上內容足夠開發一個插件,這裏是一些擴展知識點。
上文中說過: 在拿到AST以後,ESLint會以"從上至下"再"從下至上"的順序遍歷每一個選擇器兩次。
咱們所監聽的選擇器默認會在"從上至下"的過程當中觸發,若是須要在"從下至上"的過程當中執行則須要添加:exit
,在上文中CallExpression
就變爲CallExpression:exit
。
注意:一段代碼解析後可能包含屢次同一個選擇器,選擇器的鉤子也會屢次觸發。
修復效果:
// 修復前 setTimeout(() => { }, 1000) // 修復後 變量名故意寫錯 爲了讓用戶去修改它 const countNumber1 = 1000 setTimeout(() => { }, countNumber2)
// rule文件 module.exports = { meta: { docs: { description: 'setTimeout 第二個參數禁止是數字' }, fixable: 'code' // 打開修復功能 } }
context.report()
上提供一個fix
函數:把上文的context.report
修改一下,增長一個fix
方法便可,更詳細的介紹能夠看一下文檔。
context.report({ node, message: 'setTimeout第二個參數禁止是數字', fix(fixer) { const numberValue = timeNode.value; const statementString = `const countNumber = ${numberValue}\n` return [ // 修改數字爲變量 fixer.replaceTextRange(node.arguments[1].range, 'countNumber'), // 在setTimeout以前增長一行聲明變量的代碼 用戶自行修改變量名 fixer.insertTextBeforeRange(node.range, statementString), ]; } });
呼~ 這篇博客斷斷續續,寫了好幾周,終於完成了!
你們有看到這篇博客的話,建議跟着博客的一塊兒動手寫一下,動手實操一下比你mark一百篇文章都來的有用,花不了很長時間的,但願各位看完本文,都可以更深刻的瞭解到ESLint的運行原理。
前端進階積累、公衆號、GitHub、wx:OBkoro一、郵箱:obkoro1@foxmail.com
ESLint插件是向基友yeyan1996學習的,在遇到問題的時候,也是他指點個人,特此感謝。
參考資料: