© Fu Cheng 2018

Fu Cheng, Exploring Java 9, https://doi.org/10.1007/978-1-4842-3330-6_11

11. Nashorn

Fu Cheng

(1)Auckland, New Zealand

Nashorn ( http://openjdk.java.net/projects/nashorn/ ) is the JavaScript engine that was introduced in Java 8 to replace the old engine based on Mozilla Rhino. In Java 8 Nashorn was based on ECMAScript 5. Some new features of ECMAScript 6 have already been implemented in the Nashorn engine in Java 9. The Nashorn-related API is in the module jdk.scripting.nashorn.

Getting the Nashorn Engine

You can use the JSR 223 ( https://www.jcp.org/en/jsr/detail?id=223 ) ScriptEngine to get the Nashorn engine; see Listing 11-1. The expression new ScriptEngineManager().getEngineByName("Nashorn") is the standard way to get an instance of the Nashorn ScriptEngine. However, the created ScriptEngine is only for ECMAScript 5. You need to pass the extra argument --language=es6 to enable ECMAScript 6 features. Listing 11-1 shows you how to use the class NashornScriptEngineFactory directly to create the ScriptEngine for ECMAScript 6.

Listing 11-1. Creating the Nashorn ScriptEngine
public class NashornTest {
  private final ScriptEngine es5Engine =
      new ScriptEngineManager().getEngineByName("Nashorn");
  private final ScriptEngine es6Engine =
      new NashornScriptEngineFactory().getScriptEngine("--language=es6");


  @Test
  public void testSimpleEval() throws Exception {
    assertEquals(2, es5Engine.eval("1 + 1"));
  }
}

You can also use the jjs tool to try out ECMAScript 6’s new features.

$ jjs --language=es6

ECMAScript 6 Features

The following sections show ECMAScript 6’s features that are supported by Nashorn in Java 9.

Template Strings

Nashorn supports the ECMAScript 6 template strings ( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings ), untagged and tagged; see Listing 11-2.

Listing 11-2. Template Strings
@Test
public void testTemplateString() throws Exception {
  final Bindings bindings = new SimpleBindings();
  bindings.put("name", "Alex");
  final Object result = this.es6Engine.eval("Hello ${name}", bindings);
  assertEquals("Hello Alex", result);
}

Binary and Octal Literals

Nashorn also supports binary (b) and octal (o) literals; see Listing 11-3.

Listing 11-3. Binary and Octal Literals
@Test
public void testBinaryAndOctalLiterals() throws Exception {
  assertEquals(503, this.es6Engine.eval("0b111110111"));
  assertEquals(503, this.es6Engine.eval("0o767"));
}

Iterators and for..of Loops

Nashorn supports iterators and for..of loops. The JavaScript code in Listing 11-4 creates a random iterator to generate random numbers, then it uses a for..of loop to get the first 10 elements in the iterator.

Listing 11-4. JavaScript Iterator
let random = {
  [Symbol.iterator]() {
  return {
      next() {
        return { done: false, value: Math.random() }
      }
    }
  }
}


let result = [];
for (var n of random) {
  if (result.length >= 10)
    break;
  result.push(n);
}


result

The method eval() in Listing 11-5 takes a JavaScript file name and evaluates it with a Bindings object. Since the JavaScript code in Listing 11-4 returns an array, the return value of evaluation is a jdk.nashorn.api.scripting.ScriptObjectMirror object. In Listing 11-5, I use its method isArray() to verify that the value is an array and check that the array size is 10.

Listing 11-5. Evaluating JavaScript in Files
@Test
public void testIterator() throws Exception {
  final ScriptObjectMirror result =
      (ScriptObjectMirror) eval("iterator", null);
  assertTrue(result.isArray());
  assertEquals(10, result.size());
}


private Object eval(final String fileName, final Bindings inputBindings)
    throws ScriptException {
  final Bindings bindings = Optional.ofNullable(inputBindings)
      .orElse(new SimpleBindings());
  return this.es6Engine.eval(
      new InputStreamReader(
          NashornTest.class.getResourceAsStream(
              String.format("/%s.js", fileName)
          )),
      bindings
  );
}

Functions

In Nashorn, you can use the arrow functions ( https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions ). Listing 11-6 is the content of the JavaScript file function.js. It uses the arrow to create the function add.

Listing 11-6. JavaScript Arrow Functions
let add = (a, b) => a + b;

add(1, 2)

In Listing 11-7, I verify the result of the function invocation in Listing 11-6 using the method eval().

Listing 11-7. Verifying the Result of the Function Invocation
@Test
public void testFunction() throws Exception {
  final Object result = eval("function", null);
  assertEquals(3, result);
}

Parser API

Nashorn has a standard parser API you can use to parse ECMAScript source code into abstract syntax trees (ASTs) . This API is useful when you want to perform analysis of ECMAScript source code. With this API, you can get information about the source code, but you cannot modify existing source code. The parser API is in the package jdk.nashorn.api.tree.

Basic Parsing

In the method testSimpleParser() of Listing 11-8, I create a new instance of Parser using the method Parser.create(String… options). The parameter options is a list of options to configure the parser’s behavior. Here I use --language=es6 to enable the ECMAScript 6 parsing mode. Parser has different parse() methods for parsing source code. All these parse() methods take arguments to specify the source code using File, Reader, String, URL, Path, or jdk.nashorn.api.scripting.ScriptObjectMirror objects and use another argument of type DiagnosticListener to specify a listener to receive parsing errors. Here I parse the JavaScript code function a() {return 'hello';}, which only contains one function declaration. The interface DiagnosticListener only contains one method report(Diagnostic diagnostic). The interface Diagnostic contains methods for retrieving information about diagnostic data, including kind, code, message, line number, column number, and file name; see Table 11-1. Here I simply print out the data to console.

Table 11-1. Methods of Diagnostic

Method

Description

Diagnostic.Kind getKind()

Returns what kind of diagnostic this is

long getPosition()

Returns the character offset that indicates the location of the problem

String getFileName()

Returns the source file name

long getLineNumber()

Returns the line number of the character offset returned by getPosition()

long getColumnNumber()

Returns the column number of the character offset returned by getPosition()

String getCode()

Returns a code indicating the type of diagnostic

String getMessage()

Returns a message for this diagnostic

The enum Diagnostic.Kind represents different kinds of diagnostics. It has the following values: ERROR, WARNING, MANDATORY_WARNING, NOTE, and OTHER.

The return value of parse() is an instance of the interface CompilationUnitTree, which represents the parsed abstract syntax tree. The method accept(TreeVisitor<R,D> visitor, D data) accepts a visitor to visit the tree using the visitor pattern. Nashorn provides classes, SimpleTreeVisitorES5_1 and SimpleTreeVisitorES6, as simple implementations for ECMAScript 5.1 and 6, respectively. Listing 11-8 extends the class SimpleTreeVisitorES6 and overrides the method visitFunctionDeclaration() to verify that there is one function declaration in the source code.

Listing 11-8. Simple JavaScript Code Parsing
public class ParserAPITest {
  @Test
  public void testSimpleParser() throws Exception {
    final Parser parser = Parser.create("--language=es6");
    final CompilationUnitTree tree =
        parser.parse(
            "test.js",
            "function a() {return 'hello';}",
            System.out::println
        );
    final List<String> functions = new ArrayList<>();
    tree.accept(new SimpleTreeVisitorES6<Void, Void>() {
      @Override
      public Void visitFunctionDeclaration(
          final FunctionDeclarationTree node,
          final Void r) {
        final String name = node.getName().getName();
        assertEquals("a", name);
        functions.add(name);
        return null;
      }
    }, null);
    assertEquals(1, functions.size());
  }
}

Parsing Error

In Listing 11-9, I parse the JavaScript code that has a syntax error. I provide an implementation of DiagnosticListener that verifies the line number of the error.

Listing 11-9. JavaScript Code Parse Error
@Test
public void testParseError() throws Exception {
  final Parser parser = Parser.create("--language=es6");
  final List<Diagnostic> errors = new ArrayList<>();
  parser.parse(
    "error.js",
    "function error() { var a = 1;",
    diagnostic -> {
      assertEquals(1, diagnostic.getLineNumber());
      assertEquals(Diagnostic.Kind.ERROR, diagnostic.getKind());
      errors.add(diagnostic);
    }
  );
  assertEquals(1, errors.size());
}

Analyzing Function Complexity

Now I’ll show you a complicated example using the parser API. Listing 11-10 shows the class FunctionLengthAnalyzer that analyzes the length of functions in the source code. I add visitors for FunctionDeclarationTree and FunctionExpressionTree. To measure the length of a function, I first get the start and end character offset of the function body in the source code using the methods getStartPosition() and getEndPosition(), respectively. Then I calculate the number of characters in the method body by subtracting these two offsets. For FunctionExpressionTree nodes, the function can be anonymous, so I create a name for these anonymous functions. Here I analyze the source code of jQuery 3.

Listing 11-10. Analyzing the Length of Functions
import com.google.common.collect.Lists;
import io.vavr.Tuple2;
import jdk.nashorn.api.tree.*;


import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;


public class FunctionLengthAnalyzer {
  public List<Tuple2<String, Long>> analyze(final URL url) throws IOException {
    final Parser parser = Parser.create();
    final CompilationUnitTree tree = parser.parse(url, System.out::println);
    final List<Tuple2<String, Long>> result = Lists.newArrayList();
    tree.accept(new SimpleTreeVisitorES5_1<Void, Void>() {
      @Override
      public Void visitFunctionDeclaration(
          final FunctionDeclarationTree node,
          final Void r) {
        result.add(new Tuple2<>(
            node.getName().getName(),
            node.getBody().getEndPosition() - node.getBody().getStartPosition()));
        return super.visitFunctionDeclaration(node, r);
      }


      @Override
      public Void visitFunctionExpression(
          final FunctionExpressionTree node,
          final Void r) {
        final String name = Optional.ofNullable(node.getName())
            .map(IdentifierTree::getName)
            .orElse("anonymous_" + node.getBody().getStartPosition());
        result.add(new Tuple2<>(
            name,
            node.getBody().getEndPosition() - node.getBody().getStartPosition()));
        return super.visitFunctionExpression(node, r);
      }
    }, null);
    Collections.sort(result,
        Collections.reverseOrder(Comparator.comparingLong(Tuple2::_2)));
    return result;
  }


  public static void main(final String[] args) throws IOException {
    final List<Tuple2<String, Long>> result =
        new FunctionLengthAnalyzer().analyze(
            new URL("https://code.jquery.com/jquery-3.2.1.js"));
    result.forEach(tuple2 ->
        System.out.printf("%30s -> %s%n", tuple2._1, tuple2._2));
  }
}

The function length calculated in Listing 11-10 is not very intuitive. It would be better if I could get the start and end line number of a function body and then calculate the lines of code for a function. Unfortunately, the line number is not exposed in the parser API. I can only use the character offset in the source code. For FunctionDeclarationTree nodes, the method getBody() returns a BlockTree object that has the method getStatements() to return a list of statements in the method body. I can also use the number of statements to measure the complexity of a function.

Summary

In this chapter, we discussed the new ECMAScript 6 features supported by Nashorn in Java 9 and the new parser API you can use to parse JavaScript code. In the next chapter, we’ll discuss changes related to I/O in Java 9.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.182.62