8285488: Improve DocFinder

8287796: Stop auto-inheriting documentation for subclasses of exceptions whose documentation is inherited
8291869: Match exceptions using types of javax.lang.model, not strings
8288045: Clean up ParamTaglet
8288046: Clean up ThrowsTaglet
8295277: Expand {@inheritDoc} in @throws fully

Reviewed-by: jjg
This commit is contained in:
Pavel Rappo 2022-11-16 09:43:21 +00:00
parent 97ab2c3ea6
commit 499406c764
26 changed files with 1855 additions and 636 deletions

View File

@ -811,17 +811,17 @@ public class TagletWriterImpl extends TagletWriter {
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 class TagletWriterImpl extends TagletWriter {
}
@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> 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

View File

@ -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}

View File

@ -33,6 +33,7 @@ import java.util.LinkedList;
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.Content;
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 @@ public abstract class MemberSummaryBuilder extends AbstractMemberBuilder {
if (property != null && member instanceof ExecutableElement ee) {
configuration.cmtUtils.updatePropertyMethodComment(ee, property);
}
List<? extends DocTree> 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<List<? extends DocTree>> r = docFinder.search((ExecutableElement) member, (m -> {
var firstSentenceTrees = utils.getFirstSentenceTrees(m);
Optional<List<? extends DocTree>> 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));
}

View File

@ -32,11 +32,13 @@ import javax.lang.model.element.ExecutableElement;
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 @@ public class MethodBuilder extends AbstractMemberBuilder {
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<ExecutableElement> 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);
}

View File

@ -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

View File

@ -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.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.Result;
import jdk.javadoc.internal.doclets.toolkit.util.Utils;
/**
@ -61,52 +64,75 @@ public class InheritDocTaglet extends BaseTaglet {
* such tag.</p>
*
* @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<Documentation> 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<? extends DocTree> mainDescription, ExecutableElement method) { }
private static Optional<Documentation> extractMainDescription(ExecutableElement m,
boolean extractFirstSentenceOnly,
Utils utils) {
List<? extends DocTree> 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);
}
}

View File

@ -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 @@ import jdk.javadoc.internal.doclets.toolkit.util.DocFinder;
*/
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<? extends DocTree> inlineTags,
boolean isValidInheritDocTag) {
}
}

View File

@ -28,17 +28,19 @@ package jdk.javadoc.internal.doclets.toolkit.taglets;
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 class ParamTaglet extends BaseTaglet implements InheritableTaglet {
}
@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<? extends Element> 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<? extends Element> 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<ParamTree> tags = input.isTypeVariableParamTag
? utils.getTypeParamTrees(ee)
: utils.getParamTrees(ee);
List<? extends Element> parameters = input.isTypeVariableParamTag
? ee.getTypeParameters()
: ee.getParameters();
Map<String, Integer> 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<String, Integer> 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 @@ public class ParamTaglet extends BaseTaglet implements InheritableTaglet {
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<Documentation> extract(Utils utils, ExecutableElement method, Integer position, boolean typeParam) {
var ch = utils.getCommentHelper(method);
List<ParamTree> tags = typeParam
? utils.getTypeParamTrees(method)
: utils.getParamTrees(method);
List<? extends Element> 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.

View File

@ -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 class ReturnTaglet extends BaseTaglet implements InheritableTaglet {
}
@Override
public void inherit(DocFinder.Input input, DocFinder.Output output) {
Utils utils = input.utils;
CommentHelper ch = utils.getCommentHelper(input.element);
ReturnTree tag = null;
List<? extends ReturnTree> tags = utils.getReturnTrees(input.element);
if (!tags.isEmpty()) {
tag = tags.get(0);
} else {
List<? extends DocTree> 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 class ReturnTaglet extends BaseTaglet implements InheritableTaglet {
@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<? extends ReturnTree> 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 class ReturnTaglet extends BaseTaglet implements InheritableTaglet {
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<? extends DocTree> 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<Documentation> 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<? extends ReturnTree> blockTags = utils.getBlockTags(method, DocTree.Kind.RETURN, ReturnTree.class).stream();
Stream<? extends ReturnTree> 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();
}
}

View File

@ -27,16 +27,17 @@ 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.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 class SeeTaglet extends BaseTaglet implements InheritableTaglet {
}
@Override
public void inherit(DocFinder.Input input, DocFinder.Output output) {
List<? extends SeeTree> 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 class SeeTaglet extends BaseTaglet implements InheritableTaglet {
Utils utils = writer.configuration().utils;
List<? extends SeeTree> 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<Documentation> 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<? extends SeeTree> seeTrees, ExecutableElement method) { }
private static Optional<Documentation> extract(Utils utils, ExecutableElement method) {
List<? extends SeeTree> tags = utils.getSeeTrees(method);
return tags.isEmpty() ? Optional.empty() : Optional.of(new Documentation(tags, method));
}
}

View File

@ -27,16 +27,20 @@ package jdk.javadoc.internal.doclets.toolkit.taglets;
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 @@ public class SimpleTaglet extends BaseTaglet implements InheritableTaglet {
}
@Override
public void inherit(DocFinder.Input input, DocFinder.Output output) {
List<? extends DocTree> 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<? extends DocTree> description, ExecutableElement method) { }
private Optional<Documentation> extractFirst(ExecutableElement m, Utils utils) {
List<? extends DocTree> 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;

View File

@ -27,15 +27,16 @@ 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.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 class SpecTaglet extends BaseTaglet implements InheritableTaglet {
}
@Override
public void inherit(Input input, DocFinder.Output output) {
List<? extends SpecTree> 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 class SpecTaglet extends BaseTaglet implements InheritableTaglet {
Utils utils = writer.configuration().utils;
List<? extends SpecTree> 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<Documentation> 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<? extends SpecTree> seeTrees, ExecutableElement method) { }
private static Optional<Documentation> extract(Utils utils, ExecutableElement method) {
List<? extends SpecTree> tags = utils.getSpecTrees(method);
return tags.isEmpty() ? Optional.empty() : Optional.of(new Documentation(tags, method));
}
}

View File

@ -598,7 +598,7 @@ public class TagletManager {
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));

View File

@ -223,25 +223,15 @@ public abstract class TagletWriter {
*/
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> content);
/**
* Returns the output for a {@code {@value}} tag.

View File

@ -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.DocTree;
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);
}
// 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);
}
}
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);
}
@Override
public Content getAllBlockTagOutput(Element holder, TagletWriter writer) {
var utils = writer.configuration().utils;
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
}
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<? extends TypeMirror> thrownTypes = instantiatedType.getThrownTypes();
Map<String, TypeMirror> typeSubstitutions = getSubstitutedThrownTypes(
List<? extends TypeMirror> substitutedExceptionTypes = instantiatedType.getThrownTypes();
List<? extends TypeMirror> originalExceptionTypes = executable.getThrownTypes();
Map<TypeMirror, TypeMirror> typeSubstitutions = getSubstitutedThrownTypes(
utils.typeUtils,
executable.getThrownTypes(),
thrownTypes);
Map<ThrowsTree, ExecutableElement> tagsMap = new LinkedHashMap<>();
utils.getThrowsTrees(executable).forEach(t -> tagsMap.put(t, executable));
Content result = writer.getOutputInstance();
Set<String> 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<TypeMirror> alreadyDocumentedExceptions = new HashSet<>();
List<ThrowsTree> 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<ThrowsTree, ExecutableElement> 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<ThrowsTree, ExecutableElement> 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();
}
private void outputAnExceptionTagDeeply(ExceptionSectionBuilder exceptionSection,
Element originalExceptionElement,
ThrowsTree tag,
ExecutableElement holder,
Set<TypeMirror> alreadyDocumentedExceptions,
Map<TypeMirror, TypeMirror> 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<TypeMirror> alreadyDocumentedExceptions,
Map<TypeMirror, TypeMirror> 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} -> <text>
//
// And here's an example of when we append to the current entry:
//
// ... -> <text> {@inheritDoc} <text> -> <text>
// 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<ThrowsTree, ExecutableElement> 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<ThrowsTree, ExecutableElement> 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();
}
}
}
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();
}
}
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. <X extends Exception>)
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);
}
@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;
}
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(); }
}
}
/*
* 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<ThrowsTree, ExecutableElement> expandShallowly(Element exceptionType,
ExecutableElement holder)
throws Failure.ExceptionTypeNotFound,
Failure.NotExceptionType,
Failure.Invalid,
Failure.UnsupportedTypeParameter,
DocFinder.NoOverriddenMethodsFound
{
ElementKind kind = exceptionType.getKind();
DocFinder.Criterion<Map<ThrowsTree, ExecutableElement>, 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<Map<ThrowsTree, ExecutableElement>> 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<Map<ThrowsTree, ExecutableElement>> c) {
return c.value();
}
return Map.of(); // an empty map is effectively ordered
}
private static Result<Map<ThrowsTree, ExecutableElement>> toResult(Element target,
ExecutableElement holder,
List<ThrowsTree> 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();
}
/*
* 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<ThrowsTree, ExecutableElement> toExceptionTags(ExecutableElement holder,
List<ThrowsTree> tags)
{
// preserve the tag order using the linked hash map
var map = new LinkedHashMap<ThrowsTree, ExecutableElement>();
for (var t : tags) {
var prev = map.put(t, holder);
assert prev == null; // there should be no equal exception tags
}
return map;
}
private List<ThrowsTree> findByTypeElement(Element targetExceptionType,
ExecutableElement executable)
throws Failure.ExceptionTypeNotFound,
Failure.NotExceptionType
{
var result = new LinkedList<ThrowsTree>();
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 {
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;
}
Content build() {
return result;
}
// for debugging purposes only
boolean debugEntryBegun() {
return began;
}
}
/**
* 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
* 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<String, TypeMirror> getSubstitutedThrownTypes(Types types,
List<? extends TypeMirror> declaredThrownTypes,
List<? extends TypeMirror> instantiatedThrownTypes) {
if (!declaredThrownTypes.equals(instantiatedThrownTypes)) {
Map<String, TypeMirror> map = new HashMap<>();
Iterator<? extends TypeMirror> i1 = declaredThrownTypes.iterator();
Iterator<? extends TypeMirror> 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 Map<TypeMirror, TypeMirror> getSubstitutedThrownTypes(Types types,
List<? extends TypeMirror> declaredThrownTypes,
List<? extends TypeMirror> instantiatedThrownTypes) {
Map<TypeMirror, TypeMirror> 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);
}
return map;
}
return Map.of();
// 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);
}
/**
* 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<ThrowsTree, ExecutableElement> throwsTags,
Set<String> alreadyDocumented,
Map<String, TypeMirror> typeSubstitutions,
TagletWriter writer) {
var utils = writer.configuration().utils;
Content result = writer.getOutputInstance();
var documentedInThisCall = new HashSet<String>();
Map<ThrowsTree, ExecutableElement> 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;
}
if (alreadyDocumented.isEmpty() && documentedInThisCall.isEmpty()) {
result.add(writer.getThrowsHeader());
}
result.add(writer.throwsTagOutput(e, t, substituteType));
if (substituteType != null) {
documentedInThisCall.add(substituteType.toString());
} else {
documentedInThisCall.add(te != null
? utils.getFullyQualifiedName(te, false)
: excName);
}
});
alreadyDocumented.addAll(documentedInThisCall);
return result;
private static AssertionError newAssertionError(Object... objects) {
return new AssertionError(Arrays.toString(objects));
}
/*
* A single @throws tag from an overriding method can correspond to multiple
* @throws tags from an overridden method.
*/
private Map<ThrowsTree, ExecutableElement> flatten(Map<ThrowsTree, ExecutableElement> throwsTags,
TagletWriter writer) {
Map<ThrowsTree, ExecutableElement> 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 static String diagnosticDescriptionOf(Element e) {
var name = e instanceof QualifiedNameable q ? q.getQualifiedName() : e.getSimpleName();
return name + " (" + detailedDescriptionOf(e) + ")";
}
private Map<ThrowsTree, ExecutableElement> expand(ThrowsTree tag,
ExecutableElement e,
TagletWriter writer) {
// 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.
// peek into @throws description
if (tag.getDescription().stream().noneMatch(d -> d.getKind() == DocTree.Kind.INHERIT_DOC)) {
// nothing to inherit
return Map.of(tag, e);
}
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);
}
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();
}
Map<ThrowsTree, ExecutableElement> 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.
*/
private Content inheritThrowsDocumentation(ExecutableElement holder,
List<? extends TypeMirror> declaredExceptionTypes,
Set<String> alreadyDocumented,
Map<String, TypeMirror> 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<ThrowsTree, ExecutableElement> 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);
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() ? "<unnamed package>" : p.getQualifiedName();
}
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));
case MODULE -> {
var m = (ModuleElement) e;
// might use i18n in the future, if there's value in displaying an unnamed module
yield m.isUnnamed() ? "<unnamed module>" : m.getQualifiedName();
}
default -> e.getSimpleName();
};
if (e.getEnclosingElement() == null) {
return thisElementDescription;
}
result.add(throwsTagsOutput(declaredExceptionTags, alreadyDocumented, typeSubstitutions,
writer));
return result;
}
private Content linkToUndocumentedDeclaredExceptions(List<? extends TypeMirror> declaredExceptionTypes,
Set<String> 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));
}
}
return result;
var enclosingElementDescription = detailedDescriptionOf(e.getEnclosingElement());
return enclosingElementDescription + " " + thisElementDescription;
}
}

View File

@ -56,6 +56,7 @@ import com.sun.source.util.DocTrees;
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.element.TypeElement;
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 class CommentHelper {
}
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 class CommentHelper {
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<ExecutableElement> inheritedDoc = docFinder.search(ee,
(m -> {
Optional<ExecutableElement> 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);
}
/**

View File

@ -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 {
@FunctionalInterface
public interface Criterion<T, X extends Throwable> {
Result<T> apply(ExecutableElement method) throws X;
}
/**
* The element to search documentation from.
*/
public Element element;
private final Function<ExecutableElement, ExecutableElement> overriddenMethodLookup;
private final BiFunction<ExecutableElement, ExecutableElement, Iterable<ExecutableElement>> implementedMethodsLookup;
/**
* The taglet to search for documentation on behalf of. Null if we want
* to search for overall documentation.
*/
public InheritableTaglet taglet;
DocFinder(Function<ExecutableElement, ExecutableElement> overriddenMethodLookup,
BiFunction<ExecutableElement, ExecutableElement, Iterable<ExecutableElement>> implementedMethodsLookup) {
this.overriddenMethodLookup = overriddenMethodLookup;
this.implementedMethodsLookup = implementedMethodsLookup;
}
/**
* The id of the tag to retrieve documentation for.
*/
public String tagId;
@SuppressWarnings("serial")
public static final class NoOverriddenMethodsFound extends Exception {
/**
* The tag to retrieve documentation for. This is only used for the
* {@code {@inheritDoc}} tag.
*/
public final DocTreeInfo docTreeInfo;
// only DocFinder should instantiate this exception
private NoOverriddenMethodsFound() { }
}
/**
* True if we only want to search for the first sentence.
*/
public boolean isFirstSentence;
public <T, X extends Throwable> Result<T> search(ExecutableElement method,
Criterion<T, X> criterion)
throws X
{
return search(method, true, criterion);
}
/**
* True if we are looking for documentation to replace the {@code {@inheritDoc}} tag.
*/
public boolean isInheritDocTag;
/**
* Used to distinguish between type variable param tags and regular
* param tags.
*/
public boolean isTypeVariableParamTag;
public final Utils utils;
public Input(Utils utils,
Element element,
InheritableTaglet taglet,
String tagId) {
this(utils, element);
this.taglet = taglet;
this.tagId = tagId;
}
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 Input(Utils utils, Element element, InheritableTaglet taglet) {
this(utils, element);
this.taglet = taglet;
}
public Input(Utils utils, Element element) {
this.element = Objects.requireNonNull(element);
this.utils = utils;
this.docTreeInfo = new DocTreeInfo(null, null);
}
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;
}
private Input copy() {
var copy = new Input(utils, element, taglet, docTreeInfo,
isFirstSentence, isInheritDocTag);
copy.tagId = tagId;
copy.isTypeVariableParamTag = isTypeVariableParamTag;
return copy;
}
/**
* 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 + '}';
public <T, X extends Throwable> Result<T> search(ExecutableElement method,
boolean includeMethod,
Criterion<T, X> 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);
}
}
/**
* The class that encapsulates the output.
public <T, X extends Throwable> Result<T> trySearch(ExecutableElement method,
Criterion<T, X> criterion)
throws NoOverriddenMethodsFound, X
{
return search0(method, false, true, criterion);
}
/*
* 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.
*/
public static class Output {
private <T, X extends Throwable> Result<T> search0(ExecutableElement method,
boolean includeMethodInSearch,
boolean throwExceptionIfDoesNotOverride,
Criterion<T, X> 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<ExecutableElement> methods = methodsOverriddenBy(method);
if (throwExceptionIfDoesNotOverride && !methods.hasNext() ) {
throw new NoOverriddenMethodsFound();
}
Result<T> r = includeMethodInSearch ? criterion.apply(method) : Result.CONTINUE();
if (!(r instanceof Result.Continue<T>)) {
return r;
}
while (methods.hasNext()) {
ExecutableElement m = methods.next();
r = search0(m, true, false /* don't check for overrides */, criterion);
if (r instanceof Result.Conclude<T>) {
return r;
}
}
return r;
}
/**
* The tag that holds the documentation. Null if documentation
* is not held by a tag.
*/
public DocTree holderTag;
// We see both overridden and implemented methods as overridden
// (see JLS 8.4.8.1. Overriding (by Instance Methods))
private Iterator<ExecutableElement> methodsOverriddenBy(ExecutableElement method) {
// TODO: create a lazy iterator if required
var list = new ArrayList<ExecutableElement>();
ExecutableElement overridden = overriddenMethodLookup.apply(method);
if (overridden != null) {
list.add(overridden);
}
implementedMethodsLookup.apply(method, method).forEach(list::add);
return list.iterator();
}
/**
* The element that holds the documentation.
*/
public Element holder;
private static final Result<?> SKIP = new Skipped<>();
private static final Result<?> CONTINUE = new Continued<>();
/**
* The inherited documentation.
*/
public List<? extends DocTree> inlineTags = List.of();
/*
* 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 sealed interface Result<T> {
/**
* False if documentation could not be inherited.
*/
public boolean isValidInheritDocTag = true;
sealed interface Skip<T> extends Result<T> permits Skipped { }
/**
* 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.
*/
public final List<DocTree> tagList = new ArrayList<>();
sealed interface Continue<T> extends Result<T> permits Continued { }
/**
* For debugging purposes.
sealed interface Conclude<T> extends Result<T> 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.
*/
@Override
public String toString() {
String encl = holder == null ? "" : holder.getEnclosingElement().toString() + "::";
return "Output{" + "holderTag=" + holderTag
+ ", holder=" + encl + holder
+ ", inlineTags=" + inlineTags
+ ", isValidInheritDocTag=" + isValidInheritDocTag
+ ", tagList=" + tagList + '}';
@SuppressWarnings("unchecked")
static <T> Result<T> SKIP() {
return (Result<T>) SKIP;
}
/*
* Continues the search.
*/
@SuppressWarnings("unchecked")
static <T> Result<T> CONTINUE() {
return (Result<T>) CONTINUE;
}
/*
* Concludes the search with the given result.
*/
static <T> Result<T> CONCLUDE(T value) {
return new Concluded<>(value);
}
/*
* 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.
*/
default Optional<T> toOptional() {
return Optional.empty();
}
/*
* 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.
*/
static <T> Result<T> fromOptional(Optional<T> 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<T>() implements DocFinder.Result.Skip<T> { }
private record Continued<T>() implements DocFinder.Result.Continue<T> { }
private record Concluded<T>(T value) implements DocFinder.Result.Conclude<T> {
Concluded {
Objects.requireNonNull(value);
}
if (!output.inlineTags.isEmpty()) {
return output;
@Override
public Optional<T> toOptional() {
return Optional.of(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<ExecutableElement> implementedMethods = vmt.getImplementedMethods(m);
for (ExecutableElement implementedMethod : implementedMethods) {
inheritedSearchInput.element = implementedMethod;
output = search(configuration, inheritedSearchInput);
output.isValidInheritDocTag = true;
if (!output.inlineTags.isEmpty()) {
return output;
}
}
}
return output;
}
}

View File

@ -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 class Utils {
private SimpleElementVisitor14<String, Void> 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 class Utils {
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 class Utils {
boolean isPreview(Element el);
}
public DocFinder docFinder() {
return docFinder;
}
private DocFinder newDocFinder() {
return new DocFinder(this::overriddenMethod, this::implementedMethods);
}
private Iterable<ExecutableElement> implementedMethods(ExecutableElement originalMethod, ExecutableElement m) {
var type = configuration.utils.getEnclosingTypeElement(m);
return configuration.getVisibleMemberTable(type).getImplementedMethods(originalMethod);
}
}

View File

@ -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 @@ public class TestHtmlDefinitionListTag extends JavadocTester {
<dt>Throws:</dt>
<dd><code>java.lang.IllegalArgumentException</code> - if the <code>owner</code>'s
<code>GraphicsConfiguration</code> is not from a screen device</dd>
<dd><code>HeadlessException</code></dd>
<dd><code>java.awt.HeadlessException</code></dd>
</dl>""",
"""
<dl class="notes">
@ -304,7 +304,7 @@ public class TestHtmlDefinitionListTag extends JavadocTester {
<dt>Throws:</dt>
<dd><code>java.lang.IllegalArgumentException</code> - if the <code>owner</code>'s
<code>GraphicsConfiguration</code> is not from a screen device</dd>
<dd><code>HeadlessException</code></dd>
<dd><code>java.awt.HeadlessException</code></dd>
</dl>""",
"""
<dl class="notes">

View File

@ -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 class C1 implements Serializable {
* @param test boolean value
* @exception IllegalArgumentException if the <code>owner</code>'s
* <code>GraphicsConfiguration</code> is not from a screen device
* @exception HeadlessException
* @exception java.awt.HeadlessException
*/
public C1(String title, boolean test) {

View File

@ -54,7 +54,7 @@ public class TestTagInheritance extends JavadocTester {
+ "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");
}

View File

@ -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 @@ java.lang.NullPointerException {
* @param <Q> Test 33 passes.
* @param x2 Test 35 passes.
* @throws java.io.IOException Test 37 passes.
* @throws java.util.zip.ZipException Test 39 passes.
*/
public <P,Q> String testSuperSuperMethod2(int x1, int x2) {
return null;

View File

@ -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.
}

View File

@ -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 <unnamed 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, <P>
* and <R> 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 <P extends RuntimeException> void m() { }
}
""", """
package x;
public class Child extends Parent {
/** @throws R {@inheritDoc} */
@Override
public <R extends RuntimeException> void m() { }
}
""");
javadoc("-d", base.resolve("out").toString(), "-sourcepath", src.toString(), "x");
checkExit(Exit.OK);
checkOutput("x/Child.html", true, """
<dl class="notes">
<dt>Overrides:</dt>
<dd><code><a href="Parent.html#m()">m</a></code>&nbsp;in class&nbsp;<code>\
<a href="Parent.html" title="class in x">Parent</a></code></dd>
<dt>Throws:</dt>
<dd><code>R</code> - sometimes</dd>
</dl>
""");
}
/*
* 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 <K> corresponds to Parent.m's <V>
* - Child.m's <V> corresponds to Parent.m's <K>
*/
@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 <K extends RuntimeException, V extends RuntimeException> void m() { }
}
""", """
package x;
public class Child extends Parent {
/**
* @throws K {@inheritDoc}
* @throws V {@inheritDoc}
*/
@Override
public <V extends RuntimeException, K extends RuntimeException> void m() { }
}
""");
javadoc("-d", base.resolve("out").toString(), "-sourcepath", src.toString(), "x");
checkExit(Exit.OK);
checkOutput("x/Child.html", true, """
<dl class="notes">
<dt>Overrides:</dt>
<dd><code><a href="Parent.html#m()">m</a></code>&nbsp;in class&nbsp;<code>\
<a href="Parent.html" title="class in x">Parent</a></code></dd>
<dt>Throws:</dt>
<dd><code>K</code> - other times</dd>
<dd><code>V</code> - some of the times</dd>
</dl>
""");
}
/*
* 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 <T extends RuntimeException> 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 <unnamed module> package x class OtherClass)
/** @throws OtherClass description */
^
""");
checkOutput("x/MyClass.html", false, """
<dl class="notes">
<dt>Throws:</dt>
<dd><code><a href="OtherClass.html" title="class in x">OtherClass</a></code> - description</dd>
</dl>
""");
}
// 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 <R extends RuntimeException> 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 <unnamed module> package <unnamed package> class R)
/** @throws R {@inheritDoc} */
^
""");
checkOutput("Child.html", false, """
<dl class="notes">
<dt>Overrides:</dt>
<dd><code><a href="Parent.html#m()">m</a></code>&nbsp;in class&nbsp;<code>\
<a href="Parent.html" title="class in Unnamed Package">Parent</a></code></dd>
<dt>Throws:</dt>
<dd><code><a href="R.html" title="class in Unnamed Package">R</a></code> - sometimes</dd>
</dl>""");
checkOutput("Child.html", false, """
<dl class="notes">
<dt>Overrides:</dt>
<dd><code><a href="Parent.html#m()">m</a></code>&nbsp;in class&nbsp;<code>\
<a href="Parent.html" title="class in Unnamed Package">Parent</a></code></dd>
<dt>Throws:</dt>
<dd><code>R</code> - sometimes</dd>
</dl>""");
}
/*
* 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 <unnamed module> package <unnamed package> class P class MyException)
/** @throws P.MyException {@inheritDoc} */
^
""");
checkOutput("Child.html", false, """
<dl class="notes">
<dt>Overrides:</dt>
<dd><code><a href="pkg/Parent.html#m()">m</a></code>&nbsp;in class&nbsp;<code>\
<a href="pkg/Parent.html" title="class in pkg">Parent</a></code></dd>
<dt>Throws:</dt>
<dd><code><a href="P.MyException.html" title="class in Unnamed Package">P.MyException</a></code> - sometimes</dd>
</dl>""");
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<T extends RuntimeException> {
/** @throws T description */
public void m() { }
}
""", """
package x;
public class Child1<T extends RuntimeException> extends Parent<T> {
/** @throws T {@inheritDoc} */
@Override public void m() { }
}
""", """
package x;
public class Child2<T extends IllegalArgumentException> extends Parent<T> {
/** @throws T {@inheritDoc} */
@Override public void m() { }
}
""", """
package x;
public class Child3 extends Parent<NullPointerException> {
/** @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} */
^
""");
}
}

View File

@ -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 class TestOneToMany extends JavadocTester {
^
""");
}
@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, """
<dl class="notes">
<dt>Specified by:</dt>
<dd><code><a href="GrandParent.html#m()">m</a></code>&nbsp;in interface&nbsp;<code><a href="GrandParent.html" title="interface in x">GrandParent</a></code></dd>
<dt>Specified by:</dt>
<dd><code><a href="Parent.html#m()">m</a></code>&nbsp;in interface&nbsp;<code><a href="Parent.html" title="interface in x">Parent</a></code></dd>
<dt>Throws:</dt>
<dd><code><a href="MyRuntimeException.html" title="class in x">MyRuntimeException</a></code> - child 1</dd>
<dd><code><a href="MyRuntimeException.html" title="class in x">MyRuntimeException</a></code> - parent 1</dd>
<dd><code><a href="MyRuntimeException.html" title="class in x">MyRuntimeException</a></code> - grandparent 1</dd>
<dd><code><a href="MyRuntimeException.html" title="class in x">MyRuntimeException</a></code> - grandparent 2</dd>
</dl>
</section>
""");
}
@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, """
<dl class="notes">
<dt>Specified by:</dt>
<dd><code><a href="GrandParent.html#m()">m</a></code>&nbsp;in interface&nbsp;<code><a href="GrandParent.html" title="interface in x">GrandParent</a></code></dd>
<dt>Specified by:</dt>
<dd><code><a href="Parent.html#m()">m</a></code>&nbsp;in interface&nbsp;<code><a href="Parent.html" title="interface in x">Parent</a></code></dd>
<dt>Throws:</dt>
<dd><code><a href="MyRuntimeException.html" title="class in x">MyRuntimeException</a></code> - "'grandparent'"</dd>
</dl>
</section>
""");
}
}

View File

@ -42,7 +42,6 @@ public class TestStdDoclet {
/**
* More dummy comments.
* @throws DoesNotExist oops, javadoc does not see this
* @see DoesNotExist
*/
void run() throws Exception {