diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/TagletWriterImpl.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/TagletWriterImpl.java index e34405871fb36..ee474cb0629bd 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/TagletWriterImpl.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/TagletWriterImpl.java @@ -811,17 +811,17 @@ public Content getThrowsHeader() { return HtmlTree.DT(contents.throws_); } - @Override - public Content throwsTagOutput(Element element, ThrowsTree throwsTag, TypeMirror substituteType) { + @Deprecated(forRemoval = true) + private Content throwsTagOutput(Element element, ThrowsTree throwsTag, TypeMirror substituteType) { ContentBuilder body = new ContentBuilder(); CommentHelper ch = utils.getCommentHelper(element); Element exception = ch.getException(throwsTag); Content excName; if (substituteType != null) { - excName = htmlWriter.getLink(new HtmlLinkInfo(configuration, HtmlLinkInfo.Kind.MEMBER, + excName = htmlWriter.getLink(new HtmlLinkInfo(configuration, HtmlLinkInfo.Kind.MEMBER, substituteType)); } else if (exception == null) { - excName = RawHtml.of(throwsTag.getExceptionName().toString()); + excName = Text.of(throwsTag.getExceptionName().toString()); } else if (exception.asType() == null) { excName = Text.of(utils.getFullyQualifiedName(exception)); } else { @@ -841,9 +841,16 @@ public Content throwsTagOutput(Element element, ThrowsTree throwsTag, TypeMirror } @Override - public Content throwsTagOutput(TypeMirror throwsType) { - return HtmlTree.DD(HtmlTree.CODE(htmlWriter.getLink( - new HtmlLinkInfo(configuration, HtmlLinkInfo.Kind.MEMBER, throwsType)))); + public Content throwsTagOutput(TypeMirror throwsType, Optional content) { + var linkInfo = new HtmlLinkInfo(configuration, HtmlLinkInfo.Kind.MEMBER, throwsType); + linkInfo.excludeTypeBounds = true; + var link = htmlWriter.getLink(linkInfo); + var concat = new ContentBuilder(HtmlTree.CODE(link)); + if (content.isPresent()) { + concat.add(" - "); + concat.add(content.get()); + } + return HtmlTree.DD(concat); } @Override diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties index 0b360ba6c1647..cb48552979977 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties @@ -107,6 +107,8 @@ doclet.link.see.no_label=missing reference label doclet.see.class_or_package_not_found=Tag {0}: reference not found: {1} doclet.see.class_or_package_not_accessible=Tag {0}: reference not accessible: {1} doclet.see.nested_link=Tag {0}: nested link +doclet.throws.reference_not_found=cannot find exception type by name +doclet.throws.reference_bad_type=not an exception type: {0} doclet.tag.invalid_usage=invalid usage of tag {0} doclet.tag.invalid_input=invalid input: ''{0}'' doclet.tag.invalid=invalid @{0} diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/builders/MemberSummaryBuilder.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/builders/MemberSummaryBuilder.java index 80a5e8eb46bc8..9731052b6cc27 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/builders/MemberSummaryBuilder.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/builders/MemberSummaryBuilder.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.SortedSet; import java.util.TreeSet; import javax.lang.model.element.Element; @@ -50,6 +51,7 @@ import jdk.javadoc.internal.doclets.toolkit.MemberSummaryWriter; import jdk.javadoc.internal.doclets.toolkit.WriterFactory; import jdk.javadoc.internal.doclets.toolkit.util.DocFinder; +import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Result; import jdk.javadoc.internal.doclets.toolkit.util.Utils; import jdk.javadoc.internal.doclets.toolkit.util.VisibleMemberTable; @@ -260,19 +262,18 @@ private void buildSummary(MemberSummaryWriter writer, if (property != null && member instanceof ExecutableElement ee) { configuration.cmtUtils.updatePropertyMethodComment(ee, property); } - List firstSentenceTags = utils.getFirstSentenceTrees(member); - if (utils.isMethod(member) && firstSentenceTags.isEmpty()) { - //Inherit comments from overridden or implemented method if - //necessary. - DocFinder.Output inheritedDoc = - DocFinder.search(configuration, - new DocFinder.Input(utils, member)); - if (inheritedDoc.holder != null - && !utils.getFirstSentenceTrees(inheritedDoc.holder).isEmpty()) { - firstSentenceTags = utils.getFirstSentenceTrees(inheritedDoc.holder); - } + if (utils.isMethod(member)) { + var docFinder = utils.docFinder(); + Optional> r = docFinder.search((ExecutableElement) member, (m -> { + var firstSentenceTrees = utils.getFirstSentenceTrees(m); + Optional> optional = firstSentenceTrees.isEmpty() ? Optional.empty() : Optional.of(firstSentenceTrees); + return Result.fromOptional(optional); + })).toOptional(); + // The fact that we use `member` for possibly unrelated tags is suspicious + writer.addMemberSummary(typeElement, member, r.orElse(List.of())); + } else { + writer.addMemberSummary(typeElement, member, utils.getFirstSentenceTrees(member)); } - writer.addMemberSummary(typeElement, member, firstSentenceTags); } summaryTreeList.add(writer.getSummaryTable(typeElement)); } diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/builders/MethodBuilder.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/builders/MethodBuilder.java index b3fc04551968f..c5c4bf11df0f9 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/builders/MethodBuilder.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/builders/MethodBuilder.java @@ -32,11 +32,13 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; +import com.sun.source.doctree.DocTree; import jdk.javadoc.internal.doclets.toolkit.BaseOptions; import jdk.javadoc.internal.doclets.toolkit.Content; import jdk.javadoc.internal.doclets.toolkit.DocletException; import jdk.javadoc.internal.doclets.toolkit.MethodWriter; import jdk.javadoc.internal.doclets.toolkit.util.DocFinder; +import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Result; import static jdk.javadoc.internal.doclets.toolkit.util.VisibleMemberTable.Kind.*; @@ -165,13 +167,10 @@ protected void buildPreviewInfo(Content methodContent) { protected void buildMethodComments(Content methodContent) { if (!options.noComment()) { assert utils.isMethod(currentMethod); // not all executables are methods - ExecutableElement method = currentMethod; - if (utils.getFullBody(currentMethod).isEmpty()) { - DocFinder.Output docs = DocFinder.search(configuration, - new DocFinder.Input(utils, currentMethod)); - if (!docs.inlineTags.isEmpty()) - method = (ExecutableElement) docs.holder; - } + var docFinder = utils.docFinder(); + Optional r = docFinder.search(currentMethod, + m -> Result.fromOptional(utils.getFullBody(m).isEmpty() ? Optional.empty() : Optional.of(m))).toOptional(); + ExecutableElement method = r.orElse(currentMethod); TypeMirror containingType = method.getEnclosingElement().asType(); writer.addComments(containingType, method, methodContent); } diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/resources/doclets.properties b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/resources/doclets.properties index 1f1f5691b3f47..375fb39c8ce0f 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/resources/doclets.properties +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/resources/doclets.properties @@ -118,6 +118,9 @@ doclet.Factory=Factory: doclet.UnknownTag={0} is an unknown tag. doclet.UnknownTagLowercase={0} is an unknown tag -- same as a known tag except for case. doclet.inheritDocWithinInappropriateTag=@inheritDoc cannot be used within this tag +doclet.inheritDocNoDoc=overridden methods do not document exception type {0} +doclet.throwsInheritDocUnsupported=@inheritDoc is not supported for exception-type type parameters \ + that are not declared by a method; document such exception types directly doclet.noInheritedDoc=@inheritDoc used but {0} does not override or implement any method. doclet.tag_misuse=Tag {0} cannot be used in {1} documentation. It can only be used in the following types of documentation: {2}. doclet.Package_Summary=Package Summary diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/InheritDocTaglet.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/InheritDocTaglet.java index e109f66f14a9c..194b78a3fc871 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/InheritDocTaglet.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/InheritDocTaglet.java @@ -26,6 +26,8 @@ package jdk.javadoc.internal.doclets.toolkit.taglets; import java.util.EnumSet; +import java.util.List; +import java.util.Optional; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; @@ -38,6 +40,7 @@ import jdk.javadoc.internal.doclets.toolkit.Messages; import jdk.javadoc.internal.doclets.toolkit.util.CommentHelper; import jdk.javadoc.internal.doclets.toolkit.util.DocFinder; +import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Result; import jdk.javadoc.internal.doclets.toolkit.util.Utils; /** @@ -61,52 +64,75 @@ public InheritDocTaglet() { * such tag.

* * @param writer the writer that is writing the output. - * @param e the {@link Element} that we are documenting. + * @param method the method that we are documenting. * @param inheritDoc the {@code {@inheritDoc}} tag * @param isFirstSentence true if we only want to inherit the first sentence */ private Content retrieveInheritedDocumentation(TagletWriter writer, - Element e, + ExecutableElement method, DocTree inheritDoc, boolean isFirstSentence) { Content replacement = writer.getOutputInstance(); BaseConfiguration configuration = writer.configuration(); Messages messages = configuration.getMessages(); Utils utils = configuration.utils; - CommentHelper ch = utils.getCommentHelper(e); + CommentHelper ch = utils.getCommentHelper(method); var path = ch.getDocTreePath(inheritDoc).getParentPath(); DocTree holderTag = path.getLeaf(); - Taglet taglet = holderTag.getKind() == DocTree.Kind.DOC_COMMENT - ? null - : configuration.tagletManager.getTaglet(ch.getTagName(holderTag)); + if (holderTag.getKind() == DocTree.Kind.DOC_COMMENT) { + try { + var docFinder = utils.docFinder(); + Optional r = docFinder.trySearch(method, + m -> Result.fromOptional(extractMainDescription(m, isFirstSentence, utils))).toOptional(); + if (r.isPresent()) { + replacement = writer.commentTagsToOutput(r.get().method, null, + r.get().mainDescription, isFirstSentence); + } + } catch (DocFinder.NoOverriddenMethodsFound e) { + String signature = utils.getSimpleName(method) + + utils.flatSignature(method, writer.getCurrentPageElement()); + messages.warning(method, "doclet.noInheritedDoc", signature); + } + return replacement; + } + + Taglet taglet = configuration.tagletManager.getTaglet(ch.getTagName(holderTag)); if (taglet != null && !(taglet instanceof InheritableTaglet)) { // This tag does not support inheritance. messages.warning(path, "doclet.inheritDocWithinInappropriateTag"); return replacement; } - var input = new DocFinder.Input(utils, e, (InheritableTaglet) taglet, - new DocFinder.DocTreeInfo(holderTag, e), isFirstSentence, true); - DocFinder.Output inheritedDoc = DocFinder.search(configuration, input); - if (inheritedDoc.isValidInheritDocTag) { - if (!inheritedDoc.inlineTags.isEmpty()) { - replacement = writer.commentTagsToOutput(inheritedDoc.holder, inheritedDoc.holderTag, - inheritedDoc.inlineTags, isFirstSentence); + + InheritableTaglet.Output inheritedDoc = ((InheritableTaglet) taglet).inherit(method, holderTag, isFirstSentence, configuration); + if (inheritedDoc.isValidInheritDocTag()) { + if (!inheritedDoc.inlineTags().isEmpty()) { + replacement = writer.commentTagsToOutput(inheritedDoc.holder(), inheritedDoc.holderTag(), + inheritedDoc.inlineTags(), isFirstSentence); } } else { - String signature = utils.getSimpleName(e) + - ((utils.isExecutableElement(e)) - ? utils.flatSignature((ExecutableElement) e, writer.getCurrentPageElement()) - : e.toString()); - messages.warning(e, "doclet.noInheritedDoc", signature); + String signature = utils.getSimpleName(method) + + utils.flatSignature(method, writer.getCurrentPageElement()); + messages.warning(method, "doclet.noInheritedDoc", signature); } return replacement; } + private record Documentation(List mainDescription, ExecutableElement method) { } + + private static Optional extractMainDescription(ExecutableElement m, + boolean extractFirstSentenceOnly, + Utils utils) { + List docTrees = extractFirstSentenceOnly + ? utils.getFirstSentenceTrees(m) + : utils.getFullBody(m); + return docTrees.isEmpty() ? Optional.empty() : Optional.of(new Documentation(docTrees, m)); + } + @Override public Content getInlineTagOutput(Element e, DocTree inheritDoc, TagletWriter tagletWriter) { if (e.getKind() != ElementKind.METHOD) { return tagletWriter.getOutputInstance(); } - return retrieveInheritedDocumentation(tagletWriter, e, inheritDoc, tagletWriter.isFirstSentence); + return retrieveInheritedDocumentation(tagletWriter, (ExecutableElement) e, inheritDoc, tagletWriter.isFirstSentence); } } diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/InheritableTaglet.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/InheritableTaglet.java index 169597133e0be..48d25b6419939 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/InheritableTaglet.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/InheritableTaglet.java @@ -25,7 +25,13 @@ package jdk.javadoc.internal.doclets.toolkit.taglets; -import jdk.javadoc.internal.doclets.toolkit.util.DocFinder; + +import java.util.List; + +import javax.lang.model.element.Element; + +import com.sun.source.doctree.DocTree; +import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration; /** * A taglet should implement this interface if it supports an {@code {@inheritDoc}} @@ -33,13 +39,24 @@ */ public interface InheritableTaglet extends Taglet { - /** - * Given an {@link jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Output} - * object, set its values with the appropriate information to inherit - * documentation. + /* + * Called by InheritDocTaglet on an inheritable taglet to expand {@inheritDoc} + * found inside a tag corresponding to that taglet. * - * @param input the input for documentation search - * @param output the output for documentation search + * When inheriting failed some assumption, or caused an error, the taglet + * can return either of: + * + * - new Output(null, null, List.of(), false) + * - new Output(null, null, List.of(), true) + * + * In the future, this could be reworked using some other mechanism, + * such as throwing an exception. */ - void inherit(DocFinder.Input input, DocFinder.Output output); + Output inherit(Element owner, DocTree tag, boolean isFirstSentence, BaseConfiguration configuration); + + record Output(DocTree holderTag, + Element holder, + List inlineTags, + boolean isValidInheritDocTag) { + } } diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ParamTaglet.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ParamTaglet.java index b0432f92866b8..d98923d7c5ba9 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ParamTaglet.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ParamTaglet.java @@ -28,17 +28,19 @@ import java.util.*; 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 com.sun.source.doctree.DocTree; import com.sun.source.doctree.ParamTree; import jdk.javadoc.doclet.Taglet.Location; +import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration; import jdk.javadoc.internal.doclets.toolkit.Content; import jdk.javadoc.internal.doclets.toolkit.Messages; import jdk.javadoc.internal.doclets.toolkit.util.CommentHelper; import jdk.javadoc.internal.doclets.toolkit.util.DocFinder; -import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Input; +import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Result; import jdk.javadoc.internal.doclets.toolkit.util.Utils; /** @@ -62,48 +64,34 @@ public ParamTaglet() { } @Override - public void inherit(DocFinder.Input input, DocFinder.Output output) { - Utils utils = input.utils; - if (input.tagId == null) { - var tag = (ParamTree) input.docTreeInfo.docTree(); - input.isTypeVariableParamTag = tag.isTypeParameter(); - ExecutableElement ee = (ExecutableElement) input.docTreeInfo.element(); - CommentHelper ch = utils.getCommentHelper(ee); - List parameters = input.isTypeVariableParamTag - ? ee.getTypeParameters() - : ee.getParameters(); - String target = ch.getParameterName(tag); - for (int i = 0; i < parameters.size(); i++) { - Element e = parameters.get(i); - String candidate = input.isTypeVariableParamTag - ? utils.getTypeName(e.asType(), false) - : utils.getSimpleName(e); - if (candidate.equals(target)) { - input.tagId = Integer.toString(i); - break; - } - } + public Output inherit(Element owner, DocTree tag, boolean isFirstSentence, BaseConfiguration configuration) { + assert owner.getKind() == ElementKind.METHOD; + assert tag.getKind() == DocTree.Kind.PARAM; + var method = (ExecutableElement) owner; + var param = (ParamTree) tag; + // find the position of an owner parameter described by the given tag + List parameterElements; + if (param.isTypeParameter()) { + parameterElements = method.getTypeParameters(); + } else { + parameterElements = method.getParameters(); } - if (input.tagId == null) - return; - int position = Integer.parseInt(input.tagId); - ExecutableElement ee = (ExecutableElement) input.element; - CommentHelper ch = utils.getCommentHelper(ee); - List tags = input.isTypeVariableParamTag - ? utils.getTypeParamTrees(ee) - : utils.getParamTrees(ee); - List parameters = input.isTypeVariableParamTag - ? ee.getTypeParameters() - : ee.getParameters(); - Map positionOfName = mapNameToPosition(utils, parameters); - for (ParamTree tag : tags) { - String paramName = ch.getParameterName(tag); - if (positionOfName.containsKey(paramName) && positionOfName.get(paramName).equals(position)) { - output.holder = input.element; - output.holderTag = tag; - output.inlineTags = ch.getBody(tag); - return; - } + Map stringIntegerMap = mapNameToPosition(configuration.utils, parameterElements); + CommentHelper ch = configuration.utils.getCommentHelper(owner); + Integer position = stringIntegerMap.get(ch.getParameterName(param)); + if (position == null) { + return new Output(null, null, List.of(), true); + } + // try to inherit description of the respective parameter in an overridden method + try { + var docFinder = configuration.utils.docFinder(); + var r = docFinder.trySearch(method, + m -> Result.fromOptional(extract(configuration.utils, m, position, param.isTypeParameter()))) + .toOptional(); + return r.map(result -> new Output(result.paramTree, result.method, result.paramTree.getDescription(), true)) + .orElseGet(() -> new Output(null, null, List.of(), true)); + } catch (DocFinder.NoOverriddenMethodsFound e) { + return new Output(null, null, List.of(), false); } } @@ -239,21 +227,35 @@ private Content getInheritedTagletOutput(ParamKind kind, boolean isFirst) { Utils utils = writer.configuration().utils; Content result = writer.getOutputInstance(); - Input input = new DocFinder.Input(writer.configuration().utils, holder, this, - Integer.toString(position), kind == ParamKind.TYPE_PARAMETER); - DocFinder.Output inheritedDoc = DocFinder.search(writer.configuration(), input); - if (!inheritedDoc.inlineTags.isEmpty()) { + var r = utils.docFinder().search((ExecutableElement) holder, + m -> Result.fromOptional(extract(utils, m, position, kind == ParamKind.TYPE_PARAMETER))) + .toOptional(); + if (r.isPresent()) { String name = kind != ParamKind.TYPE_PARAMETER ? utils.getSimpleName(param) : utils.getTypeName(param.asType(), false); - Content content = convertParam(inheritedDoc.holder, kind, writer, - (ParamTree) inheritedDoc.holderTag, - name, isFirst); + Content content = convertParam(r.get().method, kind, writer, + r.get().paramTree, name, isFirst); result.add(content); } return result; } + private record Documentation(ParamTree paramTree, ExecutableElement method) { } + + private static Optional extract(Utils utils, ExecutableElement method, Integer position, boolean typeParam) { + var ch = utils.getCommentHelper(method); + List tags = typeParam + ? utils.getTypeParamTrees(method) + : utils.getParamTrees(method); + List parameters = typeParam + ? method.getTypeParameters() + : method.getParameters(); + var positionOfName = mapNameToPosition(utils, parameters); + return tags.stream().filter(t -> position.equals(positionOfName.get(ch.getParameterName(t)))) + .map(t -> new Documentation(t, method)).findAny(); + } + /** * Converts an individual {@code ParamTree} to {@code Content}, which is * prepended with the header if the parameter is first in the list. diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ReturnTaglet.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ReturnTaglet.java index 1a7e0028a0695..e372b79cbef64 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ReturnTaglet.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ReturnTaglet.java @@ -25,22 +25,24 @@ package jdk.javadoc.internal.doclets.toolkit.taglets; -import java.util.ArrayList; import java.util.EnumSet; import java.util.List; +import java.util.Optional; +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.type.TypeMirror; import com.sun.source.doctree.DocTree; import com.sun.source.doctree.ReturnTree; import jdk.javadoc.doclet.Taglet.Location; +import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration; import jdk.javadoc.internal.doclets.toolkit.Content; import jdk.javadoc.internal.doclets.toolkit.Messages; -import jdk.javadoc.internal.doclets.toolkit.util.CommentHelper; import jdk.javadoc.internal.doclets.toolkit.util.DocFinder; -import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Input; +import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Result; import jdk.javadoc.internal.doclets.toolkit.util.Utils; /** @@ -58,27 +60,14 @@ public boolean isBlockTag() { } @Override - public void inherit(DocFinder.Input input, DocFinder.Output output) { - Utils utils = input.utils; - CommentHelper ch = utils.getCommentHelper(input.element); - - ReturnTree tag = null; - List tags = utils.getReturnTrees(input.element); - if (!tags.isEmpty()) { - tag = tags.get(0); - } else { - List firstSentence = utils.getFirstSentenceTrees(input.element); - if (firstSentence.size() == 1 && firstSentence.get(0).getKind() == DocTree.Kind.RETURN) { - tag = (ReturnTree) firstSentence.get(0); - } - } - - if (tag != null) { - output.holder = input.element; - output.holderTag = tag; - output.inlineTags = input.isFirstSentence - ? ch.getFirstSentenceTrees(output.holderTag) - : ch.getDescription(output.holderTag); + public Output inherit(Element owner, DocTree tag, boolean isFirstSentence, BaseConfiguration configuration) { + try { + var docFinder = configuration.utils.docFinder(); + var r = docFinder.trySearch((ExecutableElement) owner, m -> Result.fromOptional(extract(configuration.utils, m))).toOptional(); + return r.map(result -> new Output(result.returnTree, result.method, result.returnTree.getDescription(), true)) + .orElseGet(() -> new Output(null, null, List.of(), true)); + } catch (DocFinder.NoOverriddenMethodsFound e) { + return new Output(null, null, List.of(), false); } } @@ -89,12 +78,14 @@ public Content getInlineTagOutput(Element element, DocTree tag, TagletWriter wri @Override public Content getAllBlockTagOutput(Element holder, TagletWriter writer) { + assert holder.getKind() == ElementKind.METHOD : holder.getKind(); + var method = (ExecutableElement) holder; Messages messages = writer.configuration().getMessages(); Utils utils = writer.configuration().utils; List tags = utils.getReturnTrees(holder); - // Make sure we are not using @return tag on method with void return type. - TypeMirror returnType = utils.getReturnType(writer.getCurrentPageElement(), (ExecutableElement) holder); + // make sure we are not using @return on a method with the void return type + TypeMirror returnType = utils.getReturnType(writer.getCurrentPageElement(), method); if (returnType != null && utils.isVoid(returnType)) { if (!tags.isEmpty() && !writer.configuration().isDocLintReferenceGroupEnabled()) { messages.warning(holder, "doclet.Return_tag_on_void_method"); @@ -102,22 +93,30 @@ public Content getAllBlockTagOutput(Element holder, TagletWriter writer) { return null; } - if (!tags.isEmpty()) { - return writer.returnTagOutput(holder, tags.get(0), false); - } + // it would also be good to check if there are more than one @return + // tags and produce a warning or error similarly to how it's done + // above for a case where @return is used for void - // Check for inline tag in first sentence. - List firstSentence = utils.getFirstSentenceTrees(holder); - if (firstSentence.size() == 1 && firstSentence.get(0).getKind() == DocTree.Kind.RETURN) { - return writer.returnTagOutput(holder, (ReturnTree) firstSentence.get(0), false); - } + var docFinder = utils.docFinder(); + return docFinder.search(method, m -> Result.fromOptional(extract(utils, m))).toOptional() + .map(r -> writer.returnTagOutput(r.method, r.returnTree, false)) + .orElse(null); + } - // Inherit @return tag if necessary. - Input input = new DocFinder.Input(utils, holder, this); - DocFinder.Output inheritedDoc = DocFinder.search(writer.configuration(), input); - if (inheritedDoc.holderTag != null) { - return writer.returnTagOutput(inheritedDoc.holder, (ReturnTree) inheritedDoc.holderTag, false); - } - return null; + private record Documentation(ReturnTree returnTree, ExecutableElement method) { } + + private static Optional extract(Utils utils, ExecutableElement method) { + // TODO + // Using getBlockTags(..., Kind.RETURN) for clarity. Since @return has become a bimodal tag, + // Utils.getReturnTrees is now a misnomer: it returns only block returns, not all returns. + // We could revisit this later. + Stream blockTags = utils.getBlockTags(method, DocTree.Kind.RETURN, ReturnTree.class).stream(); + Stream mainDescriptionTags = utils.getFirstSentenceTrees(method).stream() + .mapMulti((t, c) -> { + if (t.getKind() == DocTree.Kind.RETURN) c.accept((ReturnTree) t); + }); + // this method should not check validity of @return tags, hence findAny and not findFirst or what have you + return Stream.concat(blockTags, mainDescriptionTags) + .map(t -> new Documentation(t, method)).findAny(); } } diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SeeTaglet.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SeeTaglet.java index b6714509792e8..ff5a396758248 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SeeTaglet.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SeeTaglet.java @@ -27,16 +27,17 @@ import java.util.EnumSet; import java.util.List; +import java.util.Optional; import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; import com.sun.source.doctree.DocTree; import com.sun.source.doctree.SeeTree; import jdk.javadoc.doclet.Taglet.Location; +import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration; import jdk.javadoc.internal.doclets.toolkit.Content; -import jdk.javadoc.internal.doclets.toolkit.util.CommentHelper; -import jdk.javadoc.internal.doclets.toolkit.util.DocFinder; -import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Input; +import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Result; import jdk.javadoc.internal.doclets.toolkit.util.Utils; /** @@ -49,16 +50,8 @@ public SeeTaglet() { } @Override - public void inherit(DocFinder.Input input, DocFinder.Output output) { - List tags = input.utils.getSeeTrees(input.element); - if (!tags.isEmpty()) { - CommentHelper ch = input.utils.getCommentHelper(input.element); - output.holder = input.element; - output.holderTag = tags.get(0); - output.inlineTags = input.isFirstSentence - ? ch.getFirstSentenceTrees(output.holderTag) - : ch.getReference(output.holderTag); - } + public Output inherit(Element owner, DocTree tag, boolean isFirstSentence, BaseConfiguration configuration) { + throw new UnsupportedOperationException("Not yet implemented"); } @Override @@ -66,14 +59,23 @@ public Content getAllBlockTagOutput(Element holder, TagletWriter writer) { Utils utils = writer.configuration().utils; List tags = utils.getSeeTrees(holder); Element e = holder; - if (tags.isEmpty() && utils.isMethod(holder)) { - Input input = new DocFinder.Input(utils, holder, this); - DocFinder.Output inheritedDoc = DocFinder.search(writer.configuration(), input); - if (inheritedDoc.holder != null) { - tags = utils.getSeeTrees(inheritedDoc.holder); - e = inheritedDoc.holder; + if (utils.isMethod(holder)) { + var docFinder = utils.docFinder(); + Optional result = docFinder.search((ExecutableElement) holder, + m -> Result.fromOptional(extract(utils, m))).toOptional(); + if (result.isPresent()) { + ExecutableElement m = result.get().method(); + tags = utils.getSeeTrees(m); + e = m; } } return writer.seeTagOutput(e, tags); } + + private record Documentation(List seeTrees, ExecutableElement method) { } + + private static Optional extract(Utils utils, ExecutableElement method) { + List tags = utils.getSeeTrees(method); + return tags.isEmpty() ? Optional.empty() : Optional.of(new Documentation(tags, method)); + } } diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SimpleTaglet.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SimpleTaglet.java index bd65ed2ae9311..e8c0a831bbda1 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SimpleTaglet.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SimpleTaglet.java @@ -27,16 +27,20 @@ import java.util.EnumSet; import java.util.List; +import java.util.Optional; import java.util.Set; import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; import com.sun.source.doctree.DocTree; import jdk.javadoc.doclet.Taglet.Location; +import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration; import jdk.javadoc.internal.doclets.toolkit.Content; -import jdk.javadoc.internal.doclets.toolkit.util.CommentHelper; import jdk.javadoc.internal.doclets.toolkit.util.DocFinder; +import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Result; import jdk.javadoc.internal.doclets.toolkit.util.Utils; /** @@ -159,18 +163,31 @@ private static boolean isEnabled(String locations) { } @Override - public void inherit(DocFinder.Input input, DocFinder.Output output) { - List tags = input.utils.getBlockTags(input.element, this); - if (!tags.isEmpty()) { - output.holder = input.element; - output.holderTag = tags.get(0); - CommentHelper ch = input.utils.getCommentHelper(output.holder); - output.inlineTags = input.isFirstSentence - ? ch.getFirstSentenceTrees(output.holderTag) - : ch.getTags(output.holderTag); + public Output inherit(Element owner, DocTree tag, boolean isFirstSentence, BaseConfiguration configuration) { + assert owner.getKind() == ElementKind.METHOD; + assert !isFirstSentence; + try { + var docFinder = configuration.utils.docFinder(); + var r = docFinder.trySearch((ExecutableElement) owner, + m -> Result.fromOptional(extractFirst(m, configuration.utils))).toOptional(); + return r.map(result -> new Output(result.tag, result.method, result.description, true)) + .orElseGet(()->new Output(null, null, List.of(), true)); + } catch (DocFinder.NoOverriddenMethodsFound e) { + return new Output(null, null, List.of(), false); } } + record Documentation(DocTree tag, List description, ExecutableElement method) { } + + private Optional extractFirst(ExecutableElement m, Utils utils) { + List tags = utils.getBlockTags(m, this); + if (tags.isEmpty()) { + return Optional.empty(); + } + DocTree t = tags.get(0); + return Optional.of(new Documentation(t, utils.getCommentHelper(m).getDescription(t), m)); + } + @Override public Content getAllBlockTagOutput(Element holder, TagletWriter writer) { Utils utils = writer.configuration().utils; diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SpecTaglet.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SpecTaglet.java index 110aa07431887..1d9d5e04b1084 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SpecTaglet.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/SpecTaglet.java @@ -27,15 +27,16 @@ import java.util.EnumSet; import java.util.List; +import java.util.Optional; import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; import com.sun.source.doctree.DocTree; import com.sun.source.doctree.SpecTree; import jdk.javadoc.doclet.Taglet.Location; +import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration; import jdk.javadoc.internal.doclets.toolkit.Content; -import jdk.javadoc.internal.doclets.toolkit.util.CommentHelper; -import jdk.javadoc.internal.doclets.toolkit.util.DocFinder; -import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Input; +import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Result; import jdk.javadoc.internal.doclets.toolkit.util.Utils; /** @@ -48,16 +49,8 @@ public SpecTaglet() { } @Override - public void inherit(Input input, DocFinder.Output output) { - List tags = input.utils.getSpecTrees(input.element); - if (!tags.isEmpty()) { - CommentHelper ch = input.utils.getCommentHelper(input.element); - output.holder = input.element; - output.holderTag = tags.get(0); - output.inlineTags = input.isFirstSentence - ? ch.getFirstSentenceTrees(output.holderTag) - : ch.getTags(output.holderTag); - } + public Output inherit(Element owner, DocTree tag, boolean isFirstSentence, BaseConfiguration configuration) { + throw new UnsupportedOperationException("Not yet implemented"); } @Override @@ -65,14 +58,23 @@ public Content getAllBlockTagOutput(Element holder, TagletWriter writer) { Utils utils = writer.configuration().utils; List tags = utils.getSpecTrees(holder); Element e = holder; - if (tags.isEmpty() && utils.isExecutableElement(holder)) { - Input input = new Input(utils, holder, this); - DocFinder.Output inheritedDoc = DocFinder.search(writer.configuration(), input); - if (inheritedDoc.holder != null) { - tags = utils.getSpecTrees(inheritedDoc.holder); - e = inheritedDoc.holder; + if (utils.isMethod(holder)) { + var docFinder = utils.docFinder(); + Optional result = docFinder.search((ExecutableElement) holder, + m -> Result.fromOptional(extract(utils, m))).toOptional(); + if (result.isPresent()) { + ExecutableElement m = result.get().method(); + tags = utils.getSpecTrees(m); + e = m; } } return writer.specTagOutput(e, tags); } + + private record Documentation(List seeTrees, ExecutableElement method) { } + + private static Optional extract(Utils utils, ExecutableElement method) { + List tags = utils.getSpecTrees(method); + return tags.isEmpty() ? Optional.empty() : Optional.of(new Documentation(tags, method)); + } } diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/TagletManager.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/TagletManager.java index 749e42ff78fd2..4696336ff588e 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/TagletManager.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/TagletManager.java @@ -598,7 +598,7 @@ private void initStandardTaglets() { addStandardTaglet(new ParamTaglet()); addStandardTaglet(new ReturnTaglet()); - addStandardTaglet(new ThrowsTaglet(), EXCEPTION); + addStandardTaglet(new ThrowsTaglet(configuration), EXCEPTION); addStandardTaglet( new SimpleTaglet(SINCE, resources.getText("doclet.Since"), EnumSet.allOf(Location.class), !nosince)); diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/TagletWriter.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/TagletWriter.java index e8174177a99e9..9aebf7475f2cb 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/TagletWriter.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/TagletWriter.java @@ -223,25 +223,15 @@ protected abstract Content snippetTagOutput(Element element, SnippetTree snippet */ protected abstract Content getThrowsHeader(); - /** - * Returns the output for a {@code @throws} tag. - * - * @param element The element that owns the doc comment - * @param throwsTag the throws tag - * @param substituteType instantiated type of a generic type-variable, or null - * - * @return the output - */ - protected abstract Content throwsTagOutput(Element element, ThrowsTree throwsTag, TypeMirror substituteType); - /** * Returns the output for a default {@code @throws} tag. * * @param throwsType the type that is thrown + * @param content the optional content to add as a description * * @return the output */ - protected abstract Content throwsTagOutput(TypeMirror throwsType); + protected abstract Content throwsTagOutput(TypeMirror throwsType, Optional content); /** * Returns the output for a {@code {@value}} tag. diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ThrowsTaglet.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ThrowsTaglet.java index aa147c3df9348..d5ec82041dd98 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ThrowsTaglet.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/taglets/ThrowsTaglet.java @@ -25,20 +25,26 @@ package jdk.javadoc.internal.doclets.toolkit.taglets; -import java.util.Collections; +import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.QualifiedNameable; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; @@ -47,252 +53,717 @@ import com.sun.source.doctree.ThrowsTree; import jdk.javadoc.doclet.Taglet.Location; +import jdk.javadoc.internal.doclets.formats.html.markup.ContentBuilder; +import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration; import jdk.javadoc.internal.doclets.toolkit.Content; import jdk.javadoc.internal.doclets.toolkit.util.DocFinder; +import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Result; +import jdk.javadoc.internal.doclets.toolkit.util.Utils; /** - * A taglet that processes {@link ThrowsTree}, which represents - * {@code @throws} and {@code @exception} tags. + * A taglet that processes {@link ThrowsTree}, which represents {@code @throws} + * and {@code @exception} tags, collectively referred to as exception tags. */ public class ThrowsTaglet extends BaseTaglet implements InheritableTaglet { - public ThrowsTaglet() { + /* + * Relevant bits from JLS + * ====================== + * + * This list is _incomplete_ because some parts cannot be summarized here + * and require careful reading of JLS. + * + * 11.1.1 The Kinds of Exceptions + * + * Throwable and all its subclasses are, collectively, the exception + * classes. + * + * 8.4.6 Method Throws + * + * Throws: + * throws ExceptionTypeList + * + * ExceptionTypeList: + * ExceptionType {, ExceptionType} + * + * ExceptionType: + * ClassType + * TypeVariable + * + * It is a compile-time error if an ExceptionType mentioned in a throws + * clause is not a subtype (4.10) of Throwable. + * + * Type variables are allowed in a throws clause even though they are + * not allowed in a catch clause (14.20). + * + * It is permitted but not required to mention unchecked exception + * classes (11.1.1) in a throws clause. + * + * 8.1.2 Generic Classes and Type Parameters + * + * It is a compile-time error if a generic class is a direct or indirect + * subclass of Throwable. + * + * 8.8.5. Constructor Throws + * + * The throws clause for a constructor is identical in structure and + * behavior to the throws clause for a method (8.4.6). + * + * 8.8. Constructor Declarations + * + * Constructor declarations are ... never inherited and therefore are not + * subject to hiding or overriding. + * + * 8.4.4. Generic Methods + * + * A method is generic if it declares one or more type variables (4.4). + * These type variables are known as the type parameters of the method. + * + * ... + * + * Two methods or constructors M and N have the same type parameters if + * both of the following are true: + * + * - M and N have same number of type parameters (possibly zero). + * ... + * + * 8.4.2. Method Signature + * + * Two methods or constructors, M and N, have the same signature if they + * have ... the same type parameters (if any) (8.4.4) ... + * ... + * The signature of a method m1 is a subsignature of the signature of + * a method m2 if either: + * + * - m2 has the same signature as m1, or + * - the signature of m1 is the same as the erasure (4.6) of the + * signature of m2. + * + * Two method signatures m1 and m2 are override-equivalent iff either + * m1 is a subsignature of m2 or m2 is a subsignature of m1. + * + * 8.4.8.1. Overriding (by Instance Methods) + * + * An instance method mC declared in or inherited by class C, overrides + * from C another method mA declared in class A, iff all of the following + * are true: + * + * ... + * - The signature of mC is a subsignature (8.4.2) of the signature of + * mA as a member of the supertype of C that names A. + */ + + public ThrowsTaglet(BaseConfiguration configuration) { + // of all language elements only constructors and methods can declare + // thrown exceptions and, hence, document them super(DocTree.Kind.THROWS, false, EnumSet.of(Location.CONSTRUCTOR, Location.METHOD)); + this.configuration = configuration; + this.utils = this.configuration.utils; } + private final BaseConfiguration configuration; + private final Utils utils; + @Override - public void inherit(DocFinder.Input input, DocFinder.Output output) { - var utils = input.utils; - Element target; - var ch = utils.getCommentHelper(input.element); - if (input.tagId == null) { - var tag = (ThrowsTree) input.docTreeInfo.docTree(); - target = ch.getException(tag); - input.tagId = target == null - ? tag.getExceptionName().getSignature() - : utils.getFullyQualifiedName(target); - } else { - target = input.utils.findClass(input.element, input.tagId); - } + public Output inherit(Element owner, DocTree tag, boolean isFirstSentence, BaseConfiguration configuration) { + // This method shouldn't be called because {@inheritDoc} tags inside + // exception tags aren't dealt with individually. {@inheritDoc} tags + // inside exception tags are collectively dealt with in + // getAllBlockTagOutput. + throw newAssertionError(owner, tag, isFirstSentence); + } - // TODO warn if target == null as we cannot guarantee type-match, but at most FQN-match. - - for (ThrowsTree tag : input.utils.getThrowsTrees(input.element)) { - Element candidate = ch.getException(tag); - if (candidate != null && (input.tagId.equals(utils.getSimpleName(candidate)) || - (input.tagId.equals(utils.getFullyQualifiedName(candidate))))) { - output.holder = input.element; - output.holderTag = tag; - output.inlineTags = ch.getBody(output.holderTag); - output.tagList.add(tag); - } else if (target != null && candidate != null && - utils.isTypeElement(candidate) && utils.isTypeElement(target) && - utils.isSubclassOf((TypeElement) candidate, (TypeElement) target)) { - output.tagList.add(tag); + @Override + public Content getAllBlockTagOutput(Element holder, TagletWriter writer) { + try { + return getAllBlockTagOutput0(holder, writer); + } catch (Failure f) { + // note that `f.holder()` is not necessarily the same as `holder` + var ch = utils.getCommentHelper(f.holder()); + var messages = configuration.getMessages(); + if (f instanceof Failure.ExceptionTypeNotFound e) { + var path = ch.getDocTreePath(e.tag().getExceptionName()); + messages.warning(path, "doclet.throws.reference_not_found"); + } else if (f instanceof Failure.NotExceptionType e) { + var path = ch.getDocTreePath(e.tag().getExceptionName()); + // output the type we found to help the user diagnose the issue + messages.warning(path, "doclet.throws.reference_bad_type", diagnosticDescriptionOf(e.type())); + } else if (f instanceof Failure.Invalid e) { + messages.error(ch.getDocTreePath(e.tag()), "doclet.inheritDocWithinInappropriateTag"); + } else if (f instanceof Failure.UnsupportedTypeParameter e) { + var path = ch.getDocTreePath(e.tag().getExceptionName()); + messages.warning(path, "doclet.throwsInheritDocUnsupported"); + } else if (f instanceof Failure.Undocumented e) { + messages.warning(ch.getDocTreePath(e.tag()), "doclet.inheritDocNoDoc", diagnosticDescriptionOf(e.exceptionElement)); + } else { + // TODO: instead of if-else, use pattern matching for switch for both + // readability and exhaustiveness when it's available + throw newAssertionError(f); } + } catch (DocFinder.NoOverriddenMethodsFound e) { + // since {@inheritDoc} in @throws is processed by ThrowsTaglet (this taglet) rather than + // InheritDocTaglet, we have to duplicate some of the behavior of the latter taglet + String signature = utils.getSimpleName(holder) + + utils.flatSignature((ExecutableElement) holder, writer.getCurrentPageElement()); + configuration.getMessages().warning(holder, "doclet.noInheritedDoc", signature); } + return writer.getOutputInstance(); // TODO: consider invalid rather than empty output } - @Override - public Content getAllBlockTagOutput(Element holder, TagletWriter writer) { - var utils = writer.configuration().utils; + private Content getAllBlockTagOutput0(Element holder, + TagletWriter writer) + throws Failure.ExceptionTypeNotFound, + Failure.NotExceptionType, + Failure.Invalid, + Failure.Undocumented, + Failure.UnsupportedTypeParameter, + DocFinder.NoOverriddenMethodsFound + { + ElementKind kind = holder.getKind(); + if (kind != ElementKind.METHOD && kind != ElementKind.CONSTRUCTOR) { + // Elements are processed by applicable taglets only. This taglet + // is only applicable to executable elements such as methods + // and constructors. + throw newAssertionError(holder, kind); + } var executable = (ExecutableElement) holder; ExecutableType instantiatedType = utils.asInstantiatedMethodType( writer.getCurrentPageElement(), executable); - List thrownTypes = instantiatedType.getThrownTypes(); - Map typeSubstitutions = getSubstitutedThrownTypes( + List substitutedExceptionTypes = instantiatedType.getThrownTypes(); + List originalExceptionTypes = executable.getThrownTypes(); + Map typeSubstitutions = getSubstitutedThrownTypes( utils.typeUtils, - executable.getThrownTypes(), - thrownTypes); - Map tagsMap = new LinkedHashMap<>(); - utils.getThrowsTrees(executable).forEach(t -> tagsMap.put(t, executable)); - Content result = writer.getOutputInstance(); - Set alreadyDocumented = new HashSet<>(); - result.add(throwsTagsOutput(tagsMap, alreadyDocumented, typeSubstitutions, writer)); - result.add(inheritThrowsDocumentation(executable, thrownTypes, alreadyDocumented, typeSubstitutions, writer)); - result.add(linkToUndocumentedDeclaredExceptions(thrownTypes, alreadyDocumented, writer)); - return result; + originalExceptionTypes, + substitutedExceptionTypes); + var exceptionSection = new ExceptionSectionBuilder(writer); + // Step 1: Document exception tags + Set alreadyDocumentedExceptions = new HashSet<>(); + List exceptionTags = utils.getThrowsTrees(executable); + for (ThrowsTree t : exceptionTags) { + Element exceptionElement = getExceptionType(t, executable); + outputAnExceptionTagDeeply(exceptionSection, exceptionElement, t, executable, alreadyDocumentedExceptions, typeSubstitutions, writer); + } + // Step 2: Document exception types from the `throws` clause (of a method) + // + // While methods can be inherited and overridden, constructors can be neither of those (JLS 8.8). + // Therefore, it's only methods that participate in Step 2, which is about inheriting exception + // documentation from ancestors. + if (executable.getKind() == ElementKind.METHOD) { + for (TypeMirror exceptionType : substitutedExceptionTypes) { + Element exceptionElement = utils.typeUtils.asElement(exceptionType); + Map r; + try { + r = expandShallowly(exceptionElement, executable); + } catch (Failure | DocFinder.NoOverriddenMethodsFound e) { + // Ignore errors here because unlike @throws tags, the `throws` clause is implicit + // documentation inheritance. It triggers a best-effort attempt to inherit + // documentation. If there are errors in ancestors, they will likely be caught + // once those ancestors are documented. + continue; + } + if (r.isEmpty()) { + // `exceptionType` is not documented by any tags from ancestors, skip it till Step 3 + continue; + } + if (!alreadyDocumentedExceptions.add(exceptionType)) { + // it expands to something that has to have been documented on Step 1, skip + continue; + } + for (Map.Entry e : r.entrySet()) { + outputAnExceptionTagDeeply(exceptionSection, exceptionElement, e.getKey(), e.getValue(), alreadyDocumentedExceptions, typeSubstitutions, writer); + } + } + } + // Step 3: List those exceptions from the `throws` clause for which no documentation was found on Step 2 + for (TypeMirror e : substitutedExceptionTypes) { + if (!alreadyDocumentedExceptions.add(e)) { + continue; + } + exceptionSection.beginEntry(e); + // this JavaDoc warning is similar to but different from that of DocLint: + // dc.missing.throws = no @throws for {0} + // TODO: comment out for now and revisit later; + // commented out because of the generated noise for readObject/writeObject for serialized-form.html: + // exceptionSection.continueEntry(writer.invalidTagOutput(configuration.getDocResources().getText("doclet.throws.undocumented", e), Optional.empty())); + // configuration.getMessages().warning(holder, "doclet.throws.undocumented", e); + exceptionSection.endEntry(); + } + assert alreadyDocumentedExceptions.containsAll(substitutedExceptionTypes); + return exceptionSection.build(); } - /** - * Returns a map of substitutions for a list of thrown types with the original type-variable - * name as a key and the instantiated type as a value. If no types need to be substituted - * an empty map is returned. - * @param declaredThrownTypes the originally declared thrown types. - * @param instantiatedThrownTypes the thrown types in the context of the current type. - * @return map of declared to instantiated thrown types or an empty map. - */ - private Map getSubstitutedThrownTypes(Types types, - List declaredThrownTypes, - List instantiatedThrownTypes) { - if (!declaredThrownTypes.equals(instantiatedThrownTypes)) { - Map map = new HashMap<>(); - Iterator i1 = declaredThrownTypes.iterator(); - Iterator i2 = instantiatedThrownTypes.iterator(); - while (i1.hasNext() && i2.hasNext()) { - TypeMirror t1 = i1.next(); - TypeMirror t2 = i2.next(); - if (!types.isSameType(t1, t2)) - map.put(t1.toString(), t2); + private void outputAnExceptionTagDeeply(ExceptionSectionBuilder exceptionSection, + Element originalExceptionElement, + ThrowsTree tag, + ExecutableElement holder, + Set alreadyDocumentedExceptions, + Map typeSubstitutions, + TagletWriter writer) + throws Failure.ExceptionTypeNotFound, + Failure.NotExceptionType, + Failure.Invalid, + Failure.Undocumented, + Failure.UnsupportedTypeParameter, + DocFinder.NoOverriddenMethodsFound + { + outputAnExceptionTagDeeply(exceptionSection, originalExceptionElement, tag, holder, true, alreadyDocumentedExceptions, typeSubstitutions, writer); + } + + private void outputAnExceptionTagDeeply(ExceptionSectionBuilder exceptionSection, + Element originalExceptionElement, + ThrowsTree tag, + ExecutableElement holder, + boolean beginNewEntry, + Set alreadyDocumentedExceptions, + Map typeSubstitutions, + TagletWriter writer) + throws Failure.ExceptionTypeNotFound, + Failure.NotExceptionType, + Failure.Invalid, + Failure.Undocumented, + Failure.UnsupportedTypeParameter, + DocFinder.NoOverriddenMethodsFound + { + var originalExceptionType = originalExceptionElement.asType(); + var exceptionType = typeSubstitutions.getOrDefault(originalExceptionType, originalExceptionType); // FIXME: ugh.......... + alreadyDocumentedExceptions.add(exceptionType); + var description = tag.getDescription(); + int i = indexOfInheritDoc(tag, holder); + if (i == -1) { + // Since the description does not contain {@inheritDoc}, we either add a new entry, or + // append to the current one. Here's an example of when we add a new entry: + // + // ... -> {@inheritDoc} -> + // + // And here's an example of when we append to the current entry: + // + // ... -> {@inheritDoc} -> + + // if we don't need to add a new entry, assume it has been added before + assert exceptionSection.debugEntryBegun() || beginNewEntry; + if (beginNewEntry) { // add a new entry? + // originalExceptionElement might be different from that that triggers this entry: for example, a + // renamed exception-type type parameter + exceptionSection.beginEntry(exceptionType); + } + // append to the current entry + exceptionSection.continueEntry(writer.commentTagsToOutput(holder, description)); + if (beginNewEntry) { // if added a new entry, end it + exceptionSection.endEntry(); + } + } else { // expand a single {@inheritDoc} + assert holder.getKind() == ElementKind.METHOD : holder.getKind(); // only methods can use {@inheritDoc} + // Is the {@inheritDoc} that we found standalone (i.e. without preceding and following text)? + boolean loneInheritDoc = description.size() == 1; + assert !loneInheritDoc || i == 0 : i; + boolean add = !loneInheritDoc && beginNewEntry; + // we add a new entry if the {@inheritDoc} that we found has something else around + // it and we can add a new entry (as instructed by the parent call) + if (add) { + exceptionSection.beginEntry(exceptionType); + } + if (i > 0) { + // if there's anything preceding {@inheritDoc}, assume an entry has been added before + assert exceptionSection.debugEntryBegun(); + Content beforeInheritDoc = writer.commentTagsToOutput(holder, description.subList(0, i)); + exceptionSection.continueEntry(beforeInheritDoc); + } + Map tags; + try { + tags = expandShallowly(originalExceptionElement, holder); + } catch (Failure.UnsupportedTypeParameter e) { + // repack to fill in missing tag information + throw new Failure.UnsupportedTypeParameter(e.element, tag, holder); + } + if (tags.isEmpty()) { + throw new Failure.Undocumented(tag, holder, originalExceptionElement); + } + // if {@inheritDoc} is the only tag in the @throws description and + // this call can add new entries to the exception section, + // so can the recursive call + boolean addNewEntryRecursively = beginNewEntry && !add; + if (!addNewEntryRecursively && tags.size() > 1) { + // current tag has more to description than just {@inheritDoc} + // and thus cannot expand to multiple tags; + // it's likely a documentation error + throw new Failure.Invalid(tag, holder); + } + for (Map.Entry e : tags.entrySet()) { + outputAnExceptionTagDeeply(exceptionSection, originalExceptionElement, e.getKey(), e.getValue(), addNewEntryRecursively, alreadyDocumentedExceptions, typeSubstitutions, writer); + } + // this might be an empty list, which is fine + if (!loneInheritDoc) { + Content afterInheritDoc = writer.commentTagsToOutput(holder, description.subList(i + 1, description.size())); + exceptionSection.continueEntry(afterInheritDoc); + } + if (add) { + exceptionSection.endEntry(); } - return map; } - return Map.of(); } - /** - * Returns the generated content for a collection of {@code @throws} tags. - * - * @param throwsTags the tags to be converted; each tag is mapped to - * a method it appears on - * @param alreadyDocumented the set of exceptions that have already been - * documented and thus must not be documented by - * this method. All exceptions documented by this - * method will be added to this set upon the - * method's return. - * @param writer the taglet-writer used by the doclet - * @return the generated content for the tags - */ - private Content throwsTagsOutput(Map throwsTags, - Set alreadyDocumented, - Map typeSubstitutions, - TagletWriter writer) { - var utils = writer.configuration().utils; - Content result = writer.getOutputInstance(); - var documentedInThisCall = new HashSet(); - Map flattenedExceptions = flatten(throwsTags, writer); - flattenedExceptions.forEach((ThrowsTree t, ExecutableElement e) -> { - var ch = utils.getCommentHelper(e); - Element te = ch.getException(t); - String excName = t.getExceptionName().toString(); - TypeMirror substituteType = typeSubstitutions.get(excName); - if (alreadyDocumented.contains(excName) - || (te != null && alreadyDocumented.contains(utils.getFullyQualifiedName(te, false))) - || (substituteType != null && alreadyDocumented.contains(substituteType.toString()))) { - return; + private static int indexOfInheritDoc(ThrowsTree tag, ExecutableElement holder) + throws Failure.Invalid + { + var description = tag.getDescription(); + int i = -1; + for (var iterator = description.listIterator(); iterator.hasNext(); ) { + DocTree t = iterator.next(); + if (t.getKind() == DocTree.Kind.INHERIT_DOC) { + if (i != -1) { + // an exception tag description contains more than one {@inheritDoc}; + // we consider it nonsensical and, hence, a documentation error + throw new Failure.Invalid(t, holder); + } + i = iterator.previousIndex(); } - if (alreadyDocumented.isEmpty() && documentedInThisCall.isEmpty()) { - result.add(writer.getThrowsHeader()); + } + return i; + } + + private Element getExceptionType(ThrowsTree tag, ExecutableElement holder) + throws Failure.ExceptionTypeNotFound, Failure.NotExceptionType + { + Element e = utils.getCommentHelper(holder).getException(tag); + if (e == null) { + throw new Failure.ExceptionTypeNotFound(tag, holder); + } + // translate to a type mirror to perform a subtype test, which covers not only + // classes (e.g. class X extends Exception) but also type variables + // (e.g. ) + var t = e.asType(); + var subtypeTestInapplicable = switch (t.getKind()) { + case EXECUTABLE, PACKAGE, MODULE -> true; + default -> false; + }; + if (subtypeTestInapplicable || !utils.typeUtils.isSubtype(t, utils.getThrowableType())) { + // Aside from documentation errors, this condition might arise if the + // source cannot be compiled or element we found is not what the + // documentation author intended. Whatever the reason is (e.g. + // see 8295543), we should not process such an element. + throw new Failure.NotExceptionType(tag, holder, e); + } + var k = e.getKind(); + assert k == ElementKind.CLASS || k == ElementKind.TYPE_PARAMETER : k; // JLS 8.4.6 + return e; + } + + @SuppressWarnings("serial") + private static sealed class Failure extends Exception { + + private final DocTree tag; + private final ExecutableElement holder; + + Failure(DocTree tag, ExecutableElement holder) { + super(); + this.tag = tag; + this.holder = holder; + } + + DocTree tag() { return tag; } + + ExecutableElement holder() { return holder; } + + static final class ExceptionTypeNotFound extends Failure { + + ExceptionTypeNotFound(ThrowsTree tag, ExecutableElement holder) { + super(tag, holder); } - result.add(writer.throwsTagOutput(e, t, substituteType)); - if (substituteType != null) { - documentedInThisCall.add(substituteType.toString()); - } else { - documentedInThisCall.add(te != null - ? utils.getFullyQualifiedName(te, false) - : excName); + + @Override ThrowsTree tag() { return (ThrowsTree) super.tag(); } + } + + static final class NotExceptionType extends Failure { + + private final Element type; + + public NotExceptionType(ThrowsTree tag, ExecutableElement holder, Element type) { + super(tag, holder); + this.type = type; } - }); - alreadyDocumented.addAll(documentedInThisCall); - return result; + + Element type() { return type; } + + @Override ThrowsTree tag() { return (ThrowsTree) super.tag(); } + } + + static final class Invalid extends Failure { + + public Invalid(DocTree tag, ExecutableElement holder) { + super(tag, holder); + } + } + + static final class Undocumented extends Failure { + + private final Element exceptionElement; + + public Undocumented(DocTree tag, ExecutableElement holder, Element exceptionElement) { + super(tag, holder); + this.exceptionElement = exceptionElement; + } + } + + static final class UnsupportedTypeParameter extends Failure { + + private final Element element; + + // careful: tag might be null + public UnsupportedTypeParameter(Element element, ThrowsTree tag, ExecutableElement holder) { + super(tag, holder); + this.element = element; + } + + @Override ThrowsTree tag() { return (ThrowsTree) super.tag(); } + } } /* - * A single @throws tag from an overriding method can correspond to multiple - * @throws tags from an overridden method. + * Returns immediately inherited tags that document the provided exception type. + * + * A map associates a doc tree with its holder element externally. Such maps + * have defined iteration order of entries, whose keys and values + * are non-null. */ - private Map flatten(Map throwsTags, - TagletWriter writer) { - Map result = new LinkedHashMap<>(); - throwsTags.forEach((tag, taggedElement) -> { - var expandedTags = expand(tag, taggedElement, writer); - assert Collections.disjoint(result.entrySet(), expandedTags.entrySet()); - result.putAll(expandedTags); - }); - return result; + private Map expandShallowly(Element exceptionType, + ExecutableElement holder) + throws Failure.ExceptionTypeNotFound, + Failure.NotExceptionType, + Failure.Invalid, + Failure.UnsupportedTypeParameter, + DocFinder.NoOverriddenMethodsFound + { + ElementKind kind = exceptionType.getKind(); + DocFinder.Criterion, Failure> criterion; + if (kind == ElementKind.CLASS) { + criterion = method -> { + var tags = findByTypeElement(exceptionType, method); + return toResult(exceptionType, method, tags); + }; + } else { + // Type parameters declared by a method are matched by position; the basis for + // such position matching is JLS sections 8.4.2 and 8.4.4. We don't match + // type parameters not declared by a method (e.g. declared by the + // enclosing class or interface) because + criterion = method -> { + // TODO: add a test for the throws clause mentioning + // a type parameter which is not declared by holder + int i = holder.getTypeParameters().indexOf((TypeParameterElement) exceptionType); + if (i == -1) { // the type parameter is not declared by `holder` + throw new Failure.UnsupportedTypeParameter(exceptionType, null /* don't know if tag-related */, holder); + } + // if one method overrides the other, then those methods must + // have the same number of type parameters (JLS 8.4.2) + assert utils.elementUtils.overrides(holder, method, (TypeElement) holder.getEnclosingElement()); + var typeParameterElement = method.getTypeParameters().get(i); + var tags = findByTypeElement(typeParameterElement, method); + return toResult(exceptionType, method, tags); + }; + } + Result> result; + try { + result = utils.docFinder().trySearch(holder, criterion); + } catch (Failure.NotExceptionType + | Failure.ExceptionTypeNotFound + | Failure.UnsupportedTypeParameter x) { + // Here's why we do this ugly exception processing: the language does not allow us to + // instantiate the exception type parameter in criterion with a union of specific + // exceptions (i.e. Failure.ExceptionTypeNotFound | Failure.NotExceptionType), + // so we instantiate it with a general Failure. We then refine the specific + // exception being thrown, from the general exception we caught. + throw x; + } catch (Failure f) { + throw newAssertionError(f); + } + if (result instanceof Result.Conclude> c) { + return c.value(); + } + return Map.of(); // an empty map is effectively ordered } - private Map expand(ThrowsTree tag, - ExecutableElement e, - TagletWriter writer) { + private static Result> toResult(Element target, + ExecutableElement holder, + List tags) { + if (!tags.isEmpty()) { + // if there are tags for the target exception type, conclude search successfully + return Result.CONCLUDE(toExceptionTags(holder, tags)); + } + return Result.CONTINUE(); +// TODO: reintroduce this back for JDK-8295800: +// if (holder.getThrownTypes().contains(target.asType())) { +// // if there are no tags for the target exception type, BUT that type is +// // mentioned in the `throws` clause, continue search +// return Result.CONTINUE(); +// } +// // there are no tags for the target exception type AND that type is not +// // mentioned in the `throws` clause, skip search on the remaining part +// // of the current branch of the hierarchy +// // TODO: expand on this and add a test in JDK-8295800; +// // for both checked and unchecked exceptions +// return Result.SKIP(); + } - // This method uses Map.of() to create maps of size zero and one. - // While such maps are effectively ordered, the syntax is more - // compact than that of LinkedHashMap. + /* + * Associates exception tags with their holder. + * + * Such a map is used as a data structure to pass around methods that output tags to content. + */ + private static Map toExceptionTags(ExecutableElement holder, + List tags) + { + // preserve the tag order using the linked hash map + var map = new LinkedHashMap(); + for (var t : tags) { + var prev = map.put(t, holder); + assert prev == null; // there should be no equal exception tags + } + return map; + } + + private List findByTypeElement(Element targetExceptionType, + ExecutableElement executable) + throws Failure.ExceptionTypeNotFound, + Failure.NotExceptionType + { + var result = new LinkedList(); + for (ThrowsTree t : utils.getThrowsTrees(executable)) { + Element candidate = getExceptionType(t, executable); + if (targetExceptionType.equals(candidate)) { + result.add(t); + } + } + return List.copyOf(result); + } + + /* + * An exception section (that is, the "Throws:" section in the Method + * or Constructor Details section) builder. + * + * The section is being built sequentially from top to bottom. + * + * Adapts one-off methods of writer to continuous building. + */ + private static class ExceptionSectionBuilder { - // peek into @throws description - if (tag.getDescription().stream().noneMatch(d -> d.getKind() == DocTree.Kind.INHERIT_DOC)) { - // nothing to inherit - return Map.of(tag, e); + private final TagletWriter writer; + private final Content result; + private ContentBuilder current; + private boolean began; + private boolean headerAdded; + private TypeMirror exceptionType; + + ExceptionSectionBuilder(TagletWriter writer) { + this.writer = writer; + this.result = writer.getOutputInstance(); + } + + void beginEntry(TypeMirror exceptionType) { + if (began) { + throw new IllegalStateException(); + } + began = true; + current = new ContentBuilder(); + this.exceptionType = exceptionType; + } + + void continueEntry(Content c) { + if (!began) { + throw new IllegalStateException(); + } + current.add(c); + } + + public void endEntry() { + if (!began) { + throw new IllegalStateException(); + } + began = false; + if (!headerAdded) { + headerAdded = true; + result.add(writer.getThrowsHeader()); + } + result.add(writer.throwsTagOutput(exceptionType, current.isEmpty() ? Optional.empty() : Optional.of(current))); + current = null; } - var input = new DocFinder.Input(writer.configuration().utils, e, this, new DocFinder.DocTreeInfo(tag, e), false, true); - var output = DocFinder.search(writer.configuration(), input); - if (output.tagList.size() <= 1) { - // outer code will handle this trivial case of inheritance - return Map.of(tag, e); + + Content build() { + return result; } - if (tag.getDescription().size() > 1) { - // there's more to description than just {@inheritDoc} - // it's likely a documentation error - var ch = writer.configuration().utils.getCommentHelper(e); - writer.configuration().getMessages().error(ch.getDocTreePath(tag), "doclet.inheritDocWithinInappropriateTag"); - return Map.of(); + + // for debugging purposes only + boolean debugEntryBegun() { + return began; } - Map tags = new LinkedHashMap<>(); - output.tagList.forEach(t -> tags.put((ThrowsTree) t, (ExecutableElement) output.holder)); - return tags; } /** - * Inherit throws documentation for exceptions that were declared but not - * documented. + * Returns a map of substitutions for a list of thrown types with the original type-variable + * as a key and the instantiated type as a value. If no types need to be substituted + * an empty map is returned. + * @param declaredThrownTypes the originally declared thrown types. + * @param instantiatedThrownTypes the thrown types in the context of the current type. + * @return map of declared to instantiated thrown types or an empty map. */ - private Content inheritThrowsDocumentation(ExecutableElement holder, - List declaredExceptionTypes, - Set alreadyDocumented, - Map typeSubstitutions, - TagletWriter writer) { - Content result = writer.getOutputInstance(); - if (holder.getKind() != ElementKind.METHOD) { - // (Optimization.) - // Of all executable elements, only methods and constructors are documented. - // Of these two, only methods inherit documentation. - // Don't waste time on constructors. - assert holder.getKind() == ElementKind.CONSTRUCTOR : holder.getKind(); - return result; - } - var utils = writer.configuration().utils; - Map declaredExceptionTags = new LinkedHashMap<>(); - for (TypeMirror declaredExceptionType : declaredExceptionTypes) { - var input = new DocFinder.Input(utils, holder, this, - utils.getTypeName(declaredExceptionType, false)); - DocFinder.Output inheritedDoc = DocFinder.search(writer.configuration(), input); - if (inheritedDoc.tagList.isEmpty()) { - input = new DocFinder.Input(utils, holder, this, - utils.getTypeName(declaredExceptionType, true)); - inheritedDoc = DocFinder.search(writer.configuration(), input); - } - if (!inheritedDoc.tagList.isEmpty()) { - if (inheritedDoc.holder == null) { - inheritedDoc.holder = holder; - } - var h = (ExecutableElement) inheritedDoc.holder; - inheritedDoc.tagList.forEach(t -> declaredExceptionTags.put((ThrowsTree) t, h)); + private Map getSubstitutedThrownTypes(Types types, + List declaredThrownTypes, + List instantiatedThrownTypes) { + Map map = new HashMap<>(); + var i1 = declaredThrownTypes.iterator(); + var i2 = instantiatedThrownTypes.iterator(); + while (i1.hasNext() && i2.hasNext()) { + TypeMirror t1 = i1.next(); + TypeMirror t2 = i2.next(); + if (!types.isSameType(t1, t2)) { + map.put(t1, t2); } } - result.add(throwsTagsOutput(declaredExceptionTags, alreadyDocumented, typeSubstitutions, - writer)); - return result; + // correspondence between types is established positionally, i.e. + // pairwise, which means that the lists must have the same + // number of elements; if they don't, this algorithm is + // broken + assert !i1.hasNext() && !i2.hasNext(); + // copyOf is unordered and this is fine: this map is for queries; + // it doesn't control rendering order + return Map.copyOf(map); } - private Content linkToUndocumentedDeclaredExceptions(List declaredExceptionTypes, - Set alreadyDocumented, - TagletWriter writer) { - // TODO: assert declaredExceptionTypes are instantiated - var utils = writer.configuration().utils; - Content result = writer.getOutputInstance(); - for (TypeMirror declaredExceptionType : declaredExceptionTypes) { - TypeElement te = utils.asTypeElement(declaredExceptionType); - if (te != null && - !alreadyDocumented.contains(declaredExceptionType.toString()) && - !alreadyDocumented.contains(utils.getFullyQualifiedName(te, false))) { - if (alreadyDocumented.isEmpty()) { - result.add(writer.getThrowsHeader()); - } - result.add(writer.throwsTagOutput(declaredExceptionType)); - alreadyDocumented.add(utils.getSimpleName(te)); + private static AssertionError newAssertionError(Object... objects) { + return new AssertionError(Arrays.toString(objects)); + } + + private static String diagnosticDescriptionOf(Element e) { + var name = e instanceof QualifiedNameable q ? q.getQualifiedName() : e.getSimpleName(); + return name + " (" + detailedDescriptionOf(e) + ")"; + } + + private static String detailedDescriptionOf(Element e) { + // It might be important to describe the element in detail. Sometimes + // elements share the same simple and/or qualified name. Outputting + // individual components of that name as well as their kinds helps + // the user disambiguate such elements. + var lowerCasedKind = e.getKind().toString().toLowerCase(Locale.ROOT); + var thisElementDescription = lowerCasedKind + " " + switch (e.getKind()) { + // A package is never enclosed in a package and a module is + // never never enclosed in a module, no matter what their + // qualified name might suggest. Get their qualified + // name directly. Also, unnamed packages and + // modules require special treatment. + case PACKAGE -> { + var p = (PackageElement) e; + // might use i18n in the future + yield p.isUnnamed() ? "" : p.getQualifiedName(); + } + case MODULE -> { + var m = (ModuleElement) e; + // might use i18n in the future, if there's value in displaying an unnamed module + yield m.isUnnamed() ? "" : m.getQualifiedName(); } + default -> e.getSimpleName(); + }; + if (e.getEnclosingElement() == null) { + return thisElementDescription; } - return result; + var enclosingElementDescription = detailedDescriptionOf(e.getEnclosingElement()); + return enclosingElementDescription + " " + thisElementDescription; } } diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/CommentHelper.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/CommentHelper.java index 0dc2db9a455ed..e3ffdbe3a49b3 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/CommentHelper.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/CommentHelper.java @@ -56,6 +56,7 @@ import com.sun.source.util.SimpleDocTreeVisitor; import com.sun.source.util.TreePath; import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration; +import jdk.javadoc.internal.doclets.toolkit.util.DocFinder.Result; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; @@ -66,6 +67,7 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import java.util.List; +import java.util.Optional; import static com.sun.source.doctree.DocTree.Kind.SEE; import static com.sun.source.doctree.DocTree.Kind.SERIAL_FIELD; @@ -125,6 +127,7 @@ public String getParameterName(ParamTree p) { } Element getElement(ReferenceTree rtree) { + // We need to lookup type variables and other types Utils utils = configuration.utils; // likely a synthesized tree if (path == null) { @@ -533,12 +536,15 @@ public DocTreePath getDocTreePath(DocTree dtree) { private DocTreePath getInheritedDocTreePath(DocTree dtree, ExecutableElement ee) { Utils utils = configuration.utils; - DocFinder.Output inheritedDoc = - DocFinder.search(configuration, - new DocFinder.Input(utils, ee)); - return inheritedDoc.holder == ee + var docFinder = utils.docFinder(); + Optional inheritedDoc = docFinder.search(ee, + (m -> { + Optional optional = utils.getFullBody(m).isEmpty() ? Optional.empty() : Optional.of(m); + return Result.fromOptional(optional); + })).toOptional(); + return inheritedDoc.isEmpty() || inheritedDoc.get().equals(ee) ? null - : utils.getCommentHelper(inheritedDoc.holder).getDocTreePath(dtree); + : utils.getCommentHelper(inheritedDoc.get()).getDocTreePath(dtree); } /** diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/DocFinder.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/DocFinder.java index d20eb8f1c0d0a..b34f6cf41626b 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/DocFinder.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/DocFinder.java @@ -25,239 +25,218 @@ package jdk.javadoc.internal.doclets.toolkit.util; -import java.util.*; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; -import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import com.sun.source.doctree.DocTree; -import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration; -import jdk.javadoc.internal.doclets.toolkit.taglets.InheritableTaglet; - -/** - * Search for the requested documentation. Inherit documentation if necessary. - */ public class DocFinder { - public record DocTreeInfo(DocTree docTree, Element element) { } - - /** - * The class that encapsulates the input. + /* + * A specialized, possibly stateful, function that accepts a method in the + * hierarchy and returns a value that controls the search or throws an + * exception, which terminates the search and transparently bubbles + * up the stack. */ - public static class Input { - - /** - * The element to search documentation from. - */ - public Element element; - - /** - * The taglet to search for documentation on behalf of. Null if we want - * to search for overall documentation. - */ - public InheritableTaglet taglet; - - /** - * The id of the tag to retrieve documentation for. - */ - public String tagId; - - /** - * The tag to retrieve documentation for. This is only used for the - * {@code {@inheritDoc}} tag. - */ - public final DocTreeInfo docTreeInfo; + @FunctionalInterface + public interface Criterion { + Result apply(ExecutableElement method) throws X; + } - /** - * True if we only want to search for the first sentence. - */ - public boolean isFirstSentence; + private final Function overriddenMethodLookup; + private final BiFunction> implementedMethodsLookup; - /** - * True if we are looking for documentation to replace the {@code {@inheritDoc}} tag. - */ - public boolean isInheritDocTag; + DocFinder(Function overriddenMethodLookup, + BiFunction> implementedMethodsLookup) { + this.overriddenMethodLookup = overriddenMethodLookup; + this.implementedMethodsLookup = implementedMethodsLookup; + } - /** - * Used to distinguish between type variable param tags and regular - * param tags. - */ - public boolean isTypeVariableParamTag; + @SuppressWarnings("serial") + public static final class NoOverriddenMethodsFound extends Exception { - public final Utils utils; + // only DocFinder should instantiate this exception + private NoOverriddenMethodsFound() { } + } - public Input(Utils utils, - Element element, - InheritableTaglet taglet, - String tagId) { - this(utils, element); - this.taglet = taglet; - this.tagId = tagId; - } + public Result search(ExecutableElement method, + Criterion criterion) + throws X + { + return search(method, true, criterion); + } - public Input(Utils utils, - Element element, - InheritableTaglet taglet, - String tagId, - boolean isTypeVariableParamTag) { - this(utils, element); - this.taglet = taglet; - this.tagId = tagId; - this.isTypeVariableParamTag = isTypeVariableParamTag; + public Result search(ExecutableElement method, + boolean includeMethod, + Criterion criterion) + throws X + { + try { + return search0(method, includeMethod, false, criterion); + } catch (NoOverriddenMethodsFound e) { + // should not happen because the exception flag is unset + throw new AssertionError(e); } + } - public Input(Utils utils, Element element, InheritableTaglet taglet) { - this(utils, element); - this.taglet = taglet; - } + public Result trySearch(ExecutableElement method, + Criterion criterion) + throws NoOverriddenMethodsFound, X + { + return search0(method, false, true, criterion); + } - public Input(Utils utils, Element element) { - this.element = Objects.requireNonNull(element); - this.utils = utils; - this.docTreeInfo = new DocTreeInfo(null, null); + /* + * Searches through the overridden methods hierarchy of the provided method. + * + * Depending on how it is instructed, the search begins from either the given + * method or the first method that the given method overrides. The search + * then applies the given criterion to methods it encounters, in the + * hierarchy order, until either of the following happens: + * + * - the criterion concludes the search + * - the criterion throws an exception + * - the hierarchy is exhausted + * + * If the search succeeds, the returned result is of type Conclude. + * Otherwise, the returned result is generally that of the most + * recent call to Criterion::apply. + * + * If the given method overrides no methods (i.e. hierarchy consists of the + * given method only) and the search is instructed to detect that, the + * search terminates with an exception. + */ + private Result search0(ExecutableElement method, + boolean includeMethodInSearch, + boolean throwExceptionIfDoesNotOverride, + Criterion criterion) + throws NoOverriddenMethodsFound, X + { + // if the "overrides" check is requested and does not pass, throw the exception + // first so that it trumps the result that the search would otherwise had + Iterator methods = methodsOverriddenBy(method); + if (throwExceptionIfDoesNotOverride && !methods.hasNext() ) { + throw new NoOverriddenMethodsFound(); } - - public Input(Utils utils, - Element element, - InheritableTaglet taglet, - DocTreeInfo dtInfo, - boolean isFirstSentence, - boolean isInheritDocTag) { - this.utils = utils; - this.element = Objects.requireNonNull(element); - this.taglet = taglet; - this.isFirstSentence = isFirstSentence; - this.isInheritDocTag = isInheritDocTag; - this.docTreeInfo = dtInfo; + Result r = includeMethodInSearch ? criterion.apply(method) : Result.CONTINUE(); + if (!(r instanceof Result.Continue)) { + return r; } - - private Input copy() { - var copy = new Input(utils, element, taglet, docTreeInfo, - isFirstSentence, isInheritDocTag); - copy.tagId = tagId; - copy.isTypeVariableParamTag = isTypeVariableParamTag; - return copy; + while (methods.hasNext()) { + ExecutableElement m = methods.next(); + r = search0(m, true, false /* don't check for overrides */, criterion); + if (r instanceof Result.Conclude) { + return r; + } } + return r; + } - /** - * For debugging purposes. - */ - @Override - public String toString() { - String encl = element == null ? "" : element.getEnclosingElement().toString() + "::"; - return "Input{" + "element=" + encl + element - + ", taglet=" + taglet - + ", tagId=" + tagId + ", tag=" + docTreeInfo - + ", isFirstSentence=" + isFirstSentence - + ", isInheritDocTag=" + isInheritDocTag - + ", isTypeVariableParamTag=" + isTypeVariableParamTag - + ", utils=" + utils + '}'; + // We see both overridden and implemented methods as overridden + // (see JLS 8.4.8.1. Overriding (by Instance Methods)) + private Iterator methodsOverriddenBy(ExecutableElement method) { + // TODO: create a lazy iterator if required + var list = new ArrayList(); + ExecutableElement overridden = overriddenMethodLookup.apply(method); + if (overridden != null) { + list.add(overridden); } + implementedMethodsLookup.apply(method, method).forEach(list::add); + return list.iterator(); } - /** - * The class that encapsulates the output. + private static final Result SKIP = new Skipped<>(); + private static final Result CONTINUE = new Continued<>(); + + /* + * Use static factory methods to get the desired result to return from + * Criterion. Use instanceof to check for a result type returned from + * a search. If a use case permits and you prefer Optional API, use + * the fromOptional/toOptional convenience methods to get and + * check for the result respectively. */ - public static class Output { + public sealed interface Result { - /** - * The tag that holds the documentation. Null if documentation - * is not held by a tag. - */ - public DocTree holderTag; + sealed interface Skip extends Result permits Skipped { } - /** - * The element that holds the documentation. + sealed interface Continue extends Result permits Continued { } + + sealed interface Conclude extends Result permits Concluded { + + T value(); + } + + /* + * Skips the search on the part of the hierarchy above the method for + * which this result is returned and continues the search from that + * method sibling, if any. */ - public Element holder; + @SuppressWarnings("unchecked") + static Result SKIP() { + return (Result) SKIP; + } - /** - * The inherited documentation. + /* + * Continues the search. */ - public List inlineTags = List.of(); + @SuppressWarnings("unchecked") + static Result CONTINUE() { + return (Result) CONTINUE; + } - /** - * False if documentation could not be inherited. + /* + * Concludes the search with the given result. */ - public boolean isValidInheritDocTag = true; + static Result CONCLUDE(T value) { + return new Concluded<>(value); + } - /** - * When automatically inheriting throws tags, you sometimes must inherit - * more than one tag. For example, if a method declares that it throws - * IOException and the overridden method has {@code @throws} tags for IOException and - * ZipException, both tags would be inherited because ZipException is a - * subclass of IOException. This allows multiple tag inheritance. + /* + * Translates this Result into Optional. + * + * Convenience method. Call on the result of a search if you are only + * interested in whether the search succeeded or failed and you + * prefer the Optional API. */ - public final List tagList = new ArrayList<>(); + default Optional toOptional() { + return Optional.empty(); + } - /** - * For debugging purposes. + /* + * Translates the given Optional into a binary decision whether to + * conclude the search or continue it. + * + * Convenience method. Use in Criterion that can easily provide + * suitable Optional. Don't use if Criterion needs to skip. */ - @Override - public String toString() { - String encl = holder == null ? "" : holder.getEnclosingElement().toString() + "::"; - return "Output{" + "holderTag=" + holderTag - + ", holder=" + encl + holder - + ", inlineTags=" + inlineTags - + ", isValidInheritDocTag=" + isValidInheritDocTag - + ", tagList=" + tagList + '}'; + static Result fromOptional(Optional optional) { + return optional.map(Result::CONCLUDE).orElseGet(Result::CONTINUE); } } - /** - * Search for the requested comments in the given element. If it does not - * have comments, return the inherited comments if possible. - * - * @param input the input object used to perform the search. - * - * @return an Output object representing the documentation that was found. - */ - public static Output search(BaseConfiguration configuration, Input input) { - Output output = new Output(); - Utils utils = configuration.utils; - if (input.isInheritDocTag) { - //Do nothing because "element" does not have any documentation. - //All it has is {@inheritDoc}. - } else if (input.taglet == null) { - //We want overall documentation. - output.inlineTags = input.isFirstSentence - ? utils.getFirstSentenceTrees(input.element) - : utils.getFullBody(input.element); - output.holder = input.element; - } else { - input.taglet.inherit(input, output); - } + // Note: we hide records behind interfaces, as implementation detail. + // We don't directly implement Result with these records because it + // would require more exposure and commitment than is desired. For + // example, there would need to be public constructors, which + // would circumvent static factory methods. + + private record Skipped() implements DocFinder.Result.Skip { } - if (!output.inlineTags.isEmpty()) { - return output; + private record Continued() implements DocFinder.Result.Continue { } + + private record Concluded(T value) implements DocFinder.Result.Conclude { + + Concluded { + Objects.requireNonNull(value); } - output.isValidInheritDocTag = false; - Input inheritedSearchInput = input.copy(); - inheritedSearchInput.isInheritDocTag = false; - if (utils.isMethod(input.element)) { - ExecutableElement m = (ExecutableElement) input.element; - ExecutableElement overriddenMethod = utils.overriddenMethod(m); - if (overriddenMethod != null) { - inheritedSearchInput.element = overriddenMethod; - output = search(configuration, inheritedSearchInput); - output.isValidInheritDocTag = true; - if (!output.inlineTags.isEmpty()) { - return output; - } - } - TypeElement encl = utils.getEnclosingTypeElement(input.element); - VisibleMemberTable vmt = configuration.getVisibleMemberTable(encl); - List implementedMethods = vmt.getImplementedMethods(m); - for (ExecutableElement implementedMethod : implementedMethods) { - inheritedSearchInput.element = implementedMethod; - output = search(configuration, inheritedSearchInput); - output.isValidInheritDocTag = true; - if (!output.inlineTags.isEmpty()) { - return output; - } - } + + @Override + public Optional toOptional() { + return Optional.of(value); } - return output; } } diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Utils.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Utils.java index 10d06848e9f8d..ee6ea2906d9f2 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Utils.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Utils.java @@ -138,6 +138,7 @@ public class Utils { public final Types typeUtils; public final Comparators comparators; private final JavaScriptScanner javaScriptScanner; + private final DocFinder docFinder = newDocFinder(); public Utils(BaseConfiguration c) { configuration = c; @@ -1914,6 +1915,9 @@ public String getSimpleName(Element e) { private SimpleElementVisitor14 snvisitor = null; + // If `e` is a static nested class, this method will return e's simple name + // preceded by `.` and an outer type; this is not how JLS defines "simple + // name". See "Simple Name", "Qualified Name", "Fully Qualified Name". private String getSimpleName0(Element e) { if (snvisitor == null) { snvisitor = new SimpleElementVisitor14<>() { @@ -1927,7 +1931,7 @@ public String visitType(TypeElement e, Void p) { StringBuilder sb = new StringBuilder(e.getSimpleName().toString()); Element enclosed = e.getEnclosingElement(); while (enclosed != null - && (enclosed.getKind().isClass() || enclosed.getKind().isInterface())) { + && (enclosed.getKind().isDeclaredType())) { sb.insert(0, enclosed.getSimpleName() + "."); enclosed = enclosed.getEnclosingElement(); } @@ -2796,4 +2800,16 @@ public interface PreviewFlagProvider { boolean isPreview(Element el); } + public DocFinder docFinder() { + return docFinder; + } + + private DocFinder newDocFinder() { + return new DocFinder(this::overriddenMethod, this::implementedMethods); + } + + private Iterable implementedMethods(ExecutableElement originalMethod, ExecutableElement m) { + var type = configuration.utils.getEnclosingTypeElement(m); + return configuration.getVisibleMemberTable(type).getImplementedMethods(originalMethod); + } } diff --git a/test/langtools/jdk/javadoc/doclet/testHtmlDefinitionListTag/TestHtmlDefinitionListTag.java b/test/langtools/jdk/javadoc/doclet/testHtmlDefinitionListTag/TestHtmlDefinitionListTag.java index cf4c4bc33762e..a778de81727d4 100644 --- a/test/langtools/jdk/javadoc/doclet/testHtmlDefinitionListTag/TestHtmlDefinitionListTag.java +++ b/test/langtools/jdk/javadoc/doclet/testHtmlDefinitionListTag/TestHtmlDefinitionListTag.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2022, 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 @@ -185,7 +185,7 @@ void checkCommentDeprecated(boolean expectFound) {
Throws:
java.lang.IllegalArgumentException - if the owner's GraphicsConfiguration is not from a screen device
-
HeadlessException
+
java.awt.HeadlessException
""", """
@@ -304,7 +304,7 @@ void checkNoDeprecated() {
Throws:
java.lang.IllegalArgumentException - if the owner's GraphicsConfiguration is not from a screen device
-
HeadlessException
+
java.awt.HeadlessException
""", """
diff --git a/test/langtools/jdk/javadoc/doclet/testHtmlDefinitionListTag/pkg1/C1.java b/test/langtools/jdk/javadoc/doclet/testHtmlDefinitionListTag/pkg1/C1.java index 099eec8eec0b1..36fb094d4914c 100644 --- a/test/langtools/jdk/javadoc/doclet/testHtmlDefinitionListTag/pkg1/C1.java +++ b/test/langtools/jdk/javadoc/doclet/testHtmlDefinitionListTag/pkg1/C1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2022, 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 @@ -74,7 +74,7 @@ public static enum ModalExclusionType { * @param test boolean value * @exception IllegalArgumentException if the owner's * GraphicsConfiguration is not from a screen device - * @exception HeadlessException + * @exception java.awt.HeadlessException */ public C1(String title, boolean test) { diff --git a/test/langtools/jdk/javadoc/doclet/testTagInheritance/TestTagInheritance.java b/test/langtools/jdk/javadoc/doclet/testTagInheritance/TestTagInheritance.java index fece105d6ba2f..67fecce52aa06 100644 --- a/test/langtools/jdk/javadoc/doclet/testTagInheritance/TestTagInheritance.java +++ b/test/langtools/jdk/javadoc/doclet/testTagInheritance/TestTagInheritance.java @@ -54,7 +54,7 @@ public void test() { + "does not override or implement any method."); //Test valid usage of inheritDoc tag. - for (int i = 1; i < 40; i++) { + for (int i = 1; i < 39; i++) { checkOutput("pkg/TestTagInheritance.html", true, "Test " + i + " passes"); } diff --git a/test/langtools/jdk/javadoc/doclet/testTagInheritance/pkg/TestAbstractClass.java b/test/langtools/jdk/javadoc/doclet/testTagInheritance/pkg/TestAbstractClass.java index 257057ead3574..095e655e03410 100644 --- a/test/langtools/jdk/javadoc/doclet/testTagInheritance/pkg/TestAbstractClass.java +++ b/test/langtools/jdk/javadoc/doclet/testTagInheritance/pkg/TestAbstractClass.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2003, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2022, 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 @@ -66,7 +66,6 @@ public String testAbstractClass_method2(int p1, int p2) throws java.io.IOExcepti * @param Test 33 passes. * @param x2 Test 35 passes. * @throws java.io.IOException Test 37 passes. - * @throws java.util.zip.ZipException Test 39 passes. */ public String testSuperSuperMethod2(int x1, int x2) { return null; diff --git a/test/langtools/jdk/javadoc/doclet/testThrowsInheritance/pkg/Abstract.java b/test/langtools/jdk/javadoc/doclet/testThrowsInheritance/pkg/Abstract.java index 3e23200f2b8d4..212f5a2ab790e 100644 --- a/test/langtools/jdk/javadoc/doclet/testThrowsInheritance/pkg/Abstract.java +++ b/test/langtools/jdk/javadoc/doclet/testThrowsInheritance/pkg/Abstract.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2022, 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 @@ -29,4 +29,22 @@ public abstract class Abstract { */ abstract void method() throws NullPointerException; + // NOTE: Not sure why this test suggests that IndexOutOfBoundsException + // should not appear due to compatibility with some buggy behavior. + // + // Here's the expected behavior: documentation for an exception X is never + // inherited by an overrider unless it "pulls" it by either (or both) + // of these: + // + // * tag: + // @throws X {@inheritDoc} + // * clause: + // throws ..., X,... + // + // Neither of those are applicable here. Even taking into account + // mechanisms such as the one introduced in 4947455, neither of + // NullPointerException and IndexOutOfBoundsException is a subclass + // of the other. + // + // So, IndexOutOfBoundsException should not appear in Extender. } diff --git a/test/langtools/jdk/javadoc/doclet/testThrowsInheritanceMatching/TestExceptionTypeMatching.java b/test/langtools/jdk/javadoc/doclet/testThrowsInheritanceMatching/TestExceptionTypeMatching.java new file mode 100644 index 0000000000000..73f75580da128 --- /dev/null +++ b/test/langtools/jdk/javadoc/doclet/testThrowsInheritanceMatching/TestExceptionTypeMatching.java @@ -0,0 +1,490 @@ +/* + * Copyright (c) 2022, 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 8291869 + * @library /tools/lib ../../lib + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * jdk.javadoc/jdk.javadoc.internal.tool + * @build toolbox.ToolBox javadoc.tester.* + * @run main TestExceptionTypeMatching + */ + +import javadoc.tester.JavadocTester; +import toolbox.ToolBox; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/* + * The goal of the tests in this suite is two-fold: + * + * 1. Provoke javadoc into treating like-named but different elements as + * the same element + * 2. Provoke javadoc into treating differently named but semantically + * same elements as different elements + */ +public class TestExceptionTypeMatching extends JavadocTester { + + public static void main(String... args) throws Exception { + var tester = new TestExceptionTypeMatching(); + tester.runTests(m -> new Object[]{Paths.get(m.getName())}); + } + + private final ToolBox tb = new ToolBox(); + + /* + * In Child, MyException is c.MyException, whereas in Parent, MyException + * is p.MyException. Those are different exceptions which happen to + * share the simple name. + */ + @Test + public void testDifferentPackages(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package c; + + import p.Parent; + + public class Child extends Parent { + + /** @throws MyException {@inheritDoc} */ + @Override + public void m() { } + } + """, """ + package c; + + public class MyException extends RuntimeException { } + + """, """ + package p; + + public class Parent { + + /** @throws MyException sometimes */ + public void m() { } + } + """, """ + package p; + + public class MyException extends RuntimeException { } + """); + javadoc("-d", base.resolve("out").toString(), "-sourcepath", src.toString(), "c", "p"); + checkExit(Exit.OK); + checkOutput(Output.OUT, true, """ + Child.java:7: warning: overridden methods do not document exception type c.MyException \ + (module package c class MyException) + /** @throws MyException {@inheritDoc} */ + ^ + """); + } + + /* + * Type parameters declared by methods where one of the methods overrides + * the other, are matched by position, not by name. In this example,

+ * and are semantically the same. + */ + @Test + public void testDifferentTypeVariables1(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package x; + + public class Parent { + + /** @throws P sometimes */ + public

void m() { } + } + """, """ + package x; + + public class Child extends Parent { + + /** @throws R {@inheritDoc} */ + @Override + public void m() { } + } + """); + javadoc("-d", base.resolve("out").toString(), "-sourcepath", src.toString(), "x"); + checkExit(Exit.OK); + checkOutput("x/Child.html", true, """ +

+
Overrides:
+
m in class \ + Parent
+
Throws:
+
R - sometimes
+
+ """); + } + + /* + * Type parameters declared by methods where one of the methods overrides + * the other, are matched by position, not by name. + * + * Here the match is criss-cross: + * + * - Child.m's corresponds to Parent.m's + * - Child.m's corresponds to Parent.m's + */ + @Test + public void testDifferentTypeVariables2(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package x; + + public class Parent { + + /** + * @throws K some of the times + * @throws V other times + */ + public void m() { } + } + """, """ + package x; + + public class Child extends Parent { + + /** + * @throws K {@inheritDoc} + * @throws V {@inheritDoc} + */ + @Override + public void m() { } + } + """); + javadoc("-d", base.resolve("out").toString(), "-sourcepath", src.toString(), "x"); + checkExit(Exit.OK); + checkOutput("x/Child.html", true, """ +
+
Overrides:
+
m in class \ + Parent
+
Throws:
+
K - other times
+
V - some of the times
+
+ """); + } + + /* + * X is unknown to Child.m as it isn't defined by Child.m and + * type parameters declared by methods are not inherited. + */ + @Test + public void testUndefinedTypeParameter(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package x; + + public class Parent { + + /** @throws T sometimes */ + public void m() { } + } + """, """ + package x; + + public class Child extends Parent { + + /** @throws T {@inheritDoc} */ + @Override + public void m() { } + } + """); + // turn off DocLint so that it does not interfere with diagnostics + // by raising an error for the condition we are testing: + // + // Child.java:5: error: reference not found + // /** @throws T {@inheritDoc} */ + // ^ + javadoc("-Xdoclint:none", "-d", base.resolve("out").toString(), "-sourcepath", src.toString(), "x"); + checkExit(Exit.OK); + checkOutput(Output.OUT, true, """ + Child.java:5: warning: cannot find exception type by name + /** @throws T {@inheritDoc} */ + ^ + """); + } + + // A related (but separate from this test suite) test. This test is + // introduced here because it tests for the error condition that is + // detected by JDK-8291869, which is tested by tests in this test + // suite. + // TODO: consider moving this test to a more suitable test suite. + @Test + public void testWrongType(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package x; + + public class MyClass { + + /** @throws OtherClass description */ + public void m() { } + } + """, """ + package x; + + public class OtherClass { } + """); + // turn off DocLint so that it does not interfere with diagnostics + // by raising an error for the condition we are testing + javadoc("-Xdoclint:none", "-d", base.resolve("out").toString(), "-sourcepath", src.toString(), "x"); + checkExit(Exit.OK); + checkOutput(Output.OUT, true, """ + MyClass.java:5: warning: not an exception type: \ + x.OtherClass (module package x class OtherClass) + /** @throws OtherClass description */ + ^ + """); + checkOutput("x/MyClass.html", false, """ +
+
Throws:
+
OtherClass - description
+
+ """); + } + + // A related (but separate from this test suite) test. This test is + // introduced here because it tests for the error condition that is + // detected by JDK-8291869, which is tested by tests in this test + // suite. + // TODO: consider moving this test to a more suitable test suite. + @Test + public void testExceptionTypeNotFound(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package x; + + public class MyClass { + + /** @throws un1queEn0ughS0asT0N0tBeF0und description */ + public void m() { } + } + """); + // turn off DocLint so that it does not interfere with diagnostics + // by raising an error for the condition we are testing + javadoc("-Xdoclint:none", "-d", base.resolve("out").toString(), "-sourcepath", src.toString(), "x"); + checkExit(Exit.OK); + checkOutput(Output.OUT, true, """ + MyClass.java:5: warning: cannot find exception type by name + /** @throws un1queEn0ughS0asT0N0tBeF0und description */ + ^ + """); + } + + /* + * In Child, R is a class residing in an unnamed package, whereas + * in Parent, R is a type variable. + */ + @Test + public void testTypeAndTypeParameter(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + public class Parent { + + /** @throws R sometimes */ + public void m() { } + } + """, """ + public class Child extends Parent { + + /** @throws R {@inheritDoc} */ + @Override public void m() { } + } + """, """ + public class R extends RuntimeException { } + """); + javadoc("-d", base.resolve("out").toString(), src.resolve("Parent.java").toString(), + src.resolve("Child.java").toString(), src.resolve("R.java").toString()); + checkExit(Exit.OK); + checkOutput(Output.OUT, true, """ + Child.java:3: warning: overridden methods do not document exception type R \ + (module package class R) + /** @throws R {@inheritDoc} */ + ^ + """); + checkOutput("Child.html", false, """ +
+
Overrides:
+
m in class \ + Parent
+
Throws:
+
R - sometimes
+
"""); + checkOutput("Child.html", false, """ +
+
Overrides:
+
m in class \ + Parent
+
Throws:
+
R - sometimes
+
"""); + } + + /* + * There are two different exceptions that share the same simple name: + * + * 1. P.MyException (a nested static class in an unnamed package) + * 2. P.MyException (a public class in the P package) + * + * Although unconventional, it is not prohibited for a package name to + * start with an upper case letter. This test disregards that + * convention for the setup to work: the package and the + * class should have the same FQN to be confusing. + * + * A permissible but equally unconventional alternative would be to + * keep the package lower-case but give the class a lower-case name p. + * + * This setup works likely because of JLS 6.3. Scope of a Declaration: + * + * The scope of a top level class or interface (7.6) is all class + * and interface declarations in the package in which the top + * level class or interface is declared. + */ + @Test + public void testOuterClassAndPackage(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package P; + + public class MyException extends RuntimeException { } + """, """ + package pkg; + + public class Parent { + + /** @throws P.MyException sometimes */ + public void m() { } + } + """, """ + public class Child extends pkg.Parent { + + /** @throws P.MyException {@inheritDoc} */ + @Override + public void m() { } + } + """, """ + public class P { + public static class MyException extends RuntimeException { } + } + """); + setAutomaticCheckLinks(false); // otherwise the link checker reports that P.MyException is defined twice + // (tracked by 8297085) + javadoc("-d", + base.resolve("out").toString(), + src.resolve("P").resolve("MyException.java").toString(), + src.resolve("pkg").resolve("Parent.java").toString(), + src.resolve("Child.java").toString(), + src.resolve("P.java").toString()); + checkExit(Exit.OK); + checkOutput(Output.OUT, true, """ + Child.java:3: warning: overridden methods do not document exception type P.MyException \ + (module package class P class MyException) + /** @throws P.MyException {@inheritDoc} */ + ^ + """); + checkOutput("Child.html", false, """ +
+
Overrides:
+
m in class \ + Parent
+
Throws:
+
P.MyException - sometimes
+
"""); + checkOutput("Child.html", false, "P/MyException.html"); + } + + /* + * It's unclear how to match type parameters that aren't declared by + * a method. For example, consider that for B to be a subtype of A, + * it is not necessary for A and B to have the same number or + * types of type parameters. + * + * For that reason, exception documentation inheritance involving + * such parameters is currently unsupported. This test simply + * checks that we produce helpful warnings. + */ + @Test + public void testGenericTypes(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package x; + + public class Parent { + + /** @throws T description */ + public void m() { } + } + """, """ + package x; + + public class Child1 extends Parent { + + /** @throws T {@inheritDoc} */ + @Override public void m() { } + } + """, """ + package x; + + public class Child2 extends Parent { + + /** @throws T {@inheritDoc} */ + @Override public void m() { } + } + """, """ + package x; + + public class Child3 extends Parent { + + /** @throws NullPointerException {@inheritDoc} */ + @Override public void m() { } + } + """); + javadoc("-d", base.resolve("out").toString(), "-sourcepath", src.toString(), "x"); + checkExit(Exit.OK); + checkOutput(Output.OUT, true, """ + Child1.java:5: warning: @inheritDoc is not supported for exception-type type parameters \ + that are not declared by a method; document such exception types directly + /** @throws T {@inheritDoc} */ + ^ + """); + checkOutput(Output.OUT, true, """ + Child2.java:5: warning: @inheritDoc is not supported for exception-type type parameters \ + that are not declared by a method; document such exception types directly + /** @throws T {@inheritDoc} */ + ^ + """); + checkOutput(Output.OUT, true, """ + Child3.java:5: warning: overridden methods do not document exception type java.lang.NullPointerException \ + (module java.base package java.lang class NullPointerException) + /** @throws NullPointerException {@inheritDoc} */ + ^ + """); + } +} diff --git a/test/langtools/jdk/javadoc/doclet/testThrowsInheritanceMultiple/TestOneToMany.java b/test/langtools/jdk/javadoc/doclet/testThrowsInheritanceMultiple/TestOneToMany.java index 7f8d1c2eaa5d5..f33539f80fa10 100644 --- a/test/langtools/jdk/javadoc/doclet/testThrowsInheritanceMultiple/TestOneToMany.java +++ b/test/langtools/jdk/javadoc/doclet/testThrowsInheritanceMultiple/TestOneToMany.java @@ -23,7 +23,7 @@ /* * @test - * @bug 8067757 6509045 + * @bug 8067757 6509045 8295277 * @library /tools/lib ../../lib * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main @@ -564,4 +564,178 @@ public interface I1 extends I { ^ """); } + + @Test + public void testDeeperError(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package x; + + public class MyRuntimeException extends RuntimeException { } + """, """ + package x; + + public interface I { + + /** + * @throws MyRuntimeException sometimes + * @throws MyRuntimeException rarely + */ + void m(); + } + """, """ + package x; + + public interface I1 extends I { + + /** + * @throws MyRuntimeException "{@inheritDoc}" + */ + @Override + void m(); + } + """, """ + package x; + + public interface I2 extends I1 { + + /** + * @throws MyRuntimeException '{@inheritDoc}' + */ + @Override + void m(); + } + """); + javadoc("-d", base.resolve("out").toString(), + "-sourcepath", src.toString(), + "x"); + checkExit(Exit.ERROR); + new OutputChecker(Output.OUT) + .setExpectFound(true) + .checkAnyOf( + """ + I2.java:6: error: @inheritDoc cannot be used within this tag + * @throws MyRuntimeException '{@inheritDoc}' + ^""", + """ + I1.java:6: error: @inheritDoc cannot be used within this tag + * @throws MyRuntimeException "{@inheritDoc}" + ^"""); + } + + @Test + public void testFullExpansion(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package x; + + public class MyRuntimeException extends RuntimeException { } + """, """ + package x; + + public interface Child extends Parent { + + /** + * @throws MyRuntimeException child 1 + * @throws MyRuntimeException {@inheritDoc} + */ + @Override void m(); + } + """, """ + package x; + + public interface Parent extends GrandParent { + + /** + * @throws MyRuntimeException parent 1 + * @throws MyRuntimeException {@inheritDoc} + */ + @Override void m(); + } + """, """ + package x; + + public interface GrandParent { + + /** + * @throws MyRuntimeException grandparent 1 + * @throws MyRuntimeException grandparent 2 + */ + void m(); + } + """); + javadoc("-d", base.resolve("out").toString(), + "-sourcepath", src.toString(), + "x"); + checkExit(Exit.OK); + checkOutput("x/Child.html", true, """ +
+
Specified by:
+
m in interface GrandParent
+
Specified by:
+
m in interface Parent
+
Throws:
+
MyRuntimeException - child 1
+
MyRuntimeException - parent 1
+
MyRuntimeException - grandparent 1
+
MyRuntimeException - grandparent 2
+
+ + """); + } + + @Test + public void testChainEmbeddedInheritDoc(Path base) throws Exception { + var src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package x; + + public class MyRuntimeException extends RuntimeException { } + """, """ + package x; + + public interface Child extends Parent { + + /** + * @throws MyRuntimeException "{@inheritDoc}" + */ + @Override void m(); + } + """, """ + package x; + + public interface Parent extends GrandParent { + + /** + * @throws MyRuntimeException '{@inheritDoc}' + */ + @Override void m(); + } + """, """ + package x; + + public interface GrandParent { + + /** + * @throws MyRuntimeException grandparent + */ + void m(); + } + """); + javadoc("-d", base.resolve("out").toString(), + "-sourcepath", src.toString(), + "x"); + checkExit(Exit.OK); + checkOutput("x/Child.html", true, """ +
+
Specified by:
+
m in interface GrandParent
+
Specified by:
+
m in interface Parent
+
Throws:
+
MyRuntimeException - "'grandparent'"
+
+ + """); + } } diff --git a/test/langtools/jdk/javadoc/tool/6964914/TestStdDoclet.java b/test/langtools/jdk/javadoc/tool/6964914/TestStdDoclet.java index c809164d37433..f828c5bdf9d49 100644 --- a/test/langtools/jdk/javadoc/tool/6964914/TestStdDoclet.java +++ b/test/langtools/jdk/javadoc/tool/6964914/TestStdDoclet.java @@ -42,7 +42,6 @@ public static void main(String... args) throws Exception { /** * More dummy comments. - * @throws DoesNotExist oops, javadoc does not see this * @see DoesNotExist */ void run() throws Exception {