diff options
author | George Fraser <george@fivetran.com> | 2018-12-23 12:34:11 -0800 |
---|---|---|
committer | George Fraser <george@fivetran.com> | 2018-12-23 12:34:11 -0800 |
commit | b6ad35a751145fb1e4b820d5ea11fc596a2d156d (patch) | |
tree | 75094281d7515548a990f4fd856b844bf8cb3d6c | |
parent | d6aaf1e59cb3fd68ee72e9d2753b64f7af4ae426 (diff) | |
download | java-language-server-b6ad35a751145fb1e4b820d5ea11fc596a2d156d.zip |
JavaCompilerService.referencesFile indexes all files that reference a file
-rw-r--r-- | TODOS.md | 8 | ||||
-rw-r--r-- | src/main/java/org/javacs/Index.java | 16 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavaCompilerService.java | 241 | ||||
-rw-r--r-- | src/main/java/org/javacs/QName.java | 10 | ||||
-rw-r--r-- | src/main/java/org/javacs/Ref.java | 21 | ||||
-rw-r--r-- | src/test/java/org/javacs/JavaCompilerServiceTest.java | 69 |
6 files changed, 299 insertions, 66 deletions
@@ -11,6 +11,8 @@ ## Autocomplete - Annotation fields - cc should match CamelCase +- Detail of fields, vars should be type +- Autocomplete this.x = x; this.y = y; ... in constructor ## Navigation - Go-to-subclasses @@ -18,6 +20,8 @@ ## Polish - Convert {@tag ...} to `<tag>...</tag>` (see vscode-java) - Auto-collapse imports +- Hover constructor should show constructor, not class +- String.format(...) coloring ## Simplicity - Use module-info.java instead of build files to figure out classpath @@ -25,3 +29,7 @@ ## JShell - Support .jshell extension as "scratch pad" + +# Coloring +- new Foo< shouldn't make everything gree +- void f() shouldn't mess up next line as you type it diff --git a/src/main/java/org/javacs/Index.java b/src/main/java/org/javacs/Index.java new file mode 100644 index 0000000..e910ed4 --- /dev/null +++ b/src/main/java/org/javacs/Index.java @@ -0,0 +1,16 @@ +package org.javacs; + +import java.time.Instant; +import java.util.List; + +public class Index { + public static final Index EMPTY = new Index(List.of(), Instant.EPOCH); + + public final List<Ref> refs; + public final Instant created; + + public Index(List<Ref> refs, Instant created) { + this.refs = refs; + this.created = created; + } +} diff --git a/src/main/java/org/javacs/JavaCompilerService.java b/src/main/java/org/javacs/JavaCompilerService.java index 5ae5d80..894df17 100644 --- a/src/main/java/org/javacs/JavaCompilerService.java +++ b/src/main/java/org/javacs/JavaCompilerService.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.*; +import java.nio.file.attribute.FileTime; import java.time.Duration; import java.time.Instant; import java.util.*; @@ -1276,7 +1277,7 @@ public class JavaCompilerService { var importStatic = Pattern.compile("^import +static +" + toPackage + "\\."); var startOfClass = Pattern.compile("class +\\w+"); var word = Pattern.compile("\\b" + name + "\\b"); - var foundImport = toPackage.isEmpty(); + var foundImport = toPackage.isEmpty(); // If package is empty, everyone imports it var foundWord = false; try (var read = Files.newBufferedReader(file)) { while (true) { @@ -1296,27 +1297,102 @@ public class JavaCompilerService { return foundImport && foundWord; } - private List<Path> potentialReferences(String toPackage, String toClass, Element toEl, ReportReferencesProgress progress) { - // Enumerate all files on source path + private boolean importsAnyClass(String toPackage, List<String> toClasses, Path file) { + if (toPackage.isEmpty()) return true; // If package is empty, everyone imports it + var toClass = toClasses.stream().collect(Collectors.joining("|")); + var samePackage = Pattern.compile("^package +" + toPackage + ";"); + var importClass = Pattern.compile("^import +" + toPackage + "\\.(" + toClass + ");"); + var importStar = Pattern.compile("^import +" + toPackage + "\\.\\*;"); + var importStatic = Pattern.compile("^import +static +" + toPackage + "\\."); + var startOfClass = Pattern.compile("class +\\w+"); + try (var read = Files.newBufferedReader(file)) { + while (true) { + var line = read.readLine(); + if (line == null) break; + if (startOfClass.matcher(line).find()) break; + if (samePackage.matcher(line).find()) return true; + if (importClass.matcher(line).find()) return true; + if (importStar.matcher(line).find()) return true; + if (importStatic.matcher(line).find()) return true; + if (importClass.matcher(line).find()) return true; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return false; + } + + private List<Path> allJavaSources() { var allFiles = new ArrayList<Path>(); for (var dir : sourcePath) { for (var file : javaSourcesInDir(dir)) { allFiles.add(file); } } + return allFiles; + } + + private List<Path> potentialReferences(String toPackage, String toClass, Element toEl, ReportReferencesProgress progress) { + // Enumerate all files on source path + var allFiles = allJavaSources(); progress.scanForPotentialReferences(0, allFiles.size()); // Filter for files that import toPackage.toClass and contain the word el + // TODO should probably cache this var name = toEl.getSimpleName().toString(); var result = new ArrayList<Path>(); int nScanned = 0; for (var file : allFiles) { - if (containsWord(toPackage, toClass, name, file)) + if (toPackage.isEmpty() || containsWord(toPackage, toClass, name, file)) result.add(file); progress.scanForPotentialReferences(++nScanned, allFiles.size()); } return result; } + private List<Path> potentialReferencesToClasses(String toPackage, List<String> toClasses, ReportReferencesProgress progress) { + // Enumerate all files on source path + var allFiles = allJavaSources(); + progress.scanForPotentialReferences(0, allFiles.size()); + // Filter for files that import toPackage.toClass + // TODO should probably cache this + var result = new ArrayList<Path>(); + int nScanned = 0; + for (var dir : sourcePath) { + for (var file : javaSourcesInDir(dir)) { + if (importsAnyClass(toPackage, toClasses, file)) + result.add(file); + progress.scanForPotentialReferences(++nScanned, allFiles.size()); + } + } + return result; + } + + private static String id(TreePath path) { + var parts = new ArrayList<String>(); + while (path != null) { + var part = path.getLeaf(); + if (part instanceof PackageTree) { + var pkg = (PackageTree) part; + var name = Objects.toString(pkg.getPackageName(), ""); + parts.add(name); + } else if (part instanceof ClassTree) { + var cls = (ClassTree) part; + var name = Objects.toString(cls.getSimpleName(), ""); + parts.add(name); + } else if (part instanceof MethodTree) { + var method = (MethodTree) part; + var name = Objects.toString(method.getName(), ""); + parts.add(name); + } else if (part instanceof VariableTree) { + var variable = (VariableTree) part; + var name = Objects.toString(variable.getName(), ""); + parts.add(name); + } + path = path.getParentPath(); + } + return parts.stream().collect(Collectors.joining(".")); + } + /** * Represents a batch compilation of many files. The batch context is different that the incremental context, so * methods in this class should not access `cache`. @@ -1330,23 +1406,22 @@ public class JavaCompilerService { this.roots = roots; } - private boolean toStringEquals(Object left, Object right) { + boolean toStringEquals(Object left, Object right) { return Objects.equals(Objects.toString(left, ""), Objects.toString(right, "")); } - private boolean sameSymbol(Element target, Element symbol) { - return symbol != null - && target != null - && toStringEquals(symbol.getEnclosingElement(), target.getEnclosingElement()) - && toStringEquals(symbol, target); + boolean sameSymbol(Element from, Element to) { + return to != null + && from != null + && toStringEquals(to.getEnclosingElement(), from.getEnclosingElement()) + && toStringEquals(to, from); } - private List<TreePath> actualReferences(CompilationUnitTree from, Element to) { + List<TreePath> referencesToElement(CompilationUnitTree from, Element to) { var trees = Trees.instance(task); - - class Finder extends TreeScanner<Void, Void> { - List<TreePath> results = new ArrayList<>(); - + var results = new ArrayList<TreePath>(); + class FindReferencesElement extends TreeScanner<Void, Void> { + // TODO this scans a LOT, can this be more specific? @Override public Void scan(Tree leaf, Void nothing) { if (leaf != null) { @@ -1358,13 +1433,52 @@ public class JavaCompilerService { } return null; } + } + from.accept(new FindReferencesElement(), null); + return results; + } - List<TreePath> run() { - scan(from, null); - return results; + List<Ref> index(CompilationUnitTree root) { + var trees = Trees.instance(task); + var pos = trees.getSourcePositions(); + var lines = root.getLineMap(); + var refs = new ArrayList<Ref>(); + class IndexFile extends TreePathScanner<Void, Void> { + private void accept(TreePath from) { + var to = trees.getElement(from); + // TODO skip this for anything not on the source path + if (to != null) { + if (to.getSimpleName().contentEquals("<init>")) return; + long start = pos.getStartPosition(root, from.getLeaf()), end = pos.getEndPosition(root, from.getLeaf()); + if (start == -1 || end == -1) { + LOG.warning(String.format("Position of %s in %s is %d:%d", from.getLeaf(), root.getSourceFile(), start, end)); + return; + } + int startLine = (int) lines.getLineNumber(start) - 1, startCol = (int) lines.getColumnNumber(start) - 1; + int endLine = (int) lines.getLineNumber(end) - 1, endCol = (int) lines.getColumnNumber(end) - 1; + var toPath = trees.getPath(to); + var toId = id(toPath); + if (toId.isEmpty()) return; // TODO log a warning if we can't get an ID for something on the source path + var fromFile = root.getSourceFile().toUri(); + var toFile = toPath.getCompilationUnit().getSourceFile().toUri(); + var ref = new Ref(fromFile, startLine, startCol, endLine, endCol, toFile, toId); + refs.add(ref); + } + } + + @Override + public Void visitClass(ClassTree node, Void nothing) { + super.visitClass(node, nothing); + accept(getCurrentPath()); + for (var t : node.getMembers()) { + var child = new TreePath(getCurrentPath(), t); + accept(child); + } + return null; } } - return new Finder().run(); + new IndexFile().scan(root, null); + return refs; } } @@ -1384,18 +1498,31 @@ public class JavaCompilerService { return new Batch(task, result); } - private QName className(TreePath path) { - var packageName = Objects.toString(path.getCompilationUnit().getPackageName(), ""); + private String className(TreePath path) { while (path != null) { var leaf = path.getLeaf(); if (leaf instanceof ClassTree) { var classTree = (ClassTree) leaf; var className = Objects.toString(classTree.getSimpleName(), ""); - return new QName(packageName, className); + return className; } path = path.getParentPath(); } - return new QName(packageName, ""); + return ""; + } + + private List<String> allClassNames(CompilationUnitTree root) { + var result = new ArrayList<String>(); + class FindClasses extends TreeScanner<Void, Void> { + @Override + public Void visitClass(ClassTree classTree, Void __) { + var className = Objects.toString(classTree.getSimpleName(), ""); + result.add(className); + return null; + } + } + root.accept(new FindClasses(), null); + return result; } public List<TreePath> references(URI file, String contents, int line, int character, ReportReferencesProgress progress) { @@ -1407,8 +1534,9 @@ public class JavaCompilerService { // `to` is part of a different batch than `batch = compileBatch(possible)`, // so `to.equals(...thing from batch...)` shouldn't work var toEl = trees.getElement(path); + var toPackage = Objects.toString(path.getCompilationUnit().getPackageName(), ""); var toClass = className(path); - var possible = potentialReferences(toClass.packageName, toClass.className, toEl, progress); + var possible = potentialReferences(toPackage, toClass, toEl, progress); if (possible.isEmpty()) { LOG.info("No potential references to " + toEl); return List.of(); @@ -1417,12 +1545,69 @@ public class JavaCompilerService { // TODO optimize by pruning method bodies that don't contain potential references var batch = compileBatch(possible); var result = new ArrayList<TreePath>(); - int nChecked = 0; + var nChecked = 0; + for (var f : batch.roots) { + result.addAll(batch.referencesToElement(f, toEl)); + nChecked++; + progress.checkPotentialReferences(nChecked, possible.size()); + } + return result; + } + + private Map<Path, Index> index = new HashMap<>(); + + public List<Ref> referencesFile(URI file, String contents, ReportReferencesProgress progress) { + recompile(file, contents, -1, -1); + // List all files that import file + var toPackage = Objects.toString(cache.root.getPackageName(), ""); + var toClasses = allClassNames(cache.root); + var possible = potentialReferencesToClasses(toPackage, toClasses, progress); + if (possible.isEmpty()) { + LOG.info("No potential references to " + file); + return List.of(); + } + // Reindex only files that are out-of-date + var outOfDate = new ArrayList<Path>(); + for (var p : possible) { + var i = index.getOrDefault(p, Index.EMPTY); + FileTime modified; + try { + modified = Files.getLastModifiedTime(p); + } catch (IOException e) { + throw new RuntimeException(e); + } + if (modified.toInstant().isAfter(i.created)) { + outOfDate.add(p); + } + } + progress.checkPotentialReferences(0, outOfDate.size()); + // Reindex + var batch = compileBatch(outOfDate); + var nChecked = 0; for (var f : batch.roots) { - result.addAll(batch.actualReferences(f, toEl)); + var uri = f.getSourceFile().toUri(); + var path = Paths.get(uri); + var refs = batch.index(f); + FileTime modified; + try { + modified = Files.getLastModifiedTime(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + var i = new Index(refs, modified.toInstant()); + index.put(path, i); nChecked++; progress.checkPotentialReferences(nChecked, possible.size()); } + // Assemble results + var result = new ArrayList<Ref>(); + for (var p : possible) { + var i = index.get(p); + for (var r : i.refs) { + if (r.toFile.equals(file)) + result.add(r); + } + } return result; } @@ -1459,10 +1644,10 @@ public class JavaCompilerService { if (id.matches("[A-Z]\\w+")) { unresolved.add(id); } else LOG.warning(id + " doesn't look like a class"); - } else if (d.getMessage(null).contains("cannot find symbol")) { + } else if (d.getMessage(null).contains("cannot find to")) { var lines = d.getMessage(null).split("\n"); var firstLine = lines.length > 0 ? lines[0] : ""; - LOG.warning(String.format("%s %s doesn't look like symbol-not-found", d.getCode(), firstLine)); + LOG.warning(String.format("%s %s doesn't look like to-not-found", d.getCode(), firstLine)); } } // Look at imports in other classes to help us guess how to fix imports diff --git a/src/main/java/org/javacs/QName.java b/src/main/java/org/javacs/QName.java deleted file mode 100644 index ca93beb..0000000 --- a/src/main/java/org/javacs/QName.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.javacs; - -class QName { - final String packageName, className; - - QName(String packageName, String className) { - this.packageName = packageName; - this.className = className; - } -} diff --git a/src/main/java/org/javacs/Ref.java b/src/main/java/org/javacs/Ref.java new file mode 100644 index 0000000..b602f63 --- /dev/null +++ b/src/main/java/org/javacs/Ref.java @@ -0,0 +1,21 @@ +package org.javacs; + +import java.net.URI; + +/** Reference from file:line(start,end) to a class or method */ +public class Ref { + public final URI fromFile; + public final int startLine, startCol, endLine, endCol; + public final URI toFile; + public final String toEl; + + public Ref(URI fromFile, int startLine, int startCol, int endLine, int endCol, URI toFile, String toEl) { + this.fromFile = fromFile; + this.startLine = startLine; + this.startCol = startCol; + this.endLine = endLine; + this.endCol = endCol; + this.toFile = toFile; + this.toEl = toEl; + } +} diff --git a/src/test/java/org/javacs/JavaCompilerServiceTest.java b/src/test/java/org/javacs/JavaCompilerServiceTest.java index 0c04930..b5a7f9b 100644 --- a/src/test/java/org/javacs/JavaCompilerServiceTest.java +++ b/src/test/java/org/javacs/JavaCompilerServiceTest.java @@ -53,14 +53,14 @@ public class JavaCompilerServiceTest { @Test public void element() { - var found = compiler.element(URI.create("/HelloWorld.java"), contents("/HelloWorld.java"), 3, 24); + var found = compiler.element(resourceUri("/HelloWorld.java"), contents("/HelloWorld.java"), 3, 24); assertThat(found.getSimpleName(), hasToString(containsString("println"))); } @Test public void elementWithError() { - var found = compiler.element(URI.create("/CompleteMembers.java"), contents("/CompleteMembers.java"), 3, 12); + var found = compiler.element(resourceUri("/CompleteMembers.java"), contents("/CompleteMembers.java"), 3, 12); assertThat(found, notNullValue()); } @@ -85,7 +85,7 @@ public class JavaCompilerServiceTest { public void identifiers() { var found = compiler.scopeMembers( - URI.create("/CompleteIdentifiers.java"), + resourceUri("/CompleteIdentifiers.java"), contents("/CompleteIdentifiers.java"), 13, 21, @@ -106,7 +106,7 @@ public class JavaCompilerServiceTest { public void identifiersInMiddle() { var found = compiler.scopeMembers( - URI.create("/CompleteInMiddle.java"), contents("/CompleteInMiddle.java"), 13, 21, "complete"); + resourceUri("/CompleteInMiddle.java"), contents("/CompleteInMiddle.java"), 13, 21, "complete"); var names = elementNames(found); assertThat(names, hasItem("completeLocal")); assertThat(names, hasItem("completeParam")); @@ -123,7 +123,7 @@ public class JavaCompilerServiceTest { public void completeIdentifiers() { var found = compiler.completions( - URI.create("/CompleteIdentifiers.java"), contents("/CompleteIdentifiers.java"), 13, 21) + resourceUri("/CompleteIdentifiers.java"), contents("/CompleteIdentifiers.java"), 13, 21) .items; var names = completionNames(found); assertThat(names, hasItem("completeLocal")); @@ -140,7 +140,7 @@ public class JavaCompilerServiceTest { @Test public void members() { var found = - compiler.members(URI.create("/CompleteMembers.java"), contents("/CompleteMembers.java"), 3, 14, false); + compiler.members(resourceUri("/CompleteMembers.java"), contents("/CompleteMembers.java"), 3, 14, false); var names = completionNames(found); assertThat(names, hasItem("subMethod")); assertThat(names, hasItem("superMethod")); @@ -150,7 +150,7 @@ public class JavaCompilerServiceTest { @Test public void completeMembers() { var found = - compiler.completions(URI.create("/CompleteMembers.java"), contents("/CompleteMembers.java"), 3, 15) + compiler.completions(resourceUri("/CompleteMembers.java"), contents("/CompleteMembers.java"), 3, 15) .items; var names = completionNames(found); assertThat(names, hasItem("subMethod")); @@ -162,7 +162,7 @@ public class JavaCompilerServiceTest { public void completeExpression() { var found = compiler.completions( - URI.create("/CompleteExpression.java"), contents("/CompleteExpression.java"), 3, 37) + resourceUri("/CompleteExpression.java"), contents("/CompleteExpression.java"), 3, 37) .items; var names = completionNames(found); assertThat(names, hasItem("instanceMethod")); @@ -173,7 +173,7 @@ public class JavaCompilerServiceTest { @Test public void completeClass() { var found = - compiler.completions(URI.create("/CompleteClass.java"), contents("/CompleteClass.java"), 3, 23).items; + compiler.completions(resourceUri("/CompleteClass.java"), contents("/CompleteClass.java"), 3, 23).items; var names = completionNames(found); assertThat(names, hasItems("staticMethod", "staticField")); assertThat(names, hasItems("class")); @@ -184,7 +184,7 @@ public class JavaCompilerServiceTest { @Test public void completeImports() { var found = - compiler.completions(URI.create("/CompleteImports.java"), contents("/CompleteImports.java"), 1, 18) + compiler.completions(resourceUri("/CompleteImports.java"), contents("/CompleteImports.java"), 1, 18) .items; var names = completionNames(found); assertThat(names, hasItem("List")); @@ -195,7 +195,7 @@ public class JavaCompilerServiceTest { @Test public void gotoDefinition() { var def = - compiler.definition(URI.create("/GotoDefinition.java"), 3, 12, uri -> Files.readAllText(uri)); + compiler.definition("/GotoDefinition.java"; assertTrue(def.isPresent()); var t = def.get(); @@ -211,36 +211,49 @@ public class JavaCompilerServiceTest { } */ + private final ReportReferencesProgress rrp = + new ReportReferencesProgress() { + @Override + public void scanForPotentialReferences(int nScanned, int nFiles) {} + + @Override + public void checkPotentialReferences(int nCompiled, int nPotential) {} + }; + @Test public void references() { - ReportReferencesProgress rrp = - new ReportReferencesProgress() { - @Override - public void scanForPotentialReferences(int nScanned, int nFiles) {} - - @Override - public void checkPotentialReferences(int nCompiled, int nPotential) {} - }; - var refs = - compiler.references(URI.create("/GotoDefinition.java"), contents("/GotoDefinition.java"), 6, 13, rrp); - boolean found = false; + var file = "/GotoDefinition.java"; + var refs = compiler.references(resourceUri(file), contents(file), 6, 13, rrp); + var stringify = new ArrayList<String>(); for (var t : refs) { var unit = t.getCompilationUnit(); var name = unit.getSourceFile().getName(); var trees = compiler.trees(); var pos = trees.getSourcePositions(); var lines = unit.getLineMap(); - long start = pos.getStartPosition(unit, t.getLeaf()); - long line = lines.getLineNumber(start); - if (name.endsWith("GotoDefinition.java") && line == 3) found = true; + var start = pos.getStartPosition(unit, t.getLeaf()); + var line = lines.getLineNumber(start); + if (name.endsWith("GotoDefinition.java") && line == 3) return; + stringify.add(String.format("%s:%d", name, line)); } + fail(String.format("No GotoDefinition.java:3 in %s", stringify)); + } - if (!found) fail(String.format("No GotoDefinition.java line 3 in %s", refs)); + @Test + public void referencesFile() { + var file = "/GotoDefinition.java"; + var refs = compiler.referencesFile(resourceUri(file), contents(file), rrp); + var stringify = new ArrayList<String>(); + for (var r : refs) { + if (r.fromFile.toString().endsWith("GotoDefinition.java")) return; + stringify.add(String.format("%s:%d", r.fromFile, r.startLine)); + } + fail(String.format("No GotoDefinition.java in %s", stringify)); } @Test public void overloads() { - var found = compiler.methodInvocation(URI.create("/Overloads.java"), contents("/Overloads.java"), 3, 15).get(); + var found = compiler.methodInvocation(resourceUri("/Overloads.java"), contents("/Overloads.java"), 3, 15).get(); var strings = found.overloads.stream().map(Object::toString).collect(Collectors.toList()); assertThat(strings, hasItem(containsString("print(int)"))); @@ -257,7 +270,7 @@ public class JavaCompilerServiceTest { @Test public void localDoc() { var method = - compiler.methodInvocation(URI.create("/LocalMethodDoc.java"), contents("/LocalMethodDoc.java"), 3, 21) + compiler.methodInvocation(resourceUri("/LocalMethodDoc.java"), contents("/LocalMethodDoc.java"), 3, 21) .get() .activeMethod .get(); |