8346109: Create JDK taglet for additional preview notes

Reviewed-by: ihse, liach, rriggs
This commit is contained in:
Hannes Wallnöfer 2025-04-11 13:25:50 +00:00
parent 9ead2b75ce
commit 2321722a45
8 changed files with 320 additions and 62 deletions

View File

@ -79,6 +79,8 @@ JAVADOC_TAGS := \
-tag see \
-taglet build.tools.taglet.ExtLink \
-taglet build.tools.taglet.Incubating \
-taglet build.tools.taglet.PreviewNote \
--preview-note-tag previewNote \
-tagletpath $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes \
$(CUSTOM_JAVADOC_TAGS) \
#

View File

@ -0,0 +1,127 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package build.tools.taglet;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
import com.sun.source.doctree.DocTree;
import com.sun.source.doctree.UnknownInlineTagTree;
import jdk.javadoc.doclet.Doclet;
import jdk.javadoc.doclet.DocletEnvironment;
import jdk.javadoc.doclet.Reporter;
import jdk.javadoc.doclet.StandardDoclet;
import jdk.javadoc.doclet.Taglet;
import static com.sun.source.doctree.DocTree.Kind.UNKNOWN_INLINE_TAG;
/**
* An inline tag to insert a note formatted as preview note.
* The tag can be used as follows:
*
* <pre>
* {&commat;previewNote jep-number [Preview note heading]}
* Preview note content
* {&commat;previewNote}
* </pre>
*
*/
public class PreviewNote implements Taglet {
static final String TAG_NAME = "previewNote";
Reporter reporter = null;
@Override
public void init(DocletEnvironment env, Doclet doclet) {
if (doclet instanceof StandardDoclet stdoclet) {
reporter = stdoclet.getReporter();
}
}
/**
* Returns the set of locations in which the tag may be used.
*/
@Override
public Set<Location> getAllowedLocations() {
return EnumSet.allOf(Taglet.Location.class);
}
@Override
public boolean isInlineTag() {
return true;
}
@Override
public String getName() {
return TAG_NAME;
}
@Override
public String toString(List<? extends DocTree> tags, Element elem) {
for (DocTree tag : tags) {
if (tag.getKind() == UNKNOWN_INLINE_TAG) {
UnknownInlineTagTree inlineTag = (UnknownInlineTagTree) tag;
String[] content = inlineTag.getContent().toString().trim().split("\\s+", 2);
if (!content[0].isBlank()) {
StringBuilder sb = new StringBuilder("""
<div class="preview-block" style="margin-top:10px; display:block; max-width:max-content;">
""");
if (content.length == 2) {
sb.append("""
<div class="preview-label">
""")
.append(content[1])
.append("""
</div>
""");
}
sb.append("""
<div class="preview-comment">
""");
return sb.toString();
} else {
return """
</div>
</div>
""";
}
}
}
if (reporter == null) {
throw new IllegalArgumentException("@" + TAG_NAME + " taglet content must be begin or end");
}
reporter.print(Diagnostic.Kind.ERROR, "@" + TAG_NAME + " taglet content must be begin or end");
return "";
}
}

View File

@ -38,6 +38,7 @@ import jdk.javadoc.internal.doclets.toolkit.util.DocPaths;
import jdk.javadoc.internal.doclets.toolkit.util.PreviewAPIListBuilder;
import jdk.javadoc.internal.html.Content;
import jdk.javadoc.internal.html.ContentBuilder;
import jdk.javadoc.internal.html.HtmlId;
import jdk.javadoc.internal.html.HtmlStyle;
import jdk.javadoc.internal.html.HtmlTree;
import jdk.javadoc.internal.html.Text;
@ -96,6 +97,22 @@ public class PreviewListWriter extends SummaryListWriter<PreviewAPIListBuilder>
}
}
@Override
protected void addExtraSection(Content content) {
var notes = builder.getElementNotes();
if (!notes.isEmpty()) {
addSummaryAPI(notes, HtmlId.of("preview-api-notes"),
"doclet.Preview_Notes_Elements", "doclet.Element", content);
}
}
@Override
protected void addExtraIndexLink(Content target) {
if (!builder.getElementNotes().isEmpty()) {
addIndexLink(HtmlId.of("preview-api-notes"), "doclet.Preview_Notes", target);
}
}
@Override
protected void addComments(Element e, Content desc) {
List<? extends DocTree> tags = utils.getFirstSentenceTrees(e);

View File

@ -136,6 +136,8 @@ doclet.Preview_API_Checkbox_Toggle_All=Toggle all
doclet.Preview_JEP_URL=https://openjdk.org/jeps/{0}
doclet.Preview_Label=Preview
doclet.Preview_Mark=PREVIEW
doclet.Preview_Notes=Preview API Notes
doclet.Preview_Notes_Elements=Elements containing Preview Notes
doclet.Restricted_Methods=Restricted Methods
doclet.Restricted_Mark=RESTRICTED
doclet.searchTag=Search Tag

View File

@ -236,6 +236,14 @@ public abstract class BaseOptions {
*/
private boolean noTimestamp = false;
/**
* Argument for command-line option {@code --preview-note-tag}.
* If set, the JavaDoc inline tag with the given name is considered
* a preview note and added to the preview API page.
*/
private String previewNoteTag = null;
/**
* Argument for command-line option {@code -quiet}.
* Suppress all messages
@ -549,6 +557,14 @@ public abstract class BaseOptions {
}
},
new Hidden(resources, "--preview-note-tag", 1) {
@Override
public boolean process(String option, List<String> args) {
previewNoteTag = args.getFirst();
return true;
}
},
new Hidden(resources, "-quiet") {
@Override
public boolean process(String opt, List<String> args) {
@ -940,6 +956,12 @@ public abstract class BaseOptions {
return noTimestamp;
}
/**
* Argument for command-line option {@code --preview-note-tag}.
* Name of inline tag for preview notes.
*/
public String previewNoteTag() { return previewNoteTag; }
/**
* Argument for command-line option {@code -quiet}.
* Suppress all messages

View File

@ -396,34 +396,6 @@ public class WorkArounds {
return compilerOptions.isSet("accessInternalAPI");
}
/**
* Returns a map containing {@code jdk.internal.javac.PreviewFeature.JEP} element values associated with the
* {@code jdk.internal.javac.PreviewFeature.Feature} enum constant identified by {@code feature}.
*
* This method uses internal javac features (although only reflectively).
*
* @param feature the name of the PreviewFeature.Feature enum value
* @return the map of PreviewFeature.JEP annotation element values, or an empty map
*/
public Map<String, Object> getJepInfo(String feature) {
TypeElement featureType = elementUtils.getTypeElement("jdk.internal.javac.PreviewFeature.Feature");
TypeElement jepType = elementUtils.getTypeElement("jdk.internal.javac.PreviewFeature.JEP");
var featureVar = featureType.getEnclosedElements().stream()
.filter(e -> feature.equals(e.getSimpleName().toString())).findFirst();
if (featureVar.isPresent()) {
for (AnnotationMirror anno : featureVar.get().getAnnotationMirrors()) {
if (anno.getAnnotationType().asElement().equals(jepType)) {
return anno.getElementValues().entrySet()
.stream()
.collect(Collectors.toMap(
e -> e.getKey().getSimpleName().toString(),
e -> e.getValue().getValue()));
}
}
}
return Map.of();
}
/*
* If a similar query is ever added to javax.lang.model, use that instead.
*/

View File

@ -25,15 +25,22 @@
package jdk.javadoc.internal.doclets.toolkit.util;
import com.sun.source.doctree.DocTree;
import com.sun.source.doctree.UnknownInlineTagTree;
import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
/**
* Build list of all the preview packages, classes, constructors, fields and methods.
@ -41,8 +48,9 @@ import java.util.stream.Collectors;
public class PreviewAPIListBuilder extends SummaryAPIListBuilder {
private final Map<Element, JEP> elementJeps = new HashMap<>();
private final SortedSet<Element> elementNotes = createSummarySet();
private final Map<String, JEP> jeps = new HashMap<>();
private static final JEP NULL_SENTINEL = new JEP(0, "", "");
private final String previewNoteTag;
/**
* The JEP for a preview feature in this release.
@ -61,39 +69,78 @@ public class PreviewAPIListBuilder extends SummaryAPIListBuilder {
*/
public PreviewAPIListBuilder(BaseConfiguration configuration) {
super(configuration);
buildSummaryAPIInfo();
this.previewNoteTag = configuration.getOptions().previewNoteTag();
// retrieve preview JEPs
buildPreviewFeatureInfo();
if (!jeps.isEmpty()) {
// map elements to preview JEPs and preview tags
buildSummaryAPIInfo();
// remove unused preview JEPs
jeps.entrySet().removeIf(e -> !elementJeps.containsValue(e.getValue()));
}
}
private void buildPreviewFeatureInfo() {
TypeElement featureType = utils.elementUtils.getTypeElement("jdk.internal.javac.PreviewFeature.Feature");
if (featureType == null) {
return;
}
TypeElement jepType = utils.elementUtils.getTypeElement("jdk.internal.javac.PreviewFeature.JEP");
featureType.getEnclosedElements().forEach(elem -> {
for (AnnotationMirror anno : elem.getAnnotationMirrors()) {
if (anno.getAnnotationType().asElement().equals(jepType)) {
Map<? extends ExecutableElement, ? extends AnnotationValue> values = anno.getElementValues();
jeps.put(elem.getSimpleName().toString(), new JEP(
getAnnotationElementValue(values, "number", 0),
getAnnotationElementValue(values, "title", ""),
getAnnotationElementValue(values, "status", "Preview"))
);
}
}
});
}
// Extract a single annotation element value with the given name and default value
@SuppressWarnings("unchecked")
private <R> R getAnnotationElementValue(Map<? extends ExecutableElement, ? extends AnnotationValue> values,
String name, R defaultValue) {
Optional<R> value = values.entrySet().stream()
.filter(e -> Objects.equals(e.getKey().getSimpleName().toString(), name))
.map(e -> (R) e.getValue().getValue())
.findFirst();
return value.orElse(defaultValue);
}
@Override
protected boolean belongsToSummary(Element element) {
if (!utils.isPreviewAPI(element)) {
return false;
}
String feature = Objects.requireNonNull(utils.getPreviewFeature(element),
"Preview feature not specified").toString();
JEP jep = jeps.computeIfAbsent(feature, featureName -> {
Map<String, Object> jepInfo = configuration.workArounds.getJepInfo(featureName);
if (!jepInfo.isEmpty()) {
int number = 0;
String title = "";
String status = "Preview"; // Default value is not returned by the method we used above.
for (var entry : jepInfo.entrySet()) {
switch (entry.getKey()) {
case "number" -> number = (int) entry.getValue();
case "title" -> title = (String) entry.getValue();
case "status" -> status = (String) entry.getValue();
default -> throw new IllegalArgumentException(entry.getKey());
}
}
return new JEP(number, title, status);
if (utils.isPreviewAPI(element)) {
String feature = Objects.requireNonNull(utils.getPreviewFeature(element),
"Preview feature not specified").toString();
// Preview features without JEP are not included in the list.
JEP jep = jeps.get(feature);
if (jep != null) {
elementJeps.put(element, jep);
return true;
}
}
// If preview tag is defined map elements to preview tags
if (previewNoteTag != null) {
CommentHelper ch = utils.getCommentHelper(element);
if (ch.dcTree != null) {
var jep = ch.dcTree.getFullBody().stream()
.filter(dt -> dt.getKind() == DocTree.Kind.UNKNOWN_INLINE_TAG)
.map(dt -> (UnknownInlineTagTree) dt)
.filter(t -> previewNoteTag.equals(t.getTagName()) && !t.getContent().isEmpty())
.map(this::findJEP)
.filter(Objects::nonNull)
.findFirst();
if (jep.isPresent()) {
elementNotes.add(element);
elementJeps.put(element, jep.get());
// Don't return true as this is not actual preview API.
}
}
return NULL_SENTINEL;
});
if (jep != NULL_SENTINEL) {
elementJeps.put(element, jep);
return true;
}
// Preview features without JEP are not included.
return false;
}
@ -101,10 +148,7 @@ public class PreviewAPIListBuilder extends SummaryAPIListBuilder {
* {@return a sorted set of preview feature JEPs in this release}
*/
public Set<JEP> getJEPs() {
return jeps.values()
.stream()
.filter(jep -> jep != NULL_SENTINEL)
.collect(Collectors.toCollection(TreeSet::new));
return new TreeSet<>(jeps.values());
}
/**
@ -113,4 +157,26 @@ public class PreviewAPIListBuilder extends SummaryAPIListBuilder {
public JEP getJEP(Element e) {
return elementJeps.get(e);
}
/**
* {@return a sorted set containing elements tagged with preview notes}
*/
public SortedSet<Element> getElementNotes() {
return elementNotes;
}
private JEP findJEP(UnknownInlineTagTree tag) {
var content = tag.getContent().toString().trim().split("\\s+", 2);
try {
var jnum = Integer.parseInt(content[0]);
for (var jep : jeps.values()) {
if (jep.number == jnum) {
return jep;
}
}
} catch (NumberFormatException nfe) {
// print warning?
}
return null;
}
}

View File

@ -24,7 +24,7 @@
/*
* @test
* @bug 8250768 8261976 8277300 8282452 8287597 8325325 8325874 8297879
* 8331947 8281533 8343239 8318416
* 8331947 8281533 8343239 8318416 8346109
* @summary test generated docs for items declared using preview
* @library /tools/lib ../../lib
* @modules jdk.javadoc/jdk.javadoc.internal.tool
@ -296,4 +296,54 @@ public class TestPreview extends JavadocTester {
checkOutput("m/module-summary.html", true,
"Indirect exports from the <code>java.base</code> module are");
}
// Test for JDK hidden option to add an entry for a non-preview element
// in the preview page based on the presence of a javadoc tag.
@Test
public void testPreviewNoteTag(Path base) throws IOException {
Path src = base.resolve("src");
tb.writeJavaFiles(src, """
package p;
import jdk.internal.javac.PreviewFeature;
/**
* Preview feature
*/
@PreviewFeature(feature= PreviewFeature.Feature.TEST)
public interface CoreInterface {
}
""", """
package p;
/**
* Non preview feature.
* {@previewNote 2147483647 Preview API Note}
*/
public interface NonPrevieFeature {
}
""");
javadoc("-d", "out-preview-note-tag",
"--add-exports", "java.base/jdk.internal.javac=ALL-UNNAMED",
"-tag", "previewNote:a:Preview Note:",
"--preview-note-tag", "previewNote",
"--source-path",
src.toString(),
"p");
checkExit(Exit.OK);
checkOutput("preview-list.html", true,
"""
<h2 title="Contents">Contents</h2>
<ul class="contents-list">
<li id="contents-preview-api-notes"><a href="#preview-api-notes">Preview API Notes</a></li>
<li id="contents-interface"><a href="#interface">Interfaces</a></li>""",
"""
<div class="caption"><span>Elements containing Preview Notes</span></div>""",
"""
<div class="col-summary-item-name even-row-color preview-api-notes preview-api-notes-tab1\
"><a href="p/NonPrevieFeature.html" title="interface in p">p.NonPrevieFeature</a></div>
<div class="col-second even-row-color preview-api-notes preview-api-notes-tab1">Test Feature</div>
<div class="col-last even-row-color preview-api-notes preview-api-notes-tab1">
<div class="block">Non preview feature.</div>""");
}
}