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