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 }