本文记录如何开发一个elsint插件,浏览之前请先浏览官网的文档自定义插件,自定义规定。
废话不多说,开搞。
创立一个eslint插件库
须要创立一个npm我的项目,倡议命名为eslint-plugin-*
或者@yourScope/eslint-plugin[-*]
。
在库的导出文件中,导出一个规范的插件对象:
// lib/index.jsmodule.exports = { rules: { // key为规定id, 值为规定对象,个别放在独自的文件中导出,这里下文的两个规定为例 'no-use-foo-in-var-name': require('./rules/no-use-foo-in-var-name'), }, configs: { // 提供给使用者应用该插件时,对插件内的规定的默认应用 recommended: { rules: { 'no-use-foo-in-var-name': 'error', 'no-use-string-substr': 'error', }, }, },};
接下来就是实现no-use-foo-in-var-name
规定。
创立规定
这里以创立一个简略的规定(no-use-foo-in-var-name
)为例,假设一个需要,禁止代码中命名为foo
这个单词。
首先对需要进行剖析,禁止命名为foo
,那么应该须要禁止上面的状况中应用foo
标识符(也就是规定文档中不举荐局部):
- 参数定义时;
- 导入模块时通过
as
从新定义; - 解构时从新赋值定义;
- 参数名定义;
- 对象字面量中有
foo
成员变量。
对于上面这些状况,因为存在正当场景,且将不合理场景剖析进去老本很高,所以须要疏忽(也就是规定文档中保护的上面的代码不会正告局部):
- 导入第三方库间接有的
foo
成员; - 已有对象中的
foo
成员操作,如obj.foo = 'xxxx'
; - 对已有的对象中解构中蕴含
foo
,如const { foo } = object
;
因为本章节只是用于示例,所以只对上文中禁止的状况中,挑出两个进行实现: 参数定义时禁止应用foo
变量名和对象字面量中有foo
成员变量。
首先创立一个规定对象:
// lib/rules/no-use-foo-in-var-name.jsmodule.exports = { // 规定元数据,具体浏览官网 meta: { type: 'problem', // 规定配置参数的验证器 schema: [], // 文档信息 docs: { description: '规定的形容', url: '规定文档地址', }, }, create(context) { // 规定理论逻辑 },};
而后在create
函数中实现拜访器, 首先实现对参数定义的ast节点的拜访器:
// lib/rules/no-use-foo-in-var-name.jsmodule.exports = { // ... create(context) { return { // ... VariableDeclarator(node) { const { id } = node; if (!id || id.type !== 'Identifier') { return; } const { name } = id; if (name === 'foo') { context.report({ node: id, message: '请勿应用foo来命名参数', }); } }, }; },};
接着对对象字面量中应用foo
进行解析:
// lib/rules/no-use-foo-in-var-name.jsmodule.exports = { // ... create(context) { return { // ... ObjectExpression(node) { const { properties } = node; if (!properties && properties.length === 0) { return; } properties.forEach((property) => { if (property.type !== 'Property') { return; } const { key } = property; if (!key) { return; } if ((key.type === 'Identifier' && key.name === 'foo') || (key.type === 'Literal' && key.value === 'foo') ) { context.report({ node: key, message: '请勿应用foo来命名对象属性', }); } }); }, }; },};
留神,因为js是一门动静语言,而lint是基于代码的动态剖析,所以对于一些比拟动态化的操作,尽管实际效果是不符合规范,但校验时判断的老本比拟大,这种状况个别都是放弃测验。如下面的代码中,通过const obj = { ['fo' + '0']: 123 }
就能够绕过查看。
整个规定就曾经实现。整体代码为:
// lib/rules/no-use-foo-in-var-name.jsmodule.exports = { // 规定元数据,具体浏览官网 meta: { type: 'problem', // 规定配置参数的验证器 schema: [], // 文档信息 docs: { description: '', url: '', }, }, create(context) { // 规定理论逻辑 return { ObjectExpression(node) { const { properties } = node; if (!properties && properties.length === 0) { return; } properties.forEach((property) => { if (property.type !== 'Property') { return; } const { key } = property; if (!key) { return; } if ((key.type === 'Identifier' && key.name === 'foo') || (key.type === 'Literal' && key.value === 'foo') ) { context.report({ node: key, message: '请勿应用foo来命名对象属性', }); } }); }, VariableDeclarator(node) { const { id } = node; if (!id || id.type !== 'Identifier') { return; } const { name } = id; if (name === 'foo') { context.report({ node: id, message: '请勿应用foo来命名参数', }); } }, }; },};
为了保障规定的稳定性,还须要增加测试案例:
// lib/__tests__/no-use-foo-in-var-name.spec.js// 规定对象const rule = require("../rules/no-use-foo-in-var-name"),const { RuleTester } = require("eslint");const ruleTester = new RuleTester();ruleTester.run("no-use-foo-in-var-name", rule, { valid: [ { code: "var bar = true", }, { code: "const obj = { bar: 123 }", }, ], invalid: [ { code: "var doo = true", errors: [{ message: "请勿应用foo来命名参数" }] }, { code: "const doo = '123'", errors: [{ message: "请勿应用foo来命名参数" }] }, { code: "const obj = { foo: 123 }", errors: [{ message: "请勿应用foo来命名对象属性" }] }, { code: `func({ foo: 123 })`, errors: [{ message: "请勿应用foo来命名对象属性" }] }, ]});
对于测试对象更多的用法参考:RuleTester
开发插件和规定的最佳实际
本章节次要是在开发一些规定和浏览一些已有规定后,本人总结的一些教训和理念。
规定所蕴含的内容尽可能小
一个规定的查看范畴和内容尽可能小,面向的需要场景也尽可能小。这样更加容易灵便搭配。
防小人不防君子
因为js是一门脚本语言,动静语法,具备元编程能力。而lint是基于动态语法分析,无奈涉及到运行时。所以对于标准场景,应遵循防小人无妨君子。
边界之外不要报告而是疏忽
在实现测验逻辑时,应该是在某个边界之内进行标准测验,处于边界之外时,应该疏忽。如查看申明时不容许定义申明某个变量名是,在这个申明没有变量,或者申明语句并不是惯例模式定义一个变量时,也就是不合乎的场景时,应该是疏忽,而不是报错。如示例中的代码:
const { id } = node;if (!id || id.type !== 'Identifier') { return;}
如果申明语句的id
不存在,或者id
的类型不是标识符时,那么应该立刻终止流程,而不是去提醒不符的标准。那么理论代码:
const { a } = object;
就不会去测验(没有谬误提醒)。
不要天经地义的认为一些ast节点必须有某些属性,因为可能随着es6的一直倒退,或者在其余新的js方言中,不可能的场景就会实在存在。
一个规定蕴含的内容
倡议每个规定,都应该有具体实现逻辑 + 文档 + 测试案例。
多个规定共享配置
多个规定都须要的一些配置,不倡议要求应用每个规定都去配置,而是通过通过setting
机制。
单元测试
单元测试能够保障规定的稳定性。
RuleTester
在对象在创立时,能够配置一些选项,如指定语法解析器等.ruleTester
实例在执行ruleTester.run(ruleId, ruleObject, options)
的options
中能够在配置一些内容,比方配置setting
内容。ruleTester.run
函数拜访的后果就是间接能在mocha中能执行的内容,不须要在包裹在describe
和it
函数中。- 因为jest是兼容mocha的写法,所以生成的测试内容也能够间接运行在jest中。
- 通过
ruleTester.run(ruleId, ruleObject, options)
中options
中的only
属性,能够只启动以后测试组中某一个代码验证。没有找到多个测试组如何只运行一个测试组的形式。
拜访器
规定对象的create
函数须要返回一个节点的拜访器,而后在eslint解析源码失去ast节点后,遍历这些节点,顺次调用拜访器执行,实现整个验证逻辑。跟babel相似的架构。
- 利用astexplorer辅助开发,事倍功半。
- eslint的规定对象是返回一个拜访器工厂。并且这个工厂会在解决不同的文件时,从新被调用(不同于babel插件的机制)。所以能够在工作函数的变量作用域中申明共享变量,在多个节点拜访器中通信;
- 不同于babel, 拜访器除了能指定进入还是进出这样的工夫节点外,还能够应用选择器和代码门路;
- 没有多节点的节点拜访器申明形式,倡议应用代码能力实现。
- 每个拜访器内容,都应该做好边界解决,并且尽量异样边界提前结束,应用
if...return...
形式,进步代码可读性。 - 报告时,尽量准确到有问题的ast节点,进步使用者体验。报告信息尽量精确,依据理论内容变动,而不是对立都是一个提示信息。
- eslint的ast节点跟babel的ast节点的类型存在不同,如文本字符串字面量,在eslint中是
Literal
,babel中是StringLiteral
。 - eslint中单个节点拜访器只提供
node
对象。这是因为绝对于babel,eslint不须要进行对节点的批改操作(fix时的fixer对象有提供)。eslint也没有提供节点的子元素的遍历工具,须要手动本人遍历。 - eslint的ast节点也有跟babel节点一样有
parent
属性获取父节点。 - 尽量不要在一个大节点中手动遍历子节点判断,应间接编写子节点的拜访器,而后通过
parent
对父节点进行判断,进步性能。 通过
context.getDeclaredVariables
能够疾速获取一些语句的申明变量相干,如示例中获取申明语句的变量时。但这样写,往往无奈在报告时定位到精确的点,只能提醒定位在整个语句。源码操作
通过
context.getSourceCode
函数能够获取到以后的源码对象。能够用于获取源码,单个代码token,正文等。- 倡议在
create
函数中调用获取SourceCode
对象,而后每个节点拜访器中应用。 - 罕用的函数:
getText(node)
获取指定节点的源码,getAllComments
获取所有的正文,getCommentsBefore
,getCommentsAfter
,getCommentsInside
获取指定节点的相干正文信息。
作用域链的应用
通过context.getScope
能够获取到eslint遍历器遍历以后节点时所在的作用域。留神它不能找指定节点的作用域链,只能拿到以后遍历节点的作用域。即如果应用了手动遍历子节点,那无奈获取某个子节点的作用域。
- 对于针对某些模块的应用标准时,倡议尽量走作用域链的形式去实现。不倡议通过先剖析导入语句,获取到变量名,而后对所有的标识符或者列举可能应用标识符的节点类型中去判断变量名是否统一的形式。不通过作用域的形式须要思考的场景过多,且可能须要通过枚举的的形式去解决,无奈保障百分百解决正确。
- 通过一个标识符找到其定义的流程: 首先应用
context.getScope()
拜访以后拜访节点的作用域对象,该对象中的set
属性是以后作用域中蕴含的所有变量。通过scopeSet.get(id)
,就能够获取到以后标识符对应的变量对象。该对象的defs
能够获取到变量的定义列表。 - 变量对象中的
references
能够获取到这个变量的援用列表。 references
援用列表是蕴含所有的变量,所以在做一些操作时,如果起始节点必定没有问题时,还须要剔除起始节点,能够采纳节点的坐标信息进行判断是否是同一个节点。- 获取到的变量定义时,可能是从新赋值了另外一个变量,能够通过递归,直至找到最开始的那个定义值。
类型推断
在typescipt语法中,利用typescipt对变量的类型进行类型推断,能够更加准确的进行代码测验。
- typescipt针对elsint转解析器,会提供额定的类型服务,通过
context.parserServices
裸露,次要有program
,esTreeNodeToTSNodeMap
属性。其中program
提供一些ts能力,esTreeNodeToTSNodeMap
提供把esTreeNode
转换成tsNode
的能力。 program
为typescipt中创立的对象,具体内容能够查看typescipt中的源码。program
罕用的函数有getTypeChecker
,能够获取一个类型查看器。TypeChecker
的具体内容能够查看typescipt中的源码。TypeChecker
的罕用函数getTypeAtLocation
能够获取以后节点的类型信息。