手摸手教你寫個ESLint 插件以及瞭解ESLint的運行原理

這篇文章目的是介紹如何創建一個ESLint插件和創建一個 ESLint rule,用以幫助我們更深入的理解ESLint的運行原理,並且在有必要時可以根據需求創建出一個完美滿足自己需求的Lint規則。

插件目標

禁止項目中 setTimeout的第二個參數是數字。

PS: 如果是數字的話,很容易就成爲魔鬼數字,沒有人知道爲什麼是這個數字, 這個數字有什麼含義。

使用模板初始化項目:

1. 安裝NPM包

ESLint官方爲了方便開發者開發插件,提供了使用Yeoman模板( generator-eslint)。

對於Yeoman我們只需知道它是一個腳手架工具,用於生成包含指定框架結構的工程化目錄結構。

  
    
  
  
  
  1. npm install -g yo generator-eslint

2. 創建一個文件夾:

  
    
  
  
  
  1. mkdir eslint-plugin-demo

  2. cd eslint-plugin-demo

3. 命令行初始化ESLint插件的項目結構:

  
    
  
  
  
  1. yo eslint:plugin

下面進入命令行交互流程,流程結束後生成ESLint插件項目框架和文件。

  
    
  
  
  
  1. ? What is your name? OBKoro1

  2. ? What is the plugin ID? korolint // 這個插件的ID是什麼

  3. ? Type a short description of this plugin: XX公司的定製ESLint rule // 輸入這個插件的描述

  4. ? Does this plugin contain custom ESLint rules? Yes // 這個插件包含自定義ESLint規則嗎?

  5. ? Does this plugin contain one or more processors? No // 這個插件包含一個或多個處理器嗎

  6. // 處理器用於處理js以外的文件 比如.vue文件

  7. create package.json

  8. create lib/index.js

  9. create README.md

現在可以看到在文件夾內生成了一些文件夾和文件,但我們還需要創建規則具體細節的文件。

4. 創建規則

上一個命令行生成的是ESLint插件的項目模板,這個命令行是生成ESLint插件具體規則的文件。

  
    
  
  
  
  1. yo eslint:rule // 生成 eslint rule的模板文件

創建規則命令行交互:

  
    
  
  
  
  1. ? What is your name? OBKoro1

  2. ? Where will this rule be published? (Use arrow keys) // 這個規則將在哪裏發佈?

  3. ESLint Core // 官方核心規則 (目前有200多個規則)

  4. ESLint Plugin // 選擇ESLint插件

  5. ? What is the rule ID? settimeout-no-number // 規則的ID

  6. ? Type a short description of this rule: setTimeout 第二個參數禁止是數字 // 輸入該規則的描述

  7. ? Type a short example of the code that will fail: 佔位 // 輸入一個失敗例子的代碼

  8. create docs/rules/settimeout-no-number.md

  9. create lib/rules/settimeout-no-number.js

  10. create tests/lib/rules/settimeout-no-number.js

加了具體規則文件的項目結構

  
    
  
  
  
  1. .

  2. ├── README.md

  3. ├── docs // 使用文檔

  4.    └── rules // 所有規則的文檔

  5.    └── settimeout-no-number.md // 具體規則文檔

  6. ├── lib // eslint 規則開發

  7.    ├── index.js 引入+導出rules文件夾的規則

  8.    └── rules // 此目錄下可以構建多個規則

  9.    └── settimeout-no-number.js // 規則細節

  10. ├── package.json

  11. └── tests // 單元測試

  12. └── lib

  13. └── rules

  14. └── settimeout-no-number.js // 測試該規則的文件

4. 安裝項目依賴

  
    
  
  
  
  1. npm install


以上是開發ESLint插件具體規則的準備工作,下面先來看看AST和ESLint原理的相關知識,爲我們開發ESLint rule 打一下基礎。

AST——抽象語法樹

AST是: AbstractSyntaxTree的簡稱,中文叫做:抽象語法樹。

AST的作用

將代碼抽象成樹狀數據結構,方便後續分析檢測代碼。

代碼被解析成AST的樣子

astexplorer.net是一個工具網站:它能查看代碼被解析成AST的樣子。

如下圖:在右側選中一個值時,左側對應區域也變成高亮區域,這樣可以在AST中很方便的選中對應的代碼

AST 選擇器:

下圖中被圈起來的部分,稱爲AST selectors(選擇器)。

AST 選擇器的作用:使用代碼通過選擇器來選中特定的代碼片段,然後再對代碼進行靜態分析。

AST 選擇器很多,ESLint官方專門有一個倉庫列出了所有類型的選擇器: estree

下文中開發ESLint rule就需要用到選擇器,等下用到了就懂了,現在知道一下就好了。


ESLint的運行原理

在開發規則之前,我們需要ESLint是怎麼運行的,瞭解插件爲什麼需要這麼寫。

1. 將代碼解析成AST

ESLint使用JavaScript解析器Espree把JS代碼解析成AST。

PS:解析器:是將代碼解析成AST的工具,ES6、react、vue都開發了對應的解析器所以ESLint能檢測它們的,ESLint也是因此一統前端Lint工具的。

2. 深度遍歷AST,監聽匹配過程。

在拿到AST之後,ESLint會以"從上至下"再"從下至上"的順序遍歷每個選擇器兩次。

3. 觸發監聽選擇器的 rule回調

在深度遍歷的過程中,生效的每條規則都會對其中的某一個或多個選擇器進行監聽,每當匹配到選擇器,監聽該選擇器的rule,都會觸發對應的回調。

4. 具體的檢測規則等細節內容。


開發規則

規則默認模板

打開 rule生成的模板文件 lib/rules/settimeout-no-number.js, 清理一下文件,刪掉不必要的選項:

  
    
  
  
  
  1. module.exports = {

  2. meta: {

  3. docs: {

  4. description: "setTimeout 第二個參數禁止是數字",

  5. },

  6. fixable: null, // 修復函數

  7. },

  8. // rule 核心

  9. create: function(context) {

  10. // 公共變量和函數應該在此定義

  11. return {

  12. // 返回事件鉤子

  13. };

  14. }

  15. };

刪掉的配置項,有些是ESLint官方核心規則纔是用到的配置項,有些是暫時不必瞭解的配置,需要用到的時候,可以自行查閱ESLint 文檔

create方法-監聽選擇器

上文ESLint原理第三部中提到的:在深度遍歷的過程中,生效的每條規則都會對其中的某一個或多個選擇器進行監聽,每當匹配到選擇器,監聽該選擇器的rule,都會觸發對應的回調。

create返回一個對象,對象的屬性設爲選擇器,ESLint會收集這些選擇器,在AST遍歷過程中會執行所有監聽該選擇器的回調。

  
    
  
  
  
  1. // rule 核心

  2. create: function(context) {

  3. // 公共變量和函數應該在此定義

  4. return {

  5. // 返回事件鉤子

  6. Identifier: (node) => {

  7. // node是選中的內容,是我們監聽的部分, 它的值參考AST

  8. }

  9. };

  10. }

觀察AST:

創建一個ESLint rule需要觀察代碼解析成AST,選中你要檢測的代碼,然後進行一些判斷。

以下代碼都是通過astexplorer.net在線解析的。

  
    
  
  
  
  1. setTimeout(()=>{

  2. console.log('settimeout')

  3. }, 1000)

rule完整文件

lib/rules/settimeout-no-number.js:

  
    
  
  
  
  1. module.exports = {

  2. meta: {

  3. docs: {

  4. description: "setTimeout 第二個參數禁止是數字",

  5. },

  6. fixable: null, // 修復函數

  7. },

  8. // rule 核心

  9. create: function (context) {

  10. // 公共變量和函數應該在此定義

  11. return {

  12. // 返回事件鉤子

  13. 'CallExpression': (node) => {

  14. if (node.callee.name !== 'setTimeout') return // 不是定時器即過濾

  15. const timeNode = node.arguments && node.arguments[1] // 獲取第二個參數

  16. if (!timeNode) return // 沒有第二個參數

  17. // 檢測報錯第二個參數是數字 報錯

  18. if (timeNode.type === 'Literal' && typeof timeNode.value === 'number') {

  19. context.report({

  20. node,

  21. message: 'setTimeout第二個參數禁止是數字'

  22. })

  23. }

  24. }

  25. };

  26. }

  27. };

context.report():這個方法是用來通知ESLint這段代碼是警告或錯誤的,用法如上。在這裏查看 contextcontext.report()的文檔。

規則寫完了,原理就是依據 AST解析的結果,做針對性的檢測,過濾出我們要選中的代碼,然後對代碼的值進行邏輯判斷

可能現在會有點懵逼,但是不要緊,我們來寫一下測試用例,然後用 debugger來看一下代碼是怎麼運行的。

測試用例:

測試文件 tests/lib/rules/settimeout-no-number.js:

  
    
  
  
  
  1. /**

  2. * @fileoverview setTimeout 第二個參數禁止是數字

  3. * @author OBKoro1

  4. */

  5. "use strict";

  6. var rule = require("../../../lib/rules/settimeout-no-number"), // 引入rule

  7. RuleTester = require("eslint").RuleTester;

  8. var ruleTester = new RuleTester({

  9. parserOptions: {

  10. ecmaVersion: 7, // 默認支持語法爲es5

  11. },

  12. });

  13. // 運行測試用例

  14. ruleTester.run("settimeout-no-number", rule, {

  15. // 正確的測試用例

  16. valid: [

  17. {

  18. code: 'let someNumber = 1000; setTimeout(()=>{ console.log(11) },someNumber)'

  19. },

  20. {

  21. code: 'setTimeout(()=>{ console.log(11) },someNumber)'

  22. }

  23. ],

  24. // 錯誤的測試用例

  25. invalid: [

  26. {

  27. code: 'setTimeout(()=>{ console.log(11) },1000)',

  28. errors: [{

  29. message: "setTimeout第二個參數禁止是數字", // 與rule拋出的錯誤保持一致

  30. type: "CallExpression" // rule監聽的對應鉤子

  31. }]

  32. }

  33. ]

  34. });

下面來學習一下怎麼在VSCode中調試node文件,用於觀察 rule是怎麼運行的。

實際上打 console的形式,也是可以的,但是在調試的時候打console實在是有點慢,對於node這種節點來說,信息也不全,所以我還是比較推薦通過 debugger的方式來調試 rule

在VSCode中調試node文件

  1. 點擊下圖中的設置按鈕, 將會打開一個文件 launch.json

  2. 在文件中填入如下內容,用於調試node文件。

  3. 在 rule文件中打 debugger或者在代碼行數那裏點一下小紅點。

  4. 點擊圖中的開始按鈕,進入 debugger

  
    
  
  
  
  1. {

  2. // 使用 IntelliSense 瞭解相關屬性。

  3. // 懸停以查看現有屬性的描述。

  4. // 欲瞭解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387

  5. "version": "0.2.0",

  6. "configurations": [

  7. {

  8. "type": "node",

  9. "request": "launch",

  10. "name": "啓動程序", // 調試界面的名稱

  11. // 運行項目下的這個文件:

  12. "program": "${workspaceFolder}/tests/lib/rules/settimeout-no-number.js",

  13. "args": [] // node 文件的參數

  14. },

  15. // 下面是用於調試package.json的命令 之前可以用,貌似vscode出了點bug導致現在用不了了

  16. {

  17. "name": "Launch via NPM",

  18. "type": "node",

  19. "request": "launch",

  20. "runtimeExecutable": "npm",

  21. "runtimeArgs": [

  22. "run-script", "dev" //這裏的dev就對應package.json中的scripts中的dev

  23. ],

  24. "port": 9229 //這個端口是調試的端口,不是項目啓動的端口

  25. },

  26. ]

  27. }

運行測試用例進入斷點

  1. 在 lib/rules/settimeout-no-number.js中打一些 debugger

  2. 點擊開始按鈕,以調試的形式運行測試文件 tests/lib/rules/settimeout-no-number.js

  3. 開始調試 rule


發佈插件

eslint插件都是以 npm包的形式來引用的,所以需要把插件發佈一下:

  1. 註冊:如果你還未註冊npm賬號的話,需要去註冊一下。

  2. 登錄npm: npm login

  3. 發佈 npm包: npm publish即可,ESLint已經把 package.json弄好了。

集成到項目:

安裝 npm包: npm i eslint-plugin-korolint-D

  1. 常規的方法: 引入插件一條條寫入規則

  
    
  
  
  
  1. // .eslintrc.js

  2. module.exports = {

  3. plugins: [ 'korolint' ],

  4. rules: {

  5. "korolint/settimeout-no-number": "error"

  6. }

  7. }

  1. extends繼承插件配置:

當規則比較多的時候,用戶一條條去寫,未免也太麻煩了,所以ESLint可以繼承插件的配置:

修改一下 lib/rules/index.js文件:

  
    
  
  
  
  1. 'use strict';

  2. var requireIndex = require('requireindex');

  3. const output = {

  4. rules: requireIndex(__dirname + '/rules'), // 導出所有規則

  5. configs: {

  6. // 導出自定義規則 在項目中直接引用

  7. koroRule: {

  8. plugins: ['korolint'], // 引入插件

  9. rules: {

  10. // 開啓規則

  11. 'korolint/settimeout-no-number': 'error'

  12. }

  13. }

  14. }

  15. };

  16. module.exports = output;

使用方法:

使用 extends來繼承插件的配置, extends不止這種繼承方式,即使你傳入一個npm包,一個文件的相對路徑地址,eslint也能繼承其中的配置。

  
    
  
  
  
  1. // .eslintrc.js

  2. module.exports = {

  3. extends: [ 'plugin:korolint/koroRule' ] // 繼承插件導出的配置

  4. }

PS : 這種使用方式, npm的包名不能爲 eslint-plugin-xx-xx,只能爲 eslint-plugin-xx否則會有報錯,被這個問題搞得頭疼o(╥﹏╥)o

擴展:

以上內容足夠開發一個插件,這裏是一些擴展知識點。

遍歷方向:

上文中說過: 在拿到AST之後,ESLint會以"從上至下"再"從下至上"的順序遍歷每個選擇器兩次。

我們所監聽的選擇器默認會在"從上至下"的過程中觸發,如果需要在"從下至上"的過程中執行則需要添加 :exit,在上文中 CallExpression就變爲 CallExpression:exit

注意:一段代碼解析後可能包含多次同一個選擇器,選擇器的鉤子也會多次觸發。

fix函數:自動修復rule錯誤

修復效果

  
    
  
  
  
  1. // 修復前

  2. setTimeout(() => {

  3. }, 1000)

  4. // 修復後 變量名故意寫錯 爲了讓用戶去修改它

  5. const countNumber1 = 1000

  6. setTimeout(() => {

  7. }, countNumber2)

  1. 在rule的meta對象上打開修復功能:

  
    
  
  
  
  1. // rule文件

  2. module.exports = {

  3. meta: {

  4. docs: {

  5. description: 'setTimeout 第二個參數禁止是數字'

  6. },

  7. fixable: 'code' // 打開修復功能

  8. }

  9. }

  1. 在 context.report()上提供一個 fix函數:

把上文的 context.report修改一下,增加一個 fix方法即可,更詳細的介紹可以看一下文檔。

  
    
  
  
  
  1. context.report({

  2. node,

  3. message: 'setTimeout第二個參數禁止是數字',

  4. fix(fixer) {

  5. const numberValue = timeNode.value;

  6. const statementString = `const countNumber = ${numberValue}\n`

  7. return [

  8. // 修改數字爲變量

  9. fixer.replaceTextRange(node.arguments[1].range, 'countNumber'),

  10. // 在setTimeout之前增加一行聲明變量的代碼 用戶自行修改變量名

  11. fixer.insertTextBeforeRange(node.range, statementString),

  12. ];

  13. }

  14. });

項目地址:

eslint-plugin-korolint


呼~ 這篇博客斷斷續續,寫了好幾周,終於完成了!

大家有看到這篇博客的話,建議跟着博客的一起動手寫一下,動手實操一下比你mark一百篇文章都來的有用,花不了很長時間的,希望各位看完本文,都能夠更深入的瞭解到ESLint的運行原理。

覺得我的博客對你有幫助的話,就關注一下/點個贊吧!

前端進階積累、公衆號、GitHub、wx:OBkoro1、郵箱:[email protected]

基友帶我飛

ESLint插件是向基友yeyan1996學習的,在遇到問題的時候,也是他指點我的,特此感謝。

參考資料:

創建規則ESLint 工作原理探討


本文分享自微信公衆號 - OBKoro1前端進階積累(gh_8af2fb8e54a9)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章