NashornCompleter.java revision 1391:4577d801c522
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 // Pattern to match a unfinished member selection expression. object part and "." 189 // but property name missing pattern. 190 private static final Pattern SELECT_PROP_MISSING = Pattern.compile(".*\\.\\s*"); 191 192 // Pattern to match load call 193 private static final Pattern LOAD_CALL = Pattern.compile("\\s*load\\s*\\(\\s*"); 194 195 @Override 196 public int complete(final String test, final int cursor, final List<CharSequence> result) { 197 // check that cursor is at the end of test string. Do not complete in the middle! 198 if (cursor != test.length()) { 199 return cursor; 200 } 201 202 // get the start of the last expression embedded in the given code 203 // using the partial parsing support - so that we can complete expressions 204 // inside statements, function call argument lists, array index etc. 205 final int exprStart = partialParser.getLastExpressionStart(context, test); 206 if (exprStart == -1) { 207 return cursor; 208 } 209 210 211 // extract the last expression string 212 final String exprStr = test.substring(exprStart); 213 214 // do we have an incomplete member selection expression that misses property name? 215 final boolean endsWithDot = SELECT_PROP_MISSING.matcher(exprStr).matches(); 216 217 // If this is an incomplete member selection, then it is not legal code. 218 // Make it legal by adding a random property name "x" to it. 219 final String completeExpr = endsWithDot? exprStr + "x" : exprStr; 220 221 final ExpressionTree topExpr = getTopLevelExpression(parser, completeExpr); 222 if (topExpr == null) { 223 // special case for load call that looks like "load(" with optional whitespaces 224 if (LOAD_CALL.matcher(test).matches()) { 225 String name = readFileName(context.getErr()); 226 if (name != null) { 227 // handle '\' file separator 228 if (BACKSLASH_FILE_SEPARATOR) { 229 name = name.replace("\\", "\\\\"); 230 } 231 result.add("\"" + name + "\")"); 232 return cursor + name.length() + 3; 233 } 234 } 235 236 // did not parse to be a top level expression, no suggestions! 237 return cursor; 238 } 239 240 241 // Find 'right most' expression of the top level expression 242 final Tree rightMostExpr = getRightMostExpression(topExpr); 243 if (rightMostExpr instanceof MemberSelectTree) { 244 return completeMemberSelect(exprStr, cursor, result, (MemberSelectTree)rightMostExpr, endsWithDot); 245 } else if (rightMostExpr instanceof IdentifierTree) { 246 return completeIdentifier(exprStr, cursor, result, (IdentifierTree)rightMostExpr); 247 } else { 248 // expression that we cannot handle for completion 249 return cursor; 250 } 251 } 252 253 // Internals only below this point 254 255 // read file name from the user using by showing a swing file chooser diablog 256 private static String readFileName(final PrintWriter err) { 257 // if running on AWT Headless mode, don't attempt swing dialog box! 258 if (Main.HEADLESS) { 259 return null; 260 } 261 262 final FutureTask<String> fileChooserTask = new FutureTask<String>(() -> { 263 // show a file chooser dialog box 264 final JFileChooser chooser = new JFileChooser(); 265 chooser.setFileFilter(new FileNameExtensionFilter("JavaScript Files", "js")); 266 final int retVal = chooser.showOpenDialog(null); 267 return retVal == JFileChooser.APPROVE_OPTION ? 268 chooser.getSelectedFile().getAbsolutePath() : null; 269 }); 270 271 SwingUtilities.invokeLater(fileChooserTask); 272 273 try { 274 return fileChooserTask.get(); 275 } catch (final ExecutionException | InterruptedException e) { 276 err.println(e); 277 if (Main.DEBUG) { 278 e.printStackTrace(); 279 } 280 } 281 return null; 282 } 283 284 // fill properties of the incomplete member expression 285 private int completeMemberSelect(final String exprStr, final int cursor, final List<CharSequence> result, 286 final MemberSelectTree select, final boolean endsWithDot) { 287 final ExpressionTree objExpr = select.getExpression(); 288 final String objExprCode = exprStr.substring((int)objExpr.getStartPosition(), (int)objExpr.getEndPosition()); 289 290 // try to evaluate the object expression part as a script 291 Object obj = null; 292 try { 293 obj = context.eval(global, objExprCode, global, "<suggestions>"); 294 } catch (Exception exp) { 295 // throw away the exception - this is during tab-completion 296 if (Main.DEBUG) { 297 exp.printStackTrace(); 298 } 299 } 300 301 if (obj != null && obj != ScriptRuntime.UNDEFINED) { 302 if (endsWithDot) { 303 // no user specified "prefix". List all properties of the object 304 result.addAll(propsHelper.getProperties(obj)); 305 return cursor; 306 } else { 307 // list of properties matching the user specified prefix 308 final String prefix = select.getIdentifier(); 309 result.addAll(propsHelper.getProperties(obj, prefix)); 310 return cursor - prefix.length(); 311 } 312 } 313 314 return cursor; 315 } 316 317 // fill properties for the given (partial) identifer 318 private int completeIdentifier(final String test, final int cursor, final List<CharSequence> result, 319 final IdentifierTree ident) { 320 final String name = ident.getName(); 321 result.addAll(propsHelper.getProperties(global, name)); 322 return cursor - name.length(); 323 } 324 325 // returns ExpressionTree if the given code parses to a top level expression. 326 // Or else returns null. 327 private ExpressionTree getTopLevelExpression(final Parser parser, final String code) { 328 try { 329 final CompilationUnitTree cut = parser.parse("<code>", code, null); 330 final List<? extends Tree> stats = cut.getSourceElements(); 331 if (stats.size() == 1) { 332 final Tree stat = stats.get(0); 333 if (stat instanceof ExpressionStatementTree) { 334 return ((ExpressionStatementTree)stat).getExpression(); 335 } 336 } 337 } catch (final NashornException ignored) { 338 // ignore any parser error. This is for completion anyway! 339 // And user will get that error later when the expression is evaluated. 340 } 341 342 return null; 343 } 344 345 // get the right most expreesion of the given expression 346 private Tree getRightMostExpression(final ExpressionTree expr) { 347 return expr.accept(new SimpleTreeVisitorES5_1<Tree, Void>() { 348 @Override 349 public Tree visitAssignment(final AssignmentTree at, final Void v) { 350 return getRightMostExpression(at.getExpression()); 351 } 352 353 @Override 354 public Tree visitCompoundAssignment(final CompoundAssignmentTree cat, final Void v) { 355 return getRightMostExpression(cat.getExpression()); 356 } 357 358 @Override 359 public Tree visitConditionalExpression(final ConditionalExpressionTree cet, final Void v) { 360 return getRightMostExpression(cet.getFalseExpression()); 361 } 362 363 @Override 364 public Tree visitBinary(final BinaryTree bt, final Void v) { 365 return getRightMostExpression(bt.getRightOperand()); 366 } 367 368 @Override 369 public Tree visitIdentifier(final IdentifierTree ident, final Void v) { 370 return ident; 371 } 372 373 374 @Override 375 public Tree visitInstanceOf(final InstanceOfTree it, final Void v) { 376 return it.getType(); 377 } 378 379 380 @Override 381 public Tree visitMemberSelect(final MemberSelectTree select, final Void v) { 382 return select; 383 } 384 385 @Override 386 public Tree visitNew(final NewTree nt, final Void v) { 387 final ExpressionTree call = nt.getConstructorExpression(); 388 if (call instanceof FunctionCallTree) { 389 final ExpressionTree func = ((FunctionCallTree)call).getFunctionSelect(); 390 // Is this "new Foo" or "new obj.Foo" with no user arguments? 391 // If so, we may be able to do completion of constructor name. 392 if (func.getEndPosition() == nt.getEndPosition()) { 393 return func; 394 } 395 } 396 return null; 397 } 398 399 @Override 400 public Tree visitUnary(final UnaryTree ut, final Void v) { 401 return getRightMostExpression(ut.getExpression()); 402 } 403 }, null); 404 } 405 406 // create a Parser instance that uses compatible command line options of the 407 // current ScriptEnvironment being used for REPL. 408 private static Parser createParser(final ScriptEnvironment env) { 409 final List<String> args = new ArrayList<>(); 410 if (env._const_as_var) { 411 args.add("--const-as-var"); 412 } 413 414 if (env._no_syntax_extensions) { 415 args.add("-nse"); 416 } 417 418 if (env._scripting) { 419 args.add("-scripting"); 420 } 421 422 if (env._strict) { 423 args.add("-strict"); 424 } 425 426 return Parser.create(args.toArray(new String[0])); 427 } 428} 429