diff options
-rw-r--r-- | TODOS.md | 1 | ||||
-rw-r--r-- | javaLsFlag.txt | 1 | ||||
-rw-r--r-- | lib/extension.ts | 4 | ||||
-rw-r--r-- | lib/src.zip | bin | 0 -> 61243737 bytes | |||
-rw-r--r-- | src/main/java/org/javacs/Classes.java | 2 | ||||
-rw-r--r-- | src/main/java/org/javacs/Docs.java | 170 | ||||
-rw-r--r-- | src/main/java/org/javacs/DocsIndexer.java | 109 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavaCompilerService.java | 36 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavaTextDocumentService.java | 106 | ||||
-rw-r--r-- | src/main/java/org/javacs/Javadocs.java | 294 | ||||
-rw-r--r-- | src/main/java/org/javacs/Lib.java | 15 | ||||
-rw-r--r-- | src/main/java/org/javacs/Parser.java | 4 | ||||
-rw-r--r-- | src/test/java/org/javacs/DocsTest.java | 13 | ||||
-rw-r--r-- | src/test/java/org/javacs/JavaCompilerServiceTest.java | 6 | ||||
-rw-r--r-- | src/test/java/org/javacs/JavadocsTest.java | 74 | ||||
-rw-r--r-- | src/test/resources/LocalMethodDoc.java | 7 |
16 files changed, 288 insertions, 554 deletions
@@ -75,6 +75,7 @@ * Cast to type * Import missing file * Unused return value auto-add +* Format-on-save should add missing method declarations ### Code lens * "N references" on method, class diff --git a/javaLsFlag.txt b/javaLsFlag.txt new file mode 100644 index 0000000..4babb58 --- /dev/null +++ b/javaLsFlag.txt @@ -0,0 +1 @@ +This file is a flag to help Lib#installRoot find the root directory of the extension.
\ No newline at end of file diff --git a/lib/extension.ts b/lib/extension.ts index 706e440..14a422f 100644 --- a/lib/extension.ts +++ b/lib/extension.ts @@ -51,11 +51,9 @@ export function activate(context: VSCode.ExtensionContext) { let serverOptions: ServerOptions = { command: javaExecutablePath, args: args, - options: { cwd: VSCode.workspace.rootPath } + options: { cwd: context.extensionPath } } - console.log(javaExecutablePath + ' ' + args.join(' ')); - // Copied from typescript VSCode.languages.setLanguageConfiguration('java', { indentationRules: { diff --git a/lib/src.zip b/lib/src.zip Binary files differnew file mode 100644 index 0000000..d08e5b4 --- /dev/null +++ b/lib/src.zip diff --git a/src/main/java/org/javacs/Classes.java b/src/main/java/org/javacs/Classes.java index e3c2201..72a0267 100644 --- a/src/main/java/org/javacs/Classes.java +++ b/src/main/java/org/javacs/Classes.java @@ -20,7 +20,7 @@ import java.util.stream.Collectors; class Classes { /** All exported modules in the JDK */ - private static String[] JDK_MODULES = { + static String[] JDK_MODULES = { "java.activation", "java.base", "java.compiler", diff --git a/src/main/java/org/javacs/Docs.java b/src/main/java/org/javacs/Docs.java index 3786349..659b785 100644 --- a/src/main/java/org/javacs/Docs.java +++ b/src/main/java/org/javacs/Docs.java @@ -1,10 +1,18 @@ package org.javacs; +import com.overzealous.remark.Options; +import com.overzealous.remark.Remark; import com.sun.source.doctree.DocCommentTree; +import com.sun.source.tree.*; +import com.sun.source.util.*; import java.io.*; import java.nio.file.*; import java.util.*; +import java.util.function.*; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.*; import javax.tools.*; class Docs { @@ -12,36 +20,178 @@ class Docs { /** File manager with source-path + platform sources, which we will use to look up individual source files */ private final StandardJavaFileManager fileManager; + private static Path srcZip() { + try { + var fs = FileSystems.newFileSystem(Lib.SRC_ZIP, Docs.class.getClassLoader()); + return fs.getPath("/"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + Docs(Set<Path> sourcePath) { this.fileManager = ServiceLoader.load(JavaCompiler.class).iterator().next().getStandardFileManager(__ -> {}, null, null); // Compute the full source path, including src.zip for platform classes - var allSourcePaths = new HashSet<File>(); - for (var p : sourcePath) allSourcePaths.add(p.toFile()); - // TODO src.zip + var sourcePathFiles = sourcePath.stream().map(Path::toFile).collect(Collectors.toSet()); + + try { + fileManager.setLocation(StandardLocation.SOURCE_PATH, sourcePathFiles); + fileManager.setLocationFromPaths(StandardLocation.MODULE_SOURCE_PATH, Set.of(srcZip())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private Optional<JavaFileObject> file(String className) { try { - fileManager.setLocation(StandardLocation.SOURCE_PATH, allSourcePaths); + var fromSourcePath = + fileManager.getJavaFileForInput( + StandardLocation.SOURCE_PATH, className, JavaFileObject.Kind.SOURCE); + if (fromSourcePath != null) 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); + } + return Optional.empty(); } catch (IOException e) { throw new RuntimeException(e); } } - private JavaFileObject file(String className) { + 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()); + CompilationUnitTree root; try { - return fileManager.getJavaFileForInput(StandardLocation.SOURCE_PATH, className, JavaFileObject.Kind.SOURCE); + var it = task.parse().iterator(); + if (!it.hasNext()) { + LOG.warning("Found no CompilationUnitTree in " + file); + return Optional.empty(); + } + root = it.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; } - DocCommentTree memberDoc(String className, String memberName) { - return DocsIndexer.memberDoc(className, memberName, file(className)); + Optional<DocCommentTree> memberDoc(String className, String memberName) { + Objects.requireNonNull(className); + Objects.requireNonNull(memberName); + + return findDoc(className, memberName); } - DocCommentTree classDoc(String className) { - return DocsIndexer.classDoc(className, file(className)); + Optional<DocCommentTree> classDoc(String className) { + Objects.requireNonNull(className); + + return findDoc(className, null); + } + + Optional<MethodTree> findMethod(String className, String methodName) { + Objects.requireNonNull(className); + Objects.requireNonNull(methodName); + + var file = file(className); + if (!file.isPresent()) return Optional.empty(); + var task = Parser.parseTask(file.get()); + CompilationUnitTree root; + try { + root = task.parse().iterator().next(); + } 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 (method.getName().contentEquals(methodName)) result = Optional.of(method); + } + } + } + return null; + } + } + var find = new Find(); + find.scan(root, null); + return find.result; + } + + private static final Pattern HTML_TAG = Pattern.compile("<(\\w+)>"); + + private static boolean isHtml(String text) { + Matcher tags = HTML_TAG.matcher(text); + while (tags.find()) { + String tag = tags.group(1); + String close = String.format("</%s>", tag); + int findClose = text.indexOf(close, tags.end()); + if (findClose != -1) return true; + } + return false; + } + + // TODO is this still necessary? + /** If `commentText` looks like HTML, convert it to markdown */ + static String htmlToMarkdown(String commentText) { + if (isHtml(commentText)) { + Options options = new Options(); + + options.tables = Options.Tables.CONVERT_TO_CODE_BLOCK; + options.hardwraps = true; + options.inlineLinks = true; + options.autoLinks = true; + options.reverseHtmlSmartPunctuation = true; + + return new Remark(options).convertFragment(commentText); + } else return commentText; } private static final Logger LOG = Logger.getLogger("main"); diff --git a/src/main/java/org/javacs/DocsIndexer.java b/src/main/java/org/javacs/DocsIndexer.java deleted file mode 100644 index 13f82ed..0000000 --- a/src/main/java/org/javacs/DocsIndexer.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.javacs; - -import com.sun.source.doctree.DocCommentTree; -import java.io.*; -import java.nio.file.*; -import java.util.*; -import java.util.logging.Logger; -import javax.lang.model.*; -import javax.lang.model.element.*; -import javax.tools.*; -import jdk.javadoc.doclet.*; - -public class DocsIndexer implements Doclet { - - private static String targetClass, targetMember; - private static DocCommentTree result; - - @Override - public String getName() { - return "Indexer"; - } - - @Override - public Set<Doclet.Option> getSupportedOptions() { - return Set.of(); - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.RELEASE_10; - } - - @Override - public void init(Locale locale, Reporter reporter) { - // Nothing to do - } - - @Override - public boolean run(DocletEnvironment env) { - Objects.requireNonNull(targetClass, "DocIndexer.targetClass has not been set"); - - var els = env.getSpecifiedElements(); - if (els.isEmpty()) throw new RuntimeException("No specified elements"); - var docs = env.getDocTrees(); - var elements = env.getElementUtils(); - for (var e : els) { - if (e instanceof TypeElement) { - var t = (TypeElement) e; - if (t.getQualifiedName().contentEquals(targetClass)) { - if (targetMember == null) { - result = docs.getDocCommentTree(t); - } else { - for (var member : elements.getAllMembers(t)) { - if (member.getSimpleName().contentEquals(targetMember)) { - result = docs.getDocCommentTree(member); - } - } - } - } - } - } - return true; - } - - /** Empty file manager we pass to javadoc to prevent it from roaming all over the place */ - private static final StandardJavaFileManager emptyFileManager = - ServiceLoader.load(JavaCompiler.class).iterator().next().getStandardFileManager(__ -> {}, null, null); - - private static DocumentationTool.DocumentationTask task(JavaFileObject file, String className) { - var tool = ToolProvider.getSystemDocumentationTool(); - return tool.getTask( - null, - emptyFileManager, - err -> LOG.severe(err.getMessage(null)), - DocsIndexer.class, - List.of("--ignore-source-errors", "-Xclasses", className), - List.of(file)); - } - - public static DocCommentTree classDoc(String className, JavaFileObject file) { - Objects.requireNonNull(file, "file is null"); - - try { - targetClass = className; - var task = task(file, className); - if (!task.call()) throw new RuntimeException("Documentation task failed"); - Objects.requireNonNull(result, "Documentation task did not set result"); - return result; - } finally { - targetClass = null; - } - } - - public static DocCommentTree memberDoc(String className, String memberName, JavaFileObject file) { - try { - targetClass = className; - targetMember = memberName; - var task = task(file, className); - if (!task.call()) throw new RuntimeException("Documentation task failed"); - Objects.requireNonNull(result, "Documentation task did not set result"); - return result; - } finally { - targetClass = null; - targetMember = null; - } - } - - private static final Logger LOG = Logger.getLogger("main"); -} diff --git a/src/main/java/org/javacs/JavaCompilerService.java b/src/main/java/org/javacs/JavaCompilerService.java index 9b82940..e82ab26 100644 --- a/src/main/java/org/javacs/JavaCompilerService.java +++ b/src/main/java/org/javacs/JavaCompilerService.java @@ -1,7 +1,7 @@ package org.javacs; -import com.sun.javadoc.ClassDoc; -import com.sun.javadoc.MethodDoc; +import com.google.common.collect.Sets; +import com.sun.source.doctree.DocCommentTree; import com.sun.source.tree.*; import com.sun.source.util.JavacTask; import com.sun.source.util.SourcePositions; @@ -52,7 +52,7 @@ public class JavaCompilerService { // Not modifiable! If you want to edit these, you need to create a new instance private final Set<Path> sourcePath, classPath, docPath; private final JavaCompiler compiler = ServiceLoader.load(JavaCompiler.class).iterator().next(); - private final Javadocs docs; + private final Docs docs; private final ClassSource jdkClasses = Classes.jdkTopLevelClasses(), classPathClasses; // Diagnostics from the last compilation task private final List<Diagnostic<? extends JavaFileObject>> diags = new ArrayList<>(); @@ -84,7 +84,7 @@ public class JavaCompilerService { this.sourcePath = Collections.unmodifiableSet(sourcePath); this.classPath = Collections.unmodifiableSet(classPath); this.docPath = docPath; - this.docs = new Javadocs(sourcePath, docPath); + this.docs = new Docs(Sets.union(sourcePath, docPath)); this.classPathClasses = Classes.classPathTopLevelClasses(classPath); } @@ -426,6 +426,7 @@ public class JavaCompilerService { // Add static members for (Element member : t.getEnclosedElements()) { + // TODO if this is a member reference :: then include non-statics if (member.getModifiers().contains(Modifier.STATIC) && trees.isAccessible(scope, member, (DeclaredType) t.asType())) { result.add(Completion.ofElement(member)); @@ -777,14 +778,29 @@ public class JavaCompilerService { return declaringFile.flatMap(f -> findIn(e, f, contents.apply(f.toUri()))); } - /** Look up the javadoc associated with `e` */ - public Optional<MethodDoc> methodDoc(ExecutableElement e) { - return docs.methodDoc(e); + /** 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 docs.memberDoc(className, methodName); } - /** Look up the javadoc associated with `e` */ - public Optional<ClassDoc> classDoc(TypeElement e) { - return docs.classDoc(e); + /** Find and parse 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(); + return docs.findMethod(className, methodName); + } + + /** Look up the javadoc associated with `type` */ + public Optional<DocCommentTree> classDoc(TypeElement type) { + return docs.classDoc(type.getQualifiedName().toString()); + } + + public Optional<DocCommentTree> classDoc(String qualifiedName) { + return docs.classDoc(qualifiedName); } private Stream<Path> javaSourcesInDir(Path dir) { diff --git a/src/main/java/org/javacs/JavaTextDocumentService.java b/src/main/java/org/javacs/JavaTextDocumentService.java index f74b4e2..5106afe 100644 --- a/src/main/java/org/javacs/JavaTextDocumentService.java +++ b/src/main/java/org/javacs/JavaTextDocumentService.java @@ -1,12 +1,13 @@ package org.javacs; import com.google.gson.JsonPrimitive; -import com.sun.javadoc.MethodDoc; -import com.sun.javadoc.ParamTag; -import com.sun.javadoc.Parameter; +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.ParamTree; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ImportTree; import com.sun.source.tree.LineMap; +import com.sun.source.tree.MethodTree; import com.sun.source.util.SourcePositions; import com.sun.source.util.TreePath; import com.sun.source.util.Trees; @@ -22,6 +23,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.StringJoiner; @@ -162,12 +164,12 @@ class JavaTextDocumentService implements TextDocumentService { return CompletableFuture.completedFuture(Either.forRight(new CompletionList(completions.isIncomplete, result))); } - private String resolveDocDetail(MethodDoc doc) { + private String resolveDocDetail(MethodTree doc) { StringJoiner args = new StringJoiner(", "); - for (Parameter p : doc.parameters()) { - args.add(p.name()); + for (var p : doc.getParameters()) { + args.add(p.getName()); } - return String.format("%s(%s)", doc.name(), args); + return String.format("%s(%s)", doc.getName(), args); } private String resolveDefaultDetail(ExecutableElement method) { @@ -181,6 +183,26 @@ class JavaTextDocumentService implements TextDocumentService { return String.format("%s(%s)", method.getSimpleName(), args); } + private String asMarkdown(List<? extends DocTree> lines) { + var join = new StringJoiner("\n"); + for (var l : lines) join.add(l.toString()); + var html = join.toString(); + return Docs.htmlToMarkdown(html); + } + + private String asMarkdown(DocCommentTree comment) { + var lines = comment.getFirstSentence(); + return asMarkdown(lines); + } + + private MarkupContent asMarkupContent(DocCommentTree comment) { + var markdown = asMarkdown(comment); + var content = new MarkupContent(); + content.setKind(MarkupKind.MARKDOWN); + content.setValue(markdown); + return content; + } + @Override public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved) { JsonPrimitive idJson = (JsonPrimitive) unresolved.getData(); @@ -192,36 +214,27 @@ class JavaTextDocumentService implements TextDocumentService { } if (cached.element != null) { if (cached.element instanceof ExecutableElement) { - ExecutableElement method = (ExecutableElement) cached.element; - Optional<MethodDoc> doc = server.compiler.methodDoc(method); - String detail = doc.map(this::resolveDocDetail).orElse(resolveDefaultDetail(method)); + var method = (ExecutableElement) cached.element; + var tree = server.compiler.methodTree(method); + var detail = tree.map(this::resolveDocDetail).orElse(resolveDefaultDetail(method)); unresolved.setDetail(detail); - doc.flatMap(Javadocs::commentText) - .ifPresent( - html -> { - String markdown = Javadocs.htmlToMarkdown(html); - MarkupContent content = new MarkupContent(); - content.setKind(MarkupKind.MARKDOWN); - content.setValue(markdown); - unresolved.setDocumentation(content); - }); + + var doc = server.compiler.methodDoc(method); + var markdown = doc.map(this::asMarkupContent); + markdown.ifPresent(unresolved::setDocumentation); } else if (cached.element instanceof TypeElement) { - TypeElement type = (TypeElement) cached.element; - server.compiler - .classDoc(type) - .ifPresent( - doc -> { - String html = doc.commentText(); - String markdown = Javadocs.htmlToMarkdown(html); - MarkupContent content = new MarkupContent(); - content.setKind(MarkupKind.MARKDOWN); - content.setValue(markdown); - unresolved.setDocumentation(content); - }); + var type = (TypeElement) cached.element; + var doc = server.compiler.classDoc(type); + var markdown = doc.map(this::asMarkupContent); + markdown.ifPresent(unresolved::setDocumentation); } else { LOG.info("Don't know how to look up docs for element " + cached.element); } // TODO constructors, fields + } else if (cached.notImportedClass != null) { + var doc = server.compiler.classDoc(cached.notImportedClass); + var markdown = doc.map(this::asMarkupContent); + markdown.ifPresent(unresolved::setDocumentation); } return CompletableFuture.completedFuture(unresolved); // TODO } @@ -281,10 +294,10 @@ class JavaTextDocumentService implements TextDocumentService { private Optional<String> hoverDocs(Element e) { if (e instanceof ExecutableElement) { ExecutableElement m = (ExecutableElement) e; - return server.compiler.methodDoc(m).flatMap(Javadocs::commentText).map(Javadocs::htmlToMarkdown); + return server.compiler.methodDoc(m).map(this::asMarkdown); } else if (e instanceof TypeElement) { TypeElement t = (TypeElement) e; - return server.compiler.classDoc(t).map(doc -> doc.commentText()).map(Javadocs::htmlToMarkdown); + return server.compiler.classDoc(t).map(this::asMarkdown); } else return Optional.empty(); } @@ -303,17 +316,22 @@ class JavaTextDocumentService implements TextDocumentService { } else return CompletableFuture.completedFuture(new Hover(Collections.emptyList())); } - private List<ParameterInformation> signatureParamsFromDocs(MethodDoc doc) { + private List<ParameterInformation> signatureParamsFromDocs(MethodTree method, DocCommentTree doc) { List<ParameterInformation> ps = new ArrayList<>(); Map<String, String> paramComments = new HashMap<>(); - for (ParamTag t : doc.paramTags()) { - paramComments.put(t.parameterName(), t.parameterComment()); + for (var tag : doc.getBlockTags()) { + if (tag.getKind() == DocTree.Kind.PARAM) { + var param = (ParamTree) tag; + paramComments.put(param.getName().toString(), asMarkdown(param.getDescription())); + } } - for (Parameter d : doc.parameters()) { - ParameterInformation p = new ParameterInformation(); - p.setLabel(d.name()); - p.setDocumentation(paramComments.get(d.name())); - ps.add(p); + for (var param : method.getParameters()) { + var info = new ParameterInformation(); + var name = param.getName().toString(); + info.setLabel(name); + if (paramComments.containsKey(name)) info.setDocumentation(paramComments.get(name)); + else info.setDocumentation(Objects.toString(param.getType(), "")); + ps.add(info); } return ps; } @@ -333,8 +351,10 @@ class JavaTextDocumentService implements TextDocumentService { private SignatureInformation asSignatureInformation(ExecutableElement e) { SignatureInformation i = new SignatureInformation(); - Optional<MethodDoc> doc = server.compiler.methodDoc(e); - List<ParameterInformation> ps = doc.map(this::signatureParamsFromDocs).orElse(signatureParamsFromMethod(e)); + var ps = signatureParamsFromMethod(e); + var doc = server.compiler.methodDoc(e); + var tree = server.compiler.methodTree(e); + if (doc.isPresent() && tree.isPresent()) ps = signatureParamsFromDocs(tree.get(), doc.get()); String args = ps.stream().map(p -> p.getLabel()).collect(Collectors.joining(", ")); String name = e.getSimpleName().toString(); if (name.equals("<init>")) name = e.getEnclosingElement().getSimpleName().toString(); diff --git a/src/main/java/org/javacs/Javadocs.java b/src/main/java/org/javacs/Javadocs.java deleted file mode 100644 index f993283..0000000 --- a/src/main/java/org/javacs/Javadocs.java +++ /dev/null @@ -1,294 +0,0 @@ -package org.javacs; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.overzealous.remark.Options; -import com.overzealous.remark.Remark; -import com.sun.javadoc.ClassDoc; -import com.sun.javadoc.MethodDoc; -import com.sun.javadoc.Parameter; -import com.sun.javadoc.RootDoc; -import com.sun.source.util.JavacTask; -import java.io.File; -import java.io.IOException; -import java.nio.file.*; -import java.text.BreakIterator; -import java.time.Instant; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.tools.*; - -// This class must be public so DocletInvoker can call it -public class Javadocs { - - /** Cache for performance reasons */ - private final StandardJavaFileManager fileManager; - - /** Empty file manager we pass to javadoc to prevent it from roaming all over the place */ - private final StandardJavaFileManager emptyFileManager = - ServiceLoader.load(JavaCompiler.class).iterator().next().getStandardFileManager(__ -> {}, null, null); - - /** All the classes we have indexed so far */ - private final Map<String, IndexedDoc> topLevelClasses = new ConcurrentHashMap<>(); - - private final JavacTask task; - - private static class IndexedDoc { - final RootDoc doc; - final Instant updated; - - IndexedDoc(RootDoc doc, Instant updated) { - this.doc = doc; - this.updated = updated; - } - } - - private static Path srcZip() { - try { - var fs = FileSystems.newFileSystem(Lib.SRC_ZIP, Docs.class.getClassLoader()); - return fs.getPath("/"); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - Javadocs(Set<Path> sourcePath, Set<Path> docPath) { - this.fileManager = - ServiceLoader.load(JavaCompiler.class).iterator().next().getStandardFileManager(__ -> {}, null, null); - this.task = - (JavacTask) - ServiceLoader.load(JavaCompiler.class) - .iterator() - .next() - .getTask(null, emptyFileManager, __ -> {}, null, null, null); - // Compute the full source path, including src.zip for platform classes - var sourcePathFiles = sourcePath.stream().map(Path::toFile).collect(Collectors.toSet()); - - try { - fileManager.setLocation(StandardLocation.SOURCE_PATH, sourcePathFiles); - fileManager.setLocationFromPaths(StandardLocation.MODULE_SOURCE_PATH, Set.of(srcZip())); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static final Pattern HTML_TAG = Pattern.compile("<(\\w+)>"); - - private static boolean isHtml(String text) { - Matcher tags = HTML_TAG.matcher(text); - while (tags.find()) { - String tag = tags.group(1); - String close = String.format("</%s>", tag); - int findClose = text.indexOf(close, tags.end()); - if (findClose != -1) return true; - } - return false; - } - - /** If `commentText` looks like HTML, convert it to markdown */ - static String htmlToMarkdown(String commentText) { - if (isHtml(commentText)) { - Options options = new Options(); - - options.tables = Options.Tables.CONVERT_TO_CODE_BLOCK; - options.hardwraps = true; - options.inlineLinks = true; - options.autoLinks = true; - options.reverseHtmlSmartPunctuation = true; - - return new Remark(options).convertFragment(commentText); - } else return commentText; - } - - /** Get docstring for method, using inherited method if necessary */ - static Optional<String> commentText(MethodDoc doc) { - // TODO search interfaces as well - - while (doc != null && Strings.isNullOrEmpty(doc.commentText())) doc = doc.overriddenMethod(); - - if (doc == null || Strings.isNullOrEmpty(doc.commentText())) return Optional.empty(); - else return Optional.of(doc.commentText()); - } - - // Figure out if elements and docs refer to the same thing, by computing String keys - - private String elementParamKey(VariableElement param) { - return task.getTypes().erasure(param.asType()).toString(); - } - - private String elementMethodKey(ExecutableElement method) { - TypeElement classElement = (TypeElement) method.getEnclosingElement(); - String params = method.getParameters().stream().map(this::elementParamKey).collect(Collectors.joining(",")); - return String.format("%s#%s(%s)", classElement.getQualifiedName(), method.getSimpleName(), params); - } - - private String docParamKey(Parameter doc) { - return doc.type().qualifiedTypeName() + doc.type().dimension(); - } - - private String docMethodKey(MethodDoc doc) { - String params = Arrays.stream(doc.parameters()).map(this::docParamKey).collect(Collectors.joining(",")); - return String.format("%s#%s(%s)", doc.containingClass().qualifiedName(), doc.name(), params); - } - - Optional<MethodDoc> methodDoc(ExecutableElement method) { - String key = elementMethodKey(method); - TypeElement classElement = (TypeElement) method.getEnclosingElement(); - String className = classElement.getQualifiedName().toString(); - RootDoc rootDoc = index(className); - ClassDoc classDoc = rootDoc.classNamed(className); - if (classDoc == null) { - LOG.warning("No docs for class " + className); - return Optional.empty(); - } - for (MethodDoc each : classDoc.methods(false)) { - if (docMethodKey(each).equals(key)) return Optional.of(each); - } - return Optional.empty(); - } - - Optional<ClassDoc> classDoc(TypeElement c) { - String className = c.getQualifiedName().toString(); - RootDoc index = index(className); - return Optional.ofNullable(index.classNamed(className)); - } - - /** Get or compute the javadoc for `className` */ - RootDoc index(String className) { - if (needsUpdate(className)) topLevelClasses.put(className, doIndex(className)); - - return topLevelClasses.get(className).doc; - } - - private JavaFileObject file(String className) { - try { - var fromSourcePath = - fileManager.getJavaFileForInput( - StandardLocation.SOURCE_PATH, className, JavaFileObject.Kind.SOURCE); - if (fromSourcePath != null) return fromSourcePath; - var moduleLocation = fileManager.getLocationForModule(StandardLocation.MODULE_SOURCE_PATH, "java.base"); - var fromModuleSourcePath = - fileManager.getJavaFileForInput(moduleLocation, className, JavaFileObject.Kind.SOURCE); - if (fromModuleSourcePath != null) return fromModuleSourcePath; - throw new RuntimeException("Couldn't find source file for " + className); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private boolean needsUpdate(String className) { - if (!topLevelClasses.containsKey(className)) return true; - - IndexedDoc indexed = topLevelClasses.get(className); - JavaFileObject source = file(className); - - if (source == null) return true; - - Instant modified = Instant.ofEpochMilli(source.getLastModified()); - - return indexed.updated.isBefore(modified); - } - - /** Read all the Javadoc for `className` */ - private IndexedDoc doIndex(String className) { - JavaFileObject source = file(className); - - if (source == null) { - LOG.warning("No source file for " + className); - - return new IndexedDoc(EmptyRootDoc.INSTANCE, Instant.EPOCH); - } - - LOG.info("Found " + source.toUri() + " for " + className); - - DocumentationTool.DocumentationTask task = - ToolProvider.getSystemDocumentationTool() - .getTask(null, emptyFileManager, __ -> {}, Javadocs.class, null, ImmutableList.of(source)); - - task.call(); - - return new IndexedDoc( - getSneakyReturn().orElse(EmptyRootDoc.INSTANCE), Instant.ofEpochMilli(source.getLastModified())); - } - - private Optional<RootDoc> getSneakyReturn() { - RootDoc result = sneakyReturn.get(); - sneakyReturn.remove(); - - if (result == null) { - LOG.warning("index did not return a RootDoc"); - - return Optional.empty(); - } else return Optional.of(result); - } - - /** start(RootDoc) uses this to return its result to doIndex(...) */ - private static ThreadLocal<RootDoc> sneakyReturn = new ThreadLocal<>(); - - /** - * Called by the javadoc tool - * - * <p>{@link com.sun.javadoc.Doclet} - */ - public static boolean start(RootDoc root) { - sneakyReturn.set(root); - - return true; - } - - /** Find the copy of src.zip that comes with the system-installed JDK */ - static Optional<File> findSrcZip() { - Path javaHome = Paths.get(System.getProperty("java.home")); - if (javaHome.endsWith("jre")) javaHome = javaHome.getParent(); - - var java8 = tryFind(javaHome.resolve("src.zip")); - if (java8.isPresent()) return java8; - - var java9 = tryFind(javaHome.resolve("lib").resolve("src.zip")); - return java9; - } - - private static Optional<File> tryFind(Path path) { - while (Files.isSymbolicLink(path)) { - try { - path = path.getParent().resolve(Files.readSymbolicLink(path)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - if (Files.exists(path)) return Optional.of(path.toFile()); - else { - LOG.warning(String.format("Could not find %s", path)); - - return Optional.empty(); - } - } - - /** - * Get the first sentence of a doc-comment. - * - * <p>In general, VS Code does a good job of only displaying the beginning of a doc-comment where appropriate. But - * if VS Code is displaying too much and you want to only show the first sentence, use this. - */ - public static String firstSentence(String doc) { - BreakIterator breaks = BreakIterator.getSentenceInstance(); - - breaks.setText(doc.replace('\n', ' ')); - - int start = breaks.first(), end = breaks.next(); - - if (start == -1 || end == -1) return doc; - - return doc.substring(start, end).trim(); - } - - private static final Logger LOG = Logger.getLogger("main"); -} diff --git a/src/main/java/org/javacs/Lib.java b/src/main/java/org/javacs/Lib.java new file mode 100644 index 0000000..924f98c --- /dev/null +++ b/src/main/java/org/javacs/Lib.java @@ -0,0 +1,15 @@ +package org.javacs; + +import java.nio.file.*; + +class Lib { + static Path installRoot() { + var root = Paths.get(".").toAbsolutePath(); + var p = root; + while (p != null && !Files.exists(p.resolve("javaLsFlag.txt"))) p = p.getParent(); + if (p == null) throw new RuntimeException("Couldn't find javaLsFlag.txt in any parent of " + root); + return p; + } + + static final Path SRC_ZIP = installRoot().resolve("lib/src.zip"); +} diff --git a/src/main/java/org/javacs/Parser.java b/src/main/java/org/javacs/Parser.java index 128fbca..1aa6fe2 100644 --- a/src/main/java/org/javacs/Parser.java +++ b/src/main/java/org/javacs/Parser.java @@ -42,7 +42,7 @@ class Parser { private static final StandardJavaFileManager fileManager = compiler.getStandardFileManager(__ -> {}, null, Charset.defaultCharset()); - private static JavacTask parseTask(JavaFileObject file) { + static JavacTask parseTask(JavaFileObject file) { return (JavacTask) compiler.getTask( null, @@ -53,7 +53,7 @@ class Parser { Collections.singletonList(file)); } - private static JavacTask parseTask(Path source) { + static JavacTask parseTask(Path source) { JavaFileObject file = fileManager.getJavaFileObjectsFromFiles(Collections.singleton(source.toFile())).iterator().next(); return parseTask(file); diff --git a/src/test/java/org/javacs/DocsTest.java b/src/test/java/org/javacs/DocsTest.java index 6910079..23bad26 100644 --- a/src/test/java/org/javacs/DocsTest.java +++ b/src/test/java/org/javacs/DocsTest.java @@ -12,7 +12,8 @@ public class DocsTest { var sourcePath = Set.of(JavaCompilerServiceTest.resourcesDir()); var docs = new Docs(sourcePath); var tree = docs.classDoc("ClassDoc"); - assertThat(tree, hasToString("A great class")); + assertTrue(tree.isPresent()); + assertThat(tree.get().getFirstSentence(), hasToString("A great class")); } @Test @@ -20,6 +21,14 @@ public class DocsTest { var sourcePath = Set.of(JavaCompilerServiceTest.resourcesDir()); var docs = new Docs(sourcePath); var tree = docs.memberDoc("LocalMethodDoc", "targetMethod"); - assertThat(tree, hasToString("A great method")); + assertTrue(tree.isPresent()); + assertThat(tree.get().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()); } } diff --git a/src/test/java/org/javacs/JavaCompilerServiceTest.java b/src/test/java/org/javacs/JavaCompilerServiceTest.java index f912ec4..b8ffa5c 100644 --- a/src/test/java/org/javacs/JavaCompilerServiceTest.java +++ b/src/test/java/org/javacs/JavaCompilerServiceTest.java @@ -3,7 +3,6 @@ package org.javacs; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import com.sun.javadoc.MethodDoc; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.LineMap; import com.sun.source.util.SourcePositions; @@ -20,7 +19,6 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -297,9 +295,9 @@ public class JavaCompilerServiceTest { .get() .activeMethod .get(); - Optional<MethodDoc> doc = compiler.methodDoc(method); + var doc = compiler.methodDoc(method); assertTrue(doc.isPresent()); - assertThat(Javadocs.commentText(doc.get()).orElse("<empty>"), containsString("A great method")); + assertThat(doc.toString(), containsString("A great method")); } @Test diff --git a/src/test/java/org/javacs/JavadocsTest.java b/src/test/java/org/javacs/JavadocsTest.java deleted file mode 100644 index 90e394c..0000000 --- a/src/test/java/org/javacs/JavadocsTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.javacs; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - -import com.sun.javadoc.RootDoc; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.Collections; -import org.junit.Test; - -public class JavadocsTest { - - private final Javadocs docs = - new Javadocs( - Collections.singleton(Paths.get("src/test/test-project/workspace/src")), Collections.emptySet()); - - @Test - public void findSrcZip() { - assertTrue("Can find src.zip", Javadocs.findSrcZip().isPresent()); - } - - @Test - public void findSystemDoc() throws IOException { - RootDoc root = docs.index("java.util.ArrayList"); - - assertThat(root.classes(), not(emptyArray())); - } - - /* - @Test - public void findMethodDoc() { - assertTrue( - "Found method", - docs.methodDoc( - "org.javacs.docs.TrickyDocstring#example(java.lang.String,java.lang.String[],java.util.List)") - .isPresent()); - } - - @Test - public void findParameterizedDoc() { - assertTrue( - "Found method", - docs.methodDoc("org.javacs.docs.TrickyDocstring#parameterized(java.lang.Object)").isPresent()); - } - - @Test - @Ignore // Blocked by emptyFileManager - public void findInheritedDoc() { - Optional<MethodDoc> found = docs.methodDoc("org.javacs.docs.SubDoc#method()"); - - assertTrue("Found method", found.isPresent()); - - Optional<String> docstring = found.flatMap(Javadocs::commentText); - - assertTrue("Has inherited doc", docstring.isPresent()); - assertThat("Inherited doc is not empty", docstring.get(), not(isEmptyOrNullString())); - } - - @Test - @Ignore // Doesn't work yet - public void findInterfaceDoc() { - Optional<MethodDoc> found = docs.methodDoc("org.javacs.docs.SubDoc#interfaceMethod()"); - - assertTrue("Found method", found.isPresent()); - - Optional<String> docstring = found.flatMap(Javadocs::commentText); - - assertTrue("Has inherited doc", docstring.isPresent()); - assertThat("Inherited doc is not empty", docstring.get(), not(isEmptyOrNullString())); - } - */ -} diff --git a/src/test/resources/LocalMethodDoc.java b/src/test/resources/LocalMethodDoc.java index 789f17c..701fa16 100644 --- a/src/test/resources/LocalMethodDoc.java +++ b/src/test/resources/LocalMethodDoc.java @@ -3,7 +3,10 @@ class LocalMethodDoc { targetMethod(); } - /** A great method */ - void targetMethod() { + /** + * A great method + * @param param A great param + */ + void targetMethod(int param) { } }
\ No newline at end of file |