前言
雖然現在已經有很多實用的 ESLint 插件了,但隨著項目不斷迭代發展,你可能會遇到已有 ESLint 插件不能滿足現在團隊開發的情況。這時候,你需要自己來創建一個 ESLint 插件。
本文我將帶你了解各種Lint工具的大致歷史,然后一步一步地創建一個屬于你自己的 ESLint 插件,以及教你如何利用AST
抽象語法樹來制定這個插件的規則。
以此來帶你了解 ESLint 的實現原理。
目標&涉及知識點
本文 ESLint
插件目標是在項目開發中禁用:console.time()
。
- [x] AST 抽象語法樹
- [x] ESLint
- [x] Npm 發布
- [x] 單元測試
插件腳手架構建
這里我們利用 yeoman 和 generator-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
?,當然還有正常通過不用給任何提示。
規則創建
上面講完了 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
]
},
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
模塊,它由 meta
和 create
兩部分組成。
- 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"
。
以上。
查看Npm上發布的包
查看原文