如何将嵌套CallExpression的JS AST转为带临时变量的平铺调用序列
复杂链式调用表达式拆分为独立临时变量的转换算法实现
问题描述
现有如下JavaScript复杂链式CallExpression表达式:
getM() .getZ(getF().g) .a.b.c.getX(1, 2, 3) [getP(1, 2).x.y.getQ(getY(getN(1)))] .r.getS()
期望使用acornjs解析该表达式的AST,将其转换为一系列简单调用,每次调用结果都存入临时变量,调用参数也优先转换为前置临时变量,转换结果示例如下:
const tmpM = getM() const tmpF = getF() const tmpZ = tmpM.getZ(tmpF.g) const tmpX = tmpZ.a.b.c.getX(1, 2, 3) const tmpP = getP(1, 2) const tmpN = getN(1) const tmpY = getY(tmpN) const tmpQ = tmpP.x.y.getQ(tmpY) const tmpC = tmpX[tmpQ] const tmpS = tmpC.r.getS()
已获取该表达式的acornjs输出JSON AST,但是编写通用转换算法时遇到问题,不知道如何遍历AST获取叶子节点参数并按顺序生成临时变量,现有代码输出结果不符合预期。
现有代码如下:
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) } return state.text.join('\n') } function compileCallExpression(jst, state) { 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 } state.text.push(`const ${variable} = ${path}()`) return variable } function compileMemberExpression(jst, state) { if (jst.object.type === 'MemberExpression') { const back = compileMemberExpression(jst.object, state) return `${back}.${jst.property.name}` } else if (jst.object.type === 'CallExpression') { return compileCallExpression(jst.object, state) } }
现有错误输出:
const tmp3 = getM() const tmp2 = tmp3() const tmp1 = tmp2.b.c.getX() const tmp0 = tmp1.r.getS()
解决方案
现有代码存在三个核心缺陷:
- 未处理
CallExpression的参数,所有调用的入参都没有提前展开为临时变量,也没有传入调用语句 - 未支持带计算属性的
MemberExpression(即[]取值语法),也没有区分点取值和计算取值的写法 - 没有处理链式成员访问中非调用的属性链逻辑,连续属性访问不需要每层都生成临时变量,只需拼接属性即可
可直接运行的完整实现代码如下:
const acorn = require('acorn') const fs = require('fs') const input = fs.readFileSync('./tmp/parse.in.js', 'utf-8') const ast = acorn.parse(input, { ecmaVersion: 2021, sourceType: 'module' }) const state = { lines: [], tmpCounter: 0 } /** * 编译AST节点 * @param {object} node AST节点 * @param {boolean} asSubRef 是否作为子引用使用(如果是连续成员访问/作为被调用者,不需要生成临时变量) * @returns 可以直接在代码中引用的变量名/字面量 */ function compileNode(node, asSubRef = false) { switch (node.type) { case 'Identifier': return node.name case 'Literal': return node.raw case 'CallExpression': // 先编译被调用者 const calleeRef = compileNode(node.callee, true) // 再编译所有入参 const argRefs = node.arguments.map(arg => compileNode(arg)) // 生成调用的临时变量 const callTmp = `tmp${state.tmpCounter++}` state.lines.push(`const ${callTmp} = ${calleeRef}(${argRefs.join(', ')})`) return callTmp case 'MemberExpression': // 先编译对象部分 const objRef = compileNode(node.object, true) let propRef, expr if (node.computed) { // 计算属性[]需要编译属性节点 propRef = compileNode(node.property) expr = `${objRef}[${propRef}]` } else { // 点属性直接取属性名 propRef = node.property.name expr = `${objRef}.${propRef}` } // 如果是作为子引用使用,直接返回表达式,否则生成临时变量 if (asSubRef) return expr const memTmp = `tmp${state.tmpCounter++}` state.lines.push(`const ${memTmp} = ${expr}`) return memTmp default: throw new Error(`不支持的节点类型: ${node.type}`) } } // 从根表达式开始编译 compileNode(ast.body[0].expression) // 输出结果 fs.writeFileSync('./tmp/parse.flat.js', state.lines.join('\n'))
如果需要将语义化前缀替换临时变量名,只需维护节点名称和临时变量前缀的映射表即可。
内容的提问来源于stack exchange,提问作者Lance Pollard
相关产品推荐
相关产品推荐

