diff options
-rw-r--r-- | TODOS.md | 1 | ||||
-rw-r--r-- | src/main/java/org/javacs/CompileFile.java | 31 | ||||
-rw-r--r-- | src/main/java/org/javacs/Completion.java | 5 | ||||
-rw-r--r-- | src/main/java/org/javacs/Docs.java | 170 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavaCompilerService.java | 1 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavaLanguageServer.java | 172 | ||||
-rw-r--r-- | src/main/java/org/javacs/ParseFile.java | 98 | ||||
-rw-r--r-- | src/main/java/org/javacs/Parser.java | 25 | ||||
-rw-r--r-- | src/main/java/org/javacs/Ptr.java | 231 | ||||
-rw-r--r-- | src/main/java/org/javacs/lsp/SignatureInformation.java | 8 | ||||
-rw-r--r-- | src/test/java/org/javacs/CodeLensTest.java | 4 | ||||
-rw-r--r-- | src/test/java/org/javacs/DocsTest.java | 26 | ||||
-rw-r--r-- | src/test/java/org/javacs/JavaCompilerServiceTest.java | 7 | ||||
-rw-r--r-- | src/test/java/org/javacs/PtrTest.java | 116 | ||||
-rw-r--r-- | src/test/test-project/workspace/src/org/javacs/example/Ptrs.java | 21 |
15 files changed, 633 insertions, 283 deletions
@@ -1,7 +1,6 @@ # Todo ## Bugs -- Deleted files remain in compiler, even when you restart (via classpath?) - Always shows last javadoc - Make new file, rename, edit crashes compiler - StringBuilder.length isn't autocompleting diff --git a/src/main/java/org/javacs/CompileFile.java b/src/main/java/org/javacs/CompileFile.java index 6d82132..c99c2b8 100644 --- a/src/main/java/org/javacs/CompileFile.java +++ b/src/main/java/org/javacs/CompileFile.java @@ -61,7 +61,7 @@ public class CompileFile { LOG.info("...found nothing"); return Optional.empty(); } - LOG.info(String.format("...found tree `%s`", showTree(path))); + LOG.info(String.format("...found tree `%s`", Parser.describeTree(path.getLeaf()))); // Then, convert the path to an element var el = trees.getElement(path); @@ -73,23 +73,6 @@ public class CompileFile { return Optional.of(el); } - private String showTree(TreePath path) { - var leaf = path.getLeaf(); - if (leaf instanceof MethodTree) { - var method = (MethodTree) leaf; - return method.getName() + "(...)"; - } - if (leaf instanceof ClassTree) { - var cls = (ClassTree) leaf; - return "class " + cls.getSimpleName(); - } - if (leaf instanceof BlockTree) { - var block = (BlockTree) leaf; - return String.format("{ ...%d lines... }", block.getStatements().size()); - } - return leaf.toString(); - } - public Optional<TreePath> path(Element e) { return Optional.ofNullable(trees.getPath(e)); } @@ -296,20 +279,10 @@ public class CompileFile { var name = el.getQualifiedName().toString(); thisClasses.add(name); } - // Does a pointer refer to something in this file? - Predicate<Ptr> pointsToThis = - ptr -> { - for (var c : thisClasses) { - if (ptr.toString().startsWith(c)) { - return true; - } - } - return false; - }; return i -> { // For each pointer, check if it refers to something in this file that no longer exists for (var ptr : i) { - if (pointsToThis.test(ptr) && find(ptr).isEmpty()) { + if (thisClasses.contains(ptr.qualifiedClassName()) && find(ptr).isEmpty()) { LOG.info( String.format("`%s` refers to signature that no longer exists in %s", ptr, file.getPath())); return false; diff --git a/src/main/java/org/javacs/Completion.java b/src/main/java/org/javacs/Completion.java index 8d6f93c..0b12b80 100644 --- a/src/main/java/org/javacs/Completion.java +++ b/src/main/java/org/javacs/Completion.java @@ -42,6 +42,7 @@ public class Completion { } public static class ClassName { + // TODO keep package and class name separate to avoid inner-class problems public final String name; public final boolean isImported; @@ -68,4 +69,8 @@ public class Completion { this.snippet = snippet; } } + + public Ptr ptr() { + return new Ptr(element); + } } diff --git a/src/main/java/org/javacs/Docs.java b/src/main/java/org/javacs/Docs.java index 216fa31..43b535d 100644 --- a/src/main/java/org/javacs/Docs.java +++ b/src/main/java/org/javacs/Docs.java @@ -1,10 +1,6 @@ package org.javacs; -import com.sun.source.doctree.DocCommentTree; -import com.sun.source.tree.*; -import com.sun.source.util.DocTrees; -import com.sun.source.util.TreeScanner; -import com.sun.source.util.Trees; +import com.sun.source.tree.CompilationUnitTree; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Path; @@ -12,8 +8,6 @@ import java.util.*; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.Collectors; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; import javax.tools.*; public class Docs { @@ -45,170 +39,52 @@ public class Docs { } } - /** Look up the javadoc associated with `method` */ - public Optional<DocCommentTree> methodDoc(ExecutableElement method) { - var classElement = (TypeElement) method.getEnclosingElement(); - var className = classElement.getQualifiedName().toString(); - var methodName = method.getSimpleName().toString(); - return memberDoc(className, methodName); - } - - /** Find and root the source code associated with `method` */ - public Optional<MethodTree> methodTree(ExecutableElement method) { - var classElement = (TypeElement) method.getEnclosingElement(); - var className = classElement.getQualifiedName().toString(); - var methodName = method.getSimpleName().toString(); - var parameterTypes = - method.getParameters().stream().map(p -> p.asType().toString()).collect(Collectors.toList()); - return findMethod(className, methodName, parameterTypes); - } - - /** Look up the javadoc associated with `type` */ - public Optional<DocCommentTree> classDoc(TypeElement type) { - return classDoc(type.getQualifiedName().toString()); - } + public Optional<JavaFileObject> find(Ptr ptr) { + LOG.info(String.format("...looking for file for `%s`...", ptr)); - public Optional<DocCommentTree> classDoc(String qualifiedName) { - Objects.requireNonNull(qualifiedName); - - return findDoc(qualifiedName, null); - } - - private Optional<JavaFileObject> file(String className) { + // Find the file el was declared in + var className = ptr.qualifiedClassName(); try { var fromSourcePath = fileManager.getJavaFileForInput( StandardLocation.SOURCE_PATH, className, JavaFileObject.Kind.SOURCE); - if (fromSourcePath != null) return Optional.of(fromSourcePath); + if (fromSourcePath != null) { + LOG.info(String.format("...found %s on source path", fromSourcePath.toUri())); + return Optional.of(fromSourcePath); + } for (var module : Classes.JDK_MODULES) { var moduleLocation = fileManager.getLocationForModule(StandardLocation.MODULE_SOURCE_PATH, module); if (moduleLocation == null) continue; var fromModuleSourcePath = fileManager.getJavaFileForInput(moduleLocation, className, JavaFileObject.Kind.SOURCE); - if (fromModuleSourcePath != null) return Optional.of(fromModuleSourcePath); + if (fromModuleSourcePath != null) { + LOG.info(String.format("...found %s in module %s of jdk", fromModuleSourcePath.toUri(), module)); + return Optional.of(fromModuleSourcePath); + } } - return Optional.empty(); } catch (IOException e) { throw new RuntimeException(e); } + LOG.info(String.format("...couldn't find file for top-level class `%s`", className)); + return Optional.empty(); } - private boolean memberNameEquals(Tree member, String name) { - if (member instanceof VariableTree) { - var variable = (VariableTree) member; - return variable.getName().contentEquals(name); - } else if (member instanceof MethodTree) { - var method = (MethodTree) member; - return method.getName().contentEquals(name); - } else return false; - } - - private Optional<DocCommentTree> findDoc(String className, String memberName) { - var file = file(className); - if (!file.isPresent()) return Optional.empty(); - var task = Parser.parseTask(file.get()); + public ParseFile parse(JavaFileObject file) { + // Parse that file + var task = Parser.parseTask(file); CompilationUnitTree root; try { - var it = task.parse().iterator(); - if (!it.hasNext()) { - LOG.warning("Found no CompilationUnitTree in " + file); - return Optional.empty(); - } - root = it.next(); + root = task.parse().iterator().next(); } catch (IOException e) { throw new RuntimeException(e); } - var docs = DocTrees.instance(task); - var trees = Trees.instance(task); - class Find extends TreeScanner<Void, Void> { - Optional<DocCommentTree> result = Optional.empty(); - - @Override - public Void visitClass(ClassTree node, Void aVoid) { - // TODO this will be wrong when inner class has same name as top-level class - if (node.getSimpleName().contentEquals(Parser.lastName(className))) { - if (memberName == null) { - var path = trees.getPath(root, node); - result = Optional.ofNullable(docs.getDocCommentTree(path)); - } else { - for (var member : node.getMembers()) { - if (memberNameEquals(member, memberName)) { - var path = trees.getPath(root, member); - result = Optional.ofNullable(docs.getDocCommentTree(path)); - } - } - } - } - return null; - } - } - var find = new Find(); - find.scan(root, null); - return find.result; - } - - Optional<DocCommentTree> memberDoc(String className, String memberName) { - Objects.requireNonNull(className); - Objects.requireNonNull(memberName); - - return findDoc(className, memberName); - } - - private boolean sameMethod(MethodTree candidate, String methodName, List<String> parameterTypes) { - if (!candidate.getName().contentEquals(methodName)) return false; - var params = candidate.getParameters(); - if (params.size() != parameterTypes.size()) return false; - for (int i = 0; i < params.size(); i++) { - var expect = parameterTypes.get(i); - var expectSimple = Parser.lastName(expect); - var p = params.get(i); - var t = p.getType(); - if (!(t instanceof IdentifierTree)) { - LOG.warning( - "Parameter " + p.getName() + " of method " + candidate.getName() + " is not an IdentifierTree"); - return false; - } - var id = (IdentifierTree) t; - var simple = Parser.lastName(id.getName().toString()); - - if (!simple.equals(expectSimple)) return false; - } - return true; - } - - private Optional<MethodTree> findMethod(String className, String methodName, List<String> parameterTypes) { - Objects.requireNonNull(className); - Objects.requireNonNull(methodName); - - var file = file(className); - if (!file.isPresent()) return Optional.empty(); - var task = Parser.parseTask(file.get()); - CompilationUnitTree root; + String contents; try { - root = task.parse().iterator().next(); + contents = file.getCharContent(true).toString(); } catch (IOException e) { throw new RuntimeException(e); } - class Find extends TreeScanner<Void, Void> { - Optional<MethodTree> result = Optional.empty(); - - @Override - public Void visitClass(ClassTree node, Void aVoid) { - // TODO this will be wrong when inner class has same name as top-level class - if (node.getSimpleName().contentEquals(Parser.lastName(className))) { - for (var member : node.getMembers()) { - if (member instanceof MethodTree) { - var method = (MethodTree) member; - if (sameMethod(method, methodName, parameterTypes)) result = Optional.of(method); - } - } - } - return null; - } - } - var find = new Find(); - find.scan(root, null); - return find.result; + return new ParseFile(file.toUri(), contents, task, root); } private static final Pattern HTML_TAG = Pattern.compile("<(\\w+)>"); @@ -225,7 +101,7 @@ public class Docs { } /** If `commentText` looks like HTML, convert it to markdown */ - static String htmlToMarkdown(String commentText) { + public static String htmlToMarkdown(String commentText) { if (isHtml(commentText)) { return TipFormatter.asMarkdown(commentText); } else return commentText; diff --git a/src/main/java/org/javacs/JavaCompilerService.java b/src/main/java/org/javacs/JavaCompilerService.java index 823d0bc..e701e7c 100644 --- a/src/main/java/org/javacs/JavaCompilerService.java +++ b/src/main/java/org/javacs/JavaCompilerService.java @@ -28,6 +28,7 @@ public class JavaCompilerService { // Diagnostics from the last compilation task final List<Diagnostic<? extends JavaFileObject>> diags = new ArrayList<>(); // Use the same file manager for multiple tasks, so we don't repeatedly re-compile the same files + // TODO intercept files that aren't in the batch and erase method bodies so compilation is faster final StandardJavaFileManager fileManager = new FileManagerWrapper(compiler.getStandardFileManager(diags::add, null, Charset.defaultCharset())); diff --git a/src/main/java/org/javacs/JavaLanguageServer.java b/src/main/java/org/javacs/JavaLanguageServer.java index c6a18e6..c025c00 100644 --- a/src/main/java/org/javacs/JavaLanguageServer.java +++ b/src/main/java/org/javacs/JavaLanguageServer.java @@ -4,7 +4,6 @@ import com.google.gson.*; import com.google.gson.JsonArray; import com.google.gson.JsonNull; import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; import com.sun.source.doctree.DocCommentTree; import com.sun.source.doctree.DocTree; import com.sun.source.doctree.ParamTree; @@ -345,7 +344,6 @@ class JavaLanguageServer extends LanguageServer { var content = contents(uri).content; var line = position.position.line + 1; var column = position.position.character + 1; - lastCompletions.clear(); // Figure out what kind of completion we want to do var maybeCtx = compiler.parseFile(uri, content).completionContext(line, column); if (!maybeCtx.isPresent()) { @@ -404,6 +402,7 @@ class JavaLanguageServer extends LanguageServer { i.detail = ShortTypePrinter.print(c.element.asType()); } // TODO prioritize based on usage? + // TODO prioritize based on scope if (isMemberOfObject(c.element)) { i.sortText = 9 + i.label; } else { @@ -448,15 +447,52 @@ class JavaLanguageServer extends LanguageServer { return Optional.of(new CompletionList(isIncomplete, result)); } - private String resolveDocDetail(MethodTree doc) { + private Optional<MarkupContent> findDocs(Ptr ptr) { + LOG.info(String.format("Find docs for `%s`...", ptr)); + + // Find el in the doc path + var file = compiler.docs().find(ptr); + if (!file.isPresent()) return Optional.empty(); + // Parse file and find el + var parse = compiler.docs().parse(file.get()); + var path = parse.fuzzyFind(ptr); + if (!path.isPresent()) return Optional.empty(); + // Parse the doctree associated with el + var docTree = parse.doc(path.get()); + ; + var string = asMarkupContent(docTree); + return Optional.of(string); + } + + private Optional<String> findMethodDetails(ExecutableElement method) { + LOG.info(String.format("Find details for method `%s`...", method)); + + // TODO find and parse happens twice between findDocs and findMethodDetails + // Find method in the doc path + var ptr = new Ptr(method); + var file = compiler.docs().find(ptr); + if (!file.isPresent()) return Optional.empty(); + // Parse file and find method + var parse = compiler.docs().parse(file.get()); + var path = parse.fuzzyFind(ptr); + if (!path.isPresent()) return Optional.empty(); + // Should be a MethodTree + var tree = path.get().getLeaf(); + if (!(tree instanceof MethodTree)) { + LOG.warning(String.format("...method `%s` associated with non-method tree `%s`", method, tree)); + return Optional.empty(); + } + // Write description of method using info from source + var methodTree = (MethodTree) tree; var args = new StringJoiner(", "); - for (var p : doc.getParameters()) { + for (var p : methodTree.getParameters()) { args.add(p.getName()); } - return String.format("%s %s(%s)", doc.getReturnType(), doc.getName(), args); + var details = String.format("%s %s(%s)", methodTree.getReturnType(), methodTree.getName(), args); + return Optional.of(details); } - private String resolveDefaultDetail(ExecutableElement method) { + private String defaultDetails(ExecutableElement method) { var args = new StringJoiner(", "); var missingParamNames = method.getParameters().stream().allMatch(p -> p.getSimpleName().toString().matches("arg\\d+")); @@ -500,25 +536,17 @@ class JavaLanguageServer extends LanguageServer { if (cached.element != null) { if (cached.element instanceof ExecutableElement) { var method = (ExecutableElement) cached.element; - var tree = compiler.docs().methodTree(method); - var detail = tree.map(this::resolveDocDetail).orElse(resolveDefaultDetail(method)); - unresolved.detail = detail; - - var doc = compiler.docs().methodDoc(method); - var markdown = doc.map(this::asMarkupContent); - if (markdown.isPresent()) unresolved.documentation = markdown.get(); - } else if (cached.element instanceof TypeElement) { - var type = (TypeElement) cached.element; - var doc = compiler.docs().classDoc(type); - var markdown = doc.map(this::asMarkupContent); - if (markdown.isPresent()) unresolved.documentation = markdown.get(); - } else { - LOG.info("Don't know how to look up docs for element " + cached.element); + unresolved.detail = findMethodDetails(method).orElse(defaultDetails(method)); + } + var markdown = findDocs(new Ptr(cached.element)); + if (markdown.isPresent()) { + unresolved.documentation = markdown.get(); } - // TODO constructors, fields } else if (cached.className != null) { - var doc = compiler.docs().classDoc(cached.className.name); - var markdown = doc.map(this::asMarkupContent); + var packageName = Parser.mostName(cached.className.name); + var className = Parser.lastName(cached.className.name); + var ptr = Ptr.toClass(packageName, className); + var markdown = findDocs(ptr); if (markdown.isPresent()) unresolved.documentation = markdown.get(); } return unresolved; @@ -576,17 +604,21 @@ class JavaLanguageServer extends LanguageServer { } lines.add("}"); return lines.toString(); - } else return e.toString(); + } else { + return e.toString(); + } } private Optional<String> hoverDocs(Element e) { - if (e instanceof ExecutableElement) { - var m = (ExecutableElement) e; - return compiler.docs().methodDoc(m).map(this::asMarkdown); - } else if (e instanceof TypeElement) { - var t = (TypeElement) e; - return compiler.docs().classDoc(t).map(this::asMarkdown); - } else return Optional.empty(); + var ptr = new Ptr(e); + var file = compiler.docs().find(ptr); + if (!file.isPresent()) return Optional.empty(); + var parse = compiler.docs().parse(file.get()); + var path = parse.fuzzyFind(ptr); + if (!path.isPresent()) return Optional.empty(); + var doc = parse.doc(path.get()); + var md = asMarkdown(doc); + return Optional.of(md); } // TODO change name @@ -627,7 +659,50 @@ class JavaLanguageServer extends LanguageServer { return Optional.of(new Hover(result)); } - private List<ParameterInformation> signatureParamsFromDocs(MethodTree method, DocCommentTree doc) { + private SignatureInformation asSignatureInformation(ExecutableElement e) { + // Figure out parameter info from source or from ExecutableElement + var i = new SignatureInformation(); + var ptr = new Ptr(e); + var ps = signatureParamsFromDocs(ptr).orElse(signatureParamsFromMethod(e)); + i.parameters = ps; + + // Compute label from params (which came from either source or ExecutableElement) + var name = e.getSimpleName(); + if (name.contentEquals("<init>")) name = e.getEnclosingElement().getSimpleName(); + var args = new StringJoiner(", "); + for (var p : ps) { + args.add(p.label); + } + i.label = name + "(" + args + ")"; + + return i; + } + + private List<ParameterInformation> signatureParamsFromMethod(ExecutableElement e) { + var missingParamNames = ShortTypePrinter.missingParamNames(e); + var ps = new ArrayList<ParameterInformation>(); + for (var v : e.getParameters()) { + var p = new ParameterInformation(); + if (missingParamNames) p.label = ShortTypePrinter.print(v.asType()); + else p.label = v.getSimpleName().toString(); + ps.add(p); + } + return ps; + } + + private Optional<List<ParameterInformation>> signatureParamsFromDocs(Ptr ptr) { + // Find the file ptr point to, and parse it + var file = compiler.docs().find(ptr); + if (!file.isPresent()) return Optional.empty(); + var parse = compiler.docs().parse(file.get()); + // Find the tree + var path = parse.fuzzyFind(ptr); + if (!path.isPresent()) return Optional.empty(); + if (!(path.get().getLeaf() instanceof MethodTree)) return Optional.empty(); + var method = (MethodTree) path.get().getLeaf(); + // Find the docstring on method, or empty doc if there is none + var doc = parse.doc(path.get()); + // Get param docs from @param tags var ps = new ArrayList<ParameterInformation>(); var paramComments = new HashMap<String, String>(); for (var tag : doc.getBlockTags()) { @@ -636,6 +711,7 @@ class JavaLanguageServer extends LanguageServer { paramComments.put(param.getName().toString(), asMarkdown(param.getDescription())); } } + // Get param names from source for (var param : method.getParameters()) { var info = new ParameterInformation(); var name = param.getName().toString(); @@ -649,33 +725,7 @@ class JavaLanguageServer extends LanguageServer { } ps.add(info); } - return ps; - } - - private List<ParameterInformation> signatureParamsFromMethod(ExecutableElement e) { - var missingParamNames = ShortTypePrinter.missingParamNames(e); - var ps = new ArrayList<ParameterInformation>(); - for (var v : e.getParameters()) { - var p = new ParameterInformation(); - if (missingParamNames) p.label = ShortTypePrinter.print(v.asType()); - else p.label = v.getSimpleName().toString(); - ps.add(p); - } - return ps; - } - - private SignatureInformation asSignatureInformation(ExecutableElement e) { - var i = new SignatureInformation(); - var ps = signatureParamsFromMethod(e); - var doc = compiler.docs().methodDoc(e); - var tree = compiler.docs().methodTree(e); - if (doc.isPresent() && tree.isPresent()) ps = signatureParamsFromDocs(tree.get(), doc.get()); - var args = ps.stream().map(p -> p.label).collect(Collectors.joining(", ")); - var name = e.getSimpleName().toString(); - if (name.equals("<init>")) name = e.getEnclosingElement().getSimpleName().toString(); - i.label = name + "(" + args + ")"; - i.parameters = ps; - return i; + return Optional.of(ps); } private SignatureHelp asSignatureHelp(MethodInvocation invoke) { @@ -718,7 +768,7 @@ class JavaLanguageServer extends LanguageServer { } // Figure out what file toEl is declared in - LOG.info(String.format("...looking for definition of `%s`", toEl)); + LOG.info(String.format("...looking for definition of `%s`", toEl.get())); var toUri = hoverCache.declaringFile(toEl.get()); if (!toUri.isPresent()) { LOG.info(String.format("...couldn't find declaring file, giving up")); @@ -1103,6 +1153,7 @@ class JavaLanguageServer extends LanguageServer { var line = data.get(2).getAsInt() + 1; var character = data.get(3).getAsInt() + 1; // Find the element being referenced + // TODO only update code lenses when file is saved, then return these lenses from cache updateHoverCache(uri, contents(uri).content); var el = hoverCache.element(line, character); if (el.isEmpty()) { @@ -1135,6 +1186,7 @@ class JavaLanguageServer extends LanguageServer { var edits = new ArrayList<TextEdit>(); edits.addAll(fixImports()); edits.addAll(addOverrides()); + // TODO replace var with type name when vars are copy-pasted into fields return edits; } diff --git a/src/main/java/org/javacs/ParseFile.java b/src/main/java/org/javacs/ParseFile.java index e1e1d13..45f1443 100644 --- a/src/main/java/org/javacs/ParseFile.java +++ b/src/main/java/org/javacs/ParseFile.java @@ -1,5 +1,6 @@ package org.javacs; +import com.sun.source.doctree.DocCommentTree; import com.sun.source.tree.*; import com.sun.source.util.*; import java.io.IOException; @@ -14,7 +15,6 @@ import org.javacs.lsp.*; public class ParseFile { - private final JavaCompilerService parent; private final URI file; private final String contents; private final JavacTask task; @@ -22,7 +22,10 @@ public class ParseFile { private final CompilationUnitTree root; ParseFile(JavaCompilerService parent, URI file, String contents) { - this.parent = parent; + Objects.requireNonNull(parent); + Objects.requireNonNull(file); + Objects.requireNonNull(contents); + this.file = file; this.contents = contents; this.task = CompileFocus.singleFileTask(parent, file, contents); @@ -36,7 +39,19 @@ public class ParseFile { throw new RuntimeException(e); } profiler.print(); + } + + ParseFile(URI file, String contents, JavacTask task, CompilationUnitTree root) { + Objects.requireNonNull(file); + Objects.requireNonNull(contents); + Objects.requireNonNull(task); + Objects.requireNonNull(root); + this.file = file; + this.contents = contents; + this.task = task; + this.trees = Trees.instance(task); + this.root = root; } public boolean isTestMethod(TreePath path) { @@ -270,6 +285,60 @@ public class ParseFile { return trees.getSourcePositions(); } + /** Find and source code associated with a ptr */ + public Optional<TreePath> fuzzyFind(Ptr ptr) { + LOG.info(String.format("...find fuzzy match of %s in %s ...", ptr, Parser.fileName(file))); + + class FindPtr extends TreePathScanner<Void, Void> { + int bestMatch = Ptr.NOT_MATCHED; + TreePath found; + void check() { + var path = getCurrentPath(); + var mismatch = ptr.fuzzyMatch(path); + if (mismatch < bestMatch) { + found = path; + bestMatch = mismatch; + } + } + + @Override + public Void visitClass(ClassTree node, Void aVoid) { + check(); + return super.visitClass(node, aVoid); + } + + @Override + public Void visitMethod(MethodTree node, Void aVoid) { + check(); + // Ptr can't point inside a method + return null; + } + + @Override + public Void visitVariable(VariableTree node, Void aVoid) { + check(); + // Ptr can't point inside a method + return null; + } + } + var find = new FindPtr(); + find.scan(root, null); + if (find.found != null) + LOG.info(String.format("...`%s` with score %d is best match", Parser.describeTree(find.found.getLeaf()), find.bestMatch)); + else + LOG.info("...no match found"); + return Optional.ofNullable(find.found); + } + + public DocCommentTree doc(TreePath path) { + // Find ptr in the file + // Find the documentation attached to el + var docs = DocTrees.instance(task); + var doc = docs.getDocCommentTree(path); + if (doc == null) return EMPTY_DOC; + return doc; + } + // TODO get rid of this and expose SourcePositions static Optional<Range> range(JavacTask task, String contents, TreePath path) { // Find start position @@ -321,5 +390,30 @@ public class ParseFile { return Parser.findSymbolsMatching(root, ""); } + private static final DocCommentTree EMPTY_DOC = makeEmptyDoc(); + + private static DocCommentTree makeEmptyDoc() { + var file = new StringFileObject("/** */ class Foo { }", URI.create("file:///Foo.java")); + var task = Parser.parseTask(file); + var docs = DocTrees.instance(task); + CompilationUnitTree root; + try { + root = task.parse().iterator().next(); + } catch (IOException e) { + throw new RuntimeException(e); + } + class FindEmptyDoc extends TreePathScanner<Void, Void> { + DocCommentTree found; + @Override + public Void visitClass(ClassTree t, Void __) { + found = docs.getDocCommentTree(getCurrentPath()); + return null; + } + } + var find = new FindEmptyDoc(); + find.scan(root, null); + return Objects.requireNonNull(find.found); + } + private static final Logger LOG = Logger.getLogger("main"); }
\ No newline at end of file diff --git a/src/main/java/org/javacs/Parser.java b/src/main/java/org/javacs/Parser.java index 0ded820..7e2804f 100644 --- a/src/main/java/org/javacs/Parser.java +++ b/src/main/java/org/javacs/Parser.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.ByteBuffer; @@ -275,17 +274,39 @@ class Parser { return new ExistingImports(classes, packages); } + // TODO this doesn't work for inner classes, eliminate static String mostName(String name) { var lastDot = name.lastIndexOf('.'); return lastDot == -1 ? "" : name.substring(0, lastDot); } + // TODO this doesn't work for inner classes, eliminate static String lastName(String name) { int i = name.lastIndexOf('.'); if (i == -1) return name; else return name.substring(i + 1); } + static String describeTree(Tree leaf) { + if (leaf instanceof MethodTree) { + var method = (MethodTree) leaf; + var params = new StringJoiner(", "); + for (var p : method.getParameters()) { + params.add(p.getType() + " " + p.getName()); + } + return method.getName() + "(" + params + ")"; + } + if (leaf instanceof ClassTree) { + var cls = (ClassTree) leaf; + return "class " + cls.getSimpleName(); + } + if (leaf instanceof BlockTree) { + var block = (BlockTree) leaf; + return String.format("{ ...%d lines... }", block.getStatements().size()); + } + return leaf.toString(); + } + // TODO does this really belong in Parser? private static Optional<String> resolveSymbol(String unresolved, ExistingImports imports, Set<String> classPath) { // Try to disambiguate by looking for exact matches @@ -350,7 +371,7 @@ class Parser { } static String fileName(URI uri) { - var parts = uri.getPath().split(File.separator); + var parts = uri.toString().split("/"); if (parts.length == 0) return ""; return parts[parts.length - 1]; } diff --git a/src/main/java/org/javacs/Ptr.java b/src/main/java/org/javacs/Ptr.java index 71d1b6c..2deff56 100644 --- a/src/main/java/org/javacs/Ptr.java +++ b/src/main/java/org/javacs/Ptr.java @@ -1,57 +1,124 @@ package org.javacs; +import com.sun.source.tree.*; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreeScanner; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.StringJoiner; import java.util.logging.Logger; -import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.PackageElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; +import javax.lang.model.element.*; +import javax.lang.model.type.*; /** Ptr is a reference to a named element, that can be serialized into a String. */ public class Ptr { - private final String path; + private final String packageName, className; + private final Optional<String> memberName; + private final Optional<List<String>> erasedParameterTypes; - public Ptr(String path) { - this.path = path; + public static Ptr toClass(String packageName, String className) { + return new Ptr(packageName, className); } - public static boolean canPoint(Element e) { - var inLeaf = true; - for (; e != null; e = e.getEnclosingElement()) { - var isLeaf = e instanceof ExecutableElement || e instanceof VariableElement; - if (inLeaf && !isLeaf) inLeaf = false; - if (!inLeaf && isLeaf) return false; + private Ptr(String packageName, String className) { + this.packageName = packageName; + this.className = className; + this.memberName = Optional.empty(); + this.erasedParameterTypes = Optional.empty(); + } + + public Ptr(String path) { + // Split my.pkg/Class#member into my.pkg and Class#member + var slash = path.indexOf('/'); + if (slash == -1) { + this.packageName = ""; + } else { + this.packageName = path.substring(0, slash); + path = path.substring(slash + 1); } - return true; + + // Split Class#member into Class and member + var hash = path.indexOf('#'); + if (hash == -1) { + this.className = path; + this.memberName = Optional.empty(); + this.erasedParameterTypes = Optional.empty(); + return; + } + this.className = path.substring(0, hash); + path = path.substring(hash + 1); + + // Split method(int,java.lang.String) into method and int,java.lang.String + var paren = path.indexOf('('); + if (paren == -1) { + this.memberName = Optional.of(path); + this.erasedParameterTypes = Optional.empty(); + return; + } + this.memberName = Optional.of(path.substring(0, paren)); + path = path.substring(paren + 1, path.length() - 1); + + // Split int,java.lang.String + if (path.isEmpty()) { + this.erasedParameterTypes = Optional.of(List.of()); + return; + } + var params = path.split(","); + this.erasedParameterTypes = Optional.of(List.of(params)); } public Ptr(Element e) { - var rev = new ArrayList<CharSequence>(); - while (e != null) { + var packageName = ""; + var reversedClassName = new ArrayList<CharSequence>(); + String memberName = null; + List<String> params = null; + for (; e != null; e = e.getEnclosingElement()) { if (e instanceof PackageElement) { var pkg = (PackageElement) e; - if (!pkg.isUnnamed()) rev.add(pkg.getQualifiedName()); + packageName = pkg.getQualifiedName().toString(); } else if (e instanceof TypeElement) { var type = (TypeElement) e; - rev.add(type.getSimpleName()); + reversedClassName.add(type.getSimpleName()); } else if (e instanceof ExecutableElement) { var method = (ExecutableElement) e; - // TODO overloads - rev.add(method.toString()); + memberName = method.getSimpleName().toString(); + params = new ArrayList<String>(); + for (var p : method.getParameters()) { + var type = p.asType(); + var erased = erasure(type); + if (erased == null) params.add("java.lang.Object"); + else params.add(erased.toString()); + } } else if (e instanceof VariableElement) { var field = (VariableElement) e; - rev.add(field.getSimpleName()); + memberName = field.getSimpleName().toString(); } - e = e.getEnclosingElement(); } - var name = reverseAndJoin(rev, "."); - if (!name.matches("(\\w+\\.)*(<.*>)?(\\w+|<init>)(\\(.*\\))?")) - LOG.warning(String.format("`%s` doesn't look like a name", name)); - this.path = name; + this.packageName = packageName; + this.className = reverseAndJoin(reversedClassName, "."); + this.memberName = Optional.ofNullable(memberName); + this.erasedParameterTypes = Optional.ofNullable(params); + } + + private static TypeMirror erasure(TypeMirror t) { + // Erase class by removing arguments + if (t instanceof DeclaredType) { + var d = (DeclaredType) t; + return d.asElement().asType(); + } + // Erase wildcard to upper bound + if (t instanceof WildcardType) { + var w = (WildcardType) t; + return w.getExtendsBound(); + } + // Erase type var to upper bound + if (t instanceof TypeVariable) { + var v = (TypeVariable) t; + return v.getUpperBound(); + } + return t; } private static String reverseAndJoin(List<CharSequence> parts, String sep) { @@ -62,21 +129,125 @@ public class Ptr { return join.toString(); } + // TODO eliminate className() everywhere in the codebase in favor of simpleClassName() and qualifiedClassName() + public String qualifiedClassName() { + if (packageName.isEmpty()) return className; + return packageName + "." + className; + } + + public static final int NOT_MATCHED = 100; + + public int fuzzyMatch(TreePath path) { + if (!packageName(path).equals(packageName)) return NOT_MATCHED; + if (!simpleClassName(path).equals(className)) return NOT_MATCHED; + // Methods + if (erasedParameterTypes.isPresent()) { + if (!(path.getLeaf() instanceof MethodTree)) return NOT_MATCHED; + var method = (MethodTree) path.getLeaf(); + if (!method.getName().contentEquals(memberName.get())) return NOT_MATCHED; + if (method.getParameters().size() != erasedParameterTypes.get().size()) return NOT_MATCHED; + var mismatch = 0; + for (var i = 0; i < method.getParameters().size(); i++) { + var type = method.getParameters().get(i).getType(); + var name = fuzzyTypeName(type); + var expected = erasedParameterTypes.get().get(i); + if (!expected.endsWith(name)) mismatch++; + } + return mismatch; + } + // Fields + if (memberName.isPresent()) { + if (!(path.getLeaf() instanceof VariableTree)) return NOT_MATCHED; + var field = (VariableTree) path.getLeaf(); + if (!field.getName().contentEquals(memberName.get())) return NOT_MATCHED; + return 0; + } + // Classes + return 0; + } + + private String packageName(TreePath path) { + return Objects.toString(path.getCompilationUnit().getPackageName(), ""); + } + + private String simpleClassName(TreePath path) { + var reversedClassName = new ArrayList<CharSequence>(); + for (; path != null; path = path.getParentPath()) { + if (path.getLeaf() instanceof ClassTree) { + var cls = (ClassTree) path.getLeaf(); + reversedClassName.add(cls.getSimpleName()); + } + } + return reverseAndJoin(reversedClassName, "."); + } + + private String fuzzyTypeName(Tree type) { + class FindTypeName extends TreeScanner<Void, Void> { + String found = ""; + + @Override + public Void visitIdentifier(IdentifierTree t, Void __) { + found = t.getName().toString(); + return null; + } + + @Override + public Void visitPrimitiveType(PrimitiveTypeTree t, Void __) { + found = t.getPrimitiveTypeKind().name(); + return null; + } + } + var find = new FindTypeName(); + find.scan(type, null); + if (find.found.isEmpty()) { + LOG.warning( + String.format( + "Couldn't find type name for %s `%s`", + type.getClass().getName(), Parser.describeTree(type))); + } + return find.found; + } + + public static boolean canPoint(Element e) { + var inLeaf = true; + for (; e != null; e = e.getEnclosingElement()) { + var isLeaf = e instanceof ExecutableElement || e instanceof VariableElement; + if (inLeaf && !isLeaf) inLeaf = false; + if (!inLeaf && isLeaf) return false; + } + return true; + } + @Override public boolean equals(Object other) { if (!(other instanceof Ptr)) return false; var that = (Ptr) other; - return this.path.equals(that.path); + return Objects.equals(this.packageName, that.packageName) + && Objects.equals(this.className, that.className) + && Objects.equals(that.memberName, that.memberName) + && Objects.equals(this.erasedParameterTypes, that.erasedParameterTypes); } @Override public int hashCode() { - return Objects.hash(path); + return Objects.hash(packageName, className, memberName, erasedParameterTypes); } @Override public String toString() { - return path; + var s = new StringBuilder(); + if (!packageName.isEmpty()) { + s.append(packageName).append('/'); + } + s.append(className); + if (memberName.isPresent()) { + s.append('#').append(memberName.get()); + } + if (erasedParameterTypes.isPresent()) { + var join = String.join(",", erasedParameterTypes.get()); + s.append('(').append(join).append(')'); + } + return s.toString(); } private static final Logger LOG = Logger.getLogger("main"); diff --git a/src/main/java/org/javacs/lsp/SignatureInformation.java b/src/main/java/org/javacs/lsp/SignatureInformation.java index b5a7f61..a9c3502 100644 --- a/src/main/java/org/javacs/lsp/SignatureInformation.java +++ b/src/main/java/org/javacs/lsp/SignatureInformation.java @@ -6,4 +6,12 @@ public class SignatureInformation { public String label; public MarkupContent documentation; public List<ParameterInformation> parameters; + + public SignatureInformation() {} + + public SignatureInformation(String label, MarkupContent documentation, List<ParameterInformation> parameters) { + this.label = label; + this.documentation = documentation; + this.parameters = parameters; + } } diff --git a/src/test/java/org/javacs/CodeLensTest.java b/src/test/java/org/javacs/CodeLensTest.java index 0185b73..893c605 100644 --- a/src/test/java/org/javacs/CodeLensTest.java +++ b/src/test/java/org/javacs/CodeLensTest.java @@ -80,10 +80,10 @@ public class CodeLensTest { var compile = server.compiler.compileFile(uri, contents); var signatureMatches = compile.signatureMatches(); - var good = List.of(new Ptr("org.javacs.example.ConstructorRefs.ConstructorRefs(int)")); + var good = List.of(new Ptr("org.javacs.example/ConstructorRefs#<init>(int)")); assertTrue(signatureMatches.test(good)); - var bad = List.of(new Ptr("org.javacs.example.ConstructorRefs.ConstructorRefs(int, int)")); + var bad = List.of(new Ptr("org.javacs.example/ConstructorRefs#<init>(int,int)")); assertFalse(signatureMatches.test(bad)); } } diff --git a/src/test/java/org/javacs/DocsTest.java b/src/test/java/org/javacs/DocsTest.java index dd89e46..6819974 100644 --- a/src/test/java/org/javacs/DocsTest.java +++ b/src/test/java/org/javacs/DocsTest.java @@ -11,24 +11,34 @@ public class DocsTest { public void classDoc() { var sourcePath = Set.of(JavaCompilerServiceTest.simpleProjectSrc()); var docs = new Docs(sourcePath); - var tree = docs.classDoc("ClassDoc"); - assertTrue(tree.isPresent()); - assertThat(tree.get().getFirstSentence(), hasToString("A great class")); + var ptr = new Ptr("ClassDoc"); + var file = docs.find(ptr).get(); + var parse = docs.parse(file); + var path = parse.fuzzyFind(ptr).get(); + var tree = parse.doc(path); + assertThat(tree.getFirstSentence(), hasToString("A great class")); } @Test public void memberDoc() { var sourcePath = Set.of(JavaCompilerServiceTest.simpleProjectSrc()); var docs = new Docs(sourcePath); - var tree = docs.memberDoc("LocalMethodDoc", "targetMethod"); - assertTrue(tree.isPresent()); - assertThat(tree.get().getFirstSentence(), hasToString("A great method")); + var ptr = new Ptr("LocalMethodDoc#targetMethod(int)"); + var file = docs.find(ptr).get(); + var parse = docs.parse(file); + var path = parse.fuzzyFind(ptr).get(); + var tree = parse.doc(path); + assertThat(tree.getFirstSentence(), hasToString("A great method")); } @Test public void platformDoc() { var docs = new Docs(Set.of()); - var tree = docs.classDoc("java.util.List"); - assertTrue(tree.isPresent()); + var ptr = new Ptr("java.util/List"); + var file = docs.find(ptr).get(); + var parse = docs.parse(file); + var path = parse.fuzzyFind(ptr).get(); + var tree = parse.doc(path); + assertThat(tree.getFirstSentence(), not(empty())); } } diff --git a/src/test/java/org/javacs/JavaCompilerServiceTest.java b/src/test/java/org/javacs/JavaCompilerServiceTest.java index db694b5..838d9b5 100644 --- a/src/test/java/org/javacs/JavaCompilerServiceTest.java +++ b/src/test/java/org/javacs/JavaCompilerServiceTest.java @@ -290,8 +290,11 @@ public class JavaCompilerServiceTest { var uri = resourceUri("LocalMethodDoc.java"); var contents = contents("LocalMethodDoc.java"); var method = compiler.compileFocus(uri, contents, 3, 21).methodInvocation().get().activeMethod.get(); - var doc = compiler.docs().methodDoc(method); - assertTrue(doc.isPresent()); + var ptr = new Ptr(method); + var file = compiler.docs().find(ptr).get(); + var parse = compiler.docs().parse(file); + var path = parse.fuzzyFind(ptr).get(); + var doc = parse.doc(path); assertThat(doc.toString(), containsString("A great method")); } diff --git a/src/test/java/org/javacs/PtrTest.java b/src/test/java/org/javacs/PtrTest.java new file mode 100644 index 0000000..8dcf110 --- /dev/null +++ b/src/test/java/org/javacs/PtrTest.java @@ -0,0 +1,116 @@ +package org.javacs; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.net.URI; +import org.junit.Test; + +public class PtrTest { + + static JavaLanguageServer server = LanguageServerFixture.getJavaLanguageServer(); + static String file = "/org/javacs/example/Ptrs.java"; + static URI uri = FindResource.uri(file); + static String contents = FindResource.contents(file); + static CompileFile compile = server.compiler.compileFile(uri, contents); + + @Test + public void classPtr() { + var el = compile.element(3, 15).get(); + var ptr = new Ptr(el); + assertThat(ptr.toString(), equalTo("org.javacs.example/Ptrs")); + + var copy = new Ptr(ptr.toString()); + assertThat(copy, equalTo(ptr)); + } + + @Test + public void fieldPtr() { + var el = compile.element(4, 20).get(); + var ptr = new Ptr(el); + assertThat(ptr.toString(), equalTo("org.javacs.example/Ptrs#field")); + + var copy = new Ptr(ptr.toString()); + assertThat(copy, equalTo(ptr)); + } + + @Test + public void emptyMethodPtr() { + var el = compile.element(6, 20).get(); + var ptr = new Ptr(el); + assertThat(ptr.toString(), equalTo("org.javacs.example/Ptrs#method()")); + + var copy = new Ptr(ptr.toString()); + assertThat(copy, equalTo(ptr)); + } + + @Test + public void intMethodPtr() { + var el = compile.element(8, 20).get(); + var ptr = new Ptr(el); + assertThat(ptr.toString(), equalTo("org.javacs.example/Ptrs#method(int)")); + + var copy = new Ptr(ptr.toString()); + assertThat(copy, equalTo(ptr)); + } + + @Test + public void stringMethodPtr() { + var el = compile.element(10, 20).get(); + var ptr = new Ptr(el); + assertThat(ptr.toString(), equalTo("org.javacs.example/Ptrs#method(java.lang.String)")); + + var copy = new Ptr(ptr.toString()); + assertThat(copy, equalTo(ptr)); + } + + @Test + public void constructorPtr() { + var el = compile.element(12, 13).get(); + var ptr = new Ptr(el); + assertThat(ptr.toString(), equalTo("org.javacs.example/Ptrs#<init>(int)")); + + var copy = new Ptr(ptr.toString()); + assertThat(copy, equalTo(ptr)); + } + + @Test + public void innerClassPtr() { + var el = compile.element(14, 20).get(); + var ptr = new Ptr(el); + assertThat(ptr.toString(), equalTo("org.javacs.example/Ptrs.InnerClass")); + + var copy = new Ptr(ptr.toString()); + assertThat(copy, equalTo(ptr)); + } + + @Test + public void innerFieldPtr() { + var el = compile.element(15, 20).get(); + var ptr = new Ptr(el); + assertThat(ptr.toString(), equalTo("org.javacs.example/Ptrs.InnerClass#innerField")); + + var copy = new Ptr(ptr.toString()); + assertThat(copy, equalTo(ptr)); + } + + @Test + public void innerEmptyMethodPtr() { + var el = compile.element(17, 25).get(); + var ptr = new Ptr(el); + assertThat(ptr.toString(), equalTo("org.javacs.example/Ptrs.InnerClass#innerMethod()")); + + var copy = new Ptr(ptr.toString()); + assertThat(copy, equalTo(ptr)); + } + + @Test + public void innerConstructorPtr() { + var el = compile.element(19, 21).get(); + var ptr = new Ptr(el); + assertThat(ptr.toString(), equalTo("org.javacs.example/Ptrs.InnerClass#<init>()")); + + var copy = new Ptr(ptr.toString()); + assertThat(copy, equalTo(ptr)); + } +} diff --git a/src/test/test-project/workspace/src/org/javacs/example/Ptrs.java b/src/test/test-project/workspace/src/org/javacs/example/Ptrs.java new file mode 100644 index 0000000..2d6260c --- /dev/null +++ b/src/test/test-project/workspace/src/org/javacs/example/Ptrs.java @@ -0,0 +1,21 @@ +package org.javacs.example; + +public class Ptrs { + public int field; + + public void method() { } + + public void method(int arg) { } + + public void method(String arg) { } + + public Ptrs(int arg) { } + + public class InnerClass { + public int innerField; + + public void innerMethod() { } + + public InnerClass() { } + } +}
\ No newline at end of file |