Why does escodegen and esprima generate a parenthesis wrapper on my source code?

682 views Asked by At

I am using escodegen to add an ending code on my statement as below. In the leave method, I append a .toArray() call on the end of the statement.

const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');

const ast = esprima.parse('db.find()');
let finished = false;
estraverse.traverse(ast, {
  leave: (node, parent) => {
    if (node.type === esprima.Syntax.ExpressionStatement && !finished) {
      finished = true;
      let statement = escodegen.generate(node);
      statement = `${statement.substring(0, statement.lastIndexOf(';'))}.toArray()`;
      const findAst = esprima.parse(statement);
      node.arguments = findAst.body[0].expression.arguments;
      node.callee = findAst.body[0].expression.callee;
      node.type = findAst.body[0].expression.type;
    }
  },
});

const generated = escodegen.generate(ast);
console.log('generated  code:', generated);

The output from above code is: generated code: (db.find().toArray()). I don't understand why it wraps a parenthesis on my source code. Is there anything wrong in my source code?

1

There are 1 answers

2
fonkap On BEST ANSWER

You are generating an incorrect AST. An ExpressionStatement has the form {type: "ExpressionStatement", expression... } .

You are modifying your ExpressionStatement, attaching to it arguments and callee and you are changing its type (to CallExpression). Here:

  node.arguments = findAst.body[0].expression.arguments;
  node.callee = findAst.body[0].expression.callee;
  node.type = findAst.body[0].expression.type;

Resulting a weird AST.

You can see it simply with : console.log('generated ast: %j', ast);

A quick solution is attach mentioned parts where them belong (to expression). Resulting:

let finished = false;
estraverse.traverse(ast, {
    leave: (node, parent) => {
        if (node.type === esprima.Syntax.ExpressionStatement && !finished) {
        finished = true;
        let statement = escodegen.generate(node);
        statement = `${statement.substring(0, statement.lastIndexOf(';'))}.toArray()`;
        console.log(statement);
        const findAst = esprima.parse(statement);
        node.expression.arguments = findAst.body[0].expression.arguments;
        node.expression.callee = findAst.body[0].expression.callee;
        node.expression.type = findAst.body[0].expression.type;
        }
    },
});

It will generate a correct AST, that will output the expected db.find().toArray();. But I think the code is a bit complicated and does too much work, it parses db.find() then it generates code and parses it again.

Additionally you can return this.break() in leave to stop the traverse.

In my humble opinion it would be much clear:

var new_expr = {
        type: "CallExpression",
        callee: {
            type: "MemberExpression",
            computed: false,
            object: null,
            property: {
                type: "Identifier",
                name: "toArray"
            }
        },
        arguments: []
    };

const ast3 = esprima.parse('db.find()');
estraverse.traverse(ast3, {
    leave: function(node, parent) {
        if (node.type === esprima.Syntax.ExpressionStatement) {
            new_expr.callee.object = node.expression;
            node.expression = new_expr;
            return this.break();      
        }
    },
});

I hope you find this useful.