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