diff options
author | George Fraser <george@fivetran.com> | 2019-01-08 19:22:45 -0800 |
---|---|---|
committer | George Fraser <george@fivetran.com> | 2019-01-08 19:22:45 -0800 |
commit | 727bd8341afc689c842385a234647b8f7b22de21 (patch) | |
tree | 73230e098d2a6e72fc72901f4363b46530433f15 /src/main | |
parent | 46e72551e73f1de2417102fd3d7fbc358a42cfa6 (diff) | |
download | java-language-server-727bd8341afc689c842385a234647b8f7b22de21.zip |
Eliminate SourcePath, docpath isn't quite working
Diffstat (limited to 'src/main')
-rw-r--r-- | src/main/java/org/javacs/CompileBatch.java | 2 | ||||
-rw-r--r-- | src/main/java/org/javacs/CompileFile.java | 3 | ||||
-rw-r--r-- | src/main/java/org/javacs/CompileFocus.java | 28 | ||||
-rw-r--r-- | src/main/java/org/javacs/Docs.java | 6 | ||||
-rw-r--r-- | src/main/java/org/javacs/FileStore.java | 227 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavaCompilerService.java | 102 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavaLanguageServer.java | 38 | ||||
-rw-r--r-- | src/main/java/org/javacs/Parser.java | 1 | ||||
-rw-r--r-- | src/main/java/org/javacs/SourceFileManager.java | 69 | ||||
-rw-r--r-- | src/main/java/org/javacs/SourceFileObject.java | 2 | ||||
-rw-r--r-- | src/main/java/org/javacs/SourcePath.java | 168 |
11 files changed, 249 insertions, 397 deletions
diff --git a/src/main/java/org/javacs/CompileBatch.java b/src/main/java/org/javacs/CompileBatch.java index 947ba84..de6988c 100644 --- a/src/main/java/org/javacs/CompileBatch.java +++ b/src/main/java/org/javacs/CompileBatch.java @@ -79,7 +79,7 @@ public class CompileBatch { null, parent.fileManager, parent.diags::add, - JavaCompilerService.options(parent.sourcePath, parent.classPath), + JavaCompilerService.options(parent.classPath), Collections.emptyList(), sources); } diff --git a/src/main/java/org/javacs/CompileFile.java b/src/main/java/org/javacs/CompileFile.java index b0cae2c..3ae5313 100644 --- a/src/main/java/org/javacs/CompileFile.java +++ b/src/main/java/org/javacs/CompileFile.java @@ -217,7 +217,8 @@ public class CompileFile { } } // Look at imports in other classes to help us guess how to fix imports - var sourcePathImports = Parser.existingImports(parent.allJavaFiles.get()); + // TODO cache parsed imports on a per-file basis + var sourcePathImports = Parser.existingImports(FileStore.all()); var classes = new HashSet<String>(); classes.addAll(parent.jdkClasses.classes()); classes.addAll(parent.classPathClasses.classes()); diff --git a/src/main/java/org/javacs/CompileFocus.java b/src/main/java/org/javacs/CompileFocus.java index 48b5469..882cbf1 100644 --- a/src/main/java/org/javacs/CompileFocus.java +++ b/src/main/java/org/javacs/CompileFocus.java @@ -2,7 +2,6 @@ package org.javacs; import com.sun.source.tree.*; import com.sun.source.util.*; -import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.Path; @@ -62,7 +61,7 @@ public class CompileFocus { null, parent.fileManager, parent.diags::add, - JavaCompilerService.options(parent.sourcePath, parent.classPath), + JavaCompilerService.options(parent.classPath), Collections.emptyList(), List.of(new SourceFileObject(file, contents))); } @@ -135,12 +134,8 @@ public class CompileFocus { if (!insideClass) { // If no package declaration is present, suggest package [inferred name]; if (root.getPackage() == null) { - relativeToSourcePath(file) - .ifPresent( - relative -> { - var name = relative.toString().replace(File.separatorChar, '.'); - result.add(Completion.ofSnippet("package " + name, "package " + name + ";\n\n")); - }); + var name = FileStore.suggestedPackageName(Paths.get(file)); + result.add(Completion.ofSnippet("package " + name, "package " + name + ";\n\n")); } // If no class declaration is present, suggest class [file name] var hasClassDeclaration = false; @@ -406,17 +401,6 @@ public class CompileFocus { "double", }; - private Optional<Path> relativeToSourcePath(URI source) { - var p = Paths.get(source); - for (var root : parent.sourcePath) { - if (p.startsWith(root)) { - var rel = root.relativize(p.getParent()); - return Optional.of(rel); - } - } - return Optional.empty(); - } - private List<ExecutableElement> virtualMethods(DeclaredType type) { var result = new ArrayList<ExecutableElement>(); for (var member : type.asElement().getEnclosedElements()) { @@ -701,11 +685,11 @@ public class CompileFocus { // Check sourcepath LOG.info("...checking source path"); - for (var file : parent.allJavaFiles.get()) { + for (var file : FileStore.all()) { if (tooManyItems(result.size())) return; // If file is in the same package, any class defined in the file is accessible - var pathBasedPackageName = parent.pathBasedPackageName(file); - var samePackage = pathBasedPackageName.equals(packageName) || pathBasedPackageName.isEmpty(); + var otherPackageName = FileStore.packageName(file); + var samePackage = otherPackageName.equals(packageName) || otherPackageName.isEmpty(); // If file is in a different package, only a public class with the same name as the file is accessible var maybePublic = matchesPartialName(file.getFileName().toString(), partialName); if (samePackage || maybePublic) { diff --git a/src/main/java/org/javacs/Docs.java b/src/main/java/org/javacs/Docs.java index 10a0c95..b25c0f7 100644 --- a/src/main/java/org/javacs/Docs.java +++ b/src/main/java/org/javacs/Docs.java @@ -24,12 +24,14 @@ public class Docs { } } - Docs(Set<Path> sourcePath) { + Docs(Set<Path> docPath) { this.fileManager = ServiceLoader.load(JavaCompiler.class).iterator().next().getStandardFileManager(__ -> {}, null, null); // Compute the full source path, including src.zip for platform classes - var sourcePathFiles = sourcePath.stream().map(Path::toFile).collect(Collectors.toSet()); + // TODO source path needs to somehow get from FileStore to here; maybe write a FileManager that fuses FileStore + // with docPath? + var sourcePathFiles = docPath.stream().map(Path::toFile).collect(Collectors.toSet()); try { fileManager.setLocation(StandardLocation.SOURCE_PATH, sourcePathFiles); diff --git a/src/main/java/org/javacs/FileStore.java b/src/main/java/org/javacs/FileStore.java index 3c10a64..6982caa 100644 --- a/src/main/java/org/javacs/FileStore.java +++ b/src/main/java/org/javacs/FileStore.java @@ -13,9 +13,12 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.logging.Logger; @@ -25,76 +28,146 @@ import org.javacs.lsp.DidCloseTextDocumentParams; import org.javacs.lsp.DidOpenTextDocumentParams; import org.javacs.lsp.TextDocumentContentChangeEvent; -// TODO get rid of SourcePath and track source path dynamically here - class FileStore { + private static final Set<Path> workspaceRoots = new HashSet<>(); + private static final Map<URI, VersionedContent> activeDocuments = new HashMap<>(); - /** - * modified[file] is the modified time of file. modified contains .java files and directories on the source path. If - * modified contains a directory, it contains all the .java files in that directory. modified is sorted, so you can - * list all the .java files in a directory by doing modified.tailSet(directory) - */ - private static final TreeMap<Path, Instant> modified = new TreeMap<>(); + /** javaSources[file] is the javaSources time of a .java source file. */ + private static final TreeMap<Path, Info> javaSources = new TreeMap<>(); - /** Needed in tests */ - static void reset() { - modified.clear(); - activeDocuments.clear(); + private static class Info { + final Instant modified; + final String packageName; + + Info(Instant modified, String packageName) { + this.modified = modified; + this.packageName = packageName; + } } - static List<Path> list(Path dir, String packageName) { - // If we've never indexed dir, index it now - if (!modified.containsKey(dir)) { - LOG.info(String.format("Indexing %s for the first time", dir)); - try { - modified.put(dir, Instant.EPOCH); - Files.walk(dir).filter(SourcePath::isJavaFile).forEach(FileStore::readModifiedFromDisk); - } catch (IOException e) { - throw new RuntimeException(e); + static void setWorkspaceRoots(Set<Path> newRoots) { + newRoots = normalize(newRoots); + for (var root : workspaceRoots) { + if (!newRoots.contains(root)) { + workspaceRoots.removeIf(f -> f.startsWith(root)); } } - // Walk through a section of index, starting at dir and ending when we find the next dir - var afterDir = modified.tailMap(dir, false).keySet(); - var packageDir = dir.resolve(packageName.replace('.', File.separatorChar)); - var list = new ArrayList<Path>(); - for (var file : afterDir) { - if (!SourcePath.isJavaFile(file)) continue; - if (!file.startsWith(dir)) break; - if (file.getParent().equals(packageDir)) { - list.add(file); + for (var root : newRoots) { + if (!workspaceRoots.contains(root)) { + addFiles(root); } } + workspaceRoots.clear(); + workspaceRoots.addAll(newRoots); + } + + private static Set<Path> normalize(Set<Path> newRoots) { + var normalize = new HashSet<Path>(); + for (var root : newRoots) { + normalize.add(root.toAbsolutePath().normalize()); + } + return normalize; + } + + private static void addFiles(Path root) { + try { + Files.walk(root).filter(FileStore::isJavaFile).forEach(FileStore::readInfoFromDisk); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static Collection<Path> all() { + return javaSources.keySet(); + } + + static List<Path> list(String packageName) { + var list = new ArrayList<Path>(); + for (var kv : javaSources.entrySet()) { + var file = kv.getKey(); + var info = kv.getValue(); + if (!FileStore.isJavaFile(file)) continue; + if (info.packageName.equals(packageName)) list.add(file); + } return list; } + static boolean contains(Path file) { + return isJavaFile(file) && javaSources.containsKey(file); + } + static Instant modified(Path file) { // If we've never checked before, look up modified time on disk - if (!modified.containsKey(file)) { - readModifiedFromDisk(file); + if (!javaSources.containsKey(file)) { + readInfoFromDisk(file); + } + + // Look up modified time from cache + return javaSources.get(file).modified; + } + + static String packageName(Path file) { + // If we've never checked before, look up modified time on disk + if (!javaSources.containsKey(file)) { + readInfoFromDisk(file); } // Look up modified time from cache - return modified.get(file); + return javaSources.get(file).packageName; + } + + static String suggestedPackageName(Path file) { + var sourceRoot = sourceRoot(file); + var relativePath = sourceRoot.relativize(file).getParent(); + if (relativePath == null) return ""; + return relativePath.toString().replace(File.separatorChar, '.'); + } + + private static Path sourceRoot(Path file) { + for (var dir = file.getParent(); dir != null; dir = dir.getParent()) { + for (var related : javaSourcesIn(dir)) { + if (related.equals(file)) continue; + var packageName = packageName(related); + var relativePath = Paths.get(packageName.replace('.', File.separatorChar)); + var sourceRoot = dir; + for (var i = 0; i < relativePath.getNameCount(); i++) { + sourceRoot = sourceRoot.getParent(); + } + return sourceRoot; + } + } + return file.getParent(); + } + + private static List<Path> javaSourcesIn(Path dir) { + var tail = javaSources.tailMap(dir, false); + var list = new ArrayList<Path>(); + for (var file : tail.keySet()) { + if (!file.startsWith(dir)) break; + list.add(file); + } + return list; } static void externalCreate(Path file) { - readModifiedFromDisk(file); + readInfoFromDisk(file); } static void externalChange(Path file) { - readModifiedFromDisk(file); + readInfoFromDisk(file); } static void externalDelete(Path file) { - modified.remove(file); + javaSources.remove(file); } - private static void readModifiedFromDisk(Path file) { + private static void readInfoFromDisk(Path file) { try { var time = Files.getLastModifiedTime(file).toInstant(); - modified.put(file, time); + var packageName = Parser.packageName(file); + javaSources.put(file, new Info(time, packageName)); } catch (IOException e) { throw new RuntimeException(e); } @@ -103,14 +176,14 @@ class FileStore { static void open(DidOpenTextDocumentParams params) { var document = params.textDocument; var uri = document.uri; - if (!SourcePath.isJavaFile(uri)) return; + if (!FileStore.isJavaFile(uri)) return; activeDocuments.put(uri, new VersionedContent(document.text, document.version)); } static void change(DidChangeTextDocumentParams params) { var document = params.textDocument; var uri = document.uri; - if (SourcePath.isJavaFile(uri)) { + if (FileStore.isJavaFile(uri)) { var existing = activeDocuments.get(uri); var newText = existing.content; @@ -128,7 +201,7 @@ class FileStore { static void close(DidCloseTextDocumentParams params) { var document = params.textDocument; var uri = document.uri; - if (SourcePath.isJavaFile(uri)) { + if (FileStore.isJavaFile(uri)) { // Remove from source cache activeDocuments.remove(uri); } @@ -144,7 +217,7 @@ class FileStore { } static String contents(URI file) { - if (!SourcePath.isJavaFile(file)) { + if (!FileStore.isJavaFile(file)) { throw new RuntimeException(file + " is not a java file"); } if (activeDocuments.containsKey(file)) { @@ -232,5 +305,75 @@ class FileStore { } } + static boolean isJavaFile(Path file) { + var name = file.getFileName().toString(); + // We hide module-info.java from javac, because when javac sees module-info.java + // it goes into "module mode" and starts looking for classes on the module class path. + // This becomes evident when javac starts recompiling *way too much* on each task, + // because it doesn't realize there are already up-to-date .class files. + // The better solution would be for java-language server to detect the presence of module-info.java, + // and go into its own "module mode" where it infers a module source path and a module class path. + return name.endsWith(".java") && !name.equals("module-info.java"); + } + + static boolean isJavaFile(URI uri) { + return uri.getScheme().equals("file") && isJavaFile(Paths.get(uri)); + } + + private static Set<Path> allJavaFilesInDirs(Set<Path> dirs) { + var all = new HashSet<Path>(); + for (var dir : dirs) { + try { + Files.walk(dir).filter(FileStore::isJavaFile).forEach(all::add); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return all; + } + + private static Set<Path> sourcePath(Set<Path> workspaceRoots) { + LOG.info("Searching for source roots in " + workspaceRoots); + + var certaintyThreshold = 10; + var sourceRoots = new HashMap<Path, Integer>(); + fileLoop: + for (var file : allJavaFilesInDirs(workspaceRoots)) { + // First, check if we already have a high-confidence source root containing file + for (var root : sourceRoots.keySet()) { + var confidence = sourceRoots.get(root); + if (file.startsWith(root) && confidence > certaintyThreshold) { + continue fileLoop; + } + } + // Otherwise, parse the file and look at its package declaration + var parse = Parser.parse(file); + var packageName = Objects.toString(parse.getPackageName(), ""); + var packagePath = packageName.replace('.', File.separatorChar); + // If file has no package declaration, don't try to guess a source root + // This is probably a new file that the user will add a package declaration to later + if (packagePath.isEmpty()) { + LOG.warning("Ignoring file with missing package declaration " + file); + continue fileLoop; + } + // If path to file contradicts package declaration, give up + var dir = file.getParent(); + if (!dir.endsWith(packagePath)) { + LOG.warning("Java source file " + file + " is not in " + packagePath); + continue fileLoop; + } + // Otherwise, use the package declaration to guess the source root + var up = Paths.get(packagePath).getNameCount(); + var sourceRoot = dir; + for (int i = 0; i < up; i++) { + sourceRoot = sourceRoot.getParent(); + } + // Increment our confidence in sourceRoot as a source root + var count = sourceRoots.getOrDefault(sourceRoot, 0); + sourceRoots.put(sourceRoot, count + 1); + } + return sourceRoots.keySet(); + } + private static final Logger LOG = Logger.getLogger("main"); } diff --git a/src/main/java/org/javacs/JavaCompilerService.java b/src/main/java/org/javacs/JavaCompilerService.java index 23484e9..757722f 100644 --- a/src/main/java/org/javacs/JavaCompilerService.java +++ b/src/main/java/org/javacs/JavaCompilerService.java @@ -7,7 +7,6 @@ import java.io.IOException; import java.net.URI; import java.nio.file.*; import java.util.*; -import java.util.function.Supplier; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -17,8 +16,7 @@ import javax.tools.*; // TODO eliminate uses of URI in favor of Path public class JavaCompilerService { // Not modifiable! If you want to edit these, you need to create a new instance - final Set<Path> sourcePath, classPath, docPath; - final Supplier<Set<Path>> allJavaFiles; + final Set<Path> classPath, docPath; final JavaCompiler compiler = ServiceLoader.load(JavaCompiler.class).iterator().next(); final Docs docs; final ClassSource jdkClasses = Classes.jdkTopLevelClasses(), classPathClasses; @@ -28,12 +26,7 @@ public class JavaCompilerService { // TODO intercept files that aren't in the batch and erase method bodies so compilation is faster final StandardJavaFileManager fileManager; - public JavaCompilerService( - Set<Path> sourcePath, Supplier<Set<Path>> allJavaFiles, Set<Path> classPath, Set<Path> docPath) { - System.err.println("Source path:"); - for (var p : sourcePath) { - System.err.println(" " + p); - } + public JavaCompilerService(Set<Path> classPath, Set<Path> docPath) { System.err.println("Class path:"); for (var p : classPath) { System.err.println(" " + p); @@ -42,17 +35,12 @@ public class JavaCompilerService { for (var p : docPath) { System.err.println(" " + p); } - // sourcePath and classPath can't actually be modified, because JavaCompiler remembers them from task to task - this.sourcePath = Collections.unmodifiableSet(sourcePath); - this.allJavaFiles = allJavaFiles; + // classPath can't actually be modified, because JavaCompiler remembers it from task to task this.classPath = Collections.unmodifiableSet(classPath); this.docPath = Collections.unmodifiableSet(docPath); - var docSourcePath = new HashSet<Path>(); - docSourcePath.addAll(sourcePath); - docSourcePath.addAll(docPath); - this.docs = new Docs(docSourcePath); + this.docs = new Docs(docPath); this.classPathClasses = Classes.classPathTopLevelClasses(classPath); - this.fileManager = new SourceFileManager(sourcePath); + this.fileManager = new SourceFileManager(); ; } @@ -61,11 +49,10 @@ public class JavaCompilerService { return classOrSourcePath.stream().map(p -> p.toString()).collect(Collectors.joining(File.pathSeparator)); } - static List<String> options(Set<Path> sourcePath, Set<Path> classPath) { + static List<String> options(Set<Path> classPath) { var list = new ArrayList<String>(); Collections.addAll(list, "-classpath", joinPath(classPath)); - Collections.addAll(list, "-sourcepath", joinPath(sourcePath)); // Collections.addAll(list, "-verbose"); Collections.addAll(list, "-proc:none"); Collections.addAll(list, "-g"); @@ -86,21 +73,6 @@ public class JavaCompilerService { return list; } - String pathBasedPackageName(Path javaFile) { - if (!javaFile.getFileName().toString().endsWith(".java")) { - LOG.warning(javaFile + " does not end in .java"); - return "???"; - } - for (var dir : sourcePath) { - if (!javaFile.startsWith(dir)) continue; - var packageDir = javaFile.getParent(); - var relative = dir.relativize(packageDir); - return relative.toString().replace('/', '.'); - } - LOG.warning(javaFile + " is not in the source path " + sourcePath); - return "???"; - } - public Docs docs() { return docs; } @@ -141,7 +113,7 @@ public class JavaCompilerService { // Construct list of sources var files = new ArrayList<File>(); for (var uri : uris) { - if (SourcePath.isJavaFile(uri)) { + if (FileStore.isJavaFile(uri)) { files.add(new File(uri)); } } @@ -150,7 +122,7 @@ public class JavaCompilerService { var sources = fileManager.getJavaFileObjectsFromFiles(files); // Create task - var options = options(sourcePath, classPath); + var options = options(classPath); var task = (JavacTask) compiler.getTask(null, fileManager, diags::add, options, Collections.emptyList(), sources); var trees = Trees.instance(task); @@ -336,7 +308,7 @@ public class JavaCompilerService { return files; } // If the declaring file isn't a normal file, for example if it's in src.zip - if (!SourcePath.isJavaFile(toFile.get())) { + if (!FileStore.isJavaFile(toFile.get())) { LOG.info(String.format("...%s is not on the source path", toFile.get())); return files; } @@ -418,36 +390,12 @@ public class JavaCompilerService { var lastDot = name.lastIndexOf('.'); var packageName = lastDot == -1 ? "" : name.substring(0, lastDot); var className = name.substring(lastDot + 1); - // First, look for a file named [ClassName].java - var packagePath = Paths.get(packageName.replace('.', File.separatorChar)); - var publicClassPath = packagePath.resolve(className + ".java"); - for (var root : sourcePath) { - var absPath = root.resolve(publicClassPath); - if (Files.exists(absPath) && containsTopLevelDeclaration(absPath, className)) { - return Optional.of(absPath.toUri()); - } - } - // Then, look for a secondary declaration in all java files in the package var isPublic = e.getModifiers().contains(Modifier.PUBLIC); - if (!isPublic) { - for (var root : sourcePath) { - // Create directory where this package would live, if this package is in this part of the source path - var absDir = root.resolve(packagePath); - // If package isn't in this part of the source path, keep looking - if (!Files.exists(absDir)) continue; - // List dirs - Iterable<Path> list; - try { - list = Files.list(absDir)::iterator; - } catch (IOException err) { - throw new RuntimeException(err); - } - // Check each .java file in the package for package-private class declarations - for (var f : list) { - if (SourcePath.isJavaFile(f) && containsTopLevelDeclaration(f, className)) { - return Optional.of(f.toUri()); - } - } + for (var file : FileStore.list(packageName)) { + // Public classes have to be declared in files named ClassName.java + if (isPublic && !file.getFileName().toString().equals(className + ".java")) continue; + if (containsTopLevelDeclaration(file, className)) { + return Optional.of(file.toUri()); } } return Optional.empty(); @@ -466,32 +414,20 @@ public class JavaCompilerService { return false; } - private Set<Path> possibleFiles(Element to) { + private Collection<Path> possibleFiles(Element to) { // If `to` is package-private, only look in my own package if (isPackagePrivate(to)) { var myPkg = packageName(to); - var allFiles = sourceFilesInPackages(myPkg); + var allFiles = FileStore.list(myPkg); LOG.info(String.format("...check %d files in my own package %s", allFiles.size(), myPkg)); + return allFiles; } // Otherwise search all files - var allFiles = allJavaFiles.get(); + var allFiles = FileStore.all(); LOG.info(String.format("...check %d files", allFiles.size())); return allFiles; } - /** List .java source files in package */ - private Set<Path> sourceFilesInPackages(String inPackage) { - var packagePath = Paths.get(inPackage.replace('.', File.separatorChar)); - var files = new HashSet<Path>(); - for (var f : allJavaFiles.get()) { - var dir = f.getParent(); - if (dir.endsWith(packagePath)) { - files.add(f); - } - } - return files; - } - private static Cache<String, Boolean> cacheContainsWord = new Cache<>(); private List<Path> containsWord(Collection<Path> allFiles, Element to) { @@ -605,7 +541,7 @@ public class JavaCompilerService { LOG.info(String.format("Searching for `%s`...", query)); var result = new ArrayList<TreePath>(); - var files = allJavaFiles.get(); + var files = FileStore.all(); var checked = 0; var parsed = 0; for (var file : files) { diff --git a/src/main/java/org/javacs/JavaLanguageServer.java b/src/main/java/org/javacs/JavaLanguageServer.java index 8a11e90..175e4ce 100644 --- a/src/main/java/org/javacs/JavaLanguageServer.java +++ b/src/main/java/org/javacs/JavaLanguageServer.java @@ -44,7 +44,6 @@ import org.javacs.lsp.*; class JavaLanguageServer extends LanguageServer { // TODO allow multiple workspace roots private Path workspaceRoot; - private SourcePath sourcePath; private final LanguageClient client; private Set<String> externalDependencies = Set.of(); private Set<Path> classPath = Set.of(); @@ -145,8 +144,7 @@ class JavaLanguageServer extends LanguageServer { // If classpath is specified by the user, don't infer anything if (!classPath.isEmpty()) { javaEndProgress(); - return new JavaCompilerService( - sourcePath.sourceRoots(), sourcePath::allJavaFiles, classPath, Collections.emptySet()); + return new JavaCompilerService(classPath, Collections.emptySet()); } // Otherwise, combine inference with user-specified external dependencies else { @@ -159,7 +157,7 @@ class JavaLanguageServer extends LanguageServer { var docPath = infer.buildDocPath(); javaEndProgress(); - return new JavaCompilerService(sourcePath.sourceRoots(), sourcePath::allJavaFiles, classPath, docPath); + return new JavaCompilerService(classPath, docPath); } } @@ -180,7 +178,7 @@ class JavaLanguageServer extends LanguageServer { @Override public InitializeResult initialize(InitializeParams params) { this.workspaceRoot = Paths.get(params.rootUri); - this.sourcePath = new SourcePath(Set.of(workspaceRoot)); + FileStore.setWorkspaceRoots(Set.of(Paths.get(params.rootUri))); var c = new JsonObject(); c.addProperty("textDocumentSync", 2); // Incremental @@ -260,30 +258,21 @@ class JavaLanguageServer extends LanguageServer { @Override public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { // TODO update config when pom.xml changes - var changed = false; - var created = new HashSet<Path>(); - var deleted = new HashSet<Path>(); for (var c : params.changes) { - if (!SourcePath.isJavaFile(c.uri)) continue; + if (!FileStore.isJavaFile(c.uri)) continue; var file = Paths.get(c.uri); switch (c.type) { case FileChangeType.Created: FileStore.externalCreate(file); - created.add(file); break; case FileChangeType.Changed: FileStore.externalChange(file); - if (sourcePath.update(file)) changed = true; break; case FileChangeType.Deleted: FileStore.externalDelete(file); - deleted.add(file); break; } } - if (sourcePath.create(created)) changed = true; - if (sourcePath.delete(deleted)) changed = true; - if (changed) this.compiler = createCompiler(); } private Integer completionItemKind(Element e) { @@ -342,7 +331,7 @@ class JavaLanguageServer extends LanguageServer { // TODO reuse previous compilation when changes are small var started = Instant.now(); var uri = position.textDocument.uri; - if (!SourcePath.isJavaFile(uri)) return Optional.empty(); + if (!FileStore.isJavaFile(uri)) return Optional.empty(); var content = FileStore.contents(uri); var line = position.position.line + 1; var column = position.position.character + 1; @@ -644,7 +633,7 @@ class JavaLanguageServer extends LanguageServer { public Optional<Hover> hover(TextDocumentPositionParams position) { // Compile entire file if it's changed since last hover var uri = position.textDocument.uri; - if (!SourcePath.isJavaFile(uri)) return Optional.empty(); + if (!FileStore.isJavaFile(uri)) return Optional.empty(); updateActiveFile(uri); // Find element undeer cursor @@ -748,7 +737,7 @@ class JavaLanguageServer extends LanguageServer { @Override public Optional<SignatureHelp> signatureHelp(TextDocumentPositionParams position) { var uri = position.textDocument.uri; - if (!SourcePath.isJavaFile(uri)) return Optional.empty(); + if (!FileStore.isJavaFile(uri)) return Optional.empty(); var content = FileStore.contents(uri); var line = position.position.line + 1; var column = position.position.character + 1; @@ -762,7 +751,7 @@ class JavaLanguageServer extends LanguageServer { @Override public Optional<List<Location>> gotoDefinition(TextDocumentPositionParams position) { var fromUri = position.textDocument.uri; - if (!SourcePath.isJavaFile(fromUri)) return Optional.empty(); + if (!FileStore.isJavaFile(fromUri)) return Optional.empty(); var fromLine = position.position.line + 1; var fromColumn = position.position.character + 1; @@ -803,7 +792,7 @@ class JavaLanguageServer extends LanguageServer { @Override public Optional<List<Location>> findReferences(ReferenceParams position) { var toUri = position.textDocument.uri; - if (!SourcePath.isJavaFile(toUri)) return Optional.empty(); + if (!FileStore.isJavaFile(toUri)) return Optional.empty(); var toLine = position.position.line + 1; var toColumn = position.position.character + 1; @@ -877,7 +866,7 @@ class JavaLanguageServer extends LanguageServer { @Override public List<SymbolInformation> documentSymbol(DocumentSymbolParams params) { var uri = params.textDocument.uri; - if (!SourcePath.isJavaFile(uri)) return List.of(); + if (!FileStore.isJavaFile(uri)) return List.of(); updateCachedParse(uri); var paths = cacheParse.documentSymbols(); var infos = new ArrayList<SymbolInformation>(); @@ -956,7 +945,7 @@ class JavaLanguageServer extends LanguageServer { public List<CodeLens> codeLens(CodeLensParams params) { // TODO just create a blank code lens on every method, then resolve it async var uri = params.textDocument.uri; - if (!SourcePath.isJavaFile(uri)) return List.of(); + if (!FileStore.isJavaFile(uri)) return List.of(); updateCachedParse(uri); var declarations = cacheParse.declarations(); var result = new ArrayList<CodeLens>(); @@ -1190,6 +1179,7 @@ class JavaLanguageServer extends LanguageServer { edits.addAll(fixImports()); edits.addAll(addOverrides()); // TODO replace var with type name when vars are copy-pasted into fields + // TODO replace ThisClass.staticMethod() with staticMethod() when ThisClass is useless return edits; } @@ -1364,7 +1354,7 @@ class JavaLanguageServer extends LanguageServer { public void didCloseTextDocument(DidCloseTextDocumentParams params) { FileStore.close(params); - if (SourcePath.isJavaFile(params.textDocument.uri)) { + if (FileStore.isJavaFile(params.textDocument.uri)) { // Clear diagnostics publishDiagnostics(List.of(params.textDocument.uri), List.of()); } @@ -1372,7 +1362,7 @@ class JavaLanguageServer extends LanguageServer { @Override public void didSaveTextDocument(DidSaveTextDocumentParams params) { - if (SourcePath.isJavaFile(params.textDocument.uri)) { + if (FileStore.isJavaFile(params.textDocument.uri)) { // Re-lint all active documents reportErrors(FileStore.activeDocuments()); } diff --git a/src/main/java/org/javacs/Parser.java b/src/main/java/org/javacs/Parser.java index b70b963..b6d07c1 100644 --- a/src/main/java/org/javacs/Parser.java +++ b/src/main/java/org/javacs/Parser.java @@ -269,6 +269,7 @@ class Parser { } catch (IOException e) { throw new RuntimeException(e); } + // TODO fall back on parsing file return ""; } diff --git a/src/main/java/org/javacs/SourceFileManager.java b/src/main/java/org/javacs/SourceFileManager.java index c174d2b..6af01fa 100644 --- a/src/main/java/org/javacs/SourceFileManager.java +++ b/src/main/java/org/javacs/SourceFileManager.java @@ -4,9 +4,7 @@ import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import java.net.URLClassLoader; import java.nio.charset.Charset; -import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.logging.Logger; @@ -14,11 +12,6 @@ import javax.tools.*; class SourceFileManager implements StandardJavaFileManager { private final StandardJavaFileManager delegate = createDelegateFileManager(); - private final Set<Path> sourcePath; - - SourceFileManager(Set<Path> sourcePath) { - this.sourcePath = sourcePath; - } private static StandardJavaFileManager createDelegateFileManager() { var compiler = ServiceLoader.load(JavaCompiler.class).iterator().next(); @@ -31,12 +24,7 @@ class SourceFileManager implements StandardJavaFileManager { @Override public ClassLoader getClassLoader(Location location) { - var thisClassLoader = getClass().getClassLoader(); - if (location == StandardLocation.SOURCE_PATH) { - return new URLClassLoader(urls(sourcePath), thisClassLoader); - } else { - return thisClassLoader; - } + return delegate.getClassLoader(location); } private URL[] urls(Set<Path> paths) { @@ -56,17 +44,15 @@ class SourceFileManager implements StandardJavaFileManager { public Iterable<JavaFileObject> list( Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException { if (location == StandardLocation.SOURCE_PATH) { - var dirs = sourcePath.stream(); - var files = dirs.flatMap(dir -> FileStore.list(dir, packageName).stream()); - var filter = files.map(this::asJavaFileObject).filter(this::isJavaSource); - return filter::iterator; + var stream = FileStore.list(packageName).stream().map(this::asJavaFileObject).filter(this::isJavaSource); + return stream::iterator; } else { return delegate.list(location, packageName, kinds, recurse); } } private boolean isJavaSource(JavaFileObject file) { - return SourcePath.isJavaFile(file.toUri()); + return FileStore.isJavaFile(file.toUri()); } private JavaFileObject asJavaFileObject(Path file) { @@ -77,27 +63,15 @@ class SourceFileManager implements StandardJavaFileManager { public String inferBinaryName(Location location, JavaFileObject file) { if (location == StandardLocation.SOURCE_PATH) { var source = (SourceFileObject) file; - return sourceFileBinaryName(sourcePath, source); + var packageName = FileStore.packageName(source.path); + var className = removeExtension(source.path.getFileName().toString()); + if (!packageName.isEmpty()) className = packageName + "." + className; + return className; } else { return delegate.inferBinaryName(location, file); } } - private String sourceFileBinaryName(Set<Path> path, SourceFileObject source) { - for (var root : path) { - if (source.path.startsWith(root)) { - var relativePath = root.relativize(source.path).toString(); - return binaryName(relativePath); - } - } - return null; - } - - private String binaryName(String relativePath) { - var slash = removeExtension(relativePath); - return slash.replace(File.separatorChar, '.'); - } - private String removeExtension(String fileName) { var lastDot = fileName.lastIndexOf("."); return (lastDot == -1 ? fileName : fileName.substring(0, lastDot)); @@ -122,8 +96,12 @@ class SourceFileManager implements StandardJavaFileManager { public JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind) throws IOException { if (location == StandardLocation.SOURCE_PATH) { - var relative = className.replace('.', '/') + kind.extension; - return findFileForInput(location, relative); + var packageName = Parser.mostName(className); + var simpleClassName = Parser.lastName(className); + for (var f : FileStore.list(packageName)) { + if (f.getFileName().toString().equals(simpleClassName + kind.extension)) return new SourceFileObject(f); + } + return null; } return delegate.getJavaFileForInput(location, className, kind); } @@ -131,26 +109,11 @@ class SourceFileManager implements StandardJavaFileManager { @Override public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException { if (location == StandardLocation.SOURCE_PATH) { - var relative = relativeName; - if (!packageName.isEmpty()) { - relative = packageName.replace('.', '/') + '/' + relative; - } - return findFileForInput(location, relative); + return null; } return delegate.getFileForInput(location, packageName, relativeName); } - private JavaFileObject findFileForInput(Location location, String relative) { - for (var root : sourcePath) { - var absolute = root.resolve(relative); - if (!SourcePath.isJavaFile(absolute)) return null; - if (Files.exists(absolute)) { - return new SourceFileObject(absolute); - } - } - return null; - } - @Override public JavaFileObject getJavaFileForOutput( Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { @@ -204,7 +167,7 @@ class SourceFileManager implements StandardJavaFileManager { public boolean contains(Location location, FileObject file) throws IOException { if (location == StandardLocation.SOURCE_PATH) { var source = (SourceFileObject) file; - return contains(sourcePath, source); + return FileStore.contains(source.path); } else { return delegate.contains(location, file); } diff --git a/src/main/java/org/javacs/SourceFileObject.java b/src/main/java/org/javacs/SourceFileObject.java index 46d652d..d0b32a6 100644 --- a/src/main/java/org/javacs/SourceFileObject.java +++ b/src/main/java/org/javacs/SourceFileObject.java @@ -27,7 +27,7 @@ class SourceFileObject implements JavaFileObject { } SourceFileObject(Path path, String contents) { - if (!SourcePath.isJavaFile(path)) throw new RuntimeException(path + " is not a java source"); + if (!FileStore.isJavaFile(path)) throw new RuntimeException(path + " is not a java source"); this.path = path; this.contents = contents; } diff --git a/src/main/java/org/javacs/SourcePath.java b/src/main/java/org/javacs/SourcePath.java deleted file mode 100644 index 9c17252..0000000 --- a/src/main/java/org/javacs/SourcePath.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.javacs; - -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.logging.Logger; - -class SourcePath { - private final Set<Path> workspaceRoots; - /** All .java files under workspaceRoot */ - private final Set<Path> allJavaFiles; - /** Source roots, computed from allJavaFiles by looking at package declarations. */ - private Set<Path> sourceRoots = new HashSet<>(); - - public SourcePath(Set<Path> workspaceRoots) { - this.workspaceRoots = Collections.unmodifiableSet(workspaceRoots); - this.sourceRoots = sourcePath(workspaceRoots); - this.allJavaFiles = allJavaFilesInDirs(sourceRoots); - } - - /** - * A .java file has been created. Update allJavaFiles, but don't recalculate sourceRoots until the file is saved, - * because the user hasn't had a change to type in a package name yet. - */ - boolean create(Set<Path> javaFiles) { - if (javaFiles.isEmpty()) return false; - for (var f : javaFiles) { - if (isJavaFile(f)) { - allJavaFiles.add(f); - } - } - return checkSourceRoots(); - } - - /** - * A .java file has been deleted. If the file is the last one in a particular source root, we should remove that - * source root. - */ - boolean delete(Set<Path> javaFiles) { - if (javaFiles.isEmpty()) return false; - allJavaFiles.removeAll(javaFiles); - return checkSourceRoots(); - } - - /** - * A .java file has been edited and saved. If the package declaration has been changed, we may need to recalculate - * sourceRoots. - */ - // TODO batch this - boolean update(Path javaFile) { - if (!isJavaFile(javaFile)) return false; - // Check if file is in an existing source root - for (var root : sourceRoots) { - if (!javaFile.startsWith(root)) continue; - var dir = javaFile.getParent(); - var expected = root.relativize(dir).toString().replace('/', '.'); - var parse = Parser.parse(javaFile); - var found = Objects.toString(parse.getPackageName(), ""); - if (!expected.equals(found)) { - LOG.info(String.format("%s is in %s not %s", javaFile.getFileName(), found, expected)); - return checkSourceRoots(); - } - return false; - } - - // Apparently, a new source root needs to be created - LOG.info(String.format("%s is not in the source path", javaFile)); - return checkSourceRoots(); - } - - Set<Path> sourceRoots() { - return Collections.unmodifiableSet(sourceRoots); - } - - Set<Path> allJavaFiles() { - return Collections.unmodifiableSet(allJavaFiles); - } - - private boolean checkSourceRoots() { - var newSourceRoots = sourcePath(workspaceRoots); - if (!newSourceRoots.equals(sourceRoots)) { - LOG.info(String.format("Source path has changed from %s to %s", sourceRoots, newSourceRoots)); - sourceRoots = newSourceRoots; - return true; - } - return false; - } - - static boolean isJavaFile(Path file) { - var name = file.getFileName().toString(); - // We hide module-info.java from javac, because when javac sees module-info.java - // it goes into "module mode" and starts looking for classes on the module class path. - // This becomes evident when javac starts recompiling *way too much* on each task, - // because it doesn't realize there are already up-to-date .class files. - // The better solution would be for java-language server to detect the presence of module-info.java, - // and go into its own "module mode" where it infers a module source path and a module class path. - return name.endsWith(".java") && !name.equals("module-info.java"); - } - - static boolean isJavaFile(URI uri) { - return uri.getScheme().equals("file") && isJavaFile(Paths.get(uri)); - } - - private static Set<Path> allJavaFilesInDirs(Set<Path> dirs) { - var all = new HashSet<Path>(); - for (var dir : dirs) { - try { - Files.walk(dir).filter(SourcePath::isJavaFile).forEach(all::add); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - return all; - } - - private static Set<Path> sourcePath(Set<Path> workspaceRoots) { - LOG.info("Searching for source roots in " + workspaceRoots); - - var certaintyThreshold = 10; - var sourceRoots = new HashMap<Path, Integer>(); - fileLoop: - for (var file : allJavaFilesInDirs(workspaceRoots)) { - // First, check if we already have a high-confidence source root containing file - for (var root : sourceRoots.keySet()) { - var confidence = sourceRoots.get(root); - if (file.startsWith(root) && confidence > certaintyThreshold) { - continue fileLoop; - } - } - // Otherwise, parse the file and look at its package declaration - var parse = Parser.parse(file); - var packageName = Objects.toString(parse.getPackageName(), ""); - var packagePath = packageName.replace('.', File.separatorChar); - // If file has no package declaration, don't try to guess a source root - // This is probably a new file that the user will add a package declaration to later - if (packagePath.isEmpty()) { - LOG.warning("Ignoring file with missing package declaration " + file); - continue fileLoop; - } - // If path to file contradicts package declaration, give up - var dir = file.getParent(); - if (!dir.endsWith(packagePath)) { - LOG.warning("Java source file " + file + " is not in " + packagePath); - continue fileLoop; - } - // Otherwise, use the package declaration to guess the source root - var up = Paths.get(packagePath).getNameCount(); - var sourceRoot = dir; - for (int i = 0; i < up; i++) { - sourceRoot = sourceRoot.getParent(); - } - // Increment our confidence in sourceRoot as a source root - var count = sourceRoots.getOrDefault(sourceRoot, 0); - sourceRoots.put(sourceRoot, count + 1); - } - return sourceRoots.keySet(); - } - - private static final Logger LOG = Logger.getLogger("main"); -} |