/*
 * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.nashorn.tools.jjs;

import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;

import jdk.internal.org.jline.reader.Candidate;
import jdk.internal.org.jline.reader.Completer;
import jdk.internal.org.jline.reader.LineReader;
import jdk.internal.org.jline.reader.ParsedLine;
import jdk.internal.org.jline.reader.UserInterruptException;
import jdk.nashorn.api.tree.AssignmentTree;
import jdk.nashorn.api.tree.BinaryTree;
import jdk.nashorn.api.tree.CompilationUnitTree;
import jdk.nashorn.api.tree.CompoundAssignmentTree;
import jdk.nashorn.api.tree.ConditionalExpressionTree;
import jdk.nashorn.api.tree.ExpressionTree;
import jdk.nashorn.api.tree.ExpressionStatementTree;
import jdk.nashorn.api.tree.FunctionCallTree;
import jdk.nashorn.api.tree.IdentifierTree;
import jdk.nashorn.api.tree.InstanceOfTree;
import jdk.nashorn.api.tree.MemberSelectTree;
import jdk.nashorn.api.tree.NewTree;
import jdk.nashorn.api.tree.SimpleTreeVisitorES5_1;
import jdk.nashorn.api.tree.Tree;
import jdk.nashorn.api.tree.UnaryTree;
import jdk.nashorn.api.tree.Parser;
import jdk.nashorn.api.scripting.NashornException;
import jdk.nashorn.tools.PartialParser;
import jdk.nashorn.internal.objects.NativeSyntaxError;
import jdk.nashorn.internal.objects.Global;
import jdk.nashorn.internal.runtime.ECMAException;
import jdk.nashorn.internal.runtime.Context;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import jdk.nashorn.internal.runtime.ScriptFunction;
import jdk.nashorn.internal.runtime.ScriptRuntime;

A simple source completer for nashorn. Handles code completion for expressions as well as handles incomplete single line code.
/** * A simple source completer for nashorn. Handles code completion for * expressions as well as handles incomplete single line code. */
final class NashornCompleter { private final Context context; private final Global global; private final ScriptEnvironment env; private final PartialParser partialParser; private final PropertiesHelper propsHelper; private final ScriptFunction fileChooserFunc; private final Parser parser; private static final boolean BACKSLASH_FILE_SEPARATOR = File.separatorChar == '\\'; NashornCompleter(final Context context, final Global global, final PartialParser partialParser, final PropertiesHelper propsHelper, final ScriptFunction fileChooserFunc) { this.context = context; this.global = global; this.env = context.getEnv(); this.partialParser = partialParser; this.propsHelper = propsHelper; this.fileChooserFunc = fileChooserFunc; this.parser = createParser(env); }
Is this a ECMAScript SyntaxError thrown for parse issue at the given line and column?
Params:
  • exp – Throwable to check
  • line – line number to check
  • column – column number to check
Returns:true if the given Throwable is a ECMAScript SyntaxError at given line, column
/** * Is this a ECMAScript SyntaxError thrown for parse issue at the given line and column? * * @param exp Throwable to check * @param line line number to check * @param column column number to check * * @return true if the given Throwable is a ECMAScript SyntaxError at given line, column */
boolean isSyntaxErrorAt(final Throwable exp, final int line, final int column) { if (exp instanceof ECMAException) { final ECMAException eexp = (ECMAException)exp; if (eexp.getThrown() instanceof NativeSyntaxError) { return isParseErrorAt(eexp.getCause(), line, column); } } return false; }
Is this a parse error at the given line and column?
Params:
  • exp – Throwable to check
  • line – line number to check
  • column – column number to check
Returns:true if the given Throwable is a parser error at given line, column
/** * Is this a parse error at the given line and column? * * @param exp Throwable to check * @param line line number to check * @param column column number to check * * @return true if the given Throwable is a parser error at given line, column */
boolean isParseErrorAt(final Throwable exp, final int line, final int column) { if (exp instanceof NashornException) { final NashornException nexp = (NashornException)exp; return nexp.getLineNumber() == line && nexp.getColumnNumber() == column; } return false; }
Read more lines of code if we got SyntaxError at EOF and we can it fine by by reading more lines of code from the user. This is used for multiline editing.
Params:
  • firstLine – First line of code from the user
  • exp – Exception thrown by evaluting first line code
  • in – Console to get read more lines from the user
  • prompt – Prompt to be printed to read more lines from the user
  • err – PrintWriter to print any errors in the proecess of reading
Returns:Complete code read from the user including the first line. This is null if any error or the user discarded multiline editing by Ctrl-C.
/** * Read more lines of code if we got SyntaxError at EOF and we can it fine by * by reading more lines of code from the user. This is used for multiline editing. * * @param firstLine First line of code from the user * @param exp Exception thrown by evaluting first line code * @param in Console to get read more lines from the user * @param prompt Prompt to be printed to read more lines from the user * @param err PrintWriter to print any errors in the proecess of reading * * @return Complete code read from the user including the first line. This is null * if any error or the user discarded multiline editing by Ctrl-C. */
String readMoreLines(final String firstLine, final Exception exp, final Console in, final String prompt, final PrintWriter err) { int line = 1; final StringBuilder buf = new StringBuilder(firstLine); while (true) { buf.append('\n'); String curLine = null; try { curLine = in.readLine(prompt, prompt); buf.append(curLine); line++; } catch (final Throwable th) { if (th instanceof UserInterruptException) { // Ctrl-C from user - discard the whole thing silently! return null; } else { // print anything else -- but still discard the code err.println(th); if (env._dump_on_error) { th.printStackTrace(err); } return null; } } final String allLines = buf.toString(); try { parser.parse("<shell>", allLines, null); } catch (final Exception pexp) { // Do we have a parse error at the end of current line? // If so, read more lines from the console. if (isParseErrorAt(pexp, line, curLine.length())) { continue; } else { // print anything else and bail out! err.println(pexp); if (env._dump_on_error) { pexp.printStackTrace(err); } return null; } } // We have complete parseable code! return buf.toString(); } } public boolean isComplete(String input) { try { parser.parse("<shell>", input, null); } catch (final Exception pexp) { // Do we have a parse error at the end of current line? // If so, read more lines from the console. int line = input.split("\n").length; int lastLineLen = input.length() - (input.lastIndexOf("\n") + 1); if (isParseErrorAt(pexp, line, lastLineLen)) { return false; } } return true; } // Pattern to match a unfinished member selection expression. object part and "." // but property name missing pattern. private static final Pattern SELECT_PROP_MISSING = Pattern.compile(".*\\.\\s*"); // Pattern to match load call private static final Pattern LOAD_CALL = Pattern.compile("\\s*load\\s*\\(\\s*"); public int complete(String test, int cursor, List<Candidate> candidates) { // check that cursor is at the end of test string. Do not complete in the middle! if (cursor != test.length()) { return cursor; } // get the start of the last expression embedded in the given code // using the partial parsing support - so that we can complete expressions // inside statements, function call argument lists, array index etc. final int exprStart = partialParser.getLastExpressionStart(context, test); if (exprStart == -1) { return cursor; } // extract the last expression string final String exprStr = test.substring(exprStart); // do we have an incomplete member selection expression that misses property name? final boolean endsWithDot = SELECT_PROP_MISSING.matcher(exprStr).matches(); // If this is an incomplete member selection, then it is not legal code. // Make it legal by adding a random property name "x" to it. final String completeExpr = endsWithDot? exprStr + "x" : exprStr; final ExpressionTree topExpr = getTopLevelExpression(parser, completeExpr); if (topExpr == null) { // Special case for load call that looks like "load(" with optional whitespaces. // If we have a fileChooserFunc then call it, so that the user can select a file. if (fileChooserFunc != null && LOAD_CALL.matcher(test).matches()) { String name = readFileName(context.getErr()); if (name != null) { // handle '\' file separator if (BACKSLASH_FILE_SEPARATOR) { name = name.replace("\\", "\\\\"); } candidates.add(createCandidate("\"" + name + "\")")); return cursor + name.length() + 3; } } // did not parse to be a top level expression, no suggestions! return cursor; } // Find 'right most' expression of the top level expression final Tree rightMostExpr = getRightMostExpression(topExpr); if (rightMostExpr instanceof MemberSelectTree) { return completeMemberSelect(exprStr, cursor, candidates, (MemberSelectTree)rightMostExpr, endsWithDot); } else if (rightMostExpr instanceof IdentifierTree) { return completeIdentifier(exprStr, cursor, candidates, (IdentifierTree)rightMostExpr); } else { // expression that we cannot handle for completion return cursor; } } // Internals only below this point // read file name from the user using by showing a swing file chooser diablog private String readFileName(final PrintWriter err) { try { final Object res = ScriptRuntime.apply(fileChooserFunc, null); return res instanceof String? (String)res : null; } catch (final Exception e) { err.println(e); if (Main.DEBUG) { e.printStackTrace(); } } return null; } // fill properties of the incomplete member expression private int completeMemberSelect(final String exprStr, final int cursor, final List<Candidate> candidates, final MemberSelectTree select, final boolean endsWithDot) { final ExpressionTree objExpr = select.getExpression(); final String objExprCode = exprStr.substring((int)objExpr.getStartPosition(), (int)objExpr.getEndPosition()); // try to evaluate the object expression part as a script Object obj = null; try { obj = context.eval(global, objExprCode, global, "<suggestions>"); } catch (Exception exp) { // throw away the exception - this is during tab-completion if (Main.DEBUG) { exp.printStackTrace(); } } if (obj != null && obj != ScriptRuntime.UNDEFINED) { if (endsWithDot) { // no user specified "prefix". List all properties of the object propsHelper.getProperties(obj).stream().map(this::createCandidate).forEach(candidates::add); return cursor; } else { // list of properties matching the user specified prefix final String prefix = select.getIdentifier(); propsHelper.getProperties(obj, prefix).stream().map(this::createCandidate).forEach(candidates::add); return cursor - prefix.length(); } } return cursor; } // fill properties for the given (partial) identifer private int completeIdentifier(final String test, final int cursor, final List<Candidate> candidates, final IdentifierTree ident) { final String name = ident.getName(); propsHelper.getProperties(global, name).stream().map(this::createCandidate).forEach(candidates::add); return cursor - name.length(); } // returns ExpressionTree if the given code parses to a top level expression. // Or else returns null. private ExpressionTree getTopLevelExpression(final Parser parser, final String code) { try { final CompilationUnitTree cut = parser.parse("<code>", code, null); final List<? extends Tree> stats = cut.getSourceElements(); if (stats.size() == 1) { final Tree stat = stats.get(0); if (stat instanceof ExpressionStatementTree) { return ((ExpressionStatementTree)stat).getExpression(); } } } catch (final NashornException ignored) { // ignore any parser error. This is for completion anyway! // And user will get that error later when the expression is evaluated. } return null; } // get the right most expreesion of the given expression private Tree getRightMostExpression(final ExpressionTree expr) { return expr.accept(new SimpleTreeVisitorES5_1<Tree, Void>() { @Override public Tree visitAssignment(final AssignmentTree at, final Void v) { return getRightMostExpression(at.getExpression()); } @Override public Tree visitCompoundAssignment(final CompoundAssignmentTree cat, final Void v) { return getRightMostExpression(cat.getExpression()); } @Override public Tree visitConditionalExpression(final ConditionalExpressionTree cet, final Void v) { return getRightMostExpression(cet.getFalseExpression()); } @Override public Tree visitBinary(final BinaryTree bt, final Void v) { return getRightMostExpression(bt.getRightOperand()); } @Override public Tree visitIdentifier(final IdentifierTree ident, final Void v) { return ident; } @Override public Tree visitInstanceOf(final InstanceOfTree it, final Void v) { return it.getType(); } @Override public Tree visitMemberSelect(final MemberSelectTree select, final Void v) { return select; } @Override public Tree visitNew(final NewTree nt, final Void v) { final ExpressionTree call = nt.getConstructorExpression(); if (call instanceof FunctionCallTree) { final ExpressionTree func = ((FunctionCallTree)call).getFunctionSelect(); // Is this "new Foo" or "new obj.Foo" with no user arguments? // If so, we may be able to do completion of constructor name. if (func.getEndPosition() == nt.getEndPosition()) { return func; } } return null; } @Override public Tree visitUnary(final UnaryTree ut, final Void v) { return getRightMostExpression(ut.getExpression()); } }, null); } // create a Parser instance that uses compatible command line options of the // current ScriptEnvironment being used for REPL. private static Parser createParser(final ScriptEnvironment env) { final List<String> args = new ArrayList<>(); if (env._const_as_var) { args.add("--const-as-var"); } if (env._no_syntax_extensions) { args.add("-nse"); } if (env._scripting) { args.add("-scripting"); } if (env._strict) { args.add("-strict"); } if (env._es6) { args.add("--language=es6"); } return Parser.create(args.toArray(new String[0])); } private Candidate createCandidate(String value) { return new Candidate(value, value, null, null, null, null, false); } }