1 /*
   2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.nashorn.tools.jjs;
  27 
  28 import java.io.File;
  29 import java.io.PrintWriter;
  30 import java.util.ArrayList;
  31 import java.util.List;
  32 import java.util.regex.Pattern;
  33 import javax.swing.JFileChooser;
  34 import javax.swing.filechooser.FileNameExtensionFilter;
  35 import jdk.internal.jline.console.completer.Completer;
  36 import jdk.internal.jline.console.UserInterruptException;
  37 import jdk.nashorn.api.tree.AssignmentTree;
  38 import jdk.nashorn.api.tree.BinaryTree;
  39 import jdk.nashorn.api.tree.CompilationUnitTree;
  40 import jdk.nashorn.api.tree.CompoundAssignmentTree;
  41 import jdk.nashorn.api.tree.ConditionalExpressionTree;
  42 import jdk.nashorn.api.tree.ExpressionTree;
  43 import jdk.nashorn.api.tree.ExpressionStatementTree;
  44 import jdk.nashorn.api.tree.FunctionCallTree;
  45 import jdk.nashorn.api.tree.IdentifierTree;
  46 import jdk.nashorn.api.tree.InstanceOfTree;
  47 import jdk.nashorn.api.tree.MemberSelectTree;
  48 import jdk.nashorn.api.tree.NewTree;
  49 import jdk.nashorn.api.tree.SimpleTreeVisitorES5_1;
  50 import jdk.nashorn.api.tree.Tree;
  51 import jdk.nashorn.api.tree.UnaryTree;
  52 import jdk.nashorn.api.tree.Parser;
  53 import jdk.nashorn.api.scripting.NashornException;
  54 import jdk.nashorn.tools.PartialParser;
  55 import jdk.nashorn.internal.objects.NativeSyntaxError;
  56 import jdk.nashorn.internal.objects.Global;
  57 import jdk.nashorn.internal.runtime.ECMAException;
  58 import jdk.nashorn.internal.runtime.Context;
  59 import jdk.nashorn.internal.runtime.ScriptEnvironment;
  60 import jdk.nashorn.internal.runtime.ScriptRuntime;
  61 
  62 /**
  63  * A simple source completer for nashorn. Handles code completion for
  64  * expressions as well as handles incomplete single line code.
  65  */
  66 final class NashornCompleter implements Completer {
  67     private final Context context;
  68     private final Global global;
  69     private final ScriptEnvironment env;
  70     private final PartialParser partialParser;
  71     private final PropertiesHelper propsHelper;
  72     private final Parser parser;
  73     private static final boolean BACKSLASH_FILE_SEPARATOR = File.separatorChar == '\\';
  74 
  75     NashornCompleter(final Context context, final Global global,
  76             final PartialParser partialParser, final PropertiesHelper propsHelper) {
  77         this.context = context;
  78         this.global = global;
  79         this.env = context.getEnv();
  80         this.partialParser = partialParser;
  81         this.propsHelper = propsHelper;
  82         this.parser = createParser(env);
  83     }
  84 
  85 
  86     /**
  87      * Is this a ECMAScript SyntaxError thrown for parse issue at the given line and column?
  88      *
  89      * @param exp Throwable to check
  90      * @param line line number to check
  91      * @param column column number to check
  92      *
  93      * @return true if the given Throwable is a ECMAScript SyntaxError at given line, column
  94      */
  95     boolean isSyntaxErrorAt(final Throwable exp, final int line, final int column) {
  96         if (exp instanceof ECMAException) {
  97             final ECMAException eexp = (ECMAException)exp;
  98             if (eexp.getThrown() instanceof NativeSyntaxError) {
  99                 return isParseErrorAt(eexp.getCause(), line, column);
 100             }
 101         }
 102 
 103         return false;
 104     }
 105 
 106     /**
 107      * Is this a parse error at the given line and column?
 108      *
 109      * @param exp Throwable to check
 110      * @param line line number to check
 111      * @param column column number to check
 112      *
 113      * @return true if the given Throwable is a parser error at given line, column
 114      */
 115     boolean isParseErrorAt(final Throwable exp, final int line, final int column) {
 116         if (exp instanceof NashornException) {
 117             final NashornException nexp = (NashornException)exp;
 118             return nexp.getLineNumber() == line && nexp.getColumnNumber() == column;
 119         }
 120         return false;
 121     }
 122 
 123 
 124     /**
 125      * Read more lines of code if we got SyntaxError at EOF and we can it fine by
 126      * by reading more lines of code from the user. This is used for multiline editing.
 127      *
 128      * @param firstLine First line of code from the user
 129      * @param exp       Exception thrown by evaluting first line code
 130      * @param in        Console to get read more lines from the user
 131      * @param prompt    Prompt to be printed to read more lines from the user
 132      * @param err       PrintWriter to print any errors in the proecess of reading
 133      *
 134      * @return Complete code read from the user including the first line. This is null
 135      *         if any error or the user discarded multiline editing by Ctrl-C.
 136      */
 137     String readMoreLines(final String firstLine, final Exception exp, final Console in,
 138             final String prompt, final PrintWriter err) {
 139         int line = 1;
 140         final StringBuilder buf = new StringBuilder(firstLine);
 141         while (true) {
 142             buf.append('\n');
 143             String curLine = null;
 144             try {
 145                 curLine = in.readLine(prompt);
 146                 buf.append(curLine);
 147                 line++;
 148             } catch (final Throwable th) {
 149                 if (th instanceof UserInterruptException) {
 150                     // Ctrl-C from user - discard the whole thing silently!
 151                     return null;
 152                 } else {
 153                     // print anything else -- but still discard the code
 154                     err.println(th);
 155                     if (env._dump_on_error) {
 156                         th.printStackTrace(err);
 157                     }
 158                     return null;
 159                 }
 160             }
 161 
 162             final String allLines = buf.toString();
 163             try {
 164                 parser.parse("<shell>", allLines, null);
 165             } catch (final Exception pexp) {
 166                 // Do we have a parse error at the end of current line?
 167                 // If so, read more lines from the console.
 168                 if (isParseErrorAt(pexp, line, curLine.length())) {
 169                     continue;
 170                 } else {
 171                     // print anything else and bail out!
 172                     err.println(pexp);
 173                     if (env._dump_on_error) {
 174                         pexp.printStackTrace(err);
 175                     }
 176                     return null;
 177                 }
 178             }
 179 
 180             // We have complete parseable code!
 181             return buf.toString();
 182         }
 183     }
 184 
 185     // Pattern to match a unfinished member selection expression. object part and "."
 186     // but property name missing pattern.
 187     private static final Pattern SELECT_PROP_MISSING = Pattern.compile(".*\\.\\s*");
 188 
 189     // Pattern to match load call
 190     private static final Pattern LOAD_CALL = Pattern.compile("\\s*load\\s*\\(\\s*");
 191 
 192     @Override
 193     public int complete(final String test, final int cursor, final List<CharSequence> result) {
 194         // check that cursor is at the end of test string. Do not complete in the middle!
 195         if (cursor != test.length()) {
 196             return cursor;
 197         }
 198 
 199         // get the start of the last expression embedded in the given code
 200         // using the partial parsing support - so that we can complete expressions
 201         // inside statements, function call argument lists, array index etc.
 202         final int exprStart = partialParser.getLastExpressionStart(context, test);
 203         if (exprStart == -1) {
 204             return cursor;
 205         }
 206 
 207 
 208         // extract the last expression string
 209         final String exprStr = test.substring(exprStart);
 210 
 211         // do we have an incomplete member selection expression that misses property name?
 212         final boolean endsWithDot = SELECT_PROP_MISSING.matcher(exprStr).matches();
 213 
 214         // If this is an incomplete member selection, then it is not legal code.
 215         // Make it legal by adding a random property name "x" to it.
 216         final String completeExpr = endsWithDot? exprStr + "x" : exprStr;
 217 
 218         final ExpressionTree topExpr = getTopLevelExpression(parser, completeExpr);
 219         if (topExpr == null) {
 220             // special case for load call that looks like "load(" with optional whitespaces
 221             if (LOAD_CALL.matcher(test).matches()) {
 222                 // throw a file dialog box
 223                 final JFileChooser chooser = new JFileChooser();
 224                 chooser.setFileFilter(new FileNameExtensionFilter("JavaScript Files", "js"));
 225                 int retVal = chooser.showOpenDialog(null);
 226                 if (retVal == JFileChooser.APPROVE_OPTION) {
 227                     String name = chooser.getSelectedFile().getAbsolutePath();
 228                     // handle '\' file separator
 229                     if (BACKSLASH_FILE_SEPARATOR) {
 230                         name = name.replace("\\", "\\\\");
 231                     }
 232                     result.add("\"" + name + "\")");
 233                     return cursor + name.length() + 3;
 234                 }
 235             }
 236 
 237             // did not parse to be a top level expression, no suggestions!
 238             return cursor;
 239         }
 240 
 241 
 242         // Find 'right most' expression of the top level expression
 243         final Tree rightMostExpr = getRightMostExpression(topExpr);
 244         if (rightMostExpr instanceof MemberSelectTree) {
 245             return completeMemberSelect(exprStr, cursor, result, (MemberSelectTree)rightMostExpr, endsWithDot);
 246         } else if (rightMostExpr instanceof IdentifierTree) {
 247             return completeIdentifier(exprStr, cursor, result, (IdentifierTree)rightMostExpr);
 248         } else {
 249             // expression that we cannot handle for completion
 250             return cursor;
 251         }
 252     }
 253 
 254     // Internals only below this point
 255 
 256     // fill properties of the incomplete member expression
 257     private int completeMemberSelect(final String exprStr, final int cursor, final List<CharSequence> result,
 258                 final MemberSelectTree select, final boolean endsWithDot) {
 259         final ExpressionTree objExpr = select.getExpression();
 260         final String objExprCode = exprStr.substring((int)objExpr.getStartPosition(), (int)objExpr.getEndPosition());
 261 
 262         // try to evaluate the object expression part as a script
 263         Object obj = null;
 264         try {
 265             obj = context.eval(global, objExprCode, global, "<suggestions>");
 266         } catch (Exception exp) {
 267             // throw away the exception - this is during tab-completion
 268             if (Main.DEBUG) {
 269                 exp.printStackTrace();
 270             }
 271         }
 272 
 273         if (obj != null && obj != ScriptRuntime.UNDEFINED) {
 274             if (endsWithDot) {
 275                 // no user specified "prefix". List all properties of the object
 276                 result.addAll(propsHelper.getProperties(obj));
 277                 return cursor;
 278             } else {
 279                 // list of properties matching the user specified prefix
 280                 final String prefix = select.getIdentifier();
 281                 result.addAll(propsHelper.getProperties(obj, prefix));
 282                 return cursor - prefix.length();
 283             }
 284         }
 285 
 286         return cursor;
 287     }
 288 
 289     // fill properties for the given (partial) identifer
 290     private int completeIdentifier(final String test, final int cursor, final List<CharSequence> result,
 291                 final IdentifierTree ident) {
 292         final String name = ident.getName();
 293         result.addAll(propsHelper.getProperties(global, name));
 294         return cursor - name.length();
 295     }
 296 
 297     // returns ExpressionTree if the given code parses to a top level expression.
 298     // Or else returns null.
 299     private ExpressionTree getTopLevelExpression(final Parser parser, final String code) {
 300         try {
 301             final CompilationUnitTree cut = parser.parse("<code>", code, null);
 302             final List<? extends Tree> stats = cut.getSourceElements();
 303             if (stats.size() == 1) {
 304                 final Tree stat = stats.get(0);
 305                 if (stat instanceof ExpressionStatementTree) {
 306                     return ((ExpressionStatementTree)stat).getExpression();
 307                 }
 308             }
 309         } catch (final NashornException ignored) {
 310             // ignore any parser error. This is for completion anyway!
 311             // And user will get that error later when the expression is evaluated.
 312         }
 313 
 314         return null;
 315     }
 316 
 317     // get the right most expreesion of the given expression
 318     private Tree getRightMostExpression(final ExpressionTree expr) {
 319         return expr.accept(new SimpleTreeVisitorES5_1<Tree, Void>() {
 320             @Override
 321             public Tree visitAssignment(final AssignmentTree at, final Void v) {
 322                 return getRightMostExpression(at.getExpression());
 323             }
 324 
 325             @Override
 326             public Tree visitCompoundAssignment(final CompoundAssignmentTree cat, final Void v) {
 327                 return getRightMostExpression(cat.getExpression());
 328             }
 329 
 330             @Override
 331             public Tree visitConditionalExpression(final ConditionalExpressionTree cet, final Void v) {
 332                 return getRightMostExpression(cet.getFalseExpression());
 333             }
 334 
 335             @Override
 336             public Tree visitBinary(final BinaryTree bt, final Void v) {
 337                 return getRightMostExpression(bt.getRightOperand());
 338             }
 339 
 340             @Override
 341             public Tree visitIdentifier(final IdentifierTree ident, final Void v) {
 342                 return ident;
 343             }
 344 
 345 
 346             @Override
 347             public Tree visitInstanceOf(final InstanceOfTree it, final Void v) {
 348                 return it.getType();
 349             }
 350 
 351 
 352             @Override
 353             public Tree visitMemberSelect(final MemberSelectTree select, final Void v) {
 354                 return select;
 355             }
 356 
 357             @Override
 358             public Tree visitNew(final NewTree nt, final Void v) {
 359                 final ExpressionTree call = nt.getConstructorExpression();
 360                 if (call instanceof FunctionCallTree) {
 361                     final ExpressionTree func = ((FunctionCallTree)call).getFunctionSelect();
 362                     // Is this "new Foo" or "new obj.Foo" with no user arguments?
 363                     // If so, we may be able to do completion of constructor name.
 364                     if (func.getEndPosition() == nt.getEndPosition()) {
 365                         return func;
 366                     }
 367                 }
 368                 return null;
 369             }
 370 
 371             @Override
 372             public Tree visitUnary(final UnaryTree ut, final Void v) {
 373                 return getRightMostExpression(ut.getExpression());
 374             }
 375         }, null);
 376     }
 377 
 378     // create a Parser instance that uses compatible command line options of the
 379     // current ScriptEnvironment being used for REPL.
 380     private static Parser createParser(final ScriptEnvironment env) {
 381         final List<String> args = new ArrayList<>();
 382         if (env._const_as_var) {
 383             args.add("--const-as-var");
 384         }
 385 
 386         if (env._no_syntax_extensions) {
 387             args.add("-nse");
 388         }
 389 
 390         if (env._scripting) {
 391             args.add("-scripting");
 392         }
 393 
 394         if (env._strict) {
 395             args.add("-strict");
 396         }
 397 
 398         return Parser.create(args.toArray(new String[0]));
 399     }
 400 }