【AST篇】教你如何編寫 Eslint 插件


前言

雖然現在已經有很多實用的 ESLint 插件了,但隨着項目不斷迭代發展,你可能會遇到已有 ESLint 插件不能滿足現在團隊開發的情況。這時候,你需要自己來創建一個 ESLint 插件。

本文我將帶你了解各種Lint工具的大致歷史,然后一步一步地創建一個屬於你自己的 ESLint 插件,以及教你如何利用AST抽象語法樹來制定這個插件的規則。

以此來帶你了解 ESLint 的實現原理。

課外知識:Lint 簡史

Lint 是為了解決代碼不嚴謹而導致各種問題的一種工具。比如 ===== 的混合使用會導致一些奇怪的問題。

JSLint 和 JSHint

2002年,Douglas Crockford 開發了可能是第一款針對 JavaScript 的語法檢測工具 —— JSLint,並於 2010 年開源。

JSLint 面市后,確實幫助許多 JavaScript 開發者節省了不少排查代碼錯誤的時間。但是 JSLint 的問題也很明顯—— 幾乎不可配置,所有的代碼風格和規則都是內置好的;再加上 Douglas Crockford 推崇道系「愛用不用」的優良傳統,不會向開發者妥協開放配置或者修改他覺得是對的規則。於是 Anton Kovalyov 吐槽:「JSLint 是讓你的代碼風格更像 Douglas Crockford 的而已」,並且在 2011 年 Fork 原項目開發了 JSHint。《Why I forked JSLint to JSHint》

JSHint 的特點就是可配置,同時文檔也相對完善,而且對開發者友好。很快大家就從 JSLint 轉向了 JSHint。

ESLint 的誕生

后來幾年大家都將 JSHint 作為代碼檢測工具的首選,但轉折點在2013年,Zakas 發現 JSHint 無法滿足自己制定規則需求,並且和 Anton 討論后發現這根本不可能在JShint上實現,同時 Zakas 還設想發明一個基於 AST 的 lint。於是 2013年6月份,Zakas 發布了全新 lint 工具——ESLint。《Introducing ESLint》

ESLint早期源碼

var ast = esprima.parse(text, { loc: true, range: true }), walk = astw(ast); walk(function(node) { api.emit(node.type, node); }); return messages; 復制代碼

ESLint 的逆襲

ESLint 的出現並沒有撼動 JSHint 的霸主地位。由於前者是利用 AST 處理規則,用 Esprima 解析代碼,執行速度要比只需要一步搞定的 JSHint 慢很多;其次當時已經有許多編輯器對 JSHint 支持完善,生態足夠強大。真正讓 ESLint 逆襲的是 ECMAScript 6 的出現。

2015 年 6 月,ES2015 規范正式發布。但是發布后,市面上瀏覽器對最新標准的支持情況極其有限。如果想要提前體驗最新標准的語法,就得靠 Babel 之類的工具將代碼編譯成 ES5 甚至更低的版本,同時一些實驗性的特性也能靠 Babel 轉換。 但這時候的 JSHint 短期內無法提供支持,而 ESLint 卻只需要有合適的解析器就能繼續去 lint 檢查。Babel 團隊就為 ESLint 開發了一款替代默認解析器的工具,也就是現在我們所見到的 babel-eslint,它讓 ESLint 成為率先支持 ES6 語法的 lint 工具。

也是在 2015 年,React 的應用越來越廣泛,誕生不久的 JSX 也愈加流行。ESLint 本身也不支持 JSX 語法。但是因為可擴展性,eslint-plugin-react 的出現讓 ESLint 也能支持當時 React 特有的規則。

2016 年,JSCS 開發團隊認為 ESLint 和 JSCS 實現原理太過相似,而且需要解決的問題也都一致,最終選擇合並到 ESLint,並停止 JSCS 的維護。

當前市場上主流的 lint 工具以及趨勢圖:

 

從此 ESLint 一統江湖,成為替代 JSHint 的前端主流工具。

目標&涉及知識點

本文 ESLint 插件目標是在項目開發中禁用:console.time()

  • AST 抽象語法樹
  • ESLint
  • Npm 發布
  • 單元測試

插件腳手架構建

這里我們利用 yeomangenerator-eslint 來構建插件的腳手架代碼。安裝:

npm install -g yo generator-eslint
復制代碼

本地新建文件夾eslint-plugin-demofortutorial:

mkdir eslint-plugin-demofortutorial
cd eslint-plugin-demofortutorial 復制代碼

初始化 ESLint 插件的項目結構:

yo eslint:plugin // 搭建一個初始化的目錄結構
復制代碼

 

 

此時文件的目錄結構為:

.
├── README.md
├── lib
│   ├── index.js
│   └── rules
├── package.json
└── tests
    └── lib
        └── rules
復制代碼

安裝依賴:

npm install
復制代碼

至此,環境搭建完畢。

創建規則

終端執行:

yo eslint:rule // 生成默認 eslint rule 模版文件
復制代碼

 

此時項目結構為:

 

.
├── README.md
├── docs // 使用文檔
│   └── rules
│       └── no-console-time.md
├── lib // eslint 規則開發
│   ├── index.js
│   └── rules // 此目錄下可以構建多個規則,本文只拿一個規則來講解
│       └── no-console-time.js
├── package.json
└── tests // 單元測試
    └── lib
        └── rules
            └── no-console-time.js
復制代碼

上面結構中,我們需要在 ./lib/ 目錄下去開發 Eslint 插件,這里是定義它的規則的位置。

AST 在 ESLint 中的運用

在正式寫 ESLint 插件前,你需要了解下 ESLint 的工作原理。其中 ESLint 使用方法大家應該都比較熟悉,這里不做講解,不了解的可以點擊官方文檔如何在項目中配置 ESLint

在公司團隊項目開發中,不同開發者書寫的源碼是各不相同的,那么在 ESLint 中,如何去分析每個人寫的源碼呢?

作為開發者,面對這類問題,我們必須懂得要使用 抽象的手段 !那么 Javascript 的抽象性如何體現呢?

沒錯,就是 AST (Abstract Syntax Tree(抽象語法樹)),再祭上那張看了幾百遍的圖。

 

 

ESLint 中,默認使用 esprima 來解析我們書寫的 Javascript 語句,讓其生成抽象語法樹,然后去 攔截 檢測是否符合我們規定的書寫方式,最后讓其展示報錯、警告或正常通過。 ESLint 的核心就是規則(rules),而定義規則的核心就是利用 AST 來做校驗。每條規則相互獨立,可以設置禁用off、警告warn⚠️和報錯error❌,當然還有正常通過不用給任何提示。

規則創建

上面講完了 ESLintAST 的關系之后,我們可以正式進入開發具體規則。先來看之前生成的 lib/rules/no-console-time.js:

/**
 * @fileoverview no console.time()
 * @author Allan91
 */
"use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: "no console.time()", category: "Fill me in", recommended: false }, fixable: null, // or "code" or "whitespace" schema: [ // fill in your schema ] }, create: function(context) { // variables should be defined here //---------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------- // any helper functions should go here or else delete this section //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- return { // give me methods }; } }; 復制代碼

這個文件給出了書寫規則的模版,一個規則對應一個可導出的 node 模塊,它由 metacreate 兩部分組成。

  • meta 代表了這條規則的元數據,如其類別,文檔,可接收的參數的 schema 等等。
  • create:如果說 meta 表達了我們想做什么,那么 create 則用表達了這條 rule 具體會怎么分析代碼;

Create 返回一個對象,其中最常見的鍵名AST抽象語法樹中的選擇器,在該選擇器中,我們可以獲取對應選中的內容,隨后我們可以針對選中的內容作一定的判斷,看是否滿足我們的規則。如果不滿足,可用 context.report 拋出問題,ESLint 會利用我們的配置對拋出的內容做不同的展示。

具體參數配置詳情見官方文檔

本文創建的 ESLint 插件是為了不讓開發者在項目中使用 console.time(),先看看這段代碼在抽象語法樹中的展現:

 

 

其中,我們將會利用以下內容作為判斷代碼中是否含有 console.time:

 

那么我們根據上面的AST(抽象語法書)在 lib/rules/no-console-time.js 中這樣書寫規則:

/** * @fileoverview no console.time() * @author Allan91 */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: "no console.time()", category: "Fill me in", recommended: false }, fixable: null, // or "code" or "whitespace" schema: [ // fill in your schema ], // 報錯信息描述 messages: { avoidMethod: "console method '{{name}}' is forbidden.", }, }, create: function(context) { return { // 鍵名為ast中選擇器名 'CallExpression MemberExpression': (node) => { // 如果在ast中滿足以下條件,就用 context.report() 進行對外警告⚠️ if (node.property.name === 'time' && node.object.name === 'console') { context.report({ node, messageId: 'avoidMethod', data: { name: 'time', }, }); } }, }; } }; 復制代碼

再修改 lib/index.js

"use strict"; module.exports = { rules: { 'no-console-time': require('./rules/no-console-time'), }, configs: { recommended: { rules: { 'demofortutorial/no-console-time': 2, // 可以省略 eslint-plugin 前綴 }, }, }, }; 復制代碼

至此,Eslint 插件創建完成。接下去你需要做的就是將此項目發布到 npm平台。 根目錄執行:

npm publish
復制代碼

 

 

打開npm平台,可以搜索到上面發布的 eslint-plugin-demofortutorial 這個 Node 包。

 

如何使用

發布完之后在你需要的項目中安裝這個包:

npm install eslint-plugin-demofortutorial -D
復制代碼

然后在 .eslintrc.js 中配置:

"extends": [ "eslint:recommended", "plugin:eslint-plugin-demofortutorial/recommended", ], "plugins": [ 'demofortutorial' ], 復制代碼

如果之前沒有.eslintrc.js 文件,可以執行下面命令生成:

npm install -g eslint
eslint --init
復制代碼

此時,如果在當前項目的 JS 文件中書寫 console.time,會出現如下效果:

 

 

單元測試(完善)

對於完整的 npm 包來說,上面還只算是個“半成品”,我們需要寫單元測試來保證它的完整性和安全性。

下面來完成單元測試,在 ./tests/lib/rules/no-console-time.js 中編寫如下代碼:

'use strict'; // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ let rule = require('../../../lib/rules/no-console-time'); let RuleTester = require('eslint').RuleTester; // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ let ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 10, }, }); ruleTester.run('no-console-time', rule, { valid: [ // 合法示例 '_.time({a:1});', "_.time('abc');", "_.time(['a', 'b', 'c']);", "lodash.time('abc');", 'lodash.time({a:1});', 'abc.time', "lodash.time(['a', 'b', 'c']);", ], invalid: [ // 不合法示例 { code: 'console.time()', errors: [ { messageId: 'avoidMethod', }, ], }, { code: "console.time.call({}, 'hello')", errors: [ { messageId: 'avoidMethod', }, ], }, { code: "console.time.apply({}, ['hello'])", errors: [ { messageId: 'avoidMethod', }, ], }, { code: 'console.time.call(new Int32Array([1, 2, 3, 4, 5]));', errors: 1, }, ], }); 復制代碼

上面測試代碼詳細介紹見官方文檔

根目錄執行:

npm run test 復制代碼

 

 

至此,這個包的開發完成。其它規則開發也是類似,比如您可以繼續制定其它規范,比如 ️console.log()debugger警告等等。

其它

由於自動生成ESLint的項目中依賴的 eslint 版本還在 3.x 階段,會對單元測試語法解析造成如下報錯:

'Parsing error: Invalid ecmaVersion.' 復制代碼

建議將該包升級到 "eslint": "^5.16.0"



以上。

查看Github上的項目倉庫

查看Npm上發布的包





參考資料:

zhuanlan.zhihu.com/p/32297243 en.wikipedia.org/wiki/Lint_(… octoverse.github.com/ jslint.com medium.com/@anton/why-… www.nczonline.net/blog/2013/0… eslint.org jscs.info github.com/babel/babel… github.com/yannickcr/e… www.nczonline.net/blog/2016/0… medium.com/@markelog/j…


作者:Allan91
鏈接:https://juejin.im/post/5d91be23f265da5ba532a07e
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM