/* * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.internal.shellsupport.doc; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.Stack; import java.util.TreeMap; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.util.ElementFilter; import javax.tools.JavaCompiler; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.StandardLocation; import javax.tools.ToolProvider; import com.sun.source.doctree.DocCommentTree; import com.sun.source.doctree.DocTree; import com.sun.source.doctree.InheritDocTree; import com.sun.source.doctree.ParamTree; import com.sun.source.doctree.ReturnTree; import com.sun.source.doctree.ThrowsTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.DocTreePath; import com.sun.source.util.DocTreeScanner; import com.sun.source.util.DocTrees; import com.sun.source.util.JavacTask; import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; import com.sun.source.util.Trees; import com.sun.tools.javac.api.JavacTaskImpl; import com.sun.tools.javac.util.DefinedBy; import com.sun.tools.javac.util.DefinedBy.Api; import com.sun.tools.javac.util.Pair; /**Helper to find javadoc and resolve @inheritDoc. */ public abstract class JavadocHelper implements AutoCloseable { private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); /**Create the helper. * * @param mainTask JavacTask from which the further Elements originate * @param sourceLocations paths where source files should be searched * @return a JavadocHelper */ public static JavadocHelper create(JavacTask mainTask, Collection<? extends Path> sourceLocations) { StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null); try { fm.setLocationFromPaths(StandardLocation.SOURCE_PATH, sourceLocations); return new OnDemandJavadocHelper(mainTask, fm); } catch (IOException ex) { try { fm.close(); } catch (IOException closeEx) { } return new JavadocHelper() { @Override public String getResolvedDocComment(Element forElement) throws IOException { return null; } @Override public Element getSourceElement(Element forElement) throws IOException { return forElement; } @Override public void close() throws IOException {} }; } } /**Returns javadoc for the given element, if it can be found, or null otherwise. The javadoc * will have @inheritDoc resolved. * * @param forElement element for which the javadoc should be searched * @return javadoc if found, null otherwise * @throws IOException if something goes wrong in the search */ public abstract String getResolvedDocComment(Element forElement) throws IOException; /**Returns an element representing the same given program element, but the returned element will * be resolved from source, if it can be found. Returns the original element if the source for * the given element cannot be found. * * @param forElement element for which the source element should be searched * @return source element if found, the original element otherwise * @throws IOException if something goes wrong in the search */ public abstract Element getSourceElement(Element forElement) throws IOException; /**Closes the helper. * * @throws IOException if something foes wrong during the close */ @Override public abstract void close() throws IOException; private static final class OnDemandJavadocHelper extends JavadocHelper { private final JavacTask mainTask; private final JavaFileManager baseFileManager; private final StandardJavaFileManager fm; private final Map<String, Pair<JavacTask, TreePath>> signature2Source = new HashMap<>(); private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm) { this.mainTask = mainTask; this.baseFileManager = ((JavacTaskImpl) mainTask).getContext().get(JavaFileManager.class); this.fm = fm; } @Override public String getResolvedDocComment(Element forElement) throws IOException { Pair<JavacTask, TreePath> sourceElement = getSourceElement(mainTask, forElement); if (sourceElement == null) return null; return getResolvedDocComment(sourceElement.fst, sourceElement.snd); } @Override public Element getSourceElement(Element forElement) throws IOException { Pair<JavacTask, TreePath> sourceElement = getSourceElement(mainTask, forElement); if (sourceElement == null) return forElement; Element result = Trees.instance(sourceElement.fst).getElement(sourceElement.snd); if (result == null) return forElement; return result; } private String getResolvedDocComment(JavacTask task, TreePath el) throws IOException { DocTrees trees = DocTrees.instance(task); Element element = trees.getElement(el); String docComment = trees.getDocComment(el); if (docComment == null && element.getKind() == ElementKind.METHOD) { ExecutableElement executableElement = (ExecutableElement) element; Iterable<Element> superTypes = () -> superTypeForInheritDoc(task, element.getEnclosingElement()).iterator(); for (Element sup : superTypes) { for (ExecutableElement supMethod : ElementFilter.methodsIn(sup.getEnclosedElements())) { TypeElement clazz = (TypeElement) executableElement.getEnclosingElement(); if (task.getElements().overrides(executableElement, supMethod, clazz)) { Pair<JavacTask, TreePath> source = getSourceElement(task, supMethod); if (source != null) { String overriddenComment = getResolvedDocComment(source.fst, source.snd); if (overriddenComment != null) { return overriddenComment; } } } } } } DocCommentTree docCommentTree = parseDocComment(task, docComment); IOException[] exception = new IOException[1]; Map<int[], String> replace = new TreeMap<>((span1, span2) -> span2[0] - span1[0]); new DocTreeScanner<Void, Void>() { private Stack<DocTree> interestingParent = new Stack<>(); private DocCommentTree dcTree; private JavacTask inheritedJavacTask; private TreePath inheritedTreePath; private String inherited; private Map<DocTree, String> syntheticTrees = new IdentityHashMap<>(); private long lastPos = 0; @Override @DefinedBy(Api.COMPILER_TREE) public Void visitDocComment(DocCommentTree node, Void p) { dcTree = node; interestingParent.push(node); try { scan(node.getFirstSentence(), p); scan(node.getBody(), p); List<DocTree> augmentedBlockTags = new ArrayList<>(node.getBlockTags()); if (element.getKind() == ElementKind.METHOD) { ExecutableElement executableElement = (ExecutableElement) element; List<String> parameters = executableElement.getParameters() .stream() .map(param -> param.getSimpleName().toString()) .collect(Collectors.toList()); List<String> throwsList = executableElement.getThrownTypes() .stream() .map(exc -> exc.toString()) .collect(Collectors.toList()); Set<String> missingParams = new HashSet<>(parameters); Set<String> missingThrows = new HashSet<>(throwsList); boolean hasReturn = false; for (DocTree dt : augmentedBlockTags) { switch (dt.getKind()) { case PARAM: missingParams.remove(((ParamTree) dt).getName().getName().toString()); break; case THROWS: missingThrows.remove(getThrownException(task, el, docCommentTree, (ThrowsTree) dt)); break; case RETURN: hasReturn = true; break; } } for (String missingParam : missingParams) { DocTree syntheticTag = parseBlockTag(task, "@param " + missingParam + " {@inheritDoc}"); syntheticTrees.put(syntheticTag, "@param " + missingParam + " "); insertTag(augmentedBlockTags, syntheticTag, parameters, throwsList); } for (String missingThrow : missingThrows) { DocTree syntheticTag = parseBlockTag(task, "@throws " + missingThrow + " {@inheritDoc}"); syntheticTrees.put(syntheticTag, "@throws " + missingThrow + " "); insertTag(augmentedBlockTags, syntheticTag, parameters, throwsList); } if (!hasReturn) { DocTree syntheticTag = parseBlockTag(task, "@return {@inheritDoc}"); syntheticTrees.put(syntheticTag, "@return "); insertTag(augmentedBlockTags, syntheticTag, parameters, throwsList); } } scan(augmentedBlockTags, p); return null; } finally { interestingParent.pop(); } } @Override @DefinedBy(Api.COMPILER_TREE) public Void visitParam(ParamTree node, Void p) { interestingParent.push(node); try { return super.visitParam(node, p); } finally { interestingParent.pop(); } } @Override @DefinedBy(Api.COMPILER_TREE) public Void visitThrows(ThrowsTree node, Void p) { interestingParent.push(node); try { return super.visitThrows(node, p); } finally { interestingParent.pop(); } } @Override @DefinedBy(Api.COMPILER_TREE) public Void visitReturn(ReturnTree node, Void p) { interestingParent.push(node); try { return super.visitReturn(node, p); } finally { interestingParent.pop(); } } @Override @DefinedBy(Api.COMPILER_TREE) public Void visitInheritDoc(InheritDocTree node, Void p) { if (inherited == null) { try { if (element.getKind() == ElementKind.METHOD) { ExecutableElement executableElement = (ExecutableElement) element; Iterable<Element> superTypes = () -> superTypeForInheritDoc(task, element.getEnclosingElement()).iterator(); OUTER: for (Element sup : superTypes) { for (ExecutableElement supMethod : ElementFilter.methodsIn(sup.getEnclosedElements())) { if (task.getElements().overrides(executableElement, supMethod, (TypeElement) executableElement.getEnclosingElement())) { Pair<JavacTask, TreePath> source = getSourceElement(task, supMethod); if (source != null) { String overriddenComment = getResolvedDocComment(source.fst, source.snd); if (overriddenComment != null) { inheritedJavacTask = source.fst; inheritedTreePath = source.snd; inherited = overriddenComment; break OUTER; } } } } } } } catch (IOException ex) { exception[0] = ex; return null; } } if (inherited == null) { return null; } DocCommentTree inheritedDocTree = parseDocComment(inheritedJavacTask, inherited); List<List<? extends DocTree>> inheritedText = new ArrayList<>(); DocTree parent = interestingParent.peek(); switch (parent.getKind()) { case DOC_COMMENT: inheritedText.add(inheritedDocTree.getFullBody()); break; case PARAM: String paramName = ((ParamTree) parent).getName().getName().toString(); new DocTreeScanner<Void, Void>() { @Override @DefinedBy(Api.COMPILER_TREE) public Void visitParam(ParamTree node, Void p) { if (node.getName().getName().contentEquals(paramName)) { inheritedText.add(node.getDescription()); } return super.visitParam(node, p); } }.scan(inheritedDocTree, null); break; case THROWS: String thrownName = getThrownException(task, el, docCommentTree, (ThrowsTree) parent); new DocTreeScanner<Void, Void>() { @Override @DefinedBy(Api.COMPILER_TREE) public Void visitThrows(ThrowsTree node, Void p) { if (Objects.equals(getThrownException(inheritedJavacTask, inheritedTreePath, inheritedDocTree, node), thrownName)) { inheritedText.add(node.getDescription()); } return super.visitThrows(node, p); } }.scan(inheritedDocTree, null); break; case RETURN: new DocTreeScanner<Void, Void>() { @Override @DefinedBy(Api.COMPILER_TREE) public Void visitReturn(ReturnTree node, Void p) { inheritedText.add(node.getDescription()); return super.visitReturn(node, p); } }.scan(inheritedDocTree, null); break; } if (!inheritedText.isEmpty()) { long offset = trees.getSourcePositions().getStartPosition(null, inheritedDocTree, inheritedDocTree); long start = Long.MAX_VALUE; long end = Long.MIN_VALUE; for (DocTree t : inheritedText.get(0)) { start = Math.min(start, trees.getSourcePositions().getStartPosition(null, inheritedDocTree, t) - offset); end = Math.max(end, trees.getSourcePositions().getEndPosition(null, inheritedDocTree, t) - offset); } String text = inherited.substring((int) start, (int) end); if (syntheticTrees.containsKey(parent)) { replace.put(new int[] {(int) lastPos + 1, (int) lastPos}, "\n" + syntheticTrees.get(parent) + text); } else { long inheritedStart = trees.getSourcePositions().getStartPosition(null, dcTree, node); long inheritedEnd = trees.getSourcePositions().getEndPosition(null, dcTree, node); replace.put(new int[] {(int) inheritedStart, (int) inheritedEnd}, text); } } return super.visitInheritDoc(node, p); } private boolean inSynthetic; @Override @DefinedBy(Api.COMPILER_TREE) public Void scan(DocTree tree, Void p) { if (exception[0] != null) { return null; } boolean prevInSynthetic = inSynthetic; try { inSynthetic |= syntheticTrees.containsKey(tree); return super.scan(tree, p); } finally { if (!inSynthetic) { lastPos = trees.getSourcePositions().getEndPosition(null, dcTree, tree); } inSynthetic = prevInSynthetic; } } private void insertTag(List<DocTree> tags, DocTree toInsert, List<String> parameters, List<String> throwsTypes) { Comparator<DocTree> comp = (tag1, tag2) -> { if (tag1.getKind() == tag2.getKind()) { switch (toInsert.getKind()) { case PARAM: { ParamTree p1 = (ParamTree) tag1; ParamTree p2 = (ParamTree) tag2; int i1 = parameters.indexOf(p1.getName().getName().toString()); int i2 = parameters.indexOf(p2.getName().getName().toString()); return i1 - i2; } case THROWS: { ThrowsTree t1 = (ThrowsTree) tag1; ThrowsTree t2 = (ThrowsTree) tag2; int i1 = throwsTypes.indexOf(getThrownException(task, el, docCommentTree, t1)); int i2 = throwsTypes.indexOf(getThrownException(task, el, docCommentTree, t2)); return i1 - i2; } } } int i1 = tagOrder.indexOf(tag1.getKind()); int i2 = tagOrder.indexOf(tag2.getKind()); return i1 - i2; }; for (int i = 0; i < tags.size(); i++) { if (comp.compare(tags.get(i), toInsert) >= 0) { tags.add(i, toInsert); return ; } } tags.add(toInsert); } private final List<DocTree.Kind> tagOrder = Arrays.asList(DocTree.Kind.PARAM, DocTree.Kind.THROWS, DocTree.Kind.RETURN); }.scan(docCommentTree, null); if (replace.isEmpty()) return docComment; StringBuilder replacedInheritDoc = new StringBuilder(docComment); int offset = (int) trees.getSourcePositions().getStartPosition(null, docCommentTree, docCommentTree); for (Entry<int[], String> e : replace.entrySet()) { replacedInheritDoc.delete(e.getKey()[0] - offset, e.getKey()[1] - offset + 1); replacedInheritDoc.insert(e.getKey()[0] - offset, e.getValue()); } return replacedInheritDoc.toString(); } private Stream<Element> superTypeForInheritDoc(JavacTask task, Element type) { TypeElement clazz = (TypeElement) type; Stream<Element> result = interfaces(clazz); result = Stream.concat(result, interfaces(clazz).flatMap(el -> superTypeForInheritDoc(task, el))); if (clazz.getSuperclass().getKind() == TypeKind.DECLARED) { Element superClass = ((DeclaredType) clazz.getSuperclass()).asElement(); result = Stream.concat(result, Stream.of(superClass)); result = Stream.concat(result, superTypeForInheritDoc(task, superClass)); } return result; } //where: private Stream<Element> interfaces(TypeElement clazz) { return clazz.getInterfaces() .stream() .filter(tm -> tm.getKind() == TypeKind.DECLARED) .map(tm -> ((DeclaredType) tm).asElement()); } private DocTree parseBlockTag(JavacTask task, String blockTag) { DocCommentTree dc = parseDocComment(task, blockTag); return dc.getBlockTags().get(0); } private DocCommentTree parseDocComment(JavacTask task, String javadoc) { DocTrees trees = DocTrees.instance(task); try { return trees.getDocCommentTree(new SimpleJavaFileObject(new URI("mem://doc.html"), javax.tools.JavaFileObject.Kind.HTML) { @Override @DefinedBy(Api.COMPILER) public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return "<body>" + javadoc + "</body>"; } }); } catch (URISyntaxException ex) { return null; } } private String getThrownException(JavacTask task, TreePath rootOn, DocCommentTree comment, ThrowsTree tt) { DocTrees trees = DocTrees.instance(task); Element exc = trees.getElement(new DocTreePath(new DocTreePath(rootOn, comment), tt.getExceptionName())); return exc != null ? exc.toString() : null; } private Pair<JavacTask, TreePath> getSourceElement(JavacTask origin, Element el) throws IOException { String handle = elementSignature(el); Pair<JavacTask, TreePath> cached = signature2Source.get(handle); if (cached != null) { return cached.fst != null ? cached : null; } TypeElement type = topLevelType(el); if (type == null) return null; String binaryName = origin.getElements().getBinaryName(type).toString(); Pair<JavacTask, CompilationUnitTree> source = findSource(binaryName); if (source == null) return null; fillElementCache(source.fst, source.snd); cached = signature2Source.get(handle); if (cached != null) { return cached; } else { signature2Source.put(handle, Pair.of(null, null)); return null; } } //where: private String elementSignature(Element el) { switch (el.getKind()) { case ANNOTATION_TYPE: case CLASS: case ENUM: case INTERFACE: return ((TypeElement) el).getQualifiedName().toString(); case FIELD: return elementSignature(el.getEnclosingElement()) + "." + el.getSimpleName() + ":" + el.asType(); case ENUM_CONSTANT: return elementSignature(el.getEnclosingElement()) + "." + el.getSimpleName(); case EXCEPTION_PARAMETER: case LOCAL_VARIABLE: case PARAMETER: case RESOURCE_VARIABLE: return el.getSimpleName() + ":" + el.asType(); case CONSTRUCTOR: case METHOD: StringBuilder header = new StringBuilder(); header.append(elementSignature(el.getEnclosingElement())); if (el.getKind() == ElementKind.METHOD) { header.append("."); header.append(el.getSimpleName()); } header.append("("); String sep = ""; ExecutableElement method = (ExecutableElement) el; for (Iterator<? extends VariableElement> i = method.getParameters().iterator(); i.hasNext();) { VariableElement p = i.next(); header.append(sep); header.append(p.asType()); sep = ", "; } header.append(")"); return header.toString(); default: return el.toString(); } } private TypeElement topLevelType(Element el) { if (el.getKind() == ElementKind.PACKAGE) return null; while (el != null && el.getEnclosingElement().getKind() != ElementKind.PACKAGE) { el = el.getEnclosingElement(); } return el != null && (el.getKind().isClass() || el.getKind().isInterface()) ? (TypeElement) el : null; } private void fillElementCache(JavacTask task, CompilationUnitTree cut) throws IOException { Trees trees = Trees.instance(task); new TreePathScanner<Void, Void>() { @Override @DefinedBy(Api.COMPILER_TREE) public Void visitMethod(MethodTree node, Void p) { handleDeclaration(); return null; } @Override @DefinedBy(Api.COMPILER_TREE) public Void visitClass(ClassTree node, Void p) { handleDeclaration(); return super.visitClass(node, p); } @Override @DefinedBy(Api.COMPILER_TREE) public Void visitVariable(VariableTree node, Void p) { handleDeclaration(); return super.visitVariable(node, p); } private void handleDeclaration() { Element currentElement = trees.getElement(getCurrentPath()); if (currentElement != null) { signature2Source.put(elementSignature(currentElement), Pair.of(task, getCurrentPath())); } } }.scan(cut, null); } private Pair<JavacTask, CompilationUnitTree> findSource(String binaryName) throws IOException { JavaFileObject jfo = fm.getJavaFileForInput(StandardLocation.SOURCE_PATH, binaryName, JavaFileObject.Kind.SOURCE); if (jfo == null) return null; List<JavaFileObject> jfos = Arrays.asList(jfo); JavacTaskImpl task = (JavacTaskImpl) compiler.getTask(null, baseFileManager, d -> {}, null, null, jfos); Iterable<? extends CompilationUnitTree> cuts = task.parse(); task.enter(); return Pair.of(task, cuts.iterator().next()); } @Override public void close() throws IOException { fm.close(); } } }