function checkLastSegment (node) { // report problem for function if last code path segment is reachable } module.exports = { meta: { ... }, create: function(context) { // declare the state of the rule return { ReturnStatement: function(node) { // at a ReturnStatement node while going down }, // at a function expression node while going up: "FunctionExpression:exit": checkLastSegment, "ArrowFunctionExpression:exit": checkLastSegment, onCodePathStart: function (codePath, node) { // at the start of analyzing a code path }, onCodePathEnd: function(codePath, node) { // at the end of analyzing a code path } } } }
这里可以使用 VariableDeclarator 类型作为检察目标,从下面的解析树可以分析出筛选条件
以 VariableDeclarator 对象作为当前的 node
当变量名为 bar ,即 node.id.name === 'bar' ,且值为对象,即 node.init.type === 'ObjectExpression' ,代码如下:
module.exports = { meta: { ... }, create (context) { return { VariableDeclarator (node) { const isBarObj = node.id.name === 'bar' && node.init.type === 'ObjectExpression' if (!isBarObj) return // checker } } } }
就这样成功取到 bar 对象后就可以检测属性的顺序了,排序算法一大把,挑一个喜欢的用就行了,这里不啰嗦了,直接上结果:
const ORDER = ['meta', 'double'] function getOrderMap () { const orderMap = new Map() ORDER.forEach((name, i) => { orderMap.set(name, i) }) return orderMap } module.exports = { create (context) { const orderMap = getOrderMap() function checkOrder (propertiesNodes) { const properties = propertiesNodes .filter(property => property.type === 'Property') .map(property => property.key) properties.forEach((property, i) => { const propertiesAbove = properties.slice(0, i) const unorderedProperties = propertiesAbove .filter(p => orderMap.get(p.name) > orderMap.get(property.name)) .sort((p1, p2) => orderMap.get(p1.name) > orderMap.get(p2.name)) const firstUnorderedProperty = unorderedProperties[0] if (firstUnorderedProperty) { const line = firstUnorderedProperty.loc.start.line context.report({ node: property, message: `The "{{name}}" property should be above the "{{firstUnorderedPropertyName}}" property on line {{line}}.`, data: { name: property.name, firstUnorderedPropertyName: firstUnorderedProperty.name, line } }) } }) } return { VariableDeclarator (node) { const isBarObj = node.id.name === 'bar' && node.init.type === 'ObjectExpression' if (!isBarObj) return checkOrder(node.init.properties) } } } }
这里代码有点多,耐心看完其实挺简单的,大致解释下
getOrderMap 方法将数组转成 Map 类型,方面通过 get 获取下标,这里也可以处理多纬数组,例如两个 key 希望在相同的排序等级,不分上下,可以写成:
const order = [ 'meta' ['double', 'treble'] ] function getOrderMap () { const orderMap = new Map() ORDER.forEach((name, i) => { if (Array.isArray(property)) { property.forEach(p => orderMap.set(p, i)) } else { orderMap.set(property, i) } }) return orderMap }
这样 double 和 treble 就拥有相同的等级了,方便后面扩展,当然实际情况会有 n 个属性的排序规则,也可以在这个规则上轻松扩展,内部的 sort 逻辑就不赘述了。
开发就介绍到这里,通过上面安利的在线语法解析工具可以轻松反推出 lint 逻辑。
如果 rule 比较复杂,就需要大量的 utils 支持,不然每个 rule 都会显得一团糟,比较考验公共代码提取的能力
测试
如前面所讲建议使用 jest 进行测试,这里的测试和普通的单元测试还不太一样,eslint 是基于结果的测试,什么意思呢?
lint 只有两种情况,通过与不通过,只需要把通过和不通过的情况整理成两个数组,剩下的工作交给 eslint 的 RuleTester 处理就行了
上面的属性排序 rule,测试如下:
const RuleTester = require('eslint').RuleTester const rule = require('../../lib/rules/test') const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }) ruleTester.run('test rule', rule, { valid: [ `const bar = { meta: {}, double: num => num * 2 }` ], invalid: [ { code: `const bar = { double: num => num * 2, meta: {}, }`, errors: [{ message: 'The "meta" property should be above the "double" property on line 2.' }] } ] })
valid 中是希望通过的代码, invalid 中是不希望通过的代码和错误信息,到这里一个 rule 算是真正完成了。
打包输出
最后写好的 rules 需要发一个 npm 包,以便于在项目中使用,这里就不赘述怎么发包了,简单聊聊怎么优雅的把 rules 导出来。
直接上代码:
const requireIndex = require('requireindex') // import all rules in lib/rules module.exports.rules = requireIndex(`${__dirname}/rules`)
这里使用了三方依赖 requireindex ,对于批量的导出一个文件夹内的所有文件显得简洁很多。
当然前提是保证 rules 文件夹下都是 rule 文件,不要把 utils 写进去哈
总结
行文目的是国内外对于自定义 eslint rule 的相关资源较少,希望分享一些写自定义规则的经验。