--- old/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java 2016-01-22 02:17:06.896120417 -0800 +++ new/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java 2016-01-22 02:17:06.672118855 -0800 @@ -26,6 +26,7 @@ package jdk.internal.jshell.tool; import jdk.jshell.SourceCodeAnalysis.CompletionInfo; +import jdk.jshell.SourceCodeAnalysis.IndexResult; import jdk.jshell.SourceCodeAnalysis.Suggestion; import java.awt.event.ActionListener; @@ -34,8 +35,12 @@ import java.io.PrintStream; import java.io.UncheckedIOException; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; @@ -144,6 +149,11 @@ bind(DOCUMENTATION_SHORTCUT, (ActionListener) evt -> documentation(repl)); bind(CTRL_UP, (ActionListener) evt -> moveHistoryToSnippet(((EditingHistory) in.getHistory())::previousSnippet)); bind(CTRL_DOWN, (ActionListener) evt -> moveHistoryToSnippet(((EditingHistory) in.getHistory())::nextSnippet)); + for (FixComputer computer : fixComputers) { + for (String shortcuts : SHORTCUT_FIXES) { + bind(shortcuts + computer.shortcut, (ActionListener) evt -> fixes(computer)); + } + } } @Override @@ -216,6 +226,11 @@ private static final String DOCUMENTATION_SHORTCUT = "\033\133\132"; //Shift-TAB private static final String CTRL_UP = "\033\133\061\073\065\101"; //Ctrl-UP private static final String CTRL_DOWN = "\033\133\061\073\065\102"; //Ctrl-DOWN + private static final String[] SHORTCUT_FIXES = { + "\033\015", //Alt-Enter (Linux) + "\033\133\061\067\176", //F6/Alt-F1 (Mac) + "\u001BO3P" //Alt-F1 (Linux) + }; private void documentation(JShellTool repl) { String buffer = in.getCursorBuffer().buffer.toString(); @@ -290,6 +305,161 @@ history.fullHistoryReplace(source); } + private void fixes(FixComputer computer) { + String input = prefix + in.getCursorBuffer().toString(); + int cursor = prefix.length() + in.getCursorBuffer().cursor; + FixResult candidates = computer.compute(repl, input, cursor); + + try { + final boolean printError = candidates.error != null && !candidates.error.isEmpty(); + if (printError) { + in.println(candidates.error); + } + if (candidates.fixes.isEmpty()) { + in.beep(); + if (printError) { + in.redrawLine(); + in.flush(); + } + } else if (candidates.fixes.size() == 1 && !computer.showMenu) { + if (printError) { + in.redrawLine(); + in.flush(); + } + candidates.fixes.get(0).perform(in); + } else { + List fixes = new ArrayList<>(candidates.fixes); + fixes.add(0, new Fix() { + @Override + public String displayName() { + return "Do nothing"; + } + + @Override + public void perform(ConsoleReader in) throws IOException { + in.redrawLine(); + } + }); + + Map char2Fix = new HashMap<>(); + in.println(); + for (int i = 0; i < fixes.size(); i++) { + Fix fix = fixes.get(i); + char2Fix.put((char) ('0' + i), fix); + in.println("" + i + ": " + fixes.get(i).displayName()); + } + char2Fix.put((char) 3, fixes.get(0)); //Ctrl-C + in.print("Choice: "); + in.flush(); + int read; + + while (true) { + read = in.readCharacter(); + + Fix fix = char2Fix.get((char) read); + + if (fix != null) { + in.println(); + + fix.perform(in); + + in.flush(); + break; + } + } + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + public interface Fix { + public String displayName(); + public void perform(ConsoleReader in) throws IOException; + } + + public abstract static class FixComputer { + private final char shortcut; + private final boolean showMenu; + + public FixComputer(char shortcut, boolean showMenu) { + this.shortcut = shortcut; + this.showMenu = showMenu; + } + + public abstract FixResult compute(JShellTool repl, String code, int cursor); + } + + public static class FixResult { + public final List fixes; + public final String error; + + public FixResult(List fixes, String error) { + this.fixes = fixes; + this.error = error; + } + } + + private static final FixComputer[] fixComputers = new FixComputer[] { + new FixComputer('v', false) { + @Override + public FixResult compute(JShellTool repl, String code, int cursor) { + String type = repl.analysis.analyzeType(code, cursor); + if (type == null) { + return new FixResult(Collections.emptyList(), null); + } + return new FixResult(Collections.singletonList(new Fix() { + @Override + public String displayName() { + return "Create variable"; + } + @Override + public void perform(ConsoleReader in) throws IOException { + in.redrawLine(); + in.setCursorPosition(0); + in.putString(type + " = "); + in.setCursorPosition(in.getCursorBuffer().cursor - 3); + in.flush(); + } + }), null); + } + }, + new FixComputer('i', true) { + @Override + public FixResult compute(JShellTool repl, String code, int cursor) { + IndexResult res = repl.analysis.getDeclaredSymbols(code, cursor); + List fixes = new ArrayList<>(); + for (String fqn : res.getFqns()) { + fixes.add(new Fix() { + @Override + public String displayName() { + return "import: " + fqn; + } + @Override + public void perform(ConsoleReader in) throws IOException { + repl.state.eval("import " + fqn + ";"); + in.println("Imported: " + fqn); + in.redrawLine(); + } + }); + } + if (res.isResolvable()) { + return new FixResult(Collections.emptyList(), + "\nThe identifier is resolvable in this context."); + } else { + String error = ""; + if (fixes.isEmpty()) { + error = "\nNo candidate FQNs found to import."; + } + if (!res.isUpToDate()) { + error += "\nResults may be incomplete; try again later for complete results."; + } + return new FixResult(fixes, error); + } + } + } + }; + private static final class JShellUnixTerminal extends NoInterruptUnixTerminal { private final StopDetectingInputStream input; --- old/src/jdk.jshell/share/classes/jdk/jshell/Eval.java 2016-01-22 02:17:08.065128571 -0800 +++ new/src/jdk.jshell/share/classes/jdk/jshell/Eval.java 2016-01-22 02:17:07.837126981 -0800 @@ -420,7 +420,7 @@ TaskFactory.AnalyzeTask at = trialCompile(guts); if (!at.hasErrors() && at.firstCuTree() != null) { return TreeDissector.createByFirstClass(at) - .typeOfReturnStatement(at.messages(), state.maps::fullClassNameAndPackageToClass); + .typeOfReturnStatement(at, state); } return null; } --- old/src/jdk.jshell/share/classes/jdk/jshell/JShell.java 2016-01-22 02:17:09.242136781 -0800 +++ new/src/jdk.jshell/share/classes/jdk/jshell/JShell.java 2016-01-22 02:17:09.003135114 -0800 @@ -346,10 +346,20 @@ * @see JShell#onShutdown(java.util.function.Consumer) */ public List eval(String input) throws IllegalStateException { - checkIfAlive(); - List events = eval.eval(input); - events.forEach(this::notifyKeyStatusEvent); - return Collections.unmodifiableList(events); + SourceCodeAnalysisImpl a = sourceCodeAnalysis; + if (a != null) { + a.suspendIndexing(); + } + try { + checkIfAlive(); + List events = eval.eval(input); + events.forEach(this::notifyKeyStatusEvent); + return Collections.unmodifiableList(events); + } finally { + if (a != null) { + a.resumeIndexing(); + } + } } /** --- old/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java 2016-01-22 02:17:10.422145011 -0800 +++ new/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java 2016-01-22 02:17:10.188143379 -0800 @@ -74,6 +74,24 @@ */ SourceCodeAnalysis() {} + /**Infer the type of the given expression. Returns null if the type of the expression cannot + * be inferred. + * + * @param code the expression for which the type should be inferred + * @param cursor current cursor position in the given code + * @return the inferred type, or null if it cannot be inferred + */ + public abstract String analyzeType(String code, int cursor); + + /**List the possible FQNs for an identifier in the given code immediately + * to the right of the given cursor position. + * + * @param code the expression for which the candidate FQNs should be computed + * @param cursor current cursor position in the given code + * @return the gathered FQNs + */ + public abstract IndexResult getDeclaredSymbols(String code, int cursor); + /** * The result of analyzeCompletion(String input). * Describes the completeness and position of the first snippet in the given input. @@ -198,4 +216,56 @@ */ public final boolean isSmart; } + + /**List of possible qualified names. + */ + public static final class IndexResult { + + private final List fqns; + private final int simpleNameLength; + private final boolean upToDate; + private final boolean resolvable; + + public IndexResult(List fqns, int simpleNameLength, boolean upToDate, boolean resolvable) { + this.fqns = fqns; + this.simpleNameLength = simpleNameLength; + this.upToDate = upToDate; + this.resolvable = resolvable; + } + + /**Candidate fully qualified names (FQNs). + * + * @return possible fully qualified names + */ + public List getFqns() { + return fqns; + } + + /**The length of the unresolvable simple name in the original code for which the + * FQNs where computed. + * + * @return the length of the simple name; -1 if there is no name right left to the cursor for + * which the candidates could be computed + */ + public int getSimpleNameLength() { + return simpleNameLength; + } + + /**Whether the result is based on up to date data. + * + * @return true iff the results is based on up-to-date data + */ + public boolean isUpToDate() { + return upToDate; + } + + /**The given identifier refers to a resolvable element. + * + * @return true iff the given identifier refers to a resolvable element + */ + public boolean isResolvable() { + return resolvable; + } + + } } --- old/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java 2016-01-22 02:17:11.498152516 -0800 +++ new/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java 2016-01-22 02:17:11.251150794 -0800 @@ -79,14 +79,25 @@ import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Function; +import java.util.prefs.Preferences; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -99,6 +110,7 @@ import java.util.stream.StreamSupport; import javax.lang.model.SourceVersion; + import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.PackageElement; import javax.lang.model.element.QualifiedNameable; @@ -124,6 +136,10 @@ SourceCodeAnalysisImpl(JShell proc) { this.proc = proc; this.ca = new CompletenessAnalyzer(proc); + + int cpVersion = classpathVersion = 1; + + INDEXER.submit(() -> refreshIndexes(cpVersion)); } @Override @@ -203,6 +219,15 @@ @Override public List completionSuggestions(String code, int cursor, int[] anchor) { + suspendIndexing(); + try { + return completionSuggestionsImpl(code, cursor, anchor); + } finally { + resumeIndexing(); + } + } + + private List completionSuggestionsImpl(String code, int cursor, int[] anchor) { code = code.substring(0, cursor); Matcher m = JAVA_IDENTIFIER.matcher(code); String identifier = ""; @@ -589,32 +614,28 @@ .collect(toList()); } - private Set emptyContextPackages = null; - void classpathChanged() { - emptyContextPackages = null; + synchronized (currentIndexes) { + int cpVersion = ++classpathVersion; + + INDEXER.submit(() -> refreshIndexes(cpVersion)); + } } private Set listPackages(AnalyzeTask at, String enclosingPackage) { - Set packs; - - if (enclosingPackage.isEmpty() && emptyContextPackages != null) { - packs = emptyContextPackages; - } else { - packs = new HashSet<>(); - - listPackages(StandardLocation.PLATFORM_CLASS_PATH, enclosingPackage, packs); - listPackages(StandardLocation.CLASS_PATH, enclosingPackage, packs); - listPackages(StandardLocation.SOURCE_PATH, enclosingPackage, packs); - - if (enclosingPackage.isEmpty()) { - emptyContextPackages = packs; - } + synchronized (currentIndexes) { + return currentIndexes.values() + .stream() + .flatMap(idx -> idx.packages.stream()) + .filter(p -> enclosingPackage.isEmpty() || p.startsWith(enclosingPackage + ".")) + .map(p -> { + int dot = p.indexOf('.', enclosingPackage.length() + 1); + return dot == (-1) ? p : p.substring(0, dot); + }) + .distinct() + .map(p -> createPackageElement(at, p)) + .collect(Collectors.toSet()); } - - return packs.stream() - .map(pkg -> createPackageElement(at, pkg)) - .collect(Collectors.toSet()); } private PackageElement createPackageElement(AnalyzeTask at, String packageName) { @@ -625,79 +646,6 @@ return existing; } - private void listPackages(Location loc, String enclosing, Set packs) { - Iterable paths = proc.taskFactory.fileManager().getLocationAsPaths(loc); - - if (paths == null) - return ; - - for (Path p : paths) { - listPackages(p, enclosing, packs); - } - } - - private void listPackages(Path path, String enclosing, Set packages) { - try { - if (path.equals(Paths.get("JRT_MARKER_FILE"))) { - FileSystem jrtfs = FileSystems.getFileSystem(URI.create("jrt:/")); - Path modules = jrtfs.getPath("modules"); - try (DirectoryStream stream = Files.newDirectoryStream(modules)) { - for (Path c : stream) { - listDirectory(c, enclosing, packages); - } - } - } else if (!Files.isDirectory(path)) { - if (Files.exists(path)) { - ClassLoader cl = SourceCodeAnalysisImpl.class.getClassLoader(); - - try (FileSystem zip = FileSystems.newFileSystem(path, cl)) { - listDirectory(zip.getRootDirectories().iterator().next(), enclosing, packages); - } - } - } else { - listDirectory(path, enclosing, packages); - } - } catch (IOException ex) { - proc.debug(ex, "SourceCodeAnalysisImpl.listPackages(" + path.toString() + ", " + enclosing + ", " + packages + ")"); - } - } - - private void listDirectory(Path path, String enclosing, Set packages) throws IOException { - String separator = path.getFileSystem().getSeparator(); - Path resolved = path.resolve(enclosing.replace(".", separator)); - - if (Files.isDirectory(resolved)) { - try (DirectoryStream ds = Files.newDirectoryStream(resolved)) { - for (Path entry : ds) { - String name = pathName(entry); - - if (SourceVersion.isIdentifier(name) && - Files.isDirectory(entry) && - validPackageCandidate(entry)) { - packages.add(enclosing + (enclosing.isEmpty() ? "" : ".") + name); - } - } - } - } - } - - private boolean validPackageCandidate(Path p) throws IOException { - try (Stream dir = Files.list(p)) { - return dir.anyMatch(e -> Files.isDirectory(e) && SourceVersion.isIdentifier(pathName(e)) || - e.getFileName().toString().endsWith(".class")); - } - } - - private String pathName(Path p) { - String separator = p.getFileSystem().getSeparator(); - String name = p.getFileName().toString(); - - if (name.endsWith(separator)) //jars have '/' appended - name = name.substring(0, name.length() - separator.length()); - - return name; - } - private Element createArrayLengthSymbol(AnalyzeTask at, TypeMirror site) { Name length = Names.instance(at.getContext()).length; Type intType = Symtab.instance(at.getContext()).intType; @@ -965,6 +913,15 @@ @Override public String documentation(String code, int cursor) { + suspendIndexing(); + try { + return documentationImpl(code, cursor); + } finally { + resumeIndexing(); + } + } + + private String documentationImpl(String code, int cursor) { code = code.substring(0, cursor); if (code.trim().isEmpty()) { //TODO: comment handling code += ";"; @@ -1074,4 +1031,342 @@ } return arrayType; } + + @Override + public String analyzeType(String code, int cursor) { + code = code.substring(0, cursor); + if (!analyzeCompletion(code).completeness.isComplete) + return null; + + if (code.trim().isEmpty()) { //TODO: comment handling + code += ";"; + } + OuterWrap codeWrap; + switch (guessKind(code)) { + case IMPORT: + case METHOD: + return null; + default: + codeWrap = wrapInClass(Wrap.methodWrap(code)); + break; + } + AnalyzeTask at = proc.taskFactory.new AnalyzeTask(codeWrap); + SourcePositions sp = at.trees().getSourcePositions(); + CompilationUnitTree topLevel = at.firstCuTree(); + TreePath tp = pathFor(topLevel, sp, codeWrap.snippetIndexToWrapIndex(code.length())); + TypeMirror type = at.trees().getTypeMirror(tp); + + if (type == null) + return null; + + switch (type.getKind()) { + case ERROR: case NONE: case OTHER: + case PACKAGE: case VOID: + return null; //not usable + case NULL: + type = at.getElements().getTypeElement("java.lang.Object").asType(); + break; + } + + return TypePrinter.printType(at, proc, type); + } + + @Override + public IndexResult getDeclaredSymbols(String code, int cursor) { + code = code.substring(0, cursor); + if (code.trim().isEmpty()) { + return new IndexResult(Collections.emptyList(), -1, true, false); + } + OuterWrap codeWrap; + switch (guessKind(code)) { + case IMPORT: + case METHOD: + return new IndexResult(Collections.emptyList(), -1, true, false); + default: + codeWrap = wrapInClass(Wrap.methodWrap(code)); + break; + } + AnalyzeTask at = proc.taskFactory.new AnalyzeTask(codeWrap); + SourcePositions sp = at.trees().getSourcePositions(); + CompilationUnitTree topLevel = at.firstCuTree(); + TreePath tp = pathFor(topLevel, sp, codeWrap.snippetIndexToWrapIndex(code.length())); + if (tp.getLeaf().getKind() != Kind.IDENTIFIER) { + return new IndexResult(Collections.emptyList(), -1, true, false); + } + Scope scope = at.trees().getScope(tp); + TypeMirror type = at.trees().getTypeMirror(tp); + Element el = at.trees().getElement(tp); + + boolean erroneous = (type.getKind() == TypeKind.ERROR && el.getKind() == ElementKind.CLASS) || + (el.getKind() == ElementKind.PACKAGE && el.getEnclosedElements().isEmpty()); + String simpleName = ((IdentifierTree) tp.getLeaf()).getName().toString(); + boolean upToDate; + List result; + + synchronized (currentIndexes) { + upToDate = classpathVersion == indexVersion; + result = new ArrayList<>( + currentIndexes.values() + .stream() + .flatMap(idx -> idx.classSimpleName2FQN.getOrDefault(simpleName, + Collections.emptyList()).stream()) + .distinct() + .filter(fqn -> isAccessible(at, scope, fqn)) + .collect(Collectors.toSet()) + ); + } + + return new IndexResult(result, simpleName.length(), upToDate, !erroneous); + } + + private boolean isAccessible(AnalyzeTask at, Scope scope, String fqn) { + TypeElement type = at.getElements().getTypeElement(fqn); + if (type == null) + return false; + return at.trees().isAccessible(scope, type); + } + + private static final Map PATH_TO_INDEX = new HashMap<>(); + private static final ExecutorService INDEXER = Executors.newFixedThreadPool(1, r -> { + Thread t = new Thread(r); + t.setDaemon(true); + t.setUncaughtExceptionHandler((thread, ex) -> ex.printStackTrace()); + return t; + }); + + private final Object suspendLock = new Object(); + private int suspend; + + private void waitIndexingNotSuspended() { + boolean suspendedNotified = false; + synchronized (suspendLock) { + while (suspend > 0) { + if (!suspendedNotified) { + suspendedNotified = true; + } + try { + suspendLock.wait(); + } catch (InterruptedException ex) { + } + } + } + } + + public void suspendIndexing() { + synchronized (suspendLock) { + suspend++; + } + } + + public void resumeIndexing() { + synchronized (suspendLock) { + if (--suspend == 0) { + suspendLock.notifyAll(); + } + } + } + + private final Map currentIndexes = new HashMap<>(); + private int indexVersion; + private int classpathVersion; + + private void refreshIndexes(int version) { + try { + Collection paths = new ArrayList<>(); + MemoryFileManager fm = proc.taskFactory.fileManager(); + + appendPaths(fm, StandardLocation.PLATFORM_CLASS_PATH, paths); + appendPaths(fm, StandardLocation.CLASS_PATH, paths); + appendPaths(fm, StandardLocation.SOURCE_PATH, paths); + + Map newIndexes = new HashMap<>(); + + for (Path p : paths) { + ClassIndex index = PATH_TO_INDEX.get(p); + if (index != null) { + newIndexes.put(p, index); + } + } + + synchronized (currentIndexes) { + //temporary setting old data: + currentIndexes.clear(); + currentIndexes.putAll(newIndexes); + } + + for (Path p : paths) { + waitIndexingNotSuspended(); + + ClassIndex index = indexForPath(p); + newIndexes.put(p, index); + } + + synchronized (currentIndexes) { + currentIndexes.clear(); + currentIndexes.putAll(newIndexes); + } + } catch (Exception ex) { + proc.debug(ex, "SourceCodeAnalysisImpl.refreshIndexes(" + version + ")"); + } finally { + synchronized (currentIndexes) { + indexVersion = version; + } + } + } + + private void appendPaths(MemoryFileManager fm, Location loc, Collection paths) { + Iterable locationPaths = fm.getLocationAsPaths(loc); + if (locationPaths == null) + return ; + for (Path path : locationPaths) { + if (".".equals(path.toString())) { + //skip CWD + continue; + } + + paths.add(path); + } + } + + public ClassIndex indexForPath(Path path) { + if (isJRTMarkerFile(path)) { + FileSystem jrtfs = FileSystems.getFileSystem(URI.create("jrt:/")); + Path modules = jrtfs.getPath("modules"); + return PATH_TO_INDEX.compute(path, (p, index) -> { + try { + long lastModified = Files.getLastModifiedTime(modules).toMillis(); + if (index == null || index.timestamp != lastModified) { + try (DirectoryStream stream = Files.newDirectoryStream(modules)) { + index = doIndex(lastModified, path, stream); + } + } + return index; + } catch (IOException ex) { + proc.debug(ex, "SourceCodeAnalysisImpl.indexesForPath(" + path.toString() + ")"); + return new ClassIndex(-1, path, Collections.emptySet(), Collections.emptyMap()); + } + }); + } else if (!Files.isDirectory(path)) { + if (Files.exists(path)) { + return PATH_TO_INDEX.compute(path, (p, index) -> { + try { + long lastModified = Files.getLastModifiedTime(p).toMillis(); + if (index == null || index.timestamp != lastModified) { + ClassLoader cl = SourceCodeAnalysisImpl.class.getClassLoader(); + + try (FileSystem zip = FileSystems.newFileSystem(path, cl)) { + index = doIndex(lastModified, path, zip.getRootDirectories()); + } + } + return index; + } catch (IOException ex) { + proc.debug(ex, "SourceCodeAnalysisImpl.indexesForPath(" + path.toString() + ")"); + return new ClassIndex(-1, path, Collections.emptySet(), Collections.emptyMap()); + } + }); + } else { + return new ClassIndex(-1, path, Collections.emptySet(), Collections.emptyMap()); + } + } else { + return PATH_TO_INDEX.compute(path, (p, index) -> { + //no persistence for directories, as we cannot check timestamps: + if (index == null) { + index = doIndex(-1, path, Arrays.asList(p)); + } + return index; + }); + } + } + + static boolean isJRTMarkerFile(Path path) { + return path.equals(Paths.get("JRT_MARKER_FILE")); + } + + private ClassIndex doIndex(long timestamp, Path originalPath, Iterable dirs) { + Set packages = new HashSet<>(); + Map> classSimpleName2FQN = new HashMap<>(); + + for (Path d : dirs) { + try { + Files.walkFileTree(d, new FileVisitor() { + int depth; + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + waitIndexingNotSuspended(); + if (depth++ == 0) + return FileVisitResult.CONTINUE; + String dirName = dir.getFileName().toString(); + String sep = dir.getFileSystem().getSeparator(); + dirName = dirName.endsWith(sep) ? dirName.substring(0, dirName.length() - sep.length()) + : dirName; + if (SourceVersion.isIdentifier(dirName)) + return FileVisitResult.CONTINUE; + return FileVisitResult.SKIP_SUBTREE; + } + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + waitIndexingNotSuspended(); + if (file.getFileName().toString().endsWith(".class")) { + String relativePath = d.relativize(file).toString(); + String binaryName = relativePath.substring(0, relativePath.length() - 6).replace('/', '.'); + int packageDot = binaryName.lastIndexOf('.'); + if (packageDot > (-1)) { + packages.add(binaryName.substring(0, packageDot)); + } + String typeName = binaryName.replace('$', '.'); + addClassName2Map(classSimpleName2FQN, typeName); + } + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + depth--; + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException ex) { + proc.debug(ex, "doIndex(" + d.toString() + ")"); + } + } + + return new ClassIndex(timestamp, originalPath, packages, classSimpleName2FQN); + } + + private static void addClassName2Map(Map> classSimpleName2FQN, String typeName) { + int simpleNameDot = typeName.lastIndexOf('.'); + classSimpleName2FQN.computeIfAbsent(typeName.substring(simpleNameDot + 1), n -> new LinkedHashSet<>()) + .add(typeName); + } + + public static final class ClassIndex { + public final long timestamp; + public final Path forPath; + public final Set packages; + public final Map> classSimpleName2FQN; + + public ClassIndex(long timestamp, Path forPath, Set packages, Map> classSimpleName2FQN) { + this.timestamp = timestamp; + this.forPath = forPath; + this.packages = packages; + this.classSimpleName2FQN = classSimpleName2FQN; + } + + } + + public void waitBackgroundTaskFinished() throws Exception { + boolean upToDate; + synchronized (currentIndexes) { + upToDate = classpathVersion == indexVersion; + } + while (!upToDate) { + INDEXER.submit(() -> {}).get(); + synchronized (currentIndexes) { + upToDate = classpathVersion == indexVersion; + } + } + } } --- old/src/jdk.jshell/share/classes/jdk/jshell/TreeDissector.java 2016-01-22 02:17:12.715161005 -0800 +++ new/src/jdk.jshell/share/classes/jdk/jshell/TreeDissector.java 2016-01-22 02:17:12.491159443 -0800 @@ -41,18 +41,19 @@ import com.sun.tools.javac.code.Type.MethodType; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; -import com.sun.tools.javac.util.JavacMessages; import com.sun.tools.javac.util.Name; import static jdk.jshell.Util.isDoIt; import jdk.jshell.Wrap.Range; + import java.util.List; -import java.util.Locale; -import java.util.function.BinaryOperator; + import java.util.function.Predicate; import java.util.stream.Stream; import javax.lang.model.type.TypeMirror; import jdk.jshell.Util.Pair; +import jdk.jshell.TaskFactory.AnalyzeTask; + /** * Utilities for analyzing compiler API parse trees. * @author Robert Field @@ -209,7 +210,7 @@ } - ExpressionInfo typeOfReturnStatement(JavacMessages messages, BinaryOperator fullClassNameAndPackageToClass) { + ExpressionInfo typeOfReturnStatement(AnalyzeTask at, JShell state) { ExpressionInfo ei = new ExpressionInfo(); Tree unitTree = firstStatement(); if (unitTree instanceof ReturnTree) { @@ -219,9 +220,7 @@ if (viPath != null) { TypeMirror tm = trees().getTypeMirror(viPath); if (tm != null) { - Type type = (Type)tm; - TypePrinter tp = new TypePrinter(messages, fullClassNameAndPackageToClass, type); - ei.typeName = tp.visit(type, Locale.getDefault()); + ei.typeName = TypePrinter.printType(at, state, tm); switch (tm.getKind()) { case VOID: case NONE: --- old/src/jdk.jshell/share/classes/jdk/jshell/TypePrinter.java 2016-01-22 02:17:13.900169271 -0800 +++ new/src/jdk.jshell/share/classes/jdk/jshell/TypePrinter.java 2016-01-22 02:17:13.670167666 -0800 @@ -37,12 +37,21 @@ import java.util.Locale; import java.util.function.BinaryOperator; +import javax.lang.model.type.TypeMirror; +import jdk.jshell.TaskFactory.AnalyzeTask; + /** * Print types in source form. */ class TypePrinter extends Printer { private static final String OBJECT = "Object"; + public static String printType(AnalyzeTask at, JShell state, TypeMirror type) { + Type typeImpl = (Type) type; + TypePrinter tp = new TypePrinter(at.messages(), state.maps::fullClassNameAndPackageToClass, typeImpl); + return tp.visit(typeImpl, Locale.getDefault()); + } + private final JavacMessages messages; private final BinaryOperator fullClassNameAndPackageToClass; private boolean useWildCard = false; --- old/test/jdk/jshell/KullaTesting.java 2016-01-22 02:17:15.102177654 -0800 +++ new/test/jdk/jshell/KullaTesting.java 2016-01-22 02:17:14.830175757 -0800 @@ -24,6 +24,7 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.io.StringWriter; +import java.lang.reflect.Method; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -61,6 +62,7 @@ import jdk.jshell.SourceCodeAnalysis; import jdk.jshell.SourceCodeAnalysis.CompletionInfo; import jdk.jshell.SourceCodeAnalysis.Completeness; +import jdk.jshell.SourceCodeAnalysis.IndexResult; import jdk.jshell.SourceCodeAnalysis.Suggestion; import jdk.jshell.UnresolvedReferenceException; import org.testng.annotations.AfterMethod; @@ -862,6 +864,8 @@ } private List computeCompletions(String code, Boolean isSmart) { + waitIndexingFinished(); + int cursor = code.indexOf('|'); code = code.replace("|", ""); assertTrue(cursor > -1, "'|' expected, but not found in: " + code); @@ -874,6 +878,31 @@ .collect(Collectors.toList()); } + public void assertInferredType(String code, String expectedType) { + String inferredType = getAnalysis().analyzeType(code, code.length()); + + assertEquals(inferredType, expectedType, "Input: " + code + ", " + inferredType); + } + + public void assertInferredFQNs(String code, String... fqns) { + waitIndexingFinished(); + + IndexResult candidates = getAnalysis().getDeclaredSymbols(code, code.length()); + + assertEquals(candidates.getFqns(), Arrays.asList(fqns), "Input: " + code + ", " + candidates.getFqns()); + } + + protected void waitIndexingFinished() { + try { + Method waitBackgroundTaskFinished = getAnalysis().getClass().getDeclaredMethod("waitBackgroundTaskFinished"); + + waitBackgroundTaskFinished.setAccessible(true); + waitBackgroundTaskFinished.invoke(getAnalysis()); + } catch (Exception ex) { + throw new AssertionError("Cannot wait for indexing end.", ex); + } + } + public void assertDocumentation(String code, String... expected) { int cursor = code.indexOf('|'); code = code.replace("|", ""); --- /dev/null 2015-04-26 06:51:08.003313989 -0700 +++ new/test/jdk/jshell/ComputeFQNsTest.java 2016-01-22 02:17:16.027184106 -0800 @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary Test Get FQNs + * @library /tools/lib + * @build KullaTesting TestingInputStream ToolBox Compiler + * @run testng ComputeFQNsTest + */ + +import org.testng.annotations.Test; + +@Test +public class ComputeFQNsTest extends KullaTesting { + + public void testAddImport() throws Exception { + assertInferredFQNs("LinkedList", "java.util.LinkedList"); + assertInferredFQNs("ArrayList", "java.util.ArrayList"); + assertInferredFQNs("CharSequence", "java.lang.CharSequence"); + assertInferredFQNs("unresolvable"); + } + +} --- /dev/null 2015-04-26 06:51:08.003313989 -0700 +++ new/test/jdk/jshell/InferTypeTest.java 2016-01-22 02:17:17.016191005 -0800 @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary Test Type Inference + * @library /tools/lib + * @build KullaTesting TestingInputStream ToolBox Compiler + * @run testng InferTypeTest + */ + +import org.testng.annotations.Test; + +@Test +public class InferTypeTest extends KullaTesting { + + public void testTypeInference() { + assertInferredType("1", "int"); + assertEval("import java.util.*;"); + assertInferredType("new ArrayList()", "ArrayList"); + assertInferredType("null", "Object"); + assertInferredType("1 + ", null); //incomplete + assertInferredType("undef", null); //unresolvable + assertEval("List l1;"); + assertEval("List l2;"); + assertEval("List l3;"); + assertInferredType("l1", "List"); + assertInferredType("l2", "List"); + assertInferredType("l3", "List"); + assertInferredType("l1.get(0)", "String"); + assertInferredType("l2.get(0)", "String"); + assertInferredType("l3.get(0)", "Object"); + } + +}