1 /*
   2  * Copyright (c) 2015, 2018, 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.Objects;
  33 import java.util.regex.Pattern;
  34 
  35 import jdk.internal.org.jline.reader.Candidate;
  36 import jdk.internal.org.jline.reader.Completer;
  37 import jdk.internal.org.jline.reader.LineReader;
  38 import jdk.internal.org.jline.reader.ParsedLine;
  39 import jdk.internal.org.jline.reader.UserInterruptException;
  40 import jdk.nashorn.api.tree.AssignmentTree;
  41 import jdk.nashorn.api.tree.BinaryTree;
  42 import jdk.nashorn.api.tree.CompilationUnitTree;
  43 import jdk.nashorn.api.tree.CompoundAssignmentTree;
  44 import jdk.nashorn.api.tree.ConditionalExpressionTree;
  45 import jdk.nashorn.api.tree.ExpressionTree;
  46 import jdk.nashorn.api.tree.ExpressionStatementTree;
  47 import jdk.nashorn.api.tree.FunctionCallTree;
  48 import jdk.nashorn.api.tree.IdentifierTree;
  49 import jdk.nashorn.api.tree.InstanceOfTree;
  50 import jdk.nashorn.api.tree.MemberSelectTree;
  51 import jdk.nashorn.api.tree.NewTree;
  52 import jdk.nashorn.api.tree.SimpleTreeVisitorES5_1;
  53 import jdk.nashorn.api.tree.Tree;
  54 import jdk.nashorn.api.tree.UnaryTree;
  55 import jdk.nashorn.api.tree.Parser;
  56 import jdk.nashorn.api.scripting.NashornException;
  57 import jdk.nashorn.tools.PartialParser;
  58 import jdk.nashorn.internal.objects.NativeSyntaxError;
  59 import jdk.nashorn.internal.objects.Global;
  60 import jdk.nashorn.internal.runtime.ECMAException;
  61 import jdk.nashorn.internal.runtime.Context;
  62 import jdk.nashorn.internal.runtime.ScriptEnvironment;
  63 import jdk.nashorn.internal.runtime.ScriptFunction;
  64 import jdk.nashorn.internal.runtime.ScriptRuntime;
  65 
  66 /**
  67  * A simple source completer for nashorn. Handles code completion for
  68  * expressions as well as handles incomplete single line code.
  69  */
  70 final class NashornCompleter {
  71     private final Context context;
  72     private final Global global;
  73     private final ScriptEnvironment env;
  74     private final PartialParser partialParser;
  75     private final PropertiesHelper propsHelper;
  76     private final ScriptFunction fileChooserFunc;
  77     private final Parser parser;
  78     private static final boolean BACKSLASH_FILE_SEPARATOR = File.separatorChar == '\\';
  79 
  80     NashornCompleter(final Context context, final Global global,
  81             final PartialParser partialParser, final PropertiesHelper propsHelper,
  82             final ScriptFunction fileChooserFunc) {
  83         this.context = context;
  84         this.global = global;
  85         this.env = context.getEnv();
  86         this.partialParser = partialParser;
  87         this.propsHelper = propsHelper;
  88         this.fileChooserFunc = fileChooserFunc;
  89         this.parser = createParser(env);
  90     }
  91 
  92 
  93     /**
  94      * Is this a ECMAScript SyntaxError thrown for parse issue at the given line and column?
  95      *
  96      * @param exp Throwable to check
  97      * @param line line number to check
  98      * @param column column number to check
  99      *
 100      * @return true if the given Throwable is a ECMAScript SyntaxError at given line, column
 101      */
 102     boolean isSyntaxErrorAt(final Throwable exp, final int line, final int column) {
 103         if (exp instanceof ECMAException) {
 104             final ECMAException eexp = (ECMAException)exp;
 105             if (eexp.getThrown() instanceof NativeSyntaxError) {
 106                 return isParseErrorAt(eexp.getCause(), line, column);
 107             }
 108         }
 109 
 110         return false;
 111     }
 112 
 113     /**
 114      * Is this a parse error at the given line and column?
 115      *
 116      * @param exp Throwable to check
 117      * @param line line number to check
 118      * @param column column number to check
 119      *
 120      * @return true if the given Throwable is a parser error at given line, column
 121      */
 122     boolean isParseErrorAt(final Throwable exp, final int line, final int column) {
 123         if (exp instanceof NashornException) {
 124             final NashornException nexp = (NashornException)exp;
 125             return nexp.getLineNumber() == line && nexp.getColumnNumber() == column;
 126         }
 127         return false;
 128     }
 129 
 130 
 131     /**
 132      * Read more lines of code if we got SyntaxError at EOF and we can it fine by
 133      * by reading more lines of code from the user. This is used for multiline editing.
 134      *
 135      * @param firstLine First line of code from the user
 136      * @param exp       Exception thrown by evaluting first line code
 137      * @param in        Console to get read more lines from the user
 138      * @param prompt    Prompt to be printed to read more lines from the user
 139      * @param err       PrintWriter to print any errors in the proecess of reading
 140      *
 141      * @return Complete code read from the user including the first line. This is null
 142      *         if any error or the user discarded multiline editing by Ctrl-C.
 143      */
 144     String readMoreLines(final String firstLine, final Exception exp, final Console in,
 145             final String prompt, final PrintWriter err) {
 146         int line = 1;
 147         final StringBuilder buf = new StringBuilder(firstLine);
 148         while (true) {
 149             buf.append('\n');
 150             String curLine = null;
 151             try {
 152                 curLine = in.readLine(prompt, prompt);
 153                 buf.append(curLine);
 154                 line++;
 155             } catch (final Throwable th) {
 156                 if (th instanceof UserInterruptException) {
 157                     // Ctrl-C from user - discard the whole thing silently!
 158                     return null;
 159                 } else {
 160                     // print anything else -- but still discard the code
 161                     err.println(th);
 162                     if (env._dump_on_error) {
 163                         th.printStackTrace(err);
 164                     }
 165                     return null;
 166                 }
 167             }
 168 
 169             final String allLines = buf.toString();
 170             try {
 171                 parser.parse("<shell>", allLines, null);
 172             } catch (final Exception pexp) {
 173                 // Do we have a parse error at the end of current line?
 174                 // If so, read more lines from the console.
 175                 if (isParseErrorAt(pexp, line, curLine.length())) {
 176                     continue;
 177                 } else {
 178                     // print anything else and bail out!
 179                     err.println(pexp);
 180                     if (env._dump_on_error) {
 181                         pexp.printStackTrace(err);
 182                     }
 183                     return null;
 184                 }
 185             }
 186 
 187             // We have complete parseable code!
 188             return buf.toString();
 189         }
 190     }
 191 
 192     public boolean isComplete(String input) {
 193         try {
 194             parser.parse("<shell>", input, null);
 195         } catch (final Exception pexp) {
 196             // Do we have a parse error at the end of current line?
 197             // If so, read more lines from the console.
 198             int line = input.split("\n").length;
 199             int lastLineLen = input.length() - (input.lastIndexOf("\n") + 1);
 200 
 201             if (isParseErrorAt(pexp, line, lastLineLen)) {
 202                 return false;
 203             }
 204         }
 205         return true;
 206     }
 207 
 208     // Pattern to match a unfinished member selection expression. object part and "."
 209     // but property name missing pattern.
 210     private static final Pattern SELECT_PROP_MISSING = Pattern.compile(".*\\.\\s*");
 211 
 212     // Pattern to match load call
 213     private static final Pattern LOAD_CALL = Pattern.compile("\\s*load\\s*\\(\\s*");
 214 
 215     public int complete(String test, int cursor, List<Candidate> candidates) {
 216         // check that cursor is at the end of test string. Do not complete in the middle!
 217         if (cursor != test.length()) {
 218             return cursor;
 219         }
 220 
 221         // get the start of the last expression embedded in the given code
 222         // using the partial parsing support - so that we can complete expressions
 223         // inside statements, function call argument lists, array index etc.
 224         final int exprStart = partialParser.getLastExpressionStart(context, test);
 225         if (exprStart == -1) {
 226             return cursor;
 227         }
 228 
 229 
 230         // extract the last expression string
 231         final String exprStr = test.substring(exprStart);
 232 
 233         // do we have an incomplete member selection expression that misses property name?
 234         final boolean endsWithDot = SELECT_PROP_MISSING.matcher(exprStr).matches();
 235 
 236         // If this is an incomplete member selection, then it is not legal code.
 237         // Make it legal by adding a random property name "x" to it.
 238         final String completeExpr = endsWithDot? exprStr + "x" : exprStr;
 239 
 240         final ExpressionTree topExpr = getTopLevelExpression(parser, completeExpr);
 241         if (topExpr == null) {
 242             // Special case for load call that looks like "load(" with optional whitespaces.
 243             // If we have a fileChooserFunc then call it, so that the user can select a file.
 244             if (fileChooserFunc != null && LOAD_CALL.matcher(test).matches()) {
 245                 String name = readFileName(context.getErr());
 246                 if (name != null) {
 247                     // handle '\' file separator
 248                     if (BACKSLASH_FILE_SEPARATOR) {
 249                         name = name.replace("\\", "\\\\");
 250                     }
 251                     candidates.add(createCandidate("\"" + name + "\")"));
 252                     return cursor + name.length() + 3;
 253                 }
 254             }
 255 
 256             // did not parse to be a top level expression, no suggestions!
 257             return cursor;
 258         }
 259 
 260 
 261         // Find 'right most' expression of the top level expression
 262         final Tree rightMostExpr = getRightMostExpression(topExpr);
 263         if (rightMostExpr instanceof MemberSelectTree) {
 264             return completeMemberSelect(exprStr, cursor, candidates, (MemberSelectTree)rightMostExpr, endsWithDot);
 265         } else if (rightMostExpr instanceof IdentifierTree) {
 266             return completeIdentifier(exprStr, cursor, candidates, (IdentifierTree)rightMostExpr);
 267         } else {
 268             // expression that we cannot handle for completion
 269             return cursor;
 270         }
 271     }
 272 
 273     // Internals only below this point
 274 
 275     // read file name from the user using by showing a swing file chooser diablog
 276     private String readFileName(final PrintWriter err) {
 277         try {
 278             final Object res = ScriptRuntime.apply(fileChooserFunc, null);
 279             return res instanceof String? (String)res : null;
 280         } catch (final Exception e) {
 281             err.println(e);
 282             if (Main.DEBUG) {
 283                 e.printStackTrace();
 284             }
 285         }
 286         return null;
 287     }
 288 
 289     // fill properties of the incomplete member expression
 290     private int completeMemberSelect(final String exprStr, final int cursor, final List<Candidate> candidates,
 291                 final MemberSelectTree select, final boolean endsWithDot) {
 292         final ExpressionTree objExpr = select.getExpression();
 293         final String objExprCode = exprStr.substring((int)objExpr.getStartPosition(), (int)objExpr.getEndPosition());
 294 
 295         // try to evaluate the object expression part as a script
 296         Object obj = null;
 297         try {
 298             obj = context.eval(global, objExprCode, global, "<suggestions>");
 299         } catch (Exception exp) {
 300             // throw away the exception - this is during tab-completion
 301             if (Main.DEBUG) {
 302                 exp.printStackTrace();
 303             }
 304         }
 305 
 306         if (obj != null && obj != ScriptRuntime.UNDEFINED) {
 307             if (endsWithDot) {
 308                 // no user specified "prefix". List all properties of the object
 309                 propsHelper.getProperties(obj).stream().map(this::createCandidate).forEach(candidates::add);
 310                 return cursor;
 311             } else {
 312                 // list of properties matching the user specified prefix
 313                 final String prefix = select.getIdentifier();
 314                 propsHelper.getProperties(obj, prefix).stream().map(this::createCandidate).forEach(candidates::add);
 315                 return cursor - prefix.length();
 316             }
 317         }
 318 
 319         return cursor;
 320     }
 321 
 322     // fill properties for the given (partial) identifer
 323     private int completeIdentifier(final String test, final int cursor, final List<Candidate> candidates,
 324                 final IdentifierTree ident) {
 325         final String name = ident.getName();
 326         propsHelper.getProperties(global, name).stream().map(this::createCandidate).forEach(candidates::add);
 327         return cursor - name.length();
 328     }
 329 
 330     // returns ExpressionTree if the given code parses to a top level expression.
 331     // Or else returns null.
 332     private ExpressionTree getTopLevelExpression(final Parser parser, final String code) {
 333         try {
 334             final CompilationUnitTree cut = parser.parse("<code>", code, null);
 335             final List<? extends Tree> stats = cut.getSourceElements();
 336             if (stats.size() == 1) {
 337                 final Tree stat = stats.get(0);
 338                 if (stat instanceof ExpressionStatementTree) {
 339                     return ((ExpressionStatementTree)stat).getExpression();
 340                 }
 341             }
 342         } catch (final NashornException ignored) {
 343             // ignore any parser error. This is for completion anyway!
 344             // And user will get that error later when the expression is evaluated.
 345         }
 346 
 347         return null;
 348     }
 349 
 350     // get the right most expreesion of the given expression
 351     private Tree getRightMostExpression(final ExpressionTree expr) {
 352         return expr.accept(new SimpleTreeVisitorES5_1<Tree, Void>() {
 353             @Override
 354             public Tree visitAssignment(final AssignmentTree at, final Void v) {
 355                 return getRightMostExpression(at.getExpression());
 356             }
 357 
 358             @Override
 359             public Tree visitCompoundAssignment(final CompoundAssignmentTree cat, final Void v) {
 360                 return getRightMostExpression(cat.getExpression());
 361             }
 362 
 363             @Override
 364             public Tree visitConditionalExpression(final ConditionalExpressionTree cet, final Void v) {
 365                 return getRightMostExpression(cet.getFalseExpression());
 366             }
 367 
 368             @Override
 369             public Tree visitBinary(final BinaryTree bt, final Void v) {
 370                 return getRightMostExpression(bt.getRightOperand());
 371             }
 372 
 373             @Override
 374             public Tree visitIdentifier(final IdentifierTree ident, final Void v) {
 375                 return ident;
 376             }
 377 
 378 
 379             @Override
 380             public Tree visitInstanceOf(final InstanceOfTree it, final Void v) {
 381                 return it.getType();
 382             }
 383 
 384 
 385             @Override
 386             public Tree visitMemberSelect(final MemberSelectTree select, final Void v) {
 387                 return select;
 388             }
 389 
 390             @Override
 391             public Tree visitNew(final NewTree nt, final Void v) {
 392                 final ExpressionTree call = nt.getConstructorExpression();
 393                 if (call instanceof FunctionCallTree) {
 394                     final ExpressionTree func = ((FunctionCallTree)call).getFunctionSelect();
 395                     // Is this "new Foo" or "new obj.Foo" with no user arguments?
 396                     // If so, we may be able to do completion of constructor name.
 397                     if (func.getEndPosition() == nt.getEndPosition()) {
 398                         return func;
 399                     }
 400                 }
 401                 return null;
 402             }
 403 
 404             @Override
 405             public Tree visitUnary(final UnaryTree ut, final Void v) {
 406                 return getRightMostExpression(ut.getExpression());
 407             }
 408         }, null);
 409     }
 410 
 411     // create a Parser instance that uses compatible command line options of the
 412     // current ScriptEnvironment being used for REPL.
 413     private static Parser createParser(final ScriptEnvironment env) {
 414         final List<String> args = new ArrayList<>();
 415         if (env._const_as_var) {
 416             args.add("--const-as-var");
 417         }
 418 
 419         if (env._no_syntax_extensions) {
 420             args.add("-nse");
 421         }
 422 
 423         if (env._scripting) {
 424             args.add("-scripting");
 425         }
 426 
 427         if (env._strict) {
 428             args.add("-strict");
 429         }
 430 
 431         if (env._es6) {
 432             args.add("--language=es6");
 433         }
 434 
 435         return Parser.create(args.toArray(new String[0]));
 436     }
 437 
 438     private Candidate createCandidate(String value) {
 439         return new Candidate(value, value, null, null, null, null, false);
 440     }
 441 }