前言

虽然现在已经有很多实用的 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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

【AST篇】教你如何编写 Eslint 插件的更多相关文章

  1. Playmaker Input篇教程之Playmaker购买下载和导入

    Playmaker Input篇教程之Playmaker购买下载和导入 Playmaker Input篇认识Playmaker Playmaker是Unity的插件,其标志如图1-1所示.开发者使用它 ...

  2. 从0开始编写webpack插件

    1. 前言 插件(plugins)是webpack中的一等功臣.正是由于有了诸多插件的存在,才使得webpack无所不能.在webpack源码中也是使用了大量的内部插件,插件要是用的好,可以让你的工作 ...

  3. 前端工具-定制ESLint 插件以及了解ESLint的运行原理

    这篇文章目的是介绍如何创建一个ESLint插件和创建一个ESLint rule,用以帮助我们更深入的理解ESLint的运行原理,并且在有必要时可以根据需求创建出一个完美满足自己需求的Lint规则. 插 ...

  4. 一款检测代码中TODO的eslint插件

    一款检测代码中TODO的eslint插件 前言 看了我标题进来的同学应该也知道我做的是个啥东西 没错是一个eslint插件,前端魔法师们日常所使用的工具之一 什么?你不知道eslint是干嘛的--吃鲸 ...

  5. Playmaker Input篇教程之PlayMaker菜单概述

    Playmaker Input篇教程之PlayMaker菜单概述 Playmaker InputPlayMaker菜单概述 Playmaker插件被导入游戏项目以后,会自动为Unity编辑器添加一个名 ...

  6. 编写jQuery插件--实现返回顶部插件

    国庆过去一周多了,作为IT界的具有严重’工作狂‘性质的宅人,居然还没走出玩耍的心情,拖了程序猿的脚后跟了.最近工作不顺,心情不佳,想吐槽下公司,想了还是厚道点,以彼之道还施彼身,觉得自己也和他们同流合 ...

  7. [翻译]如何编写GIMP插件(一)

    近期想尝试编写gimp插件,在gimp官网看到了三篇简明教程,顺便翻译了下,由于本人英文,计算机知识有限,文中难免有warning,error出现,欢迎指正. <How to write a G ...

  8. Lua编写wireshark插件初探——解析Websocket上的MQTT协议

    一.背景 最近在做物联网流量分析时发现, App在使用MQTT协议时往往通过SSL+WebSocket+MQTT这种方式与服务器通信,在使用SSL中间人截获数据后,Wireshark不能自动解析出MQ ...

  9. 前端html、CSS快速编写代码插件-Emmet使用方法技巧详解

    前端html.CSS快速编写代码插件-Emmet使用方法技巧详解   Emmet的前身是大名鼎鼎的Zen coding,如果你从事Web前端开发的话,对该插件一定不会陌生.它使用仿CSS选择器的语法来 ...

随机推荐

  1. IDEA配置常见配置

    特别提示:本人博客部分有参考网络其他博客,但均是本人亲手编写过并验证通过.如发现博客有错误,请及时提出以免误导其他人,谢谢!欢迎转载,但记得标明文章出处:http://www.cnblogs.com/ ...

  2. shell sed应用

    sed是一个很好的文件处理工具,本身是一个管道命令,主要是以行为单位进行处理,可以将数据行进行替换.删除.新增.选取等特定工作,下面先了解一下sed的用法sed命令行格式为:sed [-nefri] ...

  3. [论文理解] CapsuleNet

    CapsuleNet 前言 找了很多资料,终于把整个流程搞懂了,其实要懂这个运算并不难,难的对我来说是怎么用代码实现,也找了github上的一些代码来看,对我来说都有点冗长,变量分布太远导致我脑袋炸了 ...

  4. gromacs2018使用踩坑记--grompp 为啥要用-r

    1. GMX grompp 概要 gmx grompp [ -f [<.mdp>] ] [ -c [<.gro / .g96 / ...>] ] [ -r [<.gro ...

  5. ListView的MyBaseAdapter的封装

    import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import j ...

  6. [VBA]汇总多个工作簿的指定工作表到同一个工作簿的指定工作表中

    sub 汇总多个工作簿() Application.ScreenUpdating = False Dim wb As Workbook, f As String, l As String, n As ...

  7. Go语言基本类型

    1.Go语言fmt包详解 fmt.Println() ###常用打印 fmt.Print() fmt.Printf() ###格式化 fmt.Sprintf() ###字符串拼接 a)普通占位符 占位 ...

  8. iOS 图表工具charts之CombinedChartView

    关于charts的系列视图介绍传送门: iOS 图表工具charts介绍 iOS 图表工具charts之LineChartView iOS 图表工具charts之BarChartView iOS 图表 ...

  9. iOS charts CombinedChartView First and last bars are shown half only

    charts 使用CombinedChartView 绘图时,发现第一个和最后一个bar只能显示一半的问题,解决方法: ChartXAxis *xAxis = chartView.xAxis; xAx ...

  10. Prism学习笔记-模块之间通信的几种方式

    在开发大型复杂系统时,我们通常会按功能将系统分成很多模块,这样模块就可以独立的并行开发.测试.部署.修改.使用Prism框架设计表现层时,我们也会遵循这个原则,按功能相关性将界面划分为多个模块,每个模 ...