You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将嵌套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()

解决方案

现有代码存在三个核心缺陷:

  1. 未处理CallExpression的参数,所有调用的入参都没有提前展开为临时变量,也没有传入调用语句
  2. 未支持带计算属性的MemberExpression(即[]取值语法),也没有区分点取值和计算取值的写法
  3. 没有处理链式成员访问中非调用的属性链逻辑,连续属性访问不需要每层都生成临时变量,只需拼接属性即可

可直接运行的完整实现代码如下:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 06:54:07