17

I have a Java application that allows users to manipulate certain objects at runtime by defining a JavaScript function. We are currently doing this with Nashorn in Java 8, but we are looking to move to Java 11. Once we're on Java 11 we'll be able to offer this functionality in GraalVM instead, but for now we need to maintain compatibility for Java 8 -> Java 11 upgrade of Nashorn scripts.

In Java 11, the behavior of Nashorn when we eval the function appears to differ depending on whether or not the function is named, which was not the case in Java 8. Here's an example using JJS in Java 11:

$ jjs -v
nashorn 11.0.6
Warning: The jjs tool is planned to be removed from a future JDK release
jjs> function foo() {}
jjs> function () {}
function () {}

Note that the first function definition returns nothing. In Java 8 it does return the function even when the function is named:

$ jjs -v
nashorn 1.8.0_252
jjs> function foo() {}
function foo() {}

The way we invoke these scripts currently is through:

CompiledScript compiled = scriptEngine.compile(userProvidedScript);
Object evaled = compiled.eval(bindings);
scriptEngine.invokeMethod(evaled, "call", evaled, ... input parameters ...)

Curious if anyone knows the root cause for this and any good workarounds? I need to support function(...) as well as function foo(...) for back-compat reasons. Since this is done inside our Java application we could potentially wrap the user supplied script somehow, or try to grab the script out of the bindings (which seems error prone, since there can be multiple scripts defined, and the Java 8 behavior would be for the last defined script to be invoked).

CheeseFerret
  • 597
  • 11
  • 21
Joel Westberg
  • 2,656
  • 1
  • 21
  • 27
  • 2
    Have you checked what is defined in the `ECMAScript 5.1` (which should be the standard used by Nashorn in Java 8 and 11) regarding the definition of named functions? May be the behavior in Java 8 was a bug that has been corrected... – Robert Jun 21 '20 at 16:53
  • I can't quite tell from the spec, but I struggle a bit though with even going down that path since `function() {}` on it's own is not a valid statement in `ECMAScript`. It's only valid because of the assignment that happens _outside_ of the script itself (`evaled = function() {}`) where the left hand side of the assignment is in Java and the right hand side is ECMAScript. For what it's worth, nodejs and the chrome browser console both are ok with the assignment `x = function...` regardless of whether it is named or not. – Joel Westberg Jun 21 '20 at 20:39
  • 1
    Adding to the above comment. It looks like anonymous top level `function() {}` is explicitly an extension of the standard in Nashorn https://wiki.openjdk.java.net/display/Nashorn/Nashorn+extensions#Nashornextensions-Anonymousfunctionstatements. The surprising thing here though is that it would behave differently in an eval than a named function would in Java 11, but not in Java 8. – Joel Westberg Jun 21 '20 at 21:05
  • Did you find a workaround here? – yu.pitomets Nov 02 '21 at 14:12
  • @yu.pitomets I answered with what I believe is a robust workaround. Should work with more cases than just the "entire script is one function declaration" case — but needs a little additional code to handle when a function declaration is not the final thing in the script. – AndrewF Feb 16 '22 at 06:18

1 Answers1

0

Probably caused by an issue with Nashorn's custom feature of anonymous function statements (these are actually called "function declarations") accidentally also applying to real named function declarations. Since this is not described in Nashorn docs I assume they didn't want that behavior and got rid of it.

Solution:

Transform the source code so that as its final act, it yields the function object that was defined by your last named function declaration.

Consider my tests to see that this will work in OpenJDK 8 and 11:

nashorn @ 1.8.0_302:

jjs> function bar() { foo(); }; function foo() { bar(); }     
function foo() { bar(); }

jjs> function bar() { foo(); }; function foo() { bar(); }; foo
function foo() { bar(); }

nashorn @ 11.0.6:

jjs> function bar() { foo(); }; function foo() { bar(); }

jjs> function bar() { foo(); }; function foo() { bar(); }; foo
function foo() { bar(); }

To figure out what name to use, you should be able to parse the JS and process its AST using the jdk.nashorn.api.tree package.

Your tree visitor / function name accumulator might look like:

private static class FuncDeclarationVisitor extends SimpleTreeVisitorES5_1<Void, Void> {
    public String lastFunctionName = null;

    @Override
    public Void visitFunctionDeclaration(FunctionDeclarationTree node, Void param) {
        // Anonymous function declaration ==> null
        IdentifierTree functionName = node.getName();
        lastFunctionName = functionName != null ? functionName.getName() : null;
        return super.visitFunctionDeclaration(node, param);
    }
}

You might invoke it like:

CompilationUnitTree parsedTree;     // Use Parser's parse method

FuncDeclarationVisitor visitor = new FuncDeclarationVisitor();
parsedTree.accept(visitor);

return visitor.lastFunctionName;     // Null if the last function declaration was anonymous

But I don't think there's any way to modify the AST and then send it to the NashornScriptEngine's compiler. You probably have to add something to the the source text itself.

Also, you probably also want your visitor to detect other types of nodes that are not function declarations, so that you don't accidentally transform the script and clobber other expressions that might be yielded (non-functions).

AndrewF
  • 1,827
  • 13
  • 25