如何编写ESLint规则将复杂方法链式调用转换为顺序调用序列?
基于ESLint实现链式调用扁平化转换方案说明
我已经使用acorn实现了将复杂方法链式调用转换为顺序临时变量调用的逻辑:
输入输出示例
输入的链式调用代码:
getM() .getZ(getF().g) .a.b.c.getX(1, 2, 3) [getP(1, 2).x.y.getQ(getY(getN(1)))] .r.getS()
转换后的代码:
const tmp2 = getF() const tmp4 = getM() const tmp3 = tmp4.getZ(tmp2.g) const tmp1 = tmp3.a.b.c.getX() const tmp5 = getN() const tmp6 = getY(tmp5) const tmp8 = getP() const tmp7 = tmp8.x.y.getQ(tmp6) const tmp0 = tmp1[tmp7].r.getS()
已有acorn实现代码
const acorn = require('acorn') const fs = require('fs') const input = fs.readFileSync('./tmp/parse.in.js', 'utf-8') const jst = acorn.parse(input, { ecmaVersion: 2021, sourceType: 'module' }) fs.writeFileSync('tmp/parse.out.js.json', JSON.stringify(jst, null, 2)) const flattenedText = flattenJST(jst.body[0].expression) fs.writeFileSync('tmp/parse.flat.json', flattenedText) function flattenJST(jst) { const state = { text: [], tmps: 0 } if (jst.type === 'CallExpression') { compileCallExpression(jst, state) } else if (jst.type === 'MemberExpression') { state.text.push(`const tmp${state.tmps++} = ${compileMemberExpression(jst, state)}`) } return state.text.join('\n') } function compileCallExpression(jst, state) { const args = [] jst.arguments.forEach(arg => { switch (arg.type) { case 'CallExpression': args.push(compileCallExpression(arg, state)) break case 'MemberExpression': args.push(compileMemberExpression(arg, state)) break } }) const variable = `tmp${state.tmps++}` let path if (jst.callee.type === 'MemberExpression') { path = compileMemberExpression(jst.callee, state) } else if (jst.callee.type === 'Identifier') { path = state.prefix ? `${state.prefix}.${jst.callee.name}` : jst.callee.name } else { throw JSON.stringify(jst) } state.text.push(`const ${variable} = ${path}(${args.join(', ')})`) return variable } function compileMemberExpression(jst, state) { if (jst.object.type === 'MemberExpression') { const back = compileMemberExpression(jst.object, state) switch (jst.property.type) { case 'Identifier': return joinProperty(`${back}`, `${jst.property.name}`, jst.computed) case 'MemberExpression': return joinProperty(back, compileMemberExpression(jst.property, state), jst.computed) break case 'CallExpression': return joinProperty(back, compileCallExpression(jst.property, state), jst.computed) break } } else if (jst.object.type === 'CallExpression') { const back = compileCallExpression(jst.object, state) switch (jst.property.type) { case 'Identifier': return joinProperty(`${back}`, `${jst.property.name}`, jst.computed) case 'MemberExpression': return joinProperty(`${back}`, compileMemberExpression(jst.property, state), jst.computed) break case 'CallExpression': return joinProperty(`${back}`, compileCallExpression(jst.property, state), jst.computed) break } } else if (jst.object.type === 'Identifier') { const back = jst.object.name switch (jst.property.type) { case 'Identifier': return joinProperty(`${back}`, `${jst.property.name}`, jst.computed) case 'MemberExpression': return compileMemberExpression(jst.property, state) break case 'CallExpression': return compileCallExpression(jst.property, state) break } } else { throw JSON.stringify(jst) } } function joinProperty(a, b, computed) { if (computed) { return `${a}[${b}]` } else { return `${a}.${b}` } }
需求说明
现在需要基于ESLint的规则体系实现相同的效果,用于JavaScript自定义规范化处理,方便后续转译为自定义编程语言。以下是基于ESLint visitor模式实现MemberExpression、CallExpression树扁平化转换的结构、工作量与最佳实践说明。
核心实现结构
ESLint规则基于AST visitor模式开发,且ESLint底层解析器就是acorn的封装,AST节点结构和你现有实现完全兼容,核心伪代码框架如下:
module.exports = { meta: { fixable: "code", // 标记支持自动修复,转换逻辑放在fix回调中 type: "suggestion", }, create(context) { // 复用原有状态结构:临时变量计数器、生成的临时变量声明列表 const state = { tmpCounter: 0, declarations: [], }; // 以下三个核心函数几乎可以直接复用你现有的acorn实现逻辑,无需大幅修改 function compileCallExpression(node) { // 原有逻辑:处理入参、递归展开嵌套调用、生成临时变量声明、返回临时变量名 } function compileMemberExpression(node) { // 原有逻辑:递归处理对象和属性、展开嵌套的调用/成员访问、返回属性路径字符串 } function joinProperty(a, b, computed) { // 原有逻辑:根据是否是计算属性拼接访问路径,完全复用 } return { // 只监听顶层表达式语句,避免重复处理嵌套子节点 ExpressionStatement(node) { const expr = node.expression; // 跳过非链式调用的简单表达式 if (!isChainedExpression(expr)) return; context.report({ node, message: "链式调用需展开为顺序临时变量声明", fix(fixer) { // 每次修复前清空状态,避免污染 state.tmpCounter = 0; state.declarations = []; // 执行转换逻辑,和原有flattenJST逻辑一致 let finalVar; if (expr.type === 'CallExpression') { finalVar = compileCallExpression(expr); } else if (expr.type === 'MemberExpression') { finalVar = compileMemberExpression(expr); state.declarations.push(`const tmp${state.tmpCounter++} = ${finalVar}`); } // 用生成的所有临时变量声明替换原链式调用代码 return fixer.replaceText(node, state.declarations.join('\n')); } }) } } } }
工作量评估
- 核心逻辑适配成本极低:现有三个核心转换函数90%以上代码可以直接复用,只需要调整状态管理方式,开发量在100行以内。
- 边界处理成本:只需额外处理临时变量命名冲突问题,可通过自定义唯一前缀,或者遍历当前作用域变量名做校验即可,工作量约50行代码。
- 总耗时:熟悉ESLint规则开发的情况下,2小时以内即可完成开发和测试。
最佳实践
- 所有转换逻辑都放在fix回调中执行,不要在visitor遍历阶段修改状态,避免多次遍历导致状态污染。
- 不要全局监听所有CallExpression和MemberExpression节点,只处理顶层链式调用表达式,避免重复处理嵌套节点,和你原有acorn的处理逻辑保持一致即可。
- 转换过程中保留原代码的参数、可选链、计算属性等所有语法特征,确保转换前后代码执行结果完全一致。
内容的提问来源于stack exchange,提问作者Lance Pollard
相关产品推荐
相关产品推荐

