如何在 JavaScript AST 中展平一系列赋值?

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

给定 JavaScript 中的表达式:

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

我得到这个 AST(来自 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": "="
    }
  ]
}

如何将此 AST 转换为重新序列化时会产生的内容:

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

基本上,扁平化 AST。如何做呢?几个小时以来,我一直在思考这个问题,并试图修改 this source code 以使其正常工作,但这有点令人费解。

每个AssignmentExpression都有一个权利属性。所以我觉得只要将它们设置在顶层到最后一个正确的项目就可以了,但由于某种原因我无法做到这一点。

我正在登录 normalize_AssignmentExpression 函数,它显示:

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: '='
}

不确定是否有帮助。

我试着做成这样,但它只输出一个:

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

这样做:

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

这是使用我正在处理的 Putout 代码转换器的最简单可能的转换,它不可扩展以进行无限分配,但适用于简单的情况,例如示例中的一个:

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

看起来是这样的:

您可以在Putout Editor中尝试。

删除嵌套块使用@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;
                }`
            }),
        }]
    ]
});

发帖的时候看到了,没时间看,然后OP发了一个有用的答案,我就忽略了。不过最近被另一个回答重新打开了,觉得挺有意思的。这是一个相当干净的解决方案:

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 向下遍历嵌套赋值表达式列表,直到找到 rightAssignmentExpression 和 returns 以外的其他内容。

handleExpression 接受一个表达式和 returns 一个表达式数组,或者是原始的,或者,如果类型是 AssignmentExpression,则为这种链的展平列表。为嵌套节点重新计算 getValue 时效率低下。我不知道摆脱这个需要多少工作。

process 大概是 throw-away 代码,在这个简单的文档中进行演示。您必须确定如何应用 handleExpression.

例如,当我 运行 Acorn 对抗样本时,我在 AssignmentExpression 周围多了一层 ExpressionStatement 包裹,并且需要更多类似这样的东西:

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

这不会尝试处理 startend 节点。它现在会弄错它们,可能值得删除它们。 (我想正确地重写它们将是一项艰巨的任务。)


最后,您确定要这样处理这些节点吗?让我印象深刻的是,您可能还必须处理类似的事情:

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

你不希望它变成

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

相反,我个人更喜欢:

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

这可能可以通过这样的版本来完成,它不需要 getValue 助手:

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