summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/org/javacs/CompileFile.java31
-rw-r--r--src/main/java/org/javacs/Completion.java5
-rw-r--r--src/main/java/org/javacs/Docs.java170
-rw-r--r--src/main/java/org/javacs/JavaCompilerService.java1
-rw-r--r--src/main/java/org/javacs/JavaLanguageServer.java172
-rw-r--r--src/main/java/org/javacs/ParseFile.java98
-rw-r--r--src/main/java/org/javacs/Parser.java25
-rw-r--r--src/main/java/org/javacs/Ptr.java231
-rw-r--r--src/main/java/org/javacs/lsp/SignatureInformation.java8
-rw-r--r--src/test/java/org/javacs/CodeLensTest.java4
-rw-r--r--src/test/java/org/javacs/DocsTest.java26
-rw-r--r--src/test/java/org/javacs/JavaCompilerServiceTest.java7
-rw-r--r--src/test/java/org/javacs/PtrTest.java116
-rw-r--r--src/test/test-project/workspace/src/org/javacs/example/Ptrs.java21
14 files changed, 633 insertions, 282 deletions
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