共计 6108 个字符,预计需要花费 16 分钟才能阅读完成。
本文记录如何开发一个 elsint 插件,浏览之前请先浏览官网的文档自定义插件,自定义规定。
废话不多说,开搞。
创立一个 eslint 插件库
须要创立一个 npm 我的项目,倡议命名为 eslint-plugin-*
或者 @yourScope/eslint-plugin[-*]
。
在库的导出文件中,导出一个规范的插件对象:
// lib/index.js
module.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.js
module.exports = {
// 规定元数据,具体浏览官网
meta: {
type: 'problem',
// 规定配置参数的验证器
schema: [],
// 文档信息
docs: {
description: '规定的形容',
url: '规定文档地址',
},
},
create(context) {// 规定理论逻辑},
};
而后在 create
函数中实现拜访器, 首先实现对参数定义的 ast 节点的拜访器:
// lib/rules/no-use-foo-in-var-name.js
module.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.js
module.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.js
module.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
能够获取以后节点的类型信息。