深刻理解 ESLint

前言

雖然如今已經有不少實用的 ESLint 插件了,但隨着項目不斷迭代發展,你可能會遇到已有 ESLint 插件不能知足如今團隊開發的狀況。這時候,你須要本身來建立一個 ESLint 插件。javascript

本文我將帶你瞭解各類Lint工具的大體歷史,而後一步一步地建立一個屬於你本身的 ESLint 插件,以及教你如何利用AST抽象語法樹來制定這個插件的規則。前端

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

課外知識:Lint 簡史

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

JSLint 和 JSHint

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

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

JSHint 的特色就是可配置,同時文檔也相對完善,並且對開發者友好。很快你們就從 JSLint 轉向了 JSHint。github

ESLint 的誕生

後來幾年你們都將 JSHint 做爲代碼檢測工具的首選,但轉折點在2013年,Zakas 發現 JSHint 沒法知足本身制定規則需求,而且和 Anton 討論後發現這根本不可能在JShint上實現,同時 Zakas 還設想發明一個基於 AST 的 lint。因而 2013年6月份,Zakas 發佈了全新 lint 工具——ESLint。《Introducing ESLint》npm

ESLint早期源碼json

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 的出現。api

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…

相關文章
相關標籤/搜索