8131027: JShell API/tool: suggest imports for a class
authorjlahoda
Mon, 29 Feb 2016 11:54:06 +0100
changeset 36160 f42d362d0d17
parent 36159 78e05cbe4ce7
child 36161 a025c0619f25
8131027: JShell API/tool: suggest imports for a class Summary: Adding two new actions to JShell: add imports and create variable. Reviewed-by: rfield
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java
langtools/src/jdk.jshell/share/classes/jdk/jshell/Eval.java
langtools/src/jdk.jshell/share/classes/jdk/jshell/JShell.java
langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java
langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java
langtools/src/jdk.jshell/share/classes/jdk/jshell/TreeDissector.java
langtools/test/jdk/jshell/ComputeFQNsTest.java
langtools/test/jdk/jshell/InferTypeTest.java
langtools/test/jdk/jshell/KullaTesting.java
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java	Thu Feb 25 11:28:25 2016 -0800
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java	Mon Feb 29 11:54:06 2016 +0100
@@ -26,6 +26,7 @@
 package jdk.internal.jshell.tool;
 
 import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
+import jdk.jshell.SourceCodeAnalysis.QualifiedNames;
 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 : FIX_COMPUTERS) {
+            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,185 @@
         history.fullHistoryReplace(source);
     }
 
+    //compute possible options/Fixes based on the selected FixComputer, present them to the user,
+    //and perform the selected one:
+    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<Fix> 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<Character, Fix> 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());
+                }
+                in.print("Choice: ");
+                in.flush();
+                int read;
+
+                read = in.readCharacter();
+
+                Fix fix = char2Fix.get((char) read);
+
+                if (fix == null) {
+                    in.beep();
+                    fix = fixes.get(0);
+                }
+
+                in.println();
+
+                fix.perform(in);
+
+                in.flush();
+            }
+        } catch (IOException ex) {
+            ex.printStackTrace();
+        }
+    }
+
+    /**
+     * A possible action which the user can choose to perform.
+     */
+    public interface Fix {
+        /**
+         * A name that should be shown to the user.
+         */
+        public String displayName();
+        /**
+         * Perform the given action.
+         */
+        public void perform(ConsoleReader in) throws IOException;
+    }
+
+    /**
+     * A factory for {@link Fix}es.
+     */
+    public abstract static class FixComputer {
+        private final char shortcut;
+        private final boolean showMenu;
+
+        /**
+         * Construct a new FixComputer. {@code shortcut} defines the key which should trigger this FixComputer.
+         * If {@code showMenu} is {@code false}, and this computer returns exactly one {@code Fix},
+         * no options will be show to the user, and the given {@code Fix} will be performed.
+         */
+        public FixComputer(char shortcut, boolean showMenu) {
+            this.shortcut = shortcut;
+            this.showMenu = showMenu;
+        }
+
+        /**
+         * Compute possible actions for the given code.
+         */
+        public abstract FixResult compute(JShellTool repl, String code, int cursor);
+    }
+
+    /**
+     * A list of {@code Fix}es with a possible error that should be shown to the user.
+     */
+    public static class FixResult {
+        public final List<Fix> fixes;
+        public final String error;
+
+        public FixResult(List<Fix> fixes, String error) {
+            this.fixes = fixes;
+            this.error = error;
+        }
+    }
+
+    private static final FixComputer[] FIX_COMPUTERS = new FixComputer[] {
+        new FixComputer('v', false) { //compute "Introduce variable" Fix:
+            @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) { //compute "Add import" Fixes:
+            @Override
+            public FixResult compute(JShellTool repl, String code, int cursor) {
+                QualifiedNames res = repl.analysis.listQualifiedNames(code, cursor);
+                List<Fix> fixes = new ArrayList<>();
+                for (String fqn : res.getNames()) {
+                    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 fully qualified names 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;
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Thu Feb 25 11:28:25 2016 -0800
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Mon Feb 29 11:54:06 2016 +0100
@@ -988,13 +988,19 @@
                 CommandKind.HELP_SUBJECT));
         registerCommand(new Command("shortcuts", "Describe shortcuts",
                 "Supported shortcuts include:\n\n" +
-                "<tab>       -- After entering the first few letters of a Java identifier,\n" +
-                "               a jshell command, or, in some cases, a jshell command argument,\n" +
-                "               press the <tab> key to complete the input.\n" +
-                "               If there is more than one completion, show possible completions.\n" +
-                "Shift-<tab> -- After the name and open parenthesis of a method or constructor invocation,\n" +
-                "               hold the <shift> key and press the <tab> to see a synopsis of all\n" +
-                "               matching methods/constructors.\n",
+                "<tab>            -- After entering the first few letters of a Java identifier,\n" +
+                "                    a jshell command, or, in some cases, a jshell command argument,\n" +
+                "                    press the <tab> key to complete the input.\n" +
+                "                    If there is more than one completion, show possible completions.\n" +
+                "Shift-<tab>      -- After the name and open parenthesis of a method or constructor invocation,\n" +
+                "                    hold the <shift> key and press the <tab> to see a synopsis of all\n" +
+                "                    matching methods/constructors.\n" +
+                "<fix-shortcut> v -- After a complete expression, press \"<fix-shortcut> v\" to introduce a new variable\n" +
+                "                    whose type is based on the type of the expression.\n" +
+                "                    The \"<fix-shortcut>\" is either Alt-F1 or Alt-Enter, depending on the platform.\n" +
+                "<fix-shortcut> i -- After an unresolvable identifier, press \"<fix-shortcut> i\" and jshell will propose\n" +
+                "                    possible fully qualified names based on the content of the specified classpath.\n" +
+                "                    The \"<fix-shortcut>\" is either Alt-F1 or Alt-Enter, depending on the platform.\n",
                 CommandKind.HELP_SUBJECT));
     }
 
--- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/Eval.java	Thu Feb 25 11:28:25 2016 -0800
+++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/Eval.java	Mon Feb 29 11:54:06 2016 +0100
@@ -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;
     }
--- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/JShell.java	Thu Feb 25 11:28:25 2016 -0800
+++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/JShell.java	Mon Feb 29 11:54:06 2016 +0100
@@ -346,10 +346,20 @@
      * @see JShell#onShutdown(java.util.function.Consumer)
      */
     public List<SnippetEvent> eval(String input) throws IllegalStateException {
-        checkIfAlive();
-        List<SnippetEvent> events = eval.eval(input);
-        events.forEach(this::notifyKeyStatusEvent);
-        return Collections.unmodifiableList(events);
+        SourceCodeAnalysisImpl a = sourceCodeAnalysis;
+        if (a != null) {
+            a.suspendIndexing();
+        }
+        try {
+            checkIfAlive();
+            List<SnippetEvent> events = eval.eval(input);
+            events.forEach(this::notifyKeyStatusEvent);
+            return Collections.unmodifiableList(events);
+        } finally {
+            if (a != null) {
+                a.resumeIndexing();
+            }
+        }
     }
 
     /**
--- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java	Thu Feb 25 11:28:25 2016 -0800
+++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java	Mon Feb 29 11:54:06 2016 +0100
@@ -70,6 +70,28 @@
     public abstract String documentation(String input, int cursor);
 
     /**
+     * Infer the type of the given expression. The expression spans from the beginning of {@code code}
+     * to the given {@code cursor} position. 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 qualified names known for the simple name in the given code immediately
+     * to the left of the given cursor position. The qualified names are gathered by inspecting the
+     * classpath used by eval (see {@link JShell#addToClasspath(java.lang.String)}).
+     *
+     * @param code the expression for which the candidate qualified names should be computed
+     * @param cursor current cursor position in the given code
+     * @return the known qualified names
+     */
+    public abstract QualifiedNames listQualifiedNames(String code, int cursor);
+
+    /**
      * Internal only constructor
      */
     SourceCodeAnalysis() {}
@@ -80,7 +102,7 @@
      */
     public static class CompletionInfo {
 
-        public CompletionInfo(Completeness completeness, int unitEndPos, String source, String remaining) {
+        CompletionInfo(Completeness completeness, int unitEndPos, String source, String remaining) {
             this.completeness = completeness;
             this.unitEndPos = unitEndPos;
             this.source = source;
@@ -198,4 +220,65 @@
          */
         public final boolean isSmart;
     }
+
+    /**
+     * List of possible qualified names.
+     */
+    public static final class QualifiedNames {
+
+        private final List<String> names;
+        private final int simpleNameLength;
+        private final boolean upToDate;
+        private final boolean resolvable;
+
+        QualifiedNames(List<String> names, int simpleNameLength, boolean upToDate, boolean resolvable) {
+            this.names = names;
+            this.simpleNameLength = simpleNameLength;
+            this.upToDate = upToDate;
+            this.resolvable = resolvable;
+        }
+
+        /**
+         * Known qualified names for the given simple name in the original code.
+         *
+         * @return known qualified names
+         */
+        public List<String> getNames() {
+            return names;
+        }
+
+        /**
+         * The length of the simple name in the original code for which the
+         * qualified names where gathered.
+         *
+         * @return the length of the simple name; -1 if there is no name immediately 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. The
+         * {@link SourceCodeAnalysis#listQualifiedNames(java.lang.String, int) listQualifiedNames}
+         * method may return before the classpath is fully inspected, in which case this method will
+         * return {@code false}. If the result is based on a fully inspected classpath, this method
+         * will return {@code true}.
+         *
+         * @return true iff the results is based on up-to-date data
+         */
+        public boolean isUpToDate() {
+            return upToDate;
+        }
+
+        /**
+         * Whether the given simple name in the original code refers to a resolvable element.
+         *
+         * @return true iff the given simple name in the original code refers to a resolvable element
+         */
+        public boolean isResolvable() {
+            return resolvable;
+        }
+
+    }
 }
--- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java	Thu Feb 25 11:28:25 2016 -0800
+++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java	Mon Feb 29 11:54:06 2016 +0100
@@ -79,13 +79,23 @@
 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.regex.Matcher;
 import java.util.regex.Pattern;
@@ -99,6 +109,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;
@@ -118,12 +129,30 @@
  * @author Robert Field
  */
 class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
+
+    private static final Map<Path, ClassIndex> 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 JShell proc;
     private final CompletenessAnalyzer ca;
+    private final Map<Path, ClassIndex> currentIndexes = new HashMap<>();
+    private int indexVersion;
+    private int classpathVersion;
+    private final Object suspendLock = new Object();
+    private int suspend;
 
     SourceCodeAnalysisImpl(JShell proc) {
         this.proc = proc;
         this.ca = new CompletenessAnalyzer(proc);
+
+        int cpVersion = classpathVersion = 1;
+
+        INDEXER.submit(() -> refreshIndexes(cpVersion));
     }
 
     @Override
@@ -203,6 +232,15 @@
 
     @Override
     public List<Suggestion> completionSuggestions(String code, int cursor, int[] anchor) {
+        suspendIndexing();
+        try {
+            return completionSuggestionsImpl(code, cursor, anchor);
+        } finally {
+            resumeIndexing();
+        }
+    }
+
+    private List<Suggestion> completionSuggestionsImpl(String code, int cursor, int[] anchor) {
         code = code.substring(0, cursor);
         Matcher m = JAVA_IDENTIFIER.matcher(code);
         String identifier = "";
@@ -390,8 +428,11 @@
 
                 long start = sp.getStartPosition(topLevel, tree);
                 long end = sp.getEndPosition(topLevel, tree);
+                long prevEnd = deepest[0] != null ? sp.getEndPosition(topLevel, deepest[0].getLeaf()) : -1;
 
-                if (start <= pos && pos <= end) {
+                if (start <= pos && pos <= end &&
+                    (start != end || prevEnd != end || deepest[0] == null ||
+                     deepest[0].getParentPath().getLeaf() != getCurrentPath().getLeaf())) {
                     deepest[0] = new TreePath(getCurrentPath(), tree);
                     return super.scan(tree, p);
                 }
@@ -589,32 +630,28 @@
                 .collect(toList());
     }
 
-    private Set<String> emptyContextPackages = null;
+    void classpathChanged() {
+        synchronized (currentIndexes) {
+            int cpVersion = ++classpathVersion;
 
-    void classpathChanged() {
-        emptyContextPackages = null;
+            INDEXER.submit(() -> refreshIndexes(cpVersion));
+        }
     }
 
     private Set<PackageElement> listPackages(AnalyzeTask at, String enclosingPackage) {
-        Set<String> 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 +662,6 @@
         return existing;
     }
 
-    private void listPackages(Location loc, String enclosing, Set<String> packs) {
-        Iterable<? extends Path> 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<String> packages) {
-        try {
-            if (path.equals(Paths.get("JRT_MARKER_FILE"))) {
-                FileSystem jrtfs = FileSystems.getFileSystem(URI.create("jrt:/"));
-                Path modules = jrtfs.getPath("modules");
-                try (DirectoryStream<Path> 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<String> packages) throws IOException {
-        String separator = path.getFileSystem().getSeparator();
-        Path resolved = path.resolve(enclosing.replace(".", separator));
-
-        if (Files.isDirectory(resolved)) {
-            try (DirectoryStream<Path> 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<Path> 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 +929,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 +1047,347 @@
         }
         return arrayType;
     }
+
+    @Override
+    public String analyzeType(String code, int cursor) {
+        code = code.substring(0, cursor);
+        CompletionInfo completionInfo = analyzeCompletion(code);
+        if (!completionInfo.completeness.isComplete)
+            return null;
+        if (completionInfo.completeness == Completeness.COMPLETE_WITH_SEMI) {
+            code += ";";
+        }
+
+        OuterWrap codeWrap;
+        switch (guessKind(code)) {
+            case IMPORT: case METHOD: case CLASS: case ENUM:
+            case INTERFACE: case ANNOTATION_TYPE: case VARIABLE:
+                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();
+        int pos = codeWrap.snippetIndexToWrapIndex(code.length());
+        TreePath tp = pathFor(topLevel, sp, pos);
+        while (ExpressionTree.class.isAssignableFrom(tp.getParentPath().getLeaf().getKind().asInterface()) &&
+               tp.getParentPath().getLeaf().getKind() != Kind.ERRONEOUS &&
+               tp.getParentPath().getParentPath() != null)
+            tp = tp.getParentPath();
+        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 TreeDissector.printType(at, proc, type);
+    }
+
+    @Override
+    public QualifiedNames listQualifiedNames(String code, int cursor) {
+        code = code.substring(0, cursor);
+        if (code.trim().isEmpty()) {
+            return new QualifiedNames(Collections.emptyList(), -1, true, false);
+        }
+        OuterWrap codeWrap;
+        switch (guessKind(code)) {
+            case IMPORT:
+                return new QualifiedNames(Collections.emptyList(), -1, true, false);
+            case METHOD:
+                codeWrap = wrapInClass(Wrap.classMemberWrap(code));
+                break;
+            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 QualifiedNames(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<String> result;
+
+        synchronized (currentIndexes) {
+            upToDate = classpathVersion == indexVersion;
+            result = currentIndexes.values()
+                                   .stream()
+                                   .flatMap(idx -> idx.classSimpleName2FQN.getOrDefault(simpleName,
+                                                                                        Collections.emptyList()).stream())
+                                   .distinct()
+                                   .filter(fqn -> isAccessible(at, scope, fqn))
+                                   .sorted()
+                                   .collect(Collectors.toList());
+        }
+
+        return new QualifiedNames(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);
+    }
+
+    //--------------------
+    // classpath indexing:
+    //--------------------
+
+    //the indexing can be suspended when a more important task is running:
+    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();
+            }
+        }
+    }
+
+    //update indexes, either initially or after a classpath change:
+    private void refreshIndexes(int version) {
+        try {
+            Collection<Path> 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<Path, ClassIndex> newIndexes = new HashMap<>();
+
+            //setup existing/last known data:
+            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);
+            }
+
+            //update/compute the indexes if needed:
+            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<Path> paths) {
+        Iterable<? extends Path> locationPaths = fm.getLocationAsPaths(loc);
+        if (locationPaths == null)
+            return ;
+        for (Path path : locationPaths) {
+            if (".".equals(path.toString())) {
+                //skip CWD
+                continue;
+            }
+
+            paths.add(path);
+        }
+    }
+
+    //create/update index a given JavaFileManager entry (which may be a JDK installation, a jar/zip file or a directory):
+    //if an index exists for the given entry, the existing index is kept unless the timestamp is modified
+    private 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<Path> 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"));
+    }
+
+    //create an index based on the content of the given dirs; the original JavaFileManager entry is originalPath.
+    private ClassIndex doIndex(long timestamp, Path originalPath, Iterable<? extends Path> dirs) {
+        Set<String> packages = new HashSet<>();
+        Map<String, Collection<String>> classSimpleName2FQN = new HashMap<>();
+
+        for (Path d : dirs) {
+            try {
+                Files.walkFileTree(d, new FileVisitor<Path>() {
+                    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<String, Collection<String>> classSimpleName2FQN, String typeName) {
+        int simpleNameDot = typeName.lastIndexOf('.');
+        classSimpleName2FQN.computeIfAbsent(typeName.substring(simpleNameDot + 1), n -> new LinkedHashSet<>())
+                           .add(typeName);
+    }
+
+    //holder for indexed data about a given path
+    public static final class ClassIndex {
+        public final long timestamp;
+        public final Path forPath;
+        public final Set<String> packages;
+        public final Map<String, Collection<String>> classSimpleName2FQN;
+
+        public ClassIndex(long timestamp, Path forPath, Set<String> packages, Map<String, Collection<String>> classSimpleName2FQN) {
+            this.timestamp = timestamp;
+            this.forPath = forPath;
+            this.packages = packages;
+            this.classSimpleName2FQN = classSimpleName2FQN;
+        }
+
+    }
+
+    //for tests, to be able to wait until the indexing finishes:
+    public void waitBackgroundTaskFinished() throws Exception {
+        boolean upToDate;
+        synchronized (currentIndexes) {
+            upToDate = classpathVersion == indexVersion;
+        }
+        while (!upToDate) {
+            INDEXER.submit(() -> {}).get();
+            synchronized (currentIndexes) {
+                upToDate = classpathVersion == indexVersion;
+            }
+        }
+    }
 }
--- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/TreeDissector.java	Thu Feb 25 11:28:25 2016 -0800
+++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/TreeDissector.java	Mon Feb 29 11:54:06 2016 +0100
@@ -41,13 +41,14 @@
 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.TaskFactory.AnalyzeTask;
 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;
@@ -209,7 +210,7 @@
     }
 
 
-    ExpressionInfo typeOfReturnStatement(JavacMessages messages, BinaryOperator<String> 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 = printType(at, state, tm);
                         switch (tm.getKind()) {
                             case VOID:
                             case NONE:
@@ -263,6 +262,12 @@
         return sg.toString();
     }
 
+    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());
+    }
+
     /**
      * Signature Generation
      */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/test/jdk/jshell/ComputeFQNsTest.java	Mon Feb 29 11:54:06 2016 +0100
@@ -0,0 +1,118 @@
+/*
+ * 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
+ * @bug 8131027
+ * @summary Test Get FQNs
+ * @library /tools/lib
+ * @build KullaTesting TestingInputStream ToolBox Compiler
+ * @run testng ComputeFQNsTest
+ */
+
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+
+import jdk.jshell.SourceCodeAnalysis.QualifiedNames;
+import static org.testng.Assert.*;
+import org.testng.annotations.Test;
+
+@Test
+public class ComputeFQNsTest extends KullaTesting {
+
+    private final Compiler compiler = new Compiler();
+    private final Path outDir = Paths.get("ComputeFQNsTest");
+
+    public void testAddImport() throws Exception {
+        compiler.compile(outDir, "package test1; public class TestClass { }", "package test2; public class TestClass { }");
+        String jarName = "test.jar";
+        compiler.jar(outDir, jarName, "test1/TestClass.class", "test2/TestClass.class");
+        addToClasspath(compiler.getPath(outDir).resolve(jarName));
+
+        assertInferredFQNs("LinkedList", "java.util.LinkedList");
+        assertInferredFQNs("ArrayList", "java.util.ArrayList");
+        assertInferredFQNs("TestClass", "test1.TestClass", "test2.TestClass");
+        assertInferredFQNs("CharSequence", "CharSequence".length(), true, "java.lang.CharSequence");
+        assertInferredFQNs("unresolvable");
+        assertInferredFQNs("void test(ArrayList", "ArrayList".length(), false, "java.util.ArrayList");
+        assertInferredFQNs("void test(ArrayList l) throws InvocationTargetException", "InvocationTargetException".length(), false, "java.lang.reflect.InvocationTargetException");
+        assertInferredFQNs("void test(ArrayList l) { ArrayList", "ArrayList".length(), false, "java.util.ArrayList");
+        assertInferredFQNs("<T extends ArrayList", "ArrayList".length(), false, "java.util.ArrayList");
+        assertInferredFQNs("Object l = Arrays", "Arrays".length(), false, "java.util.Arrays");
+        assertInferredFQNs("class X<T extends ArrayList", "ArrayList".length(), false, "java.util.ArrayList");
+        assertInferredFQNs("class X extends ArrayList", "ArrayList".length(), false, "java.util.ArrayList");
+        assertInferredFQNs("class X extends java.util.ArrayList<TypeElement", "TypeElement".length(), false, "javax.lang.model.element.TypeElement");
+        assertInferredFQNs("class X extends java.util.ArrayList<TypeMirror, TypeElement", "TypeElement".length(), false, "javax.lang.model.element.TypeElement");
+        assertInferredFQNs("class X implements TypeElement", "TypeElement".length(), false, "javax.lang.model.element.TypeElement");
+        assertInferredFQNs("class X implements TypeMirror, TypeElement", "TypeElement".length(), false, "javax.lang.model.element.TypeElement");
+        assertInferredFQNs("class X implements java.util.List<TypeElement", "TypeElement".length(), false, "javax.lang.model.element.TypeElement");
+        assertInferredFQNs("class X implements java.util.List<TypeMirror, TypeElement", "TypeElement".length(), false, "javax.lang.model.element.TypeElement");
+        assertInferredFQNs("class X { ArrayList", "ArrayList".length(), false, "java.util.ArrayList");
+    }
+
+    public void testSuspendIndexing() throws Exception {
+        compiler.compile(outDir, "package test; public class FQNTest { }");
+        String jarName = "test.jar";
+        compiler.jar(outDir, jarName, "test/FQNTest.class");
+        Path continueMarkFile = outDir.resolve("continuemark").toAbsolutePath();
+        Files.createDirectories(continueMarkFile.getParent());
+        try (Writer w = Files.newBufferedWriter(continueMarkFile)) {}
+
+        Path runMarkFile = outDir.resolve("runmark").toAbsolutePath();
+        Files.deleteIfExists(runMarkFile);
+
+        getState().sourceCodeAnalysis();
+
+        new Thread() {
+            @Override public void run() {
+                assertEval("{new java.io.FileOutputStream(\"" + runMarkFile.toAbsolutePath().toString() + "\").close();" +
+                           " while (java.nio.file.Files.exists(java.nio.file.Paths.get(\"" + continueMarkFile.toString() + "\"))) Thread.sleep(100); }");
+            }
+        }.start();
+
+        while (!Files.exists(runMarkFile))
+            Thread.sleep(100);
+
+        addToClasspath(compiler.getPath(outDir).resolve(jarName));
+
+        String code = "FQNTest";
+
+        QualifiedNames candidates = getAnalysis().listQualifiedNames(code, code.length());
+
+        assertEquals(candidates.getNames(), Arrays.asList(), "Input: " + code + ", candidates=" + candidates.getNames());
+        assertEquals(candidates.isUpToDate(), false, "Input: " + code + ", up-to-date=" + candidates.isUpToDate());
+
+        Files.delete(continueMarkFile);
+
+        waitIndexingFinished();
+
+        candidates = getAnalysis().listQualifiedNames(code, code.length());
+
+        assertEquals(candidates.getNames(), Arrays.asList("test.FQNTest"), "Input: " + code + ", candidates=" + candidates.getNames());
+        assertEquals(true, candidates.isUpToDate(), "Input: " + code + ", up-to-date=" + candidates.isUpToDate());
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/test/jdk/jshell/InferTypeTest.java	Mon Feb 29 11:54:06 2016 +0100
@@ -0,0 +1,68 @@
+/*
+ * 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
+ * @bug 8131027
+ * @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<String>()", "ArrayList<String>");
+        assertInferredType("null", "Object");
+        assertInferredType("1 + ", null); //incomplete
+        assertInferredType("undef", null);  //unresolvable
+        assertEval("List<String> l1;");
+        assertEval("List<? extends String> l2;");
+        assertEval("List<? super String> l3;");
+        assertInferredType("l1", "List<String>");
+        assertInferredType("l2", "List<? extends String>");
+        assertInferredType("l3", "List<? super String>");
+        assertInferredType("l1.get(0)", "String");
+        assertInferredType("l2.get(0)", "String");
+        assertInferredType("l3.get(0)", "Object");
+        assertInferredType("\"\" + 1", "String");
+        assertEval("int i = 0;");
+        assertInferredType("i++", "int");
+        assertInferredType("++i", "int");
+        assertInferredType("i == 0 ? l1.get(0) : l2.get(0)", "String");
+        assertInferredType("", null);
+        assertInferredType("void test() { }", null);
+        assertInferredType("class Test { }", null);
+        assertInferredType("enum Test { A; }", null);
+        assertInferredType("interface Test { }", null);
+        assertInferredType("@interface Test { }", null);
+        assertInferredType("Object o;", null);
+    }
+
+}
--- a/langtools/test/jdk/jshell/KullaTesting.java	Thu Feb 25 11:28:25 2016 -0800
+++ b/langtools/test/jdk/jshell/KullaTesting.java	Mon Feb 29 11:54:06 2016 +0100
@@ -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.QualifiedNames;
 import jdk.jshell.SourceCodeAnalysis.Suggestion;
 import jdk.jshell.UnresolvedReferenceException;
 import org.testng.annotations.AfterMethod;
@@ -862,6 +864,8 @@
     }
 
     private List<String> 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,37 @@
                           .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) {
+        assertInferredFQNs(code, code.length(), false, fqns);
+    }
+
+    public void assertInferredFQNs(String code, int simpleNameLen, boolean resolvable, String... fqns) {
+        waitIndexingFinished();
+
+        QualifiedNames candidates = getAnalysis().listQualifiedNames(code, code.length());
+
+        assertEquals(candidates.getNames(), Arrays.asList(fqns), "Input: " + code + ", candidates=" + candidates.getNames());
+        assertEquals(candidates.getSimpleNameLength(), simpleNameLen, "Input: " + code + ", simpleNameLen=" + candidates.getSimpleNameLength());
+        assertEquals(candidates.isResolvable(), resolvable, "Input: " + code + ", resolvable=" + candidates.isResolvable());
+    }
+
+    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("|", "");