Lance Pollard
Lance Pollard

Reputation: 79268

How do you flatten a sequence of assignments in a JavaScript AST?

Given this expression in JavaScript:

ipad[15] = opad[15] = some[12] = some[13] = undefined

I get this AST (from acornjs):

{
  "type": "Program",
  "body": [
    {
      "type": "AssignmentExpression",
      "left": {
        "type": "MemberExpression",
        "object": {
          "type": "Identifier",
          "start": 3943,
          "end": 3947,
          "name": "ipad"
        },
        "property": {
          "type": "Literal",
          "start": 3948,
          "end": 3950,
          "value": 15,
          "raw": "15"
        },
        "computed": true
      },
      "right": {
        "type": "AssignmentExpression",
        "left": {
          "type": "MemberExpression",
          "object": {
            "type": "Identifier",
            "start": 3954,
            "end": 3958,
            "name": "opad"
          },
          "property": {
            "type": "Literal",
            "start": 3959,
            "end": 3961,
            "value": 15,
            "raw": "15"
          },
          "computed": true
        },
        "right": {
          "type": "AssignmentExpression",
          "left": {
            "type": "MemberExpression",
            "object": {
              "type": "Identifier",
              "start": 3965,
              "end": 3969,
              "name": "some"
            },
            "property": {
              "type": "Literal",
              "start": 3970,
              "end": 3972,
              "value": 12,
              "raw": "12"
            },
            "computed": true
          },
          "right": {
            "type": "AssignmentExpression",
            "left": {
              "type": "MemberExpression",
              "object": {
                "type": "Identifier",
                "start": 3976,
                "end": 3980,
                "name": "some"
              },
              "property": {
                "type": "Literal",
                "start": 3981,
                "end": 3983,
                "value": 13,
                "raw": "13"
              },
              "computed": true
            },
            "right": {
              "type": "Identifier",
              "start": 3987,
              "end": 3996,
              "name": "undefined"
            },
            "operator": "="
          },
          "operator": "="
        },
        "operator": "="
      },
      "operator": "="
    }
  ]
}

How can I convert this AST to something that when reserialized would produce:

ipad[15] = undefined
opad[15] = undefined
some[12] = undefined
some[13] = undefined

Basically, flattening the AST. How can it be done? I have been thinking about this for hours and trying to modify this source code to get it working, but it's a bit mind bending.

Each AssignmentExpression has a right property. So I feel like just setting them at the top level to the last right item would work, but it's for some reason eluding me.

I am logging in that normalize_AssignmentExpression function, and it is showing:

RIGHT Node { type: 'Identifier', start: 3987, end: 3996, name: 'undefined' }
RIGHT {
  type: 'AssignmentExpression',
  left: {
    type: 'MemberExpression',
    object: Node { type: 'Identifier', start: 3976, end: 3980, name: 'some' },
    property: Node {
      type: 'Literal',
      start: 3981,
      end: 3983,
      value: 13,
      raw: '13'
    },
    computed: true
  },
  right: Node {
    type: 'Identifier',
    start: 3987,
    end: 3996,
    name: 'undefined'
  },
  operator: '='
}
RIGHT {
  type: 'AssignmentExpression',
  left: {
    type: 'MemberExpression',
    object: Node { type: 'Identifier', start: 3965, end: 3969, name: 'some' },
    property: Node {
      type: 'Literal',
      start: 3970,
      end: 3972,
      value: 12,
      raw: '12'
    },
    computed: true
  },
  right: {
    type: 'AssignmentExpression',
    left: {
      type: 'MemberExpression',
      object: [Node],
      property: [Node],
      computed: true
    },
    right: Node {
      type: 'Identifier',
      start: 3987,
      end: 3996,
      name: 'undefined'
    },
    operator: '='
  },
  operator: '='
}
RIGHT {
  type: 'AssignmentExpression',
  left: {
    type: 'MemberExpression',
    object: Node { type: 'Identifier', start: 3954, end: 3958, name: 'opad' },
    property: Node {
      type: 'Literal',
      start: 3959,
      end: 3961,
      value: 15,
      raw: '15'
    },
    computed: true
  },
  right: {
    type: 'AssignmentExpression',
    left: {
      type: 'MemberExpression',
      object: [Node],
      property: [Node],
      computed: true
    },
    right: {
      type: 'AssignmentExpression',
      left: [Object],
      right: [Node],
      operator: '='
    },
    operator: '='
  },
  operator: '='
}

Not sure if that helps.

I try to make it like this, but it just outputs one:

function normalize_AssignmentExpression(node, scope) {
  const [left, leftExps] = normalizeProperty(node.type, 'left', node.left.type, node.left, scope)
  let [right, rightExps] = normalizeProperty(node.type, 'right', node.right.type, node.right, scope)
  const exps = [
    ...leftExps,
    ...rightExps
  ]

  let furthestRight = right
  while (furthestRight.type === 'AssignmentExpression') {
    furthestRight = furthestRight.right
  }

  if (left.type === 'ArrayPattern') {
    const assignments = []
    left.elements.forEach(el => {
      assignments.push(
        createAssignmentExpression(
          el,
          createMemberExpression(right, el),
          node.operator
        )
      )
    })

    return [
      assignments,
      exps
    ]
  } else {
    console.log('RIGHT', right)
    const assignment = createAssignmentExpression(left, furthestRight, node.operator)
    return [
      assignment,
      exps
    ]
  }
}

Upvotes: 1

Views: 405

Answers (3)

Scott Sauyet
Scott Sauyet

Reputation: 50797

I saw this when it was posted, but didn't have time to look at it, then the OP posted an answer that worked, and I ignored it. But it was recently reopened by another answer, and I thought it interesting. Here is one fairly clean solution:

const getValue = (assignment) => 
  assignment .right .type == 'AssignmentExpression'
    ? getValue (assignment .right)
    : assignment .right

const handleExpression = (expression) => 
  expression.type === 'AssignmentExpression'
    ? [
        {...expression, right: getValue (expression)},
        ... handleExpression (expression .right)
      ]
    : expression

const process = (ast) => 
  ({...ast, body: ast .body .flatMap (handleExpression)})
  

const ast = {type: "Program", body: [{type: "AssignmentExpression", left: {type: "MemberExpression", object: {type: "Identifier", start: 3943, end: 3947, name: "ipad"}, property: {type: "Literal", start: 3948, end: 3950, value: 15, raw: "15"}, computed: true}, right: {type: "AssignmentExpression", left: {type: "MemberExpression", object: {type: "Identifier", start: 3954, end: 3958, name: "opad"}, property: {type: "Literal", start: 3959, end: 3961, value: 15, raw: "15"}, computed: true}, right: {type: "AssignmentExpression", left: {type: "MemberExpression", object: {type: "Identifier", start: 3965, end: 3969, name: "some"}, property: {type: "Literal", start: 3970, end: 3972, value: 12, raw: "12"}, computed: true}, right: {type: "AssignmentExpression", left: {type: "MemberExpression", object: {type: "Identifier", start: 3976, end: 3980, name: "some"}, property: {type: "Literal", start: 3981, end: 3983, value: 13, raw: "13"}, computed: true}, right: {type: "Identifier", start: 3987, end: 3996, name: "undefined"}, operator: "="}, operator: "="}, operator: "="}, operator: "="}]}

console .log (JSON.stringify (process (ast), null, 2))
.as-console-wrapper {max-height: 100% !important; top: 0}

getValue traverses down the list of nested Assignment expressions until it finds something other than an AssignmentExpression for right, and returns it.

handleExpression takes an expression and returns an array of expressions, either the original, or, if the type is AssignmentExpression, then a flattened list of this sort of chain. There is some inefficiency here in recalculating getValue for the nested nodes. I don't know how much work it would be to get rid of this.

process is probably throw-away code, to demonstrate in this simple document. You would have to determine how to apply handleExpression.

For instance, when I ran Acorn against the sample, I got an extra layer of ExpressionStatement wrapped around the AssignmentExpression, and needed something more like this:

const process = (ast) => ({
  ...ast, 
  body: ast .body .flatMap (
    (expr) => expr .type == 'ExpressionStatement' ? handleExpression (expr .expression) : expr
  )
})

This does not attempt to deal with start and end nodes. It will now get them wrong, and it's probably worth removing them. (Rewriting them correctly would be a huge task, I imagine.)


Finally, are you sure this is how you want to process these nodes? It strikes me that you might also have to deal with something like:

ipad[15] = opad[15] = some[12] = some[13] = someTimeConsumingFunction()

and you wouldn't want this to become

ipad[15] = someTimeConsumingFunction()
opad[15] = someTimeConsumingFunction()
some[12] = someTimeConsumingFunction()
some[13] = someTimeConsumingFunction()

Instead, I personally would prefer:

some[13] = someTimeConsumingFunction()
some[12] = some[13]
opad[15] = some[12]
ipad[15] = opad[12]

And this could probably be done by a version like this, which doesn't need the getValue helper:

const handleExpression = (expression) => 
  expression.type === 'AssignmentExpression'
    ? [... handleExpression (expression .right), {...expression, right: expression .right .left}]
    : expression

Upvotes: 1

coderaiser
coderaiser

Reputation: 827

Here is simplest possible transformation using 🐊Putout code transformer I'm working on, it's not scalable for infinite assignments, but good for simple cases like one from example :

module.exports.replace = () => ({
    '__a = __b = __c = __d = __e': `{
        __a = __e;
        __b = __e;
        __c = __e;
        __d = __e;
    }`
});

It looks this way:

enter image description here

You can try it in 🐊Putout Editor.

To remove nested blocks use @putout/plugin-remove-nested-blocks:

import putout from 'putout';

putout('ipad[15] = opad[15] = some[12] = some[13] = undefined', {
    plugins: [
        'remove-nested-blocks',
        ['flatten-sequence-assignment', {
            report: () => 'flatten assignments',
            replace: () => ({
                '__a = __b = __c = __d = __e': `{
                     __a = __e;
                     __b = __e;
                     __c = __e;
                     __d = __e;
                }`
            }),
        }]
    ]
});

Upvotes: 0

Lance Pollard
Lance Pollard

Reputation: 79268

This does it:

function normalize_AssignmentExpression(node, scope) {
  const [left, leftExps] = normalizeProperty(node.type, 'left', node.left.type, node.left, scope)
  let [right, rightExps] = normalizeProperty(node.type, 'right', node.right.type, node.right, scope)
  const exps = [
    ...leftExps,
    ...rightExps
  ]

  let furthestRight = Array.isArray(right) ? right[0] : right
  let rights = Array.isArray(right) ? right.slice(1) : []
  let lefts = [left]
  while (furthestRight.type === 'AssignmentExpression') {
    lefts.push(furthestRight.left)
    furthestRight = furthestRight.right
  }

  if (left.type === 'ArrayPattern') {
    const assignments = []
    left.elements.forEach(el => {
      assignments.push(
        createAssignmentExpression(
          el,
          createMemberExpression(right, el),
          node.operator
        )
      )
    })

    return [
      assignments,
      exps
    ]
  } else {
    const assignments = []
    lefts.forEach(l => {
      const assignment = createAssignmentExpression(l, furthestRight, node.operator)
      assignments.push(assignment)
    })
    assignments.push(...rights)
    return [
      assignments,
      exps
    ]
  }
}

Upvotes: 1

Related Questions