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