langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java
changeset 36160 f42d362d0d17
parent 34857 14d1224cfed3
child 36526 3b41f1c69604
--- 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;
+            }
+        }
+    }
 }