Algorithm to convert JavaScript AST for complex nested CallExpression into series of CallExpressions

Viewed 65

Here is a seemingly relatively complex "CallExpression" in JavaScript:

getM()
  .getZ(getF().g)
  .a.b.c.getX(1, 2, 3)
  [getP(1, 2).x.y.getQ(getY(getN(1)))]
  .r.getS()

I would like to parse that using something like acornjs into a series of "simple" calls, where each single call in the series can end with a function call, but the result must be stored into a temporary variable. Any arguments must also be made into preceding temporary variables. So the above "expression" would be something like:

const tmpM = getM()
const tmpF = getF()
const tmpZ = tmpM.getZ(tmpF)
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()

Given that the acornjs JSON AST for the first expression is this:

{
  "type": "Program",
  "start": 0,
  "end": 100,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 1,
      "end": 99,
      "expression": {
        "type": "CallExpression",
        "start": 1,
        "end": 99,
        "callee": {
          "type": "MemberExpression",
          "start": 1,
          "end": 97,
          "object": {
            "type": "MemberExpression",
            "start": 1,
            "end": 92,
            "object": {
              "type": "MemberExpression",
              "start": 1,
              "end": 87,
              "object": {
                "type": "CallExpression",
                "start": 1,
                "end": 48,
                "callee": {
                  "type": "MemberExpression",
                  "start": 1,
                  "end": 39,
                  "object": {
                    "type": "MemberExpression",
                    "start": 1,
                    "end": 34,
                    "object": {
                      "type": "MemberExpression",
                      "start": 1,
                      "end": 32,
                      "object": {
                        "type": "MemberExpression",
                        "start": 1,
                        "end": 30,
                        "object": {
                          "type": "CallExpression",
                          "start": 1,
                          "end": 25,
                          "callee": {
                            "type": "MemberExpression",
                            "start": 1,
                            "end": 15,
                            "object": {
                              "type": "CallExpression",
                              "start": 1,
                              "end": 7,
                              "callee": {
                                "type": "Identifier",
                                "start": 1,
                                "end": 5,
                                "name": "getM"
                              },
                              "arguments": [],
                              "optional": false
                            },
                            "property": {
                              "type": "Identifier",
                              "start": 11,
                              "end": 15,
                              "name": "getZ"
                            },
                            "computed": false,
                            "optional": false
                          },
                          "arguments": [
                            {
                              "type": "MemberExpression",
                              "start": 16,
                              "end": 24,
                              "object": {
                                "type": "CallExpression",
                                "start": 16,
                                "end": 22,
                                "callee": {
                                  "type": "Identifier",
                                  "start": 16,
                                  "end": 20,
                                  "name": "getF"
                                },
                                "arguments": [],
                                "optional": false
                              },
                              "property": {
                                "type": "Identifier",
                                "start": 23,
                                "end": 24,
                                "name": "g"
                              },
                              "computed": false,
                              "optional": false
                            }
                          ],
                          "optional": false
                        },
                        "property": {
                          "type": "Identifier",
                          "start": 29,
                          "end": 30,
                          "name": "a"
                        },
                        "computed": false,
                        "optional": false
                      },
                      "property": {
                        "type": "Identifier",
                        "start": 31,
                        "end": 32,
                        "name": "b"
                      },
                      "computed": false,
                      "optional": false
                    },
                    "property": {
                      "type": "Identifier",
                      "start": 33,
                      "end": 34,
                      "name": "c"
                    },
                    "computed": false,
                    "optional": false
                  },
                  "property": {
                    "type": "Identifier",
                    "start": 35,
                    "end": 39,
                    "name": "getX"
                  },
                  "computed": false,
                  "optional": false
                },
                "arguments": [
                  {
                    "type": "Literal",
                    "start": 40,
                    "end": 41,
                    "value": 1,
                    "raw": "1"
                  },
                  {
                    "type": "Literal",
                    "start": 43,
                    "end": 44,
                    "value": 2,
                    "raw": "2"
                  },
                  {
                    "type": "Literal",
                    "start": 46,
                    "end": 47,
                    "value": 3,
                    "raw": "3"
                  }
                ],
                "optional": false
              },
              "property": {
                "type": "CallExpression",
                "start": 52,
                "end": 86,
                "callee": {
                  "type": "MemberExpression",
                  "start": 52,
                  "end": 71,
                  "object": {
                    "type": "MemberExpression",
                    "start": 52,
                    "end": 66,
                    "object": {
                      "type": "MemberExpression",
                      "start": 52,
                      "end": 64,
                      "object": {
                        "type": "CallExpression",
                        "start": 52,
                        "end": 62,
                        "callee": {
                          "type": "Identifier",
                          "start": 52,
                          "end": 56,
                          "name": "getP"
                        },
                        "arguments": [
                          {
                            "type": "Literal",
                            "start": 57,
                            "end": 58,
                            "value": 1,
                            "raw": "1"
                          },
                          {
                            "type": "Literal",
                            "start": 60,
                            "end": 61,
                            "value": 2,
                            "raw": "2"
                          }
                        ],
                        "optional": false
                      },
                      "property": {
                        "type": "Identifier",
                        "start": 63,
                        "end": 64,
                        "name": "x"
                      },
                      "computed": false,
                      "optional": false
                    },
                    "property": {
                      "type": "Identifier",
                      "start": 65,
                      "end": 66,
                      "name": "y"
                    },
                    "computed": false,
                    "optional": false
                  },
                  "property": {
                    "type": "Identifier",
                    "start": 67,
                    "end": 71,
                    "name": "getQ"
                  },
                  "computed": false,
                  "optional": false
                },
                "arguments": [
                  {
                    "type": "CallExpression",
                    "start": 72,
                    "end": 85,
                    "callee": {
                      "type": "Identifier",
                      "start": 72,
                      "end": 76,
                      "name": "getY"
                    },
                    "arguments": [
                      {
                        "type": "CallExpression",
                        "start": 77,
                        "end": 84,
                        "callee": {
                          "type": "Identifier",
                          "start": 77,
                          "end": 81,
                          "name": "getN"
                        },
                        "arguments": [
                          {
                            "type": "Literal",
                            "start": 82,
                            "end": 83,
                            "value": 1,
                            "raw": "1"
                          }
                        ],
                        "optional": false
                      }
                    ],
                    "optional": false
                  }
                ],
                "optional": false
              },
              "computed": true,
              "optional": false
            },
            "property": {
              "type": "Identifier",
              "start": 91,
              "end": 92,
              "name": "r"
            },
            "computed": false,
            "optional": false
          },
          "property": {
            "type": "Identifier",
            "start": 93,
            "end": 97,
            "name": "getS"
          },
          "computed": false,
          "optional": false
        },
        "arguments": [],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}

How would you dynamically/programmatically write an algorithm to take any similar expression and convert it into a corresponding "flattened" expression like shown above, with all the temp variables?

I am lost near the beginning, because I am not sure how to walk down to the "leaf" arguments, and place them out at the beginning, and then go from there. It's hard for me to see the general structure that's going on in the first place.

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')
  // return state.out.map(serialize).join('\n')
}

function serialize(node) {
  switch (node.type) {
    case 'VariableDeclaration':

      break
  }
}

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)
  }
}

Outputs so far:

const tmp3 = getM()
const tmp2 = tmp3()
const tmp1 = tmp2.b.c.getX()
const tmp0 = tmp1.r.getS()
1 Answers

Trial and error:

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')
  // return state.out.map(serialize).join('\n')
}

function serialize(node) {
  switch (node.type) {
    case 'VariableDeclaration':

      break
  }
}

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}`
  }
}

Outputs:

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()

If anyone can come up with a more elegant solution or explain how it works, please do and I'll mark that as the correct answer.

Related