Get last evaluated expression inside function
Asked Answered
C

1

0

This is related to this other question:

Last evaluated expression in Javascript

But I wanted to provide more details about what I wanted to do and show how I finally solved the problem as some users requested in the comments.

I have snippets of Javascript that are written by users of my app. This snippets need to go to a kind of template like this:

var foo1 = function(data, options) {
    <snippet of code written by user>  
}

var foo2 = function(data, options) {
    <snippet of code written by user>  
}

...

Expressions can be very different, from simple things like this:

data.price * data.qty

To more complex tings like this:

if (data.isExternal) {
    data.email;
} else {
    data.userId;
}

The value returned by the function should be always the last evaluated expression.

Before we had something like this:

var foo1 = function(data, options) {
    return eval(<snippet of code written by user>);
}

But due to optimizations and changes we are making, we cannot keep using eval, but we need to return the last evaluated expression.

Just adding a 'return' keyword won't work because expressions can have several statements. So I need to make those functions return the last evaluated expressions.

Restrictions and clarification:

  • I cannot force users to add the 'return' keyword to all the scripts they have because there are many scripts written already and it is not very intuitive for simple expressions like 'a * b'.
  • I'm using Java and Rhino to run Javascripts on server side.
Cinerama answered 28/5, 2014 at 18:33 Comment(0)
C
0

As people pointed out in Last evaluated expression in Javascript, getting the last evaluated expression is not possible in standard Javascript.

What I finally ended up doing, as suggested by FelixKling, was to manipulate the AST of the script written by the user. This way I store the user written script and the modified version, which is the one I finally run.

For manipulating the AST I used Rhino and basically modify all EXPR_RESULT nodes to store the result in a variable that I finally return at the end of the script. Here is the code to do that:

public class ScriptsManipulationService { private static final Logger logger = LoggerFactory.getLogger(ScriptsManipulationService.class);

public String returnLastExpressionEvaluated(String script) {
    Parser jsParser = new Parser();
    try {
        AstRoot ast = jsParser.parse(script, "script", 1);
        ast.getType();
        ast.visitAll(new NodeVisitor() {
            @Override
            public boolean visit(AstNode node) {
                if (node.getType() == Token.EXPR_RESULT) {
                    ExpressionStatement exprNode = (ExpressionStatement) node;
                    Assignment assignmentNode = createAssignmentNode("_returnValue", exprNode.getExpression());
                    assignmentNode.setParent(exprNode);
                    exprNode.setExpression(assignmentNode);
                }
                return true;
            }
        });
        StringBuilder result = new StringBuilder();
        result.append("var _returnValue;\n");
        result.append(ast.toSource());
        result.append("return _returnValue;\n");
        return result.toString();
    } catch (Exception e) {
        logger.debug(LogUtils.format("Error parsing script"), e);
        return script;
    }
}

private Assignment createAssignmentNode(String varName, AstNode rightNode) {
    Assignment assignmentNode = new Assignment();
    assignmentNode.setType(Token.ASSIGN);
    Name leftNode = new Name();
    leftNode.setType(Token.NAME);
    leftNode.setIdentifier(varName);
    leftNode.setParent(assignmentNode);
    assignmentNode.setLeft(leftNode);
    rightNode.setParent(assignmentNode);
    assignmentNode.setRight(rightNode);
    return assignmentNode;
}

}

This way, if you pass the following script:

data.price * data.qty;

You will get back:

var _returnValue;
_returnValue = data.price * data.qty;
return _returnValue;

Or if you pass:

var _returnValue;
if (data.isExternal) {
    _returnValue = data.email;
} else {
    _returnValue = data.userId;
}
return _returnValue;

Please keep in mind that I haven't done an exhaustive testing and will be polishing it over time, but this should show the general idea.

Cinerama answered 28/5, 2014 at 18:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.