【AST篇】教你如何動手寫 Eslint 外掛

Allan91發表於2019-10-12

前言

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

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

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

目標&涉及知識點

本文 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 // 搭建一個初始化的目錄結構
複製程式碼

【AST篇】教你如何動手寫 Eslint 外掛

此時檔案的目錄結構為:

.
├── README.md
├── lib
│   ├── index.js
│   └── rules
├── package.json
└── tests
    └── lib
        └── rules
複製程式碼

安裝依賴:

npm install
複製程式碼

至此,環境搭建完畢。

建立規則

終端執行:

yo eslint:rule // 生成預設 eslint rule 模版檔案
複製程式碼

【AST篇】教你如何動手寫 Eslint 外掛
此時專案結構為:

.
├── 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(抽象語法樹)),再祭上那張看了幾百遍的圖。

【AST篇】教你如何動手寫 Eslint 外掛

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(),先看看這段程式碼在抽象語法樹中的展現:

【AST篇】教你如何動手寫 Eslint 外掛

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

【AST篇】教你如何動手寫 Eslint 外掛

那麼我們根據上面的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
複製程式碼

【AST篇】教你如何動手寫 Eslint 外掛

開啟npm平臺,可以搜尋到上面釋出的 eslint-plugin-demofortutorial 這個 Node 包。

【AST篇】教你如何動手寫 Eslint 外掛

如何使用

釋出完之後在你需要的專案中安裝這個包:

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,會出現如下效果:

【AST篇】教你如何動手寫 Eslint 外掛

單元測試(完善)

對於完整的 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
複製程式碼

【AST篇】教你如何動手寫 Eslint 外掛

至此,這個包的開發完成。其它規則開發也是類似,比如您可以繼續制定其它規範,比如 ️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…

課外知識: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 工具以及趨勢圖:

【AST篇】教你如何動手寫 Eslint 外掛

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

相關文章