# HG changeset patch # User jlahoda # Date 1478068717 -3600 # Node ID 3ef02797070d727b0c59219923d82413cdc77874 # Parent f7dbab23003ae842b05ba7e34fe73a60a7126d95 8131019: jshell tool: access javadoc from tool Summary: Adding internal support to resolve {@inheritDoc} and format javadoc to plain text for use by jdk.jshell and jdk.scripting.nashorn.shell, enhancing Shift- documentation in JShell with ability to show javadoc. Reviewed-by: jjg, rfield diff -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.compiler/share/classes/com/sun/tools/doclint/Entity.java --- a/langtools/src/jdk.compiler/share/classes/com/sun/tools/doclint/Entity.java Tue Nov 01 14:47:07 2016 -0700 +++ b/langtools/src/jdk.compiler/share/classes/com/sun/tools/doclint/Entity.java Wed Nov 02 07:38:37 2016 +0100 @@ -297,17 +297,21 @@ 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 -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocFormatter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocFormatter.java Wed Nov 02 07:38:37 2016 +0100 @@ -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 -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/JavadocHelper.java Wed Nov 02 07:38:37 2016 +0100 @@ -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 -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/resources/javadocformatter.properties --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/langtools/src/jdk.compiler/share/classes/jdk/internal/shellsupport/doc/resources/javadocformatter.properties Wed Nov 02 07:38:37 2016 +0100 @@ -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 -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.compiler/share/classes/module-info.java --- a/langtools/src/jdk.compiler/share/classes/module-info.java Tue Nov 01 14:47:07 2016 -0700 +++ b/langtools/src/jdk.compiler/share/classes/module-info.java Wed Nov 02 07:38:37 2016 +0100 @@ -65,6 +65,9 @@ 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 -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java --- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java Tue Nov 01 14:47:07 2016 -0700 +++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java Wed Nov 02 07:38:37 2016 +0100 @@ -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.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 @@ "\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 -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java --- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java Tue Nov 01 14:47:07 2016 -0700 +++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java Wed Nov 02 07:38:37 2016 +0100 @@ -1308,7 +1308,7 @@ 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 @@ 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 -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties --- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties Tue Nov 01 14:47:07 2016 -0700 +++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties Wed Nov 02 07:38:37 2016 +0100 @@ -145,6 +145,11 @@ 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 -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.jshell/share/classes/jdk/jshell/JShell.java --- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/JShell.java Tue Nov 01 14:47:07 2016 -0700 +++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/JShell.java Wed Nov 02 07:38:37 2016 +0100 @@ -506,6 +506,9 @@ if (!closed) { closeDown(); executionControl().close(); + if (sourceCodeAnalysis != null) { + sourceCodeAnalysis.close(); + } } } diff -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java --- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java Tue Nov 01 14:47:07 2016 -0700 +++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysis.java Wed Nov 02 07:38:37 2016 +0100 @@ -63,12 +63,16 @@ 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} @@ -266,6 +270,26 @@ } /** + * 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. */ public static final class QualifiedNames { diff -r f7dbab23003a -r 3ef02797070d langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java --- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java Tue Nov 01 14:47:07 2016 -0700 +++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java Wed Nov 02 07:38:37 2016 +0100 @@ -42,19 +42,17 @@ 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.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.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 @@ 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 @@ } @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 @@ "-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 @@ 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(); + } + + List result = Collections.emptyList(); + + 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()"); } - if (!isEmptyArgumentsContext(arguments)) { - List actuals = computeActualInvocationTypes(at, arguments, prevPath); - List fullActuals = actuals != null ? actuals : Collections.emptyList(); + 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); + } - candidates = - this.filterExecutableTypesByArguments(at, candidates, fullActuals) - .stream() - .filter(method -> parameterType(method.fst, method.snd, fullActuals.size(), true).findAny().isPresent()) - .collect(Collectors.toList()); + 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; } - try (SourceCache sourceCache = new SourceCache(at)) { - return Util.stream(candidates) - .map(method -> Util.expunge(element2String(sourceCache, method.fst))) - .collect(joining("\n")); + @Override + public String signature() { + return signature; } + + @Override + public String javadoc() { + return javadoc; + } + } private boolean isEmptyArgumentsContext(List arguments) { @@ -1178,18 +1247,6 @@ 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 @@ .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 @@ 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 @@ .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 @@ 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 @@ } 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 -r f7dbab23003a -r 3ef02797070d langtools/test/jdk/internal/shellsupport/doc/JavadocFormatterTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/langtools/test/jdk/internal/shellsupport/doc/JavadocFormatterTest.java Wed Nov 02 07:38:37 2016 +0100 @@ -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 -r f7dbab23003a -r 3ef02797070d langtools/test/jdk/internal/shellsupport/doc/JavadocHelperTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/langtools/test/jdk/internal/shellsupport/doc/JavadocHelperTest.java Wed Nov 02 07:38:37 2016 +0100 @@ -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 -r f7dbab23003a -r 3ef02797070d langtools/test/jdk/jshell/CompletionSuggestionTest.java --- a/langtools/test/jdk/jshell/CompletionSuggestionTest.java Tue Nov 01 14:47:07 2016 -0700 +++ b/langtools/test/jdk/jshell/CompletionSuggestionTest.java Wed Nov 02 07:38:37 2016 +0100 @@ -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 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 void testErroneous() { assertCompletion("Undefined.|"); + assertSignature("does.not.exist|"); } public void testClinit() { @@ -474,59 +475,63 @@ 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 @@ 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 @@ 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 -r f7dbab23003a -r 3ef02797070d langtools/test/jdk/jshell/JavadocTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/langtools/test/jdk/jshell/JavadocTest.java Wed Nov 02 07:38:37 2016 +0100 @@ -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 -r f7dbab23003a -r 3ef02797070d langtools/test/jdk/jshell/KullaTesting.java --- a/langtools/test/jdk/jshell/KullaTesting.java Tue Nov 01 14:47:07 2016 -0700 +++ b/langtools/test/jdk/jshell/KullaTesting.java Wed Nov 02 07:38:37 2016 +0100 @@ -72,11 +72,14 @@ 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 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); }