【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》
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 发布
- 单元测试
插件脚手架构建
这里我们利用 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上发布的包
参考资料:
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 插件的更多相关文章
- Playmaker Input篇教程之Playmaker购买下载和导入
Playmaker Input篇教程之Playmaker购买下载和导入 Playmaker Input篇认识Playmaker Playmaker是Unity的插件,其标志如图1-1所示.开发者使用它 ...
- 从0开始编写webpack插件
1. 前言 插件(plugins)是webpack中的一等功臣.正是由于有了诸多插件的存在,才使得webpack无所不能.在webpack源码中也是使用了大量的内部插件,插件要是用的好,可以让你的工作 ...
- 前端工具-定制ESLint 插件以及了解ESLint的运行原理
这篇文章目的是介绍如何创建一个ESLint插件和创建一个ESLint rule,用以帮助我们更深入的理解ESLint的运行原理,并且在有必要时可以根据需求创建出一个完美满足自己需求的Lint规则. 插 ...
- 一款检测代码中TODO的eslint插件
一款检测代码中TODO的eslint插件 前言 看了我标题进来的同学应该也知道我做的是个啥东西 没错是一个eslint插件,前端魔法师们日常所使用的工具之一 什么?你不知道eslint是干嘛的--吃鲸 ...
- Playmaker Input篇教程之PlayMaker菜单概述
Playmaker Input篇教程之PlayMaker菜单概述 Playmaker InputPlayMaker菜单概述 Playmaker插件被导入游戏项目以后,会自动为Unity编辑器添加一个名 ...
- 编写jQuery插件--实现返回顶部插件
国庆过去一周多了,作为IT界的具有严重’工作狂‘性质的宅人,居然还没走出玩耍的心情,拖了程序猿的脚后跟了.最近工作不顺,心情不佳,想吐槽下公司,想了还是厚道点,以彼之道还施彼身,觉得自己也和他们同流合 ...
- [翻译]如何编写GIMP插件(一)
近期想尝试编写gimp插件,在gimp官网看到了三篇简明教程,顺便翻译了下,由于本人英文,计算机知识有限,文中难免有warning,error出现,欢迎指正. <How to write a G ...
- Lua编写wireshark插件初探——解析Websocket上的MQTT协议
一.背景 最近在做物联网流量分析时发现, App在使用MQTT协议时往往通过SSL+WebSocket+MQTT这种方式与服务器通信,在使用SSL中间人截获数据后,Wireshark不能自动解析出MQ ...
- 前端html、CSS快速编写代码插件-Emmet使用方法技巧详解
前端html.CSS快速编写代码插件-Emmet使用方法技巧详解 Emmet的前身是大名鼎鼎的Zen coding,如果你从事Web前端开发的话,对该插件一定不会陌生.它使用仿CSS选择器的语法来 ...
随机推荐
- 使用Desktop App Converter打包桌面应用程序
打包具有安装程序 (.msi) 的应用程序 DesktopAppConverter.exe -Installer C:\Installer\MyAppSetup.msi -Destination C: ...
- 数组 Kotlin(5)
数组 数组在 Kotlin 中使用 Array 类来表示,它定义了 get 和 set 函数(按照运算符重载约定这会转变为 [] ) 和 size 属性,以及一些其他有用的成员函数:基本类型 clas ...
- 2.进行model和log的路径创建
第一步:使用datetime.strftime(datetime.now(), '%Y%m%d-%H%M%S') 用于生成当前时间 第二步: 使用os.path.join() 将文件的路径与subdi ...
- 全面解读php-函数
一.静态变量 二.函数的参数 三.函数的引用返回 四.外部文件的导入 五.内置函数 1.时间日期函数 2.IP处理函数 3.打印函数: 分类 函数名 说明 语言结构 print() 只能打印一个变量 ...
- C++重写实践&与java的差异
C++重写父类方法后,父类中同名的方法在子类中就无法被调用,回报这个问题: java中是没有这个问题的,显然java设计者在设计的时候有意解决了这个问题. C++实践代码: #include < ...
- vuex里面的store架构
将store文件夹分为四个文件夹,分别是actions,getters,mutations,state. action:和mutatation功能是类似的,都是修改state里面的数据,区别是acti ...
- Ubuntu 16.04安装、卸载mysql及怎么使用SQL常用操作语句
以前都是在window上操作,连接数据库,最近转Ubuntu系统,故此,记下安装过程 一,安装mysql,Ctrl+Alt+T打开终端,一步步分别输入命令 //安装mysql服务 sudo apt-g ...
- 第五周作业,LVM和TCP
1.磁盘lvm管理,完成下面要求,并写出详细过程: 1) 创建一个至少有两个PV组成的大小为20G的名为testvg的VG;要求PE大小 为16MB, 而后在卷组中创建大小为5G的逻辑卷testlv; ...
- 远程连接mysql要点 虚拟主机定义与分类
远程连接mysql:通过主机地址与端口号连接 1. 主机地址:找到主机电脑 2. 端口号:找到对应mysql软件 mysql客户端访问服务端需要进行寻找匹配:连接认证-h 主机地址 例如 -hloca ...
- 阶段3 2.Spring_02.程序间耦合_3 程序的耦合和解耦的思路分析1
编译时没有对应需要的jar包就报错.这特性就理解为程序的耦合 这种方式,它不是个错误而是个异常.编译的时候没有问题.运行时才会报错. 把注释的代码放开 程序可以正常运行 解决类之前依赖的思路 一个依赖 ...