diff --git a/langtools/src/jdk.compiler/share/classes/com/sun/tools/doclint/Entity.java b/langtools/src/jdk.compiler/share/classes/com/sun/tools/doclint/Entity.java index 4b5cf75d5dd..970cb399d53 100644 --- a/langtools/src/jdk.compiler/share/classes/com/sun/tools/doclint/Entity.java +++ b/langtools/src/jdk.compiler/share/classes/com/sun/tools/doclint/Entity.java @@ -297,17 +297,21 @@ public enum Entity { rsaquo(8250), euro(8364); - int code; + public final int code; private Entity(int code) { this.code = code; } - static boolean isValid(String name) { + public static boolean isValid(String name) { return names.containsKey(name); } - static boolean isValid(int code) { + public static Entity get(String name) { + return names.get(name); + } + + public static boolean isValid(int code) { // allow numeric codes for standard ANSI characters return codes.containsKey(code) || ( 32 <= code && code < 2127); } diff --git a/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocFormatter.java b/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocFormatter.java new file mode 100644 index 00000000000..ba42953d20d --- /dev/null +++ b/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocFormatter.java @@ -0,0 +1,706 @@ +/* + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Stack; + +import javax.lang.model.element.Name; +import javax.tools.JavaFileObject.Kind; +import javax.tools.SimpleJavaFileObject; +import javax.tools.ToolProvider; + +import com.sun.source.doctree.AttributeTree; +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.EndElementTree; +import com.sun.source.doctree.EntityTree; +import com.sun.source.doctree.InlineTagTree; +import com.sun.source.doctree.LinkTree; +import com.sun.source.doctree.LiteralTree; +import com.sun.source.doctree.ParamTree; +import com.sun.source.doctree.ReturnTree; +import com.sun.source.doctree.StartElementTree; +import com.sun.source.doctree.TextTree; +import com.sun.source.doctree.ThrowsTree; +import com.sun.source.util.DocTreeScanner; +import com.sun.source.util.DocTrees; +import com.sun.source.util.JavacTask; +import com.sun.tools.doclint.Entity; +import com.sun.tools.doclint.HtmlTag; +import com.sun.tools.javac.util.DefinedBy; +import com.sun.tools.javac.util.DefinedBy.Api; +import com.sun.tools.javac.util.StringUtils; + +/**A javadoc to plain text formatter. + * + */ +public class JavadocFormatter { + + private static final String CODE_RESET = "\033[0m"; + private static final String CODE_HIGHLIGHT = "\033[1m"; + private static final String CODE_UNDERLINE = "\033[4m"; + + private final int lineLimit; + private final boolean escapeSequencesSupported; + + /** Construct the formatter. + * + * @param lineLimit maximum line length + * @param escapeSequencesSupported whether escape sequences are supported + */ + public JavadocFormatter(int lineLimit, boolean escapeSequencesSupported) { + this.lineLimit = lineLimit; + this.escapeSequencesSupported = escapeSequencesSupported; + } + + private static final int MAX_LINE_LENGTH = 95; + private static final int SHORTEST_LINE = 30; + private static final int INDENT = 4; + + /**Format javadoc to plain text. + * + * @param header element caption that should be used + * @param javadoc to format + * @return javadoc formatted to plain text + */ + public String formatJavadoc(String header, String javadoc) { + try { + StringBuilder result = new StringBuilder(); + + result.append(escape(CODE_HIGHLIGHT)).append(header).append(escape(CODE_RESET)).append("\n"); + + if (javadoc == null) { + return result.toString(); + } + + JavacTask task = (JavacTask) ToolProvider.getSystemJavaCompiler().getTask(null, null, null, null, null, null); + DocTrees trees = DocTrees.instance(task); + DocCommentTree docComment = trees.getDocCommentTree(new SimpleJavaFileObject(new URI("mem://doc.html"), Kind.HTML) { + @Override @DefinedBy(Api.COMPILER) + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return "" + javadoc + ""; + } + }); + + new FormatJavadocScanner(result, task).scan(docComment, null); + + addNewLineIfNeeded(result); + + return result.toString(); + } catch (URISyntaxException ex) { + throw new InternalError("Unexpected exception", ex); + } + } + + private class FormatJavadocScanner extends DocTreeScanner { + private final StringBuilder result; + private final JavacTask task; + private int reflownTo; + private int indent; + private int limit = Math.min(lineLimit, MAX_LINE_LENGTH); + private boolean pre; + private Map tableColumns; + + public FormatJavadocScanner(StringBuilder result, JavacTask task) { + this.result = result; + this.task = task; + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object visitDocComment(DocCommentTree node, Object p) { + tableColumns = countTableColumns(node); + reflownTo = result.length(); + scan(node.getFirstSentence(), p); + scan(node.getBody(), p); + reflow(result, reflownTo, indent, limit); + for (Sections current : docSections.keySet()) { + boolean seenAny = false; + for (DocTree t : node.getBlockTags()) { + if (current.matches(t)) { + if (!seenAny) { + seenAny = true; + if (result.charAt(result.length() - 1) != '\n') + result.append("\n"); + result.append("\n"); + result.append(escape(CODE_UNDERLINE)) + .append(docSections.get(current)) + .append(escape(CODE_RESET)) + .append("\n"); + } + + scan(t, null); + } + } + } + return null; + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object visitText(TextTree node, Object p) { + String text = node.getBody(); + if (!pre) { + text = text.replaceAll("[ \t\r\n]+", " ").trim(); + if (text.isEmpty()) { + text = " "; + } + } else { + text = text.replaceAll("\n", "\n" + indentString(indent)); + } + result.append(text); + return null; + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object visitLink(LinkTree node, Object p) { + if (!node.getLabel().isEmpty()) { + scan(node.getLabel(), p); + } else { + result.append(node.getReference().getSignature()); + } + return null; + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object visitParam(ParamTree node, Object p) { + return formatDef(node.getName().getName(), node.getDescription()); + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object visitThrows(ThrowsTree node, Object p) { + return formatDef(node.getExceptionName().getSignature(), node.getDescription()); + } + + public Object formatDef(CharSequence name, List description) { + result.append(name); + result.append(" - "); + reflownTo = result.length(); + indent = name.length() + 3; + + if (limit - indent < SHORTEST_LINE) { + result.append("\n"); + result.append(indentString(INDENT)); + indent = INDENT; + reflownTo += INDENT; + } + try { + return scan(description, null); + } finally { + reflow(result, reflownTo, indent, limit); + result.append("\n"); + } + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object visitLiteral(LiteralTree node, Object p) { + return scan(node.getBody(), p); + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object visitReturn(ReturnTree node, Object p) { + reflownTo = result.length(); + try { + return super.visitReturn(node, p); + } finally { + reflow(result, reflownTo, 0, limit); + } + } + + Stack listStack = new Stack<>(); + Stack defStack = new Stack<>(); + Stack tableStack = new Stack<>(); + Stack> cellsStack = new Stack<>(); + Stack> headerStack = new Stack<>(); + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object visitStartElement(StartElementTree node, Object p) { + switch (HtmlTag.get(node.getName())) { + case P: + if (lastNode!= null && lastNode.getKind() == DocTree.Kind.START_ELEMENT && + HtmlTag.get(((StartElementTree) lastNode).getName()) == HtmlTag.LI) { + //ignore + break; + } + reflowTillNow(); + addNewLineIfNeeded(result); + result.append(indentString(indent)); + reflownTo = result.length(); + break; + case BLOCKQUOTE: + reflowTillNow(); + indent += INDENT; + break; + case PRE: + reflowTillNow(); + pre = true; + break; + case UL: + reflowTillNow(); + listStack.push(-1); + indent += INDENT; + break; + case OL: + reflowTillNow(); + listStack.push(1); + indent += INDENT; + break; + case DL: + reflowTillNow(); + defStack.push(indent); + break; + case LI: + reflowTillNow(); + if (!listStack.empty()) { + addNewLineIfNeeded(result); + + int top = listStack.pop(); + + if (top == (-1)) { + result.append(indentString(indent - 2)); + result.append("* "); + } else { + result.append(indentString(indent - 3)); + result.append("" + top++ + ". "); + } + + listStack.push(top); + + reflownTo = result.length(); + } + break; + case DT: + reflowTillNow(); + if (!defStack.isEmpty()) { + addNewLineIfNeeded(result); + indent = defStack.peek(); + result.append(escape(CODE_HIGHLIGHT)); + } + break; + case DD: + reflowTillNow(); + if (!defStack.isEmpty()) { + if (indent == defStack.peek()) { + result.append(escape(CODE_RESET)); + } + addNewLineIfNeeded(result); + indent = defStack.peek() + INDENT; + result.append(indentString(indent)); + } + break; + case H1: case H2: case H3: + case H4: case H5: case H6: + reflowTillNow(); + addNewLineIfNeeded(result); + result.append("\n") + .append(escape(CODE_UNDERLINE)); + reflownTo = result.length(); + break; + case TABLE: + int columns = tableColumns.get(node); + + if (columns == 0) { + break; //broken input + } + + reflowTillNow(); + addNewLineIfNeeded(result); + reflownTo = result.length(); + + tableStack.push(limit); + + limit = (limit - 1) / columns - 3; + + for (int sep = 0; sep < (limit + 3) * columns + 1; sep++) { + result.append("-"); + } + + result.append("\n"); + + break; + case TR: + if (cellsStack.size() >= tableStack.size()) { + //unclosed : + handleEndElement(node.getName()); + } + cellsStack.push(new ArrayList<>()); + headerStack.push(new ArrayList<>()); + break; + case TH: + case TD: + if (cellsStack.isEmpty()) { + //broken code + break; + } + reflowTillNow(); + result.append("\n"); + reflownTo = result.length(); + cellsStack.peek().add(result.length()); + headerStack.peek().add(HtmlTag.get(node.getName()) == HtmlTag.TH); + break; + case IMG: + for (DocTree attr : node.getAttributes()) { + if (attr.getKind() != DocTree.Kind.ATTRIBUTE) { + continue; + } + AttributeTree at = (AttributeTree) attr; + if ("alt".equals(StringUtils.toLowerCase(at.getName().toString()))) { + addSpaceIfNeeded(result); + scan(at.getValue(), null); + addSpaceIfNeeded(result); + break; + } + } + break; + default: + addSpaceIfNeeded(result); + break; + } + return null; + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object visitEndElement(EndElementTree node, Object p) { + handleEndElement(node.getName()); + return super.visitEndElement(node, p); + } + + private void handleEndElement(Name name) { + switch (HtmlTag.get(name)) { + case BLOCKQUOTE: + indent -= INDENT; + break; + case PRE: + pre = false; + addNewLineIfNeeded(result); + reflownTo = result.length(); + break; + case UL: case OL: + if (listStack.isEmpty()) { //ignore stray closing tag + break; + } + reflowTillNow(); + listStack.pop(); + indent -= INDENT; + addNewLineIfNeeded(result); + break; + case DL: + if (defStack.isEmpty()) {//ignore stray closing tag + break; + } + reflowTillNow(); + if (indent == defStack.peek()) { + result.append(escape(CODE_RESET)); + } + indent = defStack.pop(); + addNewLineIfNeeded(result); + break; + case H1: case H2: case H3: + case H4: case H5: case H6: + reflowTillNow(); + result.append(escape(CODE_RESET)) + .append("\n"); + reflownTo = result.length(); + break; + case TABLE: + if (cellsStack.size() >= tableStack.size()) { + //unclosed : + handleEndElement(task.getElements().getName("tr")); + } + + if (tableStack.isEmpty()) { + break; + } + + limit = tableStack.pop(); + break; + case TR: + if (cellsStack.isEmpty()) { + break; + } + + reflowTillNow(); + + List cells = cellsStack.pop(); + List headerFlags = headerStack.pop(); + List content = new ArrayList<>(); + int maxLines = 0; + + result.append("\n"); + + while (!cells.isEmpty()) { + int currentCell = cells.remove(cells.size() - 1); + String[] lines = result.substring(currentCell, result.length()).split("\n"); + + result.delete(currentCell - 1, result.length()); + + content.add(lines); + maxLines = Math.max(maxLines, lines.length); + } + + Collections.reverse(content); + + for (int line = 0; line < maxLines; line++) { + for (int column = 0; column < content.size(); column++) { + String[] lines = content.get(column); + String currentLine = line < lines.length ? lines[line] : ""; + result.append("| "); + boolean header = headerFlags.get(column); + if (header) { + result.append(escape(CODE_HIGHLIGHT)); + } + result.append(currentLine); + if (header) { + result.append(escape(CODE_RESET)); + } + int padding = limit - currentLine.length(); + if (padding > 0) + result.append(indentString(padding)); + result.append(" "); + } + result.append("|\n"); + } + + for (int sep = 0; sep < (limit + 3) * content.size() + 1; sep++) { + result.append("-"); + } + + result.append("\n"); + + reflownTo = result.length(); + break; + case TD: + case TH: + break; + default: + addSpaceIfNeeded(result); + break; + } + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object visitEntity(EntityTree node, Object p) { + String name = node.getName().toString(); + int code = -1; + if (name.startsWith("#")) { + try { + int v = StringUtils.toLowerCase(name).startsWith("#x") + ? Integer.parseInt(name.substring(2), 16) + : Integer.parseInt(name.substring(1), 10); + if (Entity.isValid(v)) { + code = v; + } + } catch (NumberFormatException ex) { + //ignore + } + } else { + Entity entity = Entity.get(name); + if (entity != null) { + code = entity.code; + } + } + if (code != (-1)) { + result.appendCodePoint(code); + } else { + result.append(node.toString()); + } + return super.visitEntity(node, p); + } + + private DocTree lastNode; + + @Override @DefinedBy(Api.COMPILER_TREE) + public Object scan(DocTree node, Object p) { + if (node instanceof InlineTagTree) { + addSpaceIfNeeded(result); + } + try { + return super.scan(node, p); + } finally { + if (node instanceof InlineTagTree) { + addSpaceIfNeeded(result); + } + lastNode = node; + } + } + + private void reflowTillNow() { + while (result.length() > 0 && result.charAt(result.length() - 1) == ' ') + result.delete(result.length() - 1, result.length()); + reflow(result, reflownTo, indent, limit); + reflownTo = result.length(); + } + }; + + private String escape(String sequence) { + return this.escapeSequencesSupported ? sequence : ""; + } + + private static final Map docSections = new LinkedHashMap<>(); + + static { + ResourceBundle bundle = + ResourceBundle.getBundle("jdk.internal.shellsupport.doc.resources.javadocformatter"); + docSections.put(Sections.TYPE_PARAMS, bundle.getString("CAP_TypeParameters")); + docSections.put(Sections.PARAMS, bundle.getString("CAP_Parameters")); + docSections.put(Sections.RETURNS, bundle.getString("CAP_Returns")); + docSections.put(Sections.THROWS, bundle.getString("CAP_Thrown_Exceptions")); + } + + private static String indentString(int indent) { + char[] content = new char[indent]; + Arrays.fill(content, ' '); + return new String(content); + } + + private static void reflow(StringBuilder text, int from, int indent, int limit) { + int lineStart = from; + + while (lineStart > 0 && text.charAt(lineStart - 1) != '\n') { + lineStart--; + } + + int lineChars = from - lineStart; + int pointer = from; + int lastSpace = -1; + + while (pointer < text.length()) { + if (text.charAt(pointer) == ' ') + lastSpace = pointer; + if (lineChars >= limit) { + if (lastSpace != (-1)) { + text.setCharAt(lastSpace, '\n'); + text.insert(lastSpace + 1, indentString(indent)); + lineChars = indent + pointer - lastSpace - 1; + pointer += indent; + lastSpace = -1; + } + } + lineChars++; + pointer++; + } + } + + private static void addNewLineIfNeeded(StringBuilder text) { + if (text.length() > 0 && text.charAt(text.length() - 1) != '\n') { + text.append("\n"); + } + } + + private static void addSpaceIfNeeded(StringBuilder text) { + if (text.length() == 0) + return ; + + char last = text.charAt(text.length() - 1); + + if (last != ' ' && last != '\n') { + text.append(" "); + } + } + + private static Map countTableColumns(DocCommentTree dct) { + Map result = new IdentityHashMap<>(); + + new DocTreeScanner() { + private StartElementTree currentTable; + private int currentMaxColumns; + private int currentRowColumns; + + @Override @DefinedBy(Api.COMPILER_TREE) + public Void visitStartElement(StartElementTree node, Void p) { + switch (HtmlTag.get(node.getName())) { + case TABLE: currentTable = node; break; + case TR: + currentMaxColumns = Math.max(currentMaxColumns, currentRowColumns); + currentRowColumns = 0; + break; + case TD: + case TH: currentRowColumns++; break; + } + return super.visitStartElement(node, p); + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Void visitEndElement(EndElementTree node, Void p) { + if (HtmlTag.get(node.getName()) == HtmlTag.TABLE) { + closeTable(); + } + return super.visitEndElement(node, p); + } + + @Override @DefinedBy(Api.COMPILER_TREE) + public Void visitDocComment(DocCommentTree node, Void p) { + try { + return super.visitDocComment(node, p); + } finally { + closeTable(); + } + } + + private void closeTable() { + if (currentTable != null) { + result.put(currentTable, Math.max(currentMaxColumns, currentRowColumns)); + currentTable = null; + } + } + }.scan(dct, null); + + return result; + } + + private enum Sections { + TYPE_PARAMS { + @Override public boolean matches(DocTree t) { + return t.getKind() == DocTree.Kind.PARAM && ((ParamTree) t).isTypeParameter(); + } + }, + PARAMS { + @Override public boolean matches(DocTree t) { + return t.getKind() == DocTree.Kind.PARAM && !((ParamTree) t).isTypeParameter(); + } + }, + RETURNS { + @Override public boolean matches(DocTree t) { + return t.getKind() == DocTree.Kind.RETURN; + } + }, + THROWS { + @Override public boolean matches(DocTree t) { + return t.getKind() == DocTree.Kind.THROWS; + } + }; + + public abstract boolean matches(DocTree t); + } +} diff --git a/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocHelper.java b/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocHelper.java new file mode 100644 index 00000000000..81aa169e10e --- /dev/null +++ b/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocHelper.java @@ -0,0 +1,661 @@ +/* + * 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 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> 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 sourceElement = getSourceElement(mainTask, forElement); + + if (sourceElement == null) + return null; + + return getResolvedDocComment(sourceElement.fst, sourceElement.snd); + } + + @Override + public Element getSourceElement(Element forElement) throws IOException { + Pair 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 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 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 replace = new TreeMap<>((span1, span2) -> span2[0] - span1[0]); + + new DocTreeScanner() { + private Stack interestingParent = new Stack<>(); + private DocCommentTree dcTree; + private JavacTask inheritedJavacTask; + private TreePath inheritedTreePath; + private String inherited; + private Map 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 augmentedBlockTags = new ArrayList<>(node.getBlockTags()); + if (element.getKind() == ElementKind.METHOD) { + ExecutableElement executableElement = (ExecutableElement) element; + List parameters = + executableElement.getParameters() + .stream() + .map(param -> param.getSimpleName().toString()) + .collect(Collectors.toList()); + List throwsList = + executableElement.getThrownTypes() + .stream() + .map(exc -> exc.toString()) + .collect(Collectors.toList()); + Set missingParams = new HashSet<>(parameters); + Set 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 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 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> 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() { + @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() { + @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() { + @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 tags, DocTree toInsert, List parameters, List throwsTypes) { + Comparator 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 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 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 superTypeForInheritDoc(JavacTask task, Element type) { + TypeElement clazz = (TypeElement) type; + Stream 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 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 "" + javadoc + ""; + } + }); + } 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 getSourceElement(JavacTask origin, Element el) throws IOException { + String handle = elementSignature(el); + Pair 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 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 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() { + @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 findSource(String binaryName) throws IOException { + JavaFileObject jfo = fm.getJavaFileForInput(StandardLocation.SOURCE_PATH, + binaryName, + JavaFileObject.Kind.SOURCE); + + if (jfo == null) + return null; + + List jfos = Arrays.asList(jfo); + JavacTaskImpl task = (JavacTaskImpl) compiler.getTask(null, baseFileManager, d -> {}, null, null, jfos); + Iterable cuts = task.parse(); + + task.enter(); + + return Pair.of(task, cuts.iterator().next()); + } + + @Override + public void close() throws IOException { + fm.close(); + } + } + +} diff --git a/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/resources/javadocformatter.properties b/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/resources/javadocformatter.properties new file mode 100644 index 00000000000..afaaaba6b18 --- /dev/null +++ b/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/resources/javadocformatter.properties @@ -0,0 +1,29 @@ +# +# 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. +# + +CAP_TypeParameters=Type Parameters: +CAP_Parameters=Parameters: +CAP_Returns=Returns: +CAP_Thrown_Exceptions=Thrown Exceptions: diff --git a/langtools/src/jdk.compiler/share/classes/module-info.java b/langtools/src/jdk.compiler/share/classes/module-info.java index adbd78224c0..0b66b4d2b1e 100644 --- a/langtools/src/jdk.compiler/share/classes/module-info.java +++ b/langtools/src/jdk.compiler/share/classes/module-info.java @@ -65,6 +65,9 @@ module jdk.compiler { jdk.jdeps, jdk.javadoc, jdk.jshell; + exports jdk.internal.shellsupport.doc to + jdk.jshell, + jdk.scripting.nashorn.shell; uses javax.annotation.processing.Processor; uses com.sun.source.util.Plugin; diff --git a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java index fa7fe711e8a..e53a7861f8d 100644 --- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java +++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java @@ -25,6 +25,7 @@ package jdk.internal.jshell.tool; +import jdk.jshell.SourceCodeAnalysis.Documentation; import jdk.jshell.SourceCodeAnalysis.QualifiedNames; import jdk.jshell.SourceCodeAnalysis.Suggestion; @@ -34,27 +35,30 @@ import java.io.InputStream; import java.io.InterruptedIOException; import java.io.PrintStream; import java.io.UncheckedIOException; -import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Supplier; +import java.util.function.Function; import java.util.prefs.BackingStoreException; import java.util.stream.Collectors; import java.util.stream.Stream; +import jdk.internal.shellsupport.doc.JavadocFormatter; import jdk.internal.jline.NoInterruptUnixTerminal; import jdk.internal.jline.Terminal; import jdk.internal.jline.TerminalFactory; import jdk.internal.jline.TerminalSupport; import jdk.internal.jline.WindowsTerminal; import jdk.internal.jline.console.ConsoleReader; +import jdk.internal.jline.console.CursorBuffer; import jdk.internal.jline.console.KeyMap; import jdk.internal.jline.console.UserInterruptException; import jdk.internal.jline.console.completer.Completer; @@ -259,22 +263,118 @@ class ConsoleIOContext extends IOContext { "\u001BO3P" //Alt-F1 (Linux) }; + private String lastDocumentationBuffer; + private int lastDocumentationCursor = (-1); + private void documentation(JShellTool repl) { String buffer = in.getCursorBuffer().buffer.toString(); int cursor = in.getCursorBuffer().cursor; - String doc; + boolean firstInvocation = !buffer.equals(lastDocumentationBuffer) || cursor != lastDocumentationCursor; + lastDocumentationBuffer = buffer; + lastDocumentationCursor = cursor; + List doc; + String seeMore; + Terminal term = in.getTerminal(); if (prefix.isEmpty() && buffer.trim().startsWith("/")) { - doc = repl.commandDocumentation(buffer, cursor); + doc = Arrays.asList(repl.commandDocumentation(buffer, cursor, firstInvocation)); + seeMore = "jshell.console.see.help"; } else { - doc = repl.analysis.documentation(prefix + buffer, cursor + prefix.length()); + JavadocFormatter formatter = new JavadocFormatter(term.getWidth(), + term.isAnsiSupported()); + Function convertor; + if (firstInvocation) { + convertor = d -> d.signature(); + } else { + convertor = d -> formatter.formatJavadoc(d.signature(), + d.javadoc() != null ? d.javadoc() + : repl.messageFormat("jshell.console.no.javadoc")); + } + doc = repl.analysis.documentation(prefix + buffer, cursor + prefix.length(), !firstInvocation) + .stream() + .map(convertor) + .collect(Collectors.toList()); + seeMore = "jshell.console.see.javadoc"; } try { - if (doc != null) { - in.println(); - in.println(doc); - in.redrawLine(); - in.flush(); + if (doc != null && !doc.isEmpty()) { + if (firstInvocation) { + in.println(); + in.println(doc.stream().collect(Collectors.joining("\n"))); + in.println(repl.messageFormat(seeMore)); + in.redrawLine(); + in.flush(); + } else { + in.println(); + + int height = term.getHeight(); + String lastNote = ""; + + PRINT_DOC: for (Iterator docIt = doc.iterator(); docIt.hasNext(); ) { + String currentDoc = docIt.next(); + String[] lines = currentDoc.split("\n"); + int firstLine = 0; + + PRINT_PAGE: while (true) { + int toPrint = height - 1; + + while (toPrint > 0 && firstLine < lines.length) { + in.println(lines[firstLine++]); + toPrint--; + } + + if (firstLine >= lines.length) { + break; + } + + lastNote = repl.getResourceString("jshell.console.see.next.page"); + in.print(lastNote + ConsoleReader.RESET_LINE); + in.flush(); + + while (true) { + int r = in.readCharacter(); + + switch (r) { + case ' ': continue PRINT_PAGE; + case 'q': + case 3: + break PRINT_DOC; + default: + in.beep(); + break; + } + } + } + + if (docIt.hasNext()) { + lastNote = repl.getResourceString("jshell.console.see.next.javadoc"); + in.print(lastNote + ConsoleReader.RESET_LINE); + in.flush(); + + while (true) { + int r = in.readCharacter(); + + switch (r) { + case ' ': continue PRINT_DOC; + case 'q': + case 3: + break PRINT_DOC; + default: + in.beep(); + break; + } + } + } + } + //clear the "press space" line: + in.getCursorBuffer().buffer.replace(0, buffer.length(), lastNote); + in.getCursorBuffer().cursor = 0; + in.killLine(); + in.getCursorBuffer().buffer.append(buffer); + in.getCursorBuffer().cursor = cursor; + in.redrawLine(); + in.flush(); + } } else { in.beep(); } diff --git a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java index 988fd07a541..f56fcff1ab2 100644 --- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java +++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java @@ -1308,7 +1308,7 @@ public class JShellTool implements MessageHandler { return commandCompletions.completionSuggestions(code, cursor, anchor); } - public String commandDocumentation(String code, int cursor) { + public String commandDocumentation(String code, int cursor, boolean shortDescription) { code = code.substring(0, cursor); int space = code.indexOf(' '); @@ -1316,7 +1316,7 @@ public class JShellTool implements MessageHandler { String cmd = code.substring(0, space); Command command = commands.get(cmd); if (command != null) { - return getResourceString(command.helpKey + ".summary"); + return getResourceString(command.helpKey + (shortDescription ? ".summary" : "")); } } diff --git a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties index 965f05ece54..414d35b2660 100644 --- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties +++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties @@ -145,6 +145,11 @@ jshell.err.the.snippet.cannot.be.used.with.this.command = This command does not jshell.err.retained.mode.failure = Failure in retained modes (modes cleared) -- {0} {1} jshell.console.see.more = +jshell.console.see.javadoc = +jshell.console.see.help = +jshell.console.see.next.page = -- Press space for next page, Q to quit. -- +jshell.console.see.next.javadoc = -- Press space for next javadoc, Q to quit. -- +jshell.console.no.javadoc = jshell.console.do.nothing = Do nothing jshell.console.choice = Choice: \ diff --git a/langtools/src/jdk.jshell/share/classes/jdk/jshell/JShell.java b/langtools/src/jdk.jshell/share/classes/jdk/jshell/JShell.java index aa2dfeef901..b537388c221 100644 --- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/JShell.java +++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/JShell.java @@ -506,6 +506,9 @@ public class JShell implements AutoCloseable { if (!closed) { closeDown(); executionControl().close(); + if (sourceCodeAnalysis != null) { + sourceCodeAnalysis.close(); + } } } diff --git a/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java b/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java index 0a0a05c215c..cd20f77d9f3 100644 --- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java +++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java @@ -63,12 +63,16 @@ public abstract class SourceCodeAnalysis { public abstract List completionSuggestions(String input, int cursor, int[] anchor); /** - * Compute a description/help string for the given user's input. + * Compute documentation for the given user's input. Multiple {@code Documentation} objects may + * be returned when multiple elements match the user's input (like for overloaded methods). * @param input the snippet the user wrote so far * @param cursor the current position of the cursors in the given {@code input} text - * @return description/help string for the given user's input + * @param computeJavadoc true if the javadoc for the given input should be computed in + * addition to the signature + * @return the documentations for the given user's input, if multiple elements match the input, + * multiple {@code Documentation} objects are returned. */ - public abstract String documentation(String input, int cursor); + public abstract List documentation(String input, int cursor, boolean computeJavadoc); /** * Infer the type of the given expression. The expression spans from the beginning of {@code code} @@ -265,6 +269,26 @@ public abstract class SourceCodeAnalysis { boolean matchesType(); } + /** + * A documentation for a candidate for continuation of the given user's input. + */ + public interface Documentation { + + /** + * The signature of the given element. + * + * @return the signature + */ + String signature(); + + /** + * The javadoc of the given element. + * + * @return the javadoc, or null if not found or not requested + */ + String javadoc(); + } + /** * List of possible qualified names. */ diff --git a/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java b/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java index ce7fd0eb31e..018b524d98a 100644 --- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java +++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java @@ -42,19 +42,17 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.TypeParameterTree; import com.sun.source.tree.VariableTree; -import com.sun.source.util.JavacTask; import com.sun.source.util.SourcePositions; import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; -import com.sun.source.util.Trees; import com.sun.tools.javac.api.JavacScope; -import com.sun.tools.javac.api.JavacTaskImpl; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Symbol.CompletionFailure; import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Symtab; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.ClassType; +import jdk.internal.shellsupport.doc.JavadocHelper; import com.sun.tools.javac.util.Name; import com.sun.tools.javac.util.Names; import com.sun.tools.javac.util.Pair; @@ -105,6 +103,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; @@ -123,15 +122,10 @@ import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeKind; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Types; -import javax.tools.JavaCompiler; import javax.tools.JavaFileManager.Location; -import javax.tools.JavaFileObject; -import javax.tools.StandardJavaFileManager; import javax.tools.StandardLocation; -import javax.tools.ToolProvider; import static jdk.jshell.Util.REPL_DOESNOTMATTER_CLASS_NAME; -import static java.util.stream.Collectors.joining; import static jdk.jshell.SourceCodeAnalysis.Completeness.DEFINITELY_INCOMPLETE; import static jdk.jshell.TreeDissector.printType; @@ -151,6 +145,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { private final JShell proc; private final CompletenessAnalyzer ca; + private final List closeables = new ArrayList<>(); private final Map currentIndexes = new HashMap<>(); private int indexVersion; private int classpathVersion; @@ -1097,10 +1092,10 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { } @Override - public String documentation(String code, int cursor) { + public List documentation(String code, int cursor, boolean computeJavadoc) { suspendIndexing(); try { - return documentationImpl(code, cursor); + return documentationImpl(code, cursor, computeJavadoc); } finally { resumeIndexing(); } @@ -1112,14 +1107,14 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { "-parameters" }; - private String documentationImpl(String code, int cursor) { + private List documentationImpl(String code, int cursor, boolean computeJavadoc) { code = code.substring(0, cursor); if (code.trim().isEmpty()) { //TODO: comment handling code += ";"; } if (guessKind(code) == Kind.IMPORT) - return null; + return Collections.emptyList(); OuterWrap codeWrap = proc.outerMap.wrapInTrialClass(Wrap.methodWrap(code)); AnalyzeTask at = proc.taskFactory.new AnalyzeTask(codeWrap, keepParameterNames); @@ -1128,46 +1123,120 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { TreePath tp = pathFor(topLevel, sp, codeWrap.snippetIndexToWrapIndex(cursor)); if (tp == null) - return null; + return Collections.emptyList(); TreePath prevPath = null; - while (tp != null && tp.getLeaf().getKind() != Kind.METHOD_INVOCATION && tp.getLeaf().getKind() != Kind.NEW_CLASS) { + while (tp != null && tp.getLeaf().getKind() != Kind.METHOD_INVOCATION && + tp.getLeaf().getKind() != Kind.NEW_CLASS && tp.getLeaf().getKind() != Kind.IDENTIFIER && + tp.getLeaf().getKind() != Kind.MEMBER_SELECT) { prevPath = tp; tp = tp.getParentPath(); } if (tp == null) - return null; + return Collections.emptyList(); + Stream elements; Iterable> candidates; List arguments; - if (tp.getLeaf().getKind() == Kind.METHOD_INVOCATION) { - MethodInvocationTree mit = (MethodInvocationTree) tp.getLeaf(); - candidates = methodCandidates(at, tp); - arguments = mit.getArguments(); + if (tp.getLeaf().getKind() == Kind.METHOD_INVOCATION || tp.getLeaf().getKind() == Kind.NEW_CLASS) { + if (tp.getLeaf().getKind() == Kind.METHOD_INVOCATION) { + MethodInvocationTree mit = (MethodInvocationTree) tp.getLeaf(); + candidates = methodCandidates(at, tp); + arguments = mit.getArguments(); + } else { + NewClassTree nct = (NewClassTree) tp.getLeaf(); + candidates = newClassCandidates(at, tp); + arguments = nct.getArguments(); + } + + if (!isEmptyArgumentsContext(arguments)) { + List actuals = computeActualInvocationTypes(at, arguments, prevPath); + List fullActuals = actuals != null ? actuals : Collections.emptyList(); + + candidates = + this.filterExecutableTypesByArguments(at, candidates, fullActuals) + .stream() + .filter(method -> parameterType(method.fst, method.snd, fullActuals.size(), true).findAny().isPresent()) + .collect(Collectors.toList()); + } + + elements = Util.stream(candidates).map(method -> method.fst); + } else if (tp.getLeaf().getKind() == Kind.IDENTIFIER || tp.getLeaf().getKind() == Kind.MEMBER_SELECT) { + Element el = at.trees().getElement(tp); + + if (el == null || + el.asType().getKind() == TypeKind.ERROR || + (el.getKind() == ElementKind.PACKAGE && el.getEnclosedElements().isEmpty())) { + //erroneous element: + return Collections.emptyList(); + } + + elements = Stream.of(el); } else { - NewClassTree nct = (NewClassTree) tp.getLeaf(); - candidates = newClassCandidates(at, tp); - arguments = nct.getArguments(); + return Collections.emptyList(); } - if (!isEmptyArgumentsContext(arguments)) { - List actuals = computeActualInvocationTypes(at, arguments, prevPath); - List fullActuals = actuals != null ? actuals : Collections.emptyList(); + List result = Collections.emptyList(); - candidates = - this.filterExecutableTypesByArguments(at, candidates, fullActuals) - .stream() - .filter(method -> parameterType(method.fst, method.snd, fullActuals.size(), true).findAny().isPresent()) - .collect(Collectors.toList()); + try (JavadocHelper helper = JavadocHelper.create(at.task, findSources())) { + result = elements.map(el -> constructDocumentation(at, helper, el, computeJavadoc)) + .filter(r -> r != null) + .collect(Collectors.toList()); + } catch (IOException ex) { + proc.debug(ex, "JavadocHelper.close()"); } - try (SourceCache sourceCache = new SourceCache(at)) { - return Util.stream(candidates) - .map(method -> Util.expunge(element2String(sourceCache, method.fst))) - .collect(joining("\n")); + return result; + } + + private Documentation constructDocumentation(AnalyzeTask at, JavadocHelper helper, Element el, boolean computeJavadoc) { + String javadoc = null; + try { + if (hasSyntheticParameterNames(el)) { + el = helper.getSourceElement(el); + } + if (computeJavadoc) { + javadoc = helper.getResolvedDocComment(el); + } + } catch (IOException ex) { + proc.debug(ex, "SourceCodeAnalysisImpl.element2String(..., " + el + ")"); } + String signature = Util.expunge(elementHeader(at, el, !hasSyntheticParameterNames(el), true)); + return new DocumentationImpl(signature, javadoc); + } + + public void close() { + for (AutoCloseable closeable : closeables) { + try { + closeable.close(); + } catch (Exception ex) { + proc.debug(ex, "SourceCodeAnalysisImpl.close()"); + } + } + } + + private static final class DocumentationImpl implements Documentation { + + private final String signature; + private final String javadoc; + + public DocumentationImpl(String signature, String javadoc) { + this.signature = signature; + this.javadoc = javadoc; + } + + @Override + public String signature() { + return signature; + } + + @Override + public String javadoc() { + return javadoc; + } + } private boolean isEmptyArgumentsContext(List arguments) { @@ -1178,18 +1247,6 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { return false; } - private String element2String(SourceCache sourceCache, Element el) { - try { - if (hasSyntheticParameterNames(el)) { - el = sourceCache.getSourceMethod(el); - } - } catch (IOException ex) { - proc.debug(ex, "SourceCodeAnalysisImpl.element2String(..., " + el + ")"); - } - - return Util.expunge(elementHeader(sourceCache.originalTask, el, !hasSyntheticParameterNames(el))); - } - private boolean hasSyntheticParameterNames(Element el) { if (el.getKind() != ElementKind.CONSTRUCTOR && el.getKind() != ElementKind.METHOD) return false; @@ -1204,119 +1261,6 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { .allMatch(param -> param.getSimpleName().toString().startsWith("arg")); } - private final class SourceCache implements AutoCloseable { - private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - private final Map> topLevelName2Signature2Method = new HashMap<>(); - private final AnalyzeTask originalTask; - private final StandardJavaFileManager fm; - - public SourceCache(AnalyzeTask originalTask) { - this.originalTask = originalTask; - List sources = findSources(); - if (sources.iterator().hasNext()) { - StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null); - try { - fm.setLocationFromPaths(StandardLocation.SOURCE_PATH, sources); - } catch (IOException ex) { - proc.debug(ex, "SourceCodeAnalysisImpl.SourceCache.(...)"); - try { - fm.close(); - } catch (IOException closeEx) { - proc.debug(closeEx, "SourceCodeAnalysisImpl.SourceCache.close()"); - } - fm = null; - } - this.fm = fm; - } else { - //don't waste time if there are no sources - this.fm = null; - } - } - - public Element getSourceMethod(Element method) throws IOException { - if (fm == null) - return method; - - TypeElement type = topLevelType(method); - - if (type == null) - return method; - - String binaryName = originalTask.task.getElements().getBinaryName(type).toString(); - - Map cache = topLevelName2Signature2Method.get(binaryName); - - if (cache == null) { - topLevelName2Signature2Method.put(binaryName, cache = createMethodCache(binaryName)); - } - - String handle = elementHeader(originalTask, method, false); - - return cache.getOrDefault(handle, method); - } - - private TypeElement topLevelType(Element el) { - while (el != null && el.getEnclosingElement().getKind() != ElementKind.PACKAGE) { - el = el.getEnclosingElement(); - } - - return el != null && (el.getKind().isClass() || el.getKind().isInterface()) ? (TypeElement) el : null; - } - - private Map createMethodCache(String binaryName) throws IOException { - Pair source = findSource(binaryName); - - if (source == null) - return Collections.emptyMap(); - - Map signature2Method = new HashMap<>(); - Trees trees = Trees.instance(source.fst); - - new TreePathScanner() { - @Override - public Void visitMethod(MethodTree node, Void p) { - Element currentMethod = trees.getElement(getCurrentPath()); - - if (currentMethod != null) { - signature2Method.put(elementHeader(originalTask, currentMethod, false), currentMethod); - } - - return null; - } - }.scan(source.snd, null); - - return signature2Method; - } - - private Pair findSource(String binaryName) throws IOException { - JavaFileObject jfo = fm.getJavaFileForInput(StandardLocation.SOURCE_PATH, - binaryName, - JavaFileObject.Kind.SOURCE); - - if (jfo == null) - return null; - - List jfos = Arrays.asList(jfo); - JavacTaskImpl task = (JavacTaskImpl) compiler.getTask(null, fm, d -> {}, null, null, jfos); - Iterable cuts = task.parse(); - - task.enter(); - - return Pair.of(task, cuts.iterator().next()); - } - - @Override - public void close() { - try { - if (fm != null) { - fm.close(); - } - } catch (IOException ex) { - proc.debug(ex, "SourceCodeAnalysisImpl.SourceCache.close()"); - } - } - } - private List availableSources; private List findSources() { @@ -1328,25 +1272,59 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { Path srcZip = home.resolve("src.zip"); if (!Files.isReadable(srcZip)) srcZip = home.getParent().resolve("src.zip"); - if (Files.isReadable(srcZip)) - result.add(srcZip); + if (Files.isReadable(srcZip)) { + boolean keepOpen = false; + FileSystem zipFO = null; + + try { + URI uri = URI.create("jar:" + srcZip.toUri()); + zipFO = FileSystems.newFileSystem(uri, Collections.emptyMap()); + Path root = zipFO.getRootDirectories().iterator().next(); + + if (Files.exists(root.resolve("java/lang/Object.java".replace("/", zipFO.getSeparator())))) { + //non-modular format: + result.add(srcZip); + } else if (Files.exists(root.resolve("java.base/java/lang/Object.java".replace("/", zipFO.getSeparator())))) { + //modular format: + try (DirectoryStream ds = Files.newDirectoryStream(root)) { + for (Path p : ds) { + if (Files.isDirectory(p)) { + result.add(p); + } + } + } + + keepOpen = true; + } + } catch (IOException ex) { + proc.debug(ex, "SourceCodeAnalysisImpl.findSources()"); + } finally { + if (zipFO != null) { + if (keepOpen) { + closeables.add(zipFO); + } else { + try { + zipFO.close(); + } catch (IOException ex) { + proc.debug(ex, "SourceCodeAnalysisImpl.findSources()"); + } + } + } + } + } return availableSources = result; } - private String elementHeader(AnalyzeTask at, Element el) { - return elementHeader(at, el, true); - } - - private String elementHeader(AnalyzeTask at, Element el, boolean includeParameterNames) { + private String elementHeader(AnalyzeTask at, Element el, boolean includeParameterNames, boolean useFQN) { switch (el.getKind()) { case ANNOTATION_TYPE: case CLASS: case ENUM: case INTERFACE: { TypeElement type = (TypeElement)el; String fullname = type.getQualifiedName().toString(); Element pkg = at.getElements().getPackageOf(el); - String name = pkg == null ? fullname : + String name = pkg == null || useFQN ? fullname : proc.maps.fullClassNameAndPackageToClass(fullname, ((PackageElement)pkg).getQualifiedName().toString()); - return name + typeParametersOpt(at, type.getTypeParameters()); + return name + typeParametersOpt(at, type.getTypeParameters(), includeParameterNames); } case TYPE_PARAMETER: { TypeParameterElement tp = (TypeParameterElement)el; @@ -1363,9 +1341,9 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { .collect(joining(" & ")); } case FIELD: - return elementHeader(at, el.getEnclosingElement()) + "." + el.getSimpleName() + ":" + el.asType(); + return elementHeader(at, el.getEnclosingElement(), includeParameterNames, false) + "." + el.getSimpleName() + ":" + el.asType(); case ENUM_CONSTANT: - return elementHeader(at, el.getEnclosingElement()) + "." + el.getSimpleName(); + return elementHeader(at, el.getEnclosingElement(), includeParameterNames, false) + "." + el.getSimpleName(); case EXCEPTION_PARAMETER: case LOCAL_VARIABLE: case PARAMETER: case RESOURCE_VARIABLE: return el.getSimpleName() + ":" + el.asType(); case CONSTRUCTOR: case METHOD: { @@ -1379,20 +1357,20 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { header.append(printType(at, proc, method.getReturnType())).append(" "); } else { // type parameters for the constructor - String typeParameters = typeParametersOpt(at, method.getTypeParameters()); + String typeParameters = typeParametersOpt(at, method.getTypeParameters(), includeParameterNames); if (!typeParameters.isEmpty()) { header.append(typeParameters).append(" "); } } // receiver type - String clazz = elementHeader(at, el.getEnclosingElement()); + String clazz = elementHeader(at, el.getEnclosingElement(), includeParameterNames, false); header.append(clazz); if (isMethod) { //method name with type parameters (clazz.isEmpty() ? header : header.append(".")) - .append(typeParametersOpt(at, method.getTypeParameters())) + .append(typeParametersOpt(at, method.getTypeParameters(), includeParameterNames)) .append(el.getSimpleName()); } @@ -1435,10 +1413,10 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { } return arrayType; } - private String typeParametersOpt(AnalyzeTask at, List typeParameters) { + private String typeParametersOpt(AnalyzeTask at, List typeParameters, boolean includeParameterNames) { return typeParameters.isEmpty() ? "" : typeParameters.stream() - .map(tp -> elementHeader(at, tp)) + .map(tp -> elementHeader(at, tp, includeParameterNames, false)) .collect(joining(", ", "<", ">")); } diff --git a/langtools/test/jdk/internal/shellsupport/doc/JavadocFormatterTest.java b/langtools/test/jdk/internal/shellsupport/doc/JavadocFormatterTest.java new file mode 100644 index 00000000000..e13d237cf9a --- /dev/null +++ b/langtools/test/jdk/internal/shellsupport/doc/JavadocFormatterTest.java @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2015, 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. + * + * 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. + */ + +/* + * @test + * @bug 8131019 + * @summary Test JavadocFormatter + * @library /tools/lib + * @modules jdk.compiler/jdk.internal.shellsupport.doc + * @run testng JavadocFormatterTest + */ + +import java.util.Objects; + +import jdk.internal.shellsupport.doc.JavadocFormatter; +import org.testng.annotations.Test; + +@Test +public class JavadocFormatterTest { + + private static final String CODE_RESET = "\033[0m"; + private static final String CODE_HIGHLIGHT = "\033[1m"; + private static final String CODE_UNDERLINE = "\033[4m"; + + public void testReflow() { + String actual; + String expected; + + actual = new JavadocFormatter(25, true).formatJavadoc( + "test", + "1234 1234\n1234\n1234 12345 123456789012345678901234567890 1234 1234\n1234 {@code 1234} 1234 1234\n1234 1234 123456 123456\n123456\n123456 123456 {@link String string} 1"); + + expected = CODE_HIGHLIGHT + "test" + CODE_RESET + "\n" + + "1234 1234 1234 1234 12345\n" + + "123456789012345678901234567890\n" + + "1234 1234 1234 1234 1234\n" + + "1234 1234 1234 123456\n" + + "123456 123456 123456\n" + + "123456 string 1\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + + actual = new JavadocFormatter(66, true).formatJavadoc("test", + "@param 51234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "@param 61234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "@param shortName 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 \n" + + "@param aVeryLongName1234567890123456789012345678901234567890 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 \n"); + + expected = CODE_HIGHLIGHT + "test" + CODE_RESET + "\n" + + "\n" + + CODE_UNDERLINE + "Type Parameters:" + CODE_RESET + "\n" + + "T - 51234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "E - 61234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "\n" + + CODE_UNDERLINE + "Parameters:" + CODE_RESET + "\n" + + "shortName - 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234\n" + + "aVeryLongName1234567890123456789012345678901234567890 - \n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + + actual = new JavadocFormatter(66, true).formatJavadoc("test", + "@throws ShortExcp 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 \n" + + "@throws aVeryLongException1234567890123456789012345678901234567890 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 \n"); + + expected = CODE_HIGHLIGHT + "test" + CODE_RESET + "\n" + + "\n" + + CODE_UNDERLINE + "Thrown Exceptions:" + CODE_RESET + "\n" + + "ShortExcp - 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234\n" + + "aVeryLongException1234567890123456789012345678901234567890 - \n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + actual = new JavadocFormatter(66, true).formatJavadoc("test", + "@return 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 \n"); + + expected = CODE_HIGHLIGHT + "test" + CODE_RESET + "\n" + + "\n" + + CODE_UNDERLINE + "Returns:" + CODE_RESET + "\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + + //handling of

,

:
+        actual = new JavadocFormatter(66, true).formatJavadoc("test",
+                "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " +
+                "1234 1234 1234 1234 1234 

1234 1234

1234 1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234

1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 \n" + + "

\n" +
+                "for (String data : content) {\n" +
+                "    System.err.println(data);\n" +
+                "}\n" +
+                "
\n"); + + expected = CODE_HIGHLIGHT + "test" + CODE_RESET + "\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234 1234\n" + + "1234 1234\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234\n" + + " for (String data : content) {\n" + + " System.err.println(data);\n" + + " }\n" + + " \n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + + //list handling: + actual = new JavadocFormatter(66, true).formatJavadoc("test", + "
    " + + "
  • A 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234
  • " + + "
  • B 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + "
  • C 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234
      " + + "
    1. D 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234
    2. " + + "
    3. E 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234
        " + + "
      • F 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234
          " + + "
        1. G 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + "
        " + + "
      " + + "
    " + + "
  • H 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234

    1234 1234 1234 1234 1234 1234 1234

      " + + "
    • I 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + "
    " + + "
followup" + + "
" + + "
Term1
" + + "
A 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234
" + + "
Term2" + + "
B 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + "
Term3" + + "
C 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + "
" + + "
" + + "
TermUnfinished" + + "
followup"); + + expected = CODE_HIGHLIGHT + "test" + CODE_RESET + "\n" + + " * A 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234\n" + + " * B 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234\n" + + " * C 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234\n" + + " 1. D 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234\n" + + " 2. E 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234\n" + + " * F 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234\n" + + " 1. G 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " * H 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234\n" + + " * I 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234\n" + + "followup\n" + + CODE_HIGHLIGHT + "Term1" + CODE_RESET + "\n" + + " A 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234\n" + + CODE_HIGHLIGHT + "Term2" + CODE_RESET + "\n" + + " B 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234\n" + + CODE_HIGHLIGHT + "Term3" + CODE_RESET + "\n" + + " C 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234\n" + + CODE_HIGHLIGHT + "TermUnfinished" + CODE_RESET + "\n" + + "followup\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + + //sections: + actual = new JavadocFormatter(66, true).formatJavadoc("test", + "text 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + "

1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234

" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234"); + + expected = CODE_HIGHLIGHT + "test" + CODE_RESET + "\n" + + "text 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "\n" + + CODE_UNDERLINE + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234 1234 1234" + CODE_RESET + "\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + "1234 1234 1234 1234 1234 1234 1234 1234\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + + //table: + actual = new JavadocFormatter(66, true).formatJavadoc("test", + "" + + "" + + "" + + "" + + "" + + "" + + "" + + " \n" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
A 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234B 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234C 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234
A 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234B 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + "C 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + "
A 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234B 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234C 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234
1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 12341234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234
"); + + expected = CODE_HIGHLIGHT + "test" + CODE_RESET + "\n" + + "----------------------------------------------------------------\n" + + "| " + CODE_HIGHLIGHT + "A 1234 1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "B 1234 1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "C 1234 1234 1234" + CODE_RESET + " |\n" + + "| " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " |\n" + + "| " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " |\n" + + "| " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " |\n" + + "| " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "1234 1234 1234" + CODE_RESET + " |\n" + + "| " + CODE_HIGHLIGHT + "1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "1234 1234" + CODE_RESET + " | " + CODE_HIGHLIGHT + "1234 1234" + CODE_RESET + " |\n" + + "----------------------------------------------------------------\n" + + "| A 1234 1234 1234 | B 1234 1234 1234 | C 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 | 1234 1234 | 1234 1234 |\n" + + "----------------------------------------------------------------\n" + + "| A 1234 1234 1234 | B 1234 1234 1234 | C 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 | 1234 1234 | 1234 1234 |\n" + + "----------------------------------------------------------------\n" + + "| 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 1234 | 1234 1234 1234 |\n" + + "| 1234 1234 | 1234 1234 |\n" + + "-------------------------------------------\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + + //no escape sequences: + actual = new JavadocFormatter(66, false).formatJavadoc("test", + "@param shortName 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 \n" + + "@param aVeryLongName1234567890123456789012345678901234567890 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + "1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 " + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 \n"); + + expected = "test\n" + + "\n" + + "Parameters:\n" + + "shortName - 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234\n" + + "aVeryLongName1234567890123456789012345678901234567890 - \n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n" + + " 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + + //null javadoc: + actual = new JavadocFormatter(66, true).formatJavadoc("test", null); + + expected = CODE_HIGHLIGHT + "test" + CODE_RESET + "\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + + //stray tags: + for (String tag : new String[] {"li", "ol", "h3", "table", "tr", "td", "dl", "dt", "dd"}) { + for (boolean closing : new boolean[] {false, true}) { + actual = new JavadocFormatter(66, true).formatJavadoc("test", + "<" + (closing ? "/" : "") + tag + ">text"); + + if (!actual.contains("text")) { + throw new AssertionError("Incorrect output: " + actual); + } + } + } + + //entities: + actual = new JavadocFormatter(66, false).formatJavadoc("test", + "α < A B > &broken; � �\n"); + + expected = "test\n" + + "\u03b1 < A B > &broken; � �\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + + //img: + actual = new JavadocFormatter(66, true).formatJavadoc("test", + "1234 text 1234"); + + expected = CODE_HIGHLIGHT + "test" + CODE_RESET + "\n" + + "1234 text 1234\n"; + + if (!Objects.equals(actual, expected)) { + throw new AssertionError("Incorrect output: " + actual); + } + } + +} diff --git a/langtools/test/jdk/internal/shellsupport/doc/JavadocHelperTest.java b/langtools/test/jdk/internal/shellsupport/doc/JavadocHelperTest.java new file mode 100644 index 00000000000..39d6cbe57f6 --- /dev/null +++ b/langtools/test/jdk/internal/shellsupport/doc/JavadocHelperTest.java @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2015, 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. + * + * 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. + */ + +/* + * @test + * @bug 8131019 + * @summary Test JavadocHelper + * @library /tools/lib + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * jdk.compiler/jdk.internal.shellsupport.doc + * @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask + * @run testng JavadocHelperTest + */ + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.function.Function; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import javax.lang.model.element.Element; +import javax.lang.model.util.ElementFilter; +import javax.tools.Diagnostic.Kind; +import javax.tools.DiagnosticListener; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import com.sun.source.util.JavacTask; +import jdk.internal.shellsupport.doc.JavadocHelper; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +@Test +public class JavadocHelperTest { + + public void testJavadoc() throws Exception { + doTestJavadoc("", + t -> t.getElements().getTypeElement("test.Super"), + "Top level. "); + doTestJavadoc("", + t -> getFirstMethod(t, "test.Super"), + " javadoc1A\n" + + "\n" + + " @param p1 param1A\n" + + " @param p2 param2A\n" + + " @param p3 param3A\n" + + " @throws IllegalStateException exc1A\n" + + " @throws IllegalArgumentException exc2A\n" + + " @throws IllegalAccessException exc3A\n" + + " @return valueA\n"); + } + + private Element getFirstMethod(JavacTask task, String typeName) { + return ElementFilter.methodsIn(task.getElements().getTypeElement(typeName).getEnclosedElements()).get(0); + } + + private Function getSubTest = t -> getFirstMethod(t, "test.Sub"); + + public void testInheritNoJavadoc() throws Exception { + doTestJavadoc("", + getSubTest, + " javadoc1A\n" + + "\n" + + " @param p1 param1A\n" + + " @param p2 param2A\n" + + " @param p3 param3A\n" + + " @throws IllegalStateException exc1A\n" + + " @throws IllegalArgumentException exc2A\n" + + " @throws IllegalAccessException exc3A\n" + + " @return valueA\n"); + } + + public void testInheritFull() throws Exception { + doTestJavadoc(" /**\n" + + " * Prefix {@inheritDoc} suffix.\n" + + " *\n" + + " * @param p1 prefix {@inheritDoc} suffix\n" + + " * @param p2 prefix {@inheritDoc} suffix\n" + + " * @param p3 prefix {@inheritDoc} suffix\n" + + " * @throws IllegalStateException prefix {@inheritDoc} suffix\n" + + " * @throws IllegalArgumentException prefix {@inheritDoc} suffix\n" + + " * @throws IllegalAccessException prefix {@inheritDoc} suffix\n" + + " * @return prefix {@inheritDoc} suffix\n" + + " */\n", + getSubTest, + " Prefix javadoc1 suffix.\n" + + "\n" + + " @param p1 prefix param1 suffix\n" + + " @param p2 prefix param2 suffix\n" + + " @param p3 prefix param3 suffix\n" + + " @throws IllegalStateException prefix exc1 suffix\n" + + " @throws IllegalArgumentException prefix exc2 suffix\n" + + " @throws IllegalAccessException prefix exc3 suffix\n" + + " @return prefix value suffix\n"); + } + + public void testInheritMissingParam() throws Exception { + doTestJavadoc(" /**\n" + + " * Prefix {@inheritDoc} suffix.\n" + + " *\n" + + " * @param p1 prefix {@inheritDoc} suffix\n" + + " * @param p3 prefix {@inheritDoc} suffix\n" + + " * @throws IllegalStateException prefix {@inheritDoc} suffix\n" + + " * @throws IllegalArgumentException prefix {@inheritDoc} suffix\n" + + " * @throws IllegalAccessException prefix {@inheritDoc} suffix\n" + + " * @return prefix {@inheritDoc} suffix\n" + + " */\n", + getSubTest, + " Prefix javadoc1 suffix.\n" + + "\n" + + " @param p1 prefix param1 suffix\n" + + "@param p2 param2\n" + + " @param p3 prefix param3 suffix\n" + + " @throws IllegalStateException prefix exc1 suffix\n" + + " @throws IllegalArgumentException prefix exc2 suffix\n" + + " @throws IllegalAccessException prefix exc3 suffix\n" + + " @return prefix value suffix\n"); + } + + public void testInheritMissingFirstParam() throws Exception { + doTestJavadoc(" /**\n" + + " * Prefix {@inheritDoc} suffix.\n" + + " *\n" + + " * @param p2 prefix {@inheritDoc} suffix\n" + + " * @param p3 prefix {@inheritDoc} suffix\n" + + " * @throws IllegalStateException prefix {@inheritDoc} suffix\n" + + " * @throws IllegalArgumentException prefix {@inheritDoc} suffix\n" + + " * @throws IllegalAccessException prefix {@inheritDoc} suffix\n" + + " * @return prefix {@inheritDoc} suffix\n" + + " */\n", + getSubTest, + " Prefix javadoc1 suffix.\n" + + "@param p1 param1\n" + + "\n" + + " @param p2 prefix param2 suffix\n" + + " @param p3 prefix param3 suffix\n" + + " @throws IllegalStateException prefix exc1 suffix\n" + + " @throws IllegalArgumentException prefix exc2 suffix\n" + + " @throws IllegalAccessException prefix exc3 suffix\n" + + " @return prefix value suffix\n"); + } + + public void testInheritMissingThrows() throws Exception { + doTestJavadoc(" /**\n" + + " * Prefix {@inheritDoc} suffix.\n" + + " *\n" + + " * @param p1 prefix {@inheritDoc} suffix\n" + + " * @param p2 prefix {@inheritDoc} suffix\n" + + " * @param p3 prefix {@inheritDoc} suffix\n" + + " * @throws IllegalStateException prefix {@inheritDoc} suffix\n" + + " * @throws IllegalAccessException prefix {@inheritDoc} suffix\n" + + " * @return prefix {@inheritDoc} suffix\n" + + " */\n", + getSubTest, + " Prefix javadoc1 suffix.\n" + + "\n" + + " @param p1 prefix param1 suffix\n" + + " @param p2 prefix param2 suffix\n" + + " @param p3 prefix param3 suffix\n" + + " @throws IllegalStateException prefix exc1 suffix\n" + + "@throws java.lang.IllegalArgumentException exc2\n" + + " @throws IllegalAccessException prefix exc3 suffix\n" + + " @return prefix value suffix\n"); + } + + public void testInheritMissingReturn() throws Exception { + doTestJavadoc(" /**\n" + + " * Prefix {@inheritDoc} suffix.\n" + + " *\n" + + " * @param p1 prefix {@inheritDoc} suffix\n" + + " * @param p2 prefix {@inheritDoc} suffix\n" + + " * @param p3 prefix {@inheritDoc} suffix\n" + + " * @throws IllegalStateException prefix {@inheritDoc} suffix\n" + + " * @throws IllegalArgumentException prefix {@inheritDoc} suffix\n" + + " * @throws IllegalAccessException prefix {@inheritDoc} suffix\n" + + " */\n", + getSubTest, + " Prefix javadoc1 suffix.\n" + + "\n" + + " @param p1 prefix param1 suffix\n" + + " @param p2 prefix param2 suffix\n" + + " @param p3 prefix param3 suffix\n" + + " @throws IllegalStateException prefix exc1 suffix\n" + + " @throws IllegalArgumentException prefix exc2 suffix\n" + + " @throws IllegalAccessException prefix exc3 suffix\n" + + "@return value\n"); + } + + + private void doTestJavadoc(String origJavadoc, Function getElement, String expectedJavadoc) throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + String subClass = + "package test;\n" + + "public class Sub extends Super {\n" + + origJavadoc + + " public String test(int p1, int p2, int p3) throws IllegalStateException, IllegalArgumentException, IllegalAccessException { return null;} \n" + + "}\n"; + String superClass = + "package test;\n" + + "/**Top level." + + " */\n" + + "public class Super {\n" + + " /**\n" + + " * javadoc1A\n" + + " *\n" + + " * @param p1 param1A\n" + + " * @param p2 param2A\n" + + " * @param p3 param3A\n" + + " * @throws IllegalStateException exc1A\n" + + " * @throws IllegalArgumentException exc2A\n" + + " * @throws IllegalAccessException exc3A\n" + + " * @return valueA\n" + + " */\n" + + " public String test(int p1, int p2, int p3) throws IllegalStateException, IllegalArgumentException, IllegalAccessException { return null;} \n" + + "}\n"; + + Path srcZip = Paths.get("src.zip"); + + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(srcZip))) { + out.putNextEntry(new JarEntry("test/Sub.java")); + out.write(subClass.getBytes()); + out.putNextEntry(new JarEntry("test/Super.java")); + out.write(superClass.getBytes()); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + + DiagnosticListener noErrors = d -> { + if (d.getKind() == Kind.ERROR) { + throw new AssertionError(d.getMessage(null)); + } + }; + + assertTrue(compiler.getTask(null, null, noErrors, Arrays.asList("-d", "."), null, Arrays.asList(new JFOImpl("Super", superClass), new JFOImpl("Sub", subClass))).call()); + + try (StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null)) { + fm.setLocationFromPaths(StandardLocation.CLASS_PATH, Arrays.asList(Paths.get(".").toAbsolutePath())); + JavacTask task = (JavacTask) compiler.getTask(null, fm, noErrors, null, null, null); + + Element el = getElement.apply(task); + + try (JavadocHelper helper = JavadocHelper.create(task, Arrays.asList(srcZip))) { + String javadoc = helper.getResolvedDocComment(el); + + assertEquals(javadoc, expectedJavadoc); + } + } + } + + private static final class JFOImpl extends SimpleJavaFileObject { + + private final String code; + + public JFOImpl(String name, String code) throws URISyntaxException { + super(new URI("mem:///" + name + ".java"), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return code; + } + + } +} diff --git a/langtools/test/jdk/jshell/CompletionSuggestionTest.java b/langtools/test/jdk/jshell/CompletionSuggestionTest.java index eb21ff65dac..8e355a1bd2b 100644 --- a/langtools/test/jdk/jshell/CompletionSuggestionTest.java +++ b/langtools/test/jdk/jshell/CompletionSuggestionTest.java @@ -23,12 +23,12 @@ /* * @test - * @bug 8131025 8141092 8153761 8145263 + * @bug 8131025 8141092 8153761 8145263 8131019 * @summary Test Completion and Documentation + * @library /tools/lib * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main * jdk.jdeps/com.sun.tools.javap - * @library /tools/lib * @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask * @build KullaTesting TestingInputStream Compiler * @run testng CompletionSuggestionTest @@ -305,26 +305,26 @@ public class CompletionSuggestionTest extends KullaTesting { public void testDocumentation() throws Exception { dontReadParameterNamesFromClassFile(); - assertDocumentation("System.getProperty(|", + assertSignature("System.getProperty(|", "String System.getProperty(String key)", "String System.getProperty(String key, String def)"); assertEval("char[] chars = null;"); - assertDocumentation("new String(chars, |", + assertSignature("new String(chars, |", "String(char[], int, int)"); - assertDocumentation("String.format(|", + assertSignature("String.format(|", "String String.format(String, Object...)", "String String.format(java.util.Locale, String, Object...)"); - assertDocumentation("\"\".getBytes(\"\"|", "void String.getBytes(int, int, byte[], int)", + assertSignature("\"\".getBytes(\"\"|", "void String.getBytes(int, int, byte[], int)", "byte[] String.getBytes(String) throws java.io.UnsupportedEncodingException", "byte[] String.getBytes(java.nio.charset.Charset)"); - assertDocumentation("\"\".getBytes(\"\" |", "void String.getBytes(int, int, byte[], int)", + assertSignature("\"\".getBytes(\"\" |", "void String.getBytes(int, int, byte[], int)", "byte[] String.getBytes(String) throws java.io.UnsupportedEncodingException", "byte[] String.getBytes(java.nio.charset.Charset)"); } public void testMethodsWithNoArguments() throws Exception { dontReadParameterNamesFromClassFile(); - assertDocumentation("System.out.println(|", + assertSignature("System.out.println(|", "void java.io.PrintStream.println()", "void java.io.PrintStream.println(boolean)", "void java.io.PrintStream.println(char)", @@ -339,6 +339,7 @@ public class CompletionSuggestionTest extends KullaTesting { public void testErroneous() { assertCompletion("Undefined.|"); + assertSignature("does.not.exist|"); } public void testClinit() { @@ -474,59 +475,63 @@ public class CompletionSuggestionTest extends KullaTesting { public void testDocumentationOfUserDefinedMethods() { assertEval("void f() {}"); - assertDocumentation("f(|", "void f()"); + assertSignature("f(|", "void f()"); assertEval("void f(int i) {}"); - assertDocumentation("f(|", "void f()", "void f(int i)"); + assertSignature("f(|", "void f()", "void f(int i)"); assertEval(" void f(T... ts) {}", DiagCheck.DIAG_WARNING, DiagCheck.DIAG_OK); - assertDocumentation("f(|", "void f()", "void f(int i)", "void f(T... ts)"); + assertSignature("f(|", "void f()", "void f(int i)", "void f(T... ts)"); assertEval("class A {}"); assertEval("void f(A a) {}"); - assertDocumentation("f(|", "void f()", "void f(int i)", "void f(T... ts)", "void f(A a)"); + assertSignature("f(|", "void f()", "void f(int i)", "void f(T... ts)", "void f(A a)"); + } + + public void testClass() { + assertSignature("String|", "java.lang.String"); } public void testDocumentationOfUserDefinedConstructors() { Snippet a = classKey(assertEval("class A {}")); - assertDocumentation("new A(|", "A()"); + assertSignature("new A(|", "A()"); Snippet a2 = classKey(assertEval("class A { A() {} A(int i) {}}", ste(MAIN_SNIPPET, VALID, VALID, true, null), ste(a, VALID, OVERWRITTEN, false, MAIN_SNIPPET))); - assertDocumentation("new A(|", "A()", "A(int i)"); + assertSignature("new A(|", "A()", "A(int i)"); assertEval("class A { A(T a) {} A(int i) {} A(T t, U u) {}}", ste(MAIN_SNIPPET, VALID, VALID, true, null), ste(a2, VALID, OVERWRITTEN, false, MAIN_SNIPPET)); - assertDocumentation("new A(|", "A(T a)", "A(int i)", " A(T t, U u)"); + assertSignature("new A(|", "A(T a)", "A(int i)", " A(T t, U u)"); } public void testDocumentationOfOverriddenMethods() throws Exception { dontReadParameterNamesFromClassFile(); - assertDocumentation("\"\".wait(|", + assertSignature("\"\".wait(|", "void Object.wait(long) throws InterruptedException", "void Object.wait(long, int) throws InterruptedException", "void Object.wait() throws InterruptedException"); assertEval("class Base {void method() {}}"); Snippet e = classKey(assertEval("class Extend extends Base {}")); - assertDocumentation("new Extend().method(|", "void Base.method()"); + assertSignature("new Extend().method(|", "void Base.method()"); assertEval("class Extend extends Base {void method() {}}", ste(MAIN_SNIPPET, VALID, VALID, true, null), ste(e, VALID, OVERWRITTEN, false, MAIN_SNIPPET)); - assertDocumentation("new Extend().method(|", "void Extend.method()"); + assertSignature("new Extend().method(|", "void Extend.method()"); } public void testDocumentationOfInvisibleMethods() { - assertDocumentation("Object.wait(|", ""); - assertDocumentation("\"\".indexOfSupplementary(|", ""); + assertSignature("Object.wait(|"); + assertSignature("\"\".indexOfSupplementary(|"); Snippet a = classKey(assertEval("class A {void method() {}}")); - assertDocumentation("A.method(|", ""); + assertSignature("A.method(|"); assertEval("class A {private void method() {}}", ste(MAIN_SNIPPET, VALID, VALID, true, null), ste(a, VALID, OVERWRITTEN, false, MAIN_SNIPPET)); - assertDocumentation("new A().method(|", ""); + assertSignature("new A().method(|"); } public void testDocumentationOfInvisibleConstructors() { - assertDocumentation("new Compiler(|", ""); + assertSignature("new Compiler(|"); assertEval("class A { private A() {} }"); - assertDocumentation("new A(|", ""); + assertSignature("new A(|"); } public void testDocumentationWithBoxing() { @@ -535,13 +540,13 @@ public class CompletionSuggestionTest extends KullaTesting { assertEval("Object object = null;"); assertEval("void method(int n, Object o) { }"); assertEval("void method(Object n, int o) { }"); - assertDocumentation("method(primitive,|", + assertSignature("method(primitive,|", "void method(int n, Object o)", "void method(Object n, int o)"); - assertDocumentation("method(boxed,|", + assertSignature("method(boxed,|", "void method(int n, Object o)", "void method(Object n, int o)"); - assertDocumentation("method(object,|", + assertSignature("method(object,|", "void method(Object n, int o)"); } @@ -567,7 +572,7 @@ public class CompletionSuggestionTest extends KullaTesting { void assertDoc(String generics, String expectedGenerics) { assertEval(evalFormatter.apply(generics, count)); - assertDocumentation(codeFacotry.apply(count), docFormatter.apply(expectedGenerics, count)); + assertSignature(codeFacotry.apply(count), docFormatter.apply(expectedGenerics, count)); count++; } } diff --git a/langtools/test/jdk/jshell/JavadocTest.java b/langtools/test/jdk/jshell/JavadocTest.java new file mode 100644 index 00000000000..d1b36bd3d81 --- /dev/null +++ b/langtools/test/jdk/jshell/JavadocTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2015, 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. + * + * 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. + */ + +/* + * @test + * @bug 8131019 + * @summary Test Javadoc + * @library /tools/lib + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * jdk.jshell + * @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask + * @build KullaTesting TestingInputStream Compiler + * @run testng JavadocTest + */ + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import org.testng.annotations.Test; + +@Test +public class JavadocTest extends KullaTesting { + + private final Compiler compiler = new Compiler(); + + public void testJavadoc() { + prepareZip(); + assertJavadoc("test.Clazz|", "test.Clazz\n" + + "Top level. "); + assertEval("test.Clazz clz = null;"); + assertJavadoc("clz.test(|", "String test.Clazz.test(int p) throws IllegalStateException\n" + + " javadoc1A\n" + + "\n" + + " @param p param\n" + + " @throws IllegalStateException exc\n" + + " @return value\n"); + //undefined handling: + assertJavadoc("clz.undef|"); + } + + private void prepareZip() { + String clazz = + "package test;\n" + + "/**Top level." + + " */\n" + + "public class Clazz {\n" + + " /**\n" + + " * javadoc1A\n" + + " *\n" + + " * @param p param\n" + + " * @throws IllegalStateException exc\n" + + " * @return value\n" + + " */\n" + + " public String test(int p) throws IllegalStateException { return null;}\n" + + "}\n"; + + Path srcZip = Paths.get("src.zip"); + + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(srcZip))) { + out.putNextEntry(new JarEntry("test/Clazz.java")); + out.write(clazz.getBytes()); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + + compiler.compile(clazz); + + try { + Field availableSources = getAnalysis().getClass().getDeclaredField("availableSources"); + availableSources.setAccessible(true); + availableSources.set(getAnalysis(), Arrays.asList(srcZip)); + } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException ex) { + throw new IllegalStateException(ex); + } + addToClasspath(compiler.getClassDir()); + } + +} diff --git a/langtools/test/jdk/jshell/KullaTesting.java b/langtools/test/jdk/jshell/KullaTesting.java index affda0af61e..80de7b4e7be 100644 --- a/langtools/test/jdk/jshell/KullaTesting.java +++ b/langtools/test/jdk/jshell/KullaTesting.java @@ -72,11 +72,14 @@ import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import jdk.jshell.Diag; + import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; + import static jdk.jshell.Snippet.Status.*; import static org.testng.Assert.*; import static jdk.jshell.Snippet.SubKind.METHOD_SUBKIND; +import jdk.jshell.SourceCodeAnalysis.Documentation; public class KullaTesting { @@ -946,12 +949,24 @@ public class KullaTesting { } } - public void assertDocumentation(String code, String... expected) { + public void assertSignature(String code, String... expected) { int cursor = code.indexOf('|'); code = code.replace("|", ""); assertTrue(cursor > -1, "'|' expected, but not found in: " + code); - String documentation = getAnalysis().documentation(code, cursor); - Set docSet = Stream.of(documentation.split("\r?\n")).collect(Collectors.toSet()); + List documentation = getAnalysis().documentation(code, cursor, false); + Set docSet = documentation.stream().map(doc -> doc.signature()).collect(Collectors.toSet()); + Set expectedSet = Stream.of(expected).collect(Collectors.toSet()); + assertEquals(docSet, expectedSet, "Input: " + code); + } + + public void assertJavadoc(String code, String... expected) { + int cursor = code.indexOf('|'); + code = code.replace("|", ""); + assertTrue(cursor > -1, "'|' expected, but not found in: " + code); + List documentation = getAnalysis().documentation(code, cursor, true); + Set docSet = documentation.stream() + .map(doc -> doc.signature() + "\n" + doc.javadoc()) + .collect(Collectors.toSet()); Set expectedSet = Stream.of(expected).collect(Collectors.toSet()); assertEquals(docSet, expectedSet, "Input: " + code); }