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