8320207: doclet incorrectly chooses code font for a See Also link

Reviewed-by: hannesw
This commit is contained in:
Jonathan Gibbons 2023-11-22 17:23:38 +00:00
parent 1629a9059b
commit 407cdd4cac
6 changed files with 290 additions and 27 deletions

View File

@ -117,7 +117,7 @@ public class LinkTaglet extends BaseTaglet {
* @param refTree the tree node containing the information, or {@code null} if not available
* @param refSignature the normalized signature of the target of the reference
* @param ref the target of the reference
* @param isLinkPlain {@code true} if the link should be presented in "plain" font,
* @param isPlain {@code true} if the link should be presented in "plain" font,
* or {@code false} for "code" font
* @param label the label for the link,
* or an empty item to use a default label derived from the signature
@ -130,17 +130,17 @@ public class LinkTaglet extends BaseTaglet {
DocTree refTree,
String refSignature,
Element ref,
boolean isLinkPlain,
boolean isPlain,
Content label,
BiConsumer<String, Object[]> reportWarning,
TagletWriter tagletWriter) {
var config = tagletWriter.configuration;
var htmlWriter = tagletWriter.htmlWriter;
Content labelContent = plainOrCode(isLinkPlain, label);
Content labelContent = plainOrCode(isPlain, label);
// The signature from the @see tag. We will output this text when a label is not specified.
Content text = plainOrCode(isLinkPlain,
Content text = plainOrCode(isPlain,
Text.of(Objects.requireNonNullElse(refSignature, "")));
CommentHelper ch = utils.getCommentHelper(holder);
@ -170,7 +170,7 @@ public class LinkTaglet extends BaseTaglet {
if (refPackage != null && utils.isIncluded(refPackage)) {
//@see is referencing an included package
if (labelContent.isEmpty()) {
labelContent = plainOrCode(isLinkPlain,
labelContent = plainOrCode(isPlain,
Text.of(refPackage.getQualifiedName()));
}
return htmlWriter.getPackageLink(refPackage, labelContent, refFragment);
@ -202,10 +202,10 @@ public class LinkTaglet extends BaseTaglet {
TypeMirror referencedType = ch.getReferencedType(refTree);
if (utils.isGenericType(referencedType)) {
// This is a generic type link, use the TypeMirror representation.
return plainOrCode(isLinkPlain, htmlWriter.getLink(
return plainOrCode(isPlain, htmlWriter.getLink(
new HtmlLinkInfo(config, HtmlLinkInfo.Kind.LINK_TYPE_PARAMS_AND_BOUNDS, referencedType)));
}
labelContent = plainOrCode(isLinkPlain, Text.of(utils.getSimpleName(refClass)));
labelContent = plainOrCode(isPlain, Text.of(utils.getSimpleName(refClass)));
}
return htmlWriter.getLink(new HtmlLinkInfo(config, HtmlLinkInfo.Kind.PLAIN, refClass)
.label(labelContent));
@ -267,7 +267,7 @@ public class LinkTaglet extends BaseTaglet {
return htmlWriter.getDocLink(HtmlLinkInfo.Kind.SHOW_PREVIEW, containing,
refMem, (labelContent.isEmpty()
? plainOrCode(isLinkPlain, Text.of(refMemName))
? plainOrCode(isPlain, Text.of(refMemName))
: labelContent), null, false);
}
}

View File

@ -37,6 +37,7 @@ import javax.lang.model.element.VariableElement;
import com.sun.source.doctree.DocTree;
import com.sun.source.doctree.SeeTree;
import com.sun.source.doctree.TextTree;
import jdk.javadoc.doclet.Taglet;
import jdk.javadoc.internal.doclets.formats.html.ClassWriter;
@ -174,7 +175,7 @@ public class SeeTaglet extends BaseTaglet implements InheritableTaglet {
seeTag,
refSignature,
ch.getReferencedElement(seeTag),
false,
isPlain(refSignature, label),
htmlWriter.commentTagsToContent(element, label, tagletWriter.getContext().within(seeTag)),
(key, args) -> messages.warning(ch.getDocTreePath(seeTag), key, args),
tagletWriter
@ -189,7 +190,124 @@ public class SeeTaglet extends BaseTaglet implements InheritableTaglet {
default -> throw new IllegalStateException(ref0.getKind().toString());
}
}
/**
* {@return {@code true} if the label should be rendered in plain font}
*
* The method uses a heuristic, to see if the string form of the label
* is a substring of the reference. Thus, for example:
*
* <ul>
* <li>{@code @see MyClass.MY_CONSTANT MY_CONSTANT} returns {@code true}
* <li>{@code @see MyClass.MY_CONSTANT a constant} returns {@code false}
* </ul>
*
* The result will be {@code true} (meaning, displayed in plain font) if
* any of the following are true about the label:
*
* <ul>
* <li>There is more than a single item in the list of nodes,
* suggesting there may be formatting nodes.
* <li>There is whitespace outside any parentheses,
* suggesting the label is a phrase
* <li>There are nested parentheses, or content after the parentheses,
* which cannot occur in a standalone signature
* <li>The simple name inferred from the reference does not match
* any simple name inferred from the label
* </ul>
*
* @param refSignature the signature of the target of the reference
* @param label the label
*/
private boolean isPlain(String refSignature, List<? extends DocTree> label) {
if (label.isEmpty()) {
return false;
} else if (label.size() > 1) {
return true;
}
var l0 = label.get(0);
String s;
if (l0 instanceof TextTree t) {
s = t.getBody().trim();
} else {
return true;
}
// look for whitespace outside any parens, nested parens, or characters after parens:
// all of which will not be found in a simple signature
var inParens = false;
var ids = new ArrayList<String>();
var sb = new StringBuilder();
for (var i = 0; i < s.length(); i++) {
var ch = s.charAt(i);
if (!sb.isEmpty() && !Character.isJavaIdentifierPart(ch)) {
ids.add(sb.toString());
sb.setLength(0);
}
switch (ch) {
case '(' -> {
if (inParens) {
return true;
} else {
inParens = true;
}
}
case ')' -> {
if (inParens && i < s.length() - 1) {
return true;
} else {
inParens = false;
}
}
default -> {
if (!inParens) {
if (Character.isJavaIdentifierStart(ch)
|| (!sb.isEmpty() && Character.isJavaIdentifierPart(ch))) {
sb.append(ch);
} else if (Character.isWhitespace(ch)) {
return true;
}
}
}
}
}
if (!sb.isEmpty()) {
ids.add(sb.toString());
}
if (ids.isEmpty()) {
return true;
}
// final check: does the simple name inferred from the label
// match the simple name inferred from the reference
var labelSimpleName = ids.get(ids.size() - 1);
var refSimpleName = getSimpleName(refSignature);
return !labelSimpleName.equals((refSimpleName));
}
/**
* {@return the simple name from a signature}
*
* If there is a member part in the signature, the simple name is the
* identifier after the {@code #} character.
* Otherwise, the simple name is the last identifier in the signature.
*
* @param sig the signature
*/
private String getSimpleName(String sig) {
int hash = sig.indexOf('#');
if (hash == -1 ) {
int lastDot = sig.lastIndexOf(".");
return lastDot == -1 ? sig : sig.substring(lastDot + 1);
} else {
int parens = sig.indexOf("(", hash);
return parens == -1 ? sig.substring(hash + 1) : sig.substring(hash + 1, parens);
}
}
}

View File

@ -112,8 +112,8 @@ public class TestGenericTypeLink extends JavadocTester {
" class="external-link">String</a>,<wbr><a href="A.SomeException.html" title="class\
in pkg1">A.SomeException</a>&gt;</code></li>
<li><a href="http://example.com/docs/api/java.base/java/util/List.html" title="clas\
s or interface in java.util" class="external-link"><code>Link to generic type with \
label</code></a></li>
s or interface in java.util" class="external-link">Link to generic type with label<\
/a></li>
</ul>
</dd>
</dl>"""

View File

@ -79,8 +79,8 @@ public class TestSeeLinkAnchor extends JavadocTester {
"""
Plain link to <a href="../p2/Class2.html#class2-sub-heading">sub heading above</a></div>""",
"""
<li><a href="../p2/Class2.html#class2main"><code>See main heading in p2.Class2</code></a></li>
<li><a href="../p2/package-summary.html#package-p2-heading"><code>See heading in p2</code></a></li>
<li><a href="../p2/Class2.html#class2main">See main heading in p2.Class2</a></li>
<li><a href="../p2/package-summary.html#package-p2-heading">See heading in p2</a></li>
""");
checkOrder("p2/Class2.html",
"""
@ -89,13 +89,13 @@ public class TestSeeLinkAnchor extends JavadocTester {
Plain link <a href="../p1/Class1.html#main">to Class1</a>.""");
checkOrder("p2/package-summary.html",
"""
<a href="Class2.html#class2-sub-heading"><code>See sub heading in p2.Class2</code></a>""");
<a href="Class2.html#class2-sub-heading">See sub heading in p2.Class2</a>""");
checkOrder("p2/doc-files/file.html",
"""
Plain link to <a href="../../p1/Class1.html#main">heading in p1.ClassA</a>.""",
"""
<a href="../Class2.html#class2main"><code>See main heading in p2.ClassB</code></a>""");
<a href="../Class2.html#class2main">See main heading in p2.ClassB</a>""");
}
@Test
@ -111,13 +111,13 @@ public class TestSeeLinkAnchor extends JavadocTester {
checkExit(Exit.OK);
checkOrder("m1/module-summary.html",
"""
<a href="../m2/com/m2/Class2.html#main-heading"><code>See main heading in Class2</code></a>""");
<a href="../m2/com/m2/Class2.html#main-heading">See main heading in Class2</a>""");
checkOrder("m1/com/m1/Class1.html",
"""
<a href="../../../m2/com/m2/Class2.html#sub"><code>sub heading in Class2</code></a>.""",
"""
<li><a href="../../../m2/com/m2/Class2.html#main-heading"><code>See main heading in Class2</code></a></li>
<li><a href="../../module-summary.html#module-m1-heading"><code>See heading in module m1</code></a></li>
<li><a href="../../../m2/com/m2/Class2.html#main-heading">See main heading in Class2</a></li>
<li><a href="../../module-summary.html#module-m1-heading">See heading in module m1</a></li>
""");
checkOrder("m2/com/m2/Class2.html",
"""
@ -128,7 +128,7 @@ public class TestSeeLinkAnchor extends JavadocTester {
"""
Link to <a href="../com/m2/Class2.html#main-heading"><code>heading in Class2</code></a>.""",
"""
<li><a href="../../m1/module-summary.html#module-m1-heading"><code>Heading in module m1</code></a></li>""");
<li><a href="../../m1/module-summary.html#module-m1-heading">Heading in module m1</a></li>""");
}
@Test

View File

@ -62,7 +62,7 @@ public class TestSeeTag extends JavadocTester {
<li><a href="Test.InnerOne.html#foo()"><code>Test.InnerOne.foo()</code></a></li>
<li><a href="Test.InnerOne.html#bar(java.lang.Object)"><code>Test.InnerOne.bar(Object)</code></a></li>
<li><a href="http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javadoc.html#see">Javadoc</a></li>
<li><a href="Test.InnerOne.html#baz(float)"><code>something</code></a></li>
<li><a href="Test.InnerOne.html#baz(float)">something</a></li>
<li><a href="Test.InnerOne.html#format(java.lang.String,java.lang.Object...)"><code>\
Test.InnerOne.format(java.lang.String, java.lang.Object...)</code></a></li>
</ul>
@ -80,7 +80,7 @@ public class TestSeeTag extends JavadocTester {
<dd>
<ul class="tag-list-long">
<li><code>Serializable</code></li>
<li><a href="Test.html" title="class in pkg"><code>See tag with very long label text</code></a></li>
<li><a href="Test.html" title="class in pkg">See tag with very long label text</a></li>
</ul>
</dd>
</dl>""");
@ -213,17 +213,17 @@ public class TestSeeTag extends JavadocTester {
"<section class=\"detail\" id=\"noComma()\">",
"""
<ul class="tag-list">
<li><a href="#noArgs()"><code>no args</code></a></li>
<li><a href="#oneArg(int)"><code>one arg</code></a></li>
<li><a href="#twoArgs(int,int)"><code>two args</code></a></li>
<li><a href="#noArgs()">no args</a></li>
<li><a href="#oneArg(int)">one arg</a></li>
<li><a href="#twoArgs(int,int)">two args</a></li>
</ul>""",
"<section class=\"detail\" id=\"commaInDescription()\">",
"""
<ul class="tag-list-long">
<li><a href="#noArgs()"><code>no args</code></a></li>
<li><a href="#oneArg(int)"><code>one arg</code></a></li>
<li><a href="#twoArgs(int,int)"><code>two args with a comma , in the description</code></a></li>
<li><a href="#noArgs()">no args</a></li>
<li><a href="#oneArg(int)">one arg</a></li>
<li><a href="#twoArgs(int,int)">two args with a comma , in the description</a></li>
</ul>""",
"<section class=\"detail\" id=\"commaInDefaultDescription()\">",

View File

@ -0,0 +1,145 @@
/*
* Copyright (c) 2023, 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 8320207
* @summary doclet incorrectly chooses code font for a See Also link
* @library /tools/lib ../../lib
* @modules jdk.javadoc/jdk.javadoc.internal.tool
* @build toolbox.ToolBox javadoc.tester.*
* @run main TestSeeTagFont
*/
import java.nio.file.Path;
import javadoc.tester.JavadocTester;
import toolbox.ToolBox;
public class TestSeeTagFont extends JavadocTester {
public static void main(String... args) throws Exception {
var tester = new TestSeeTagFont();
tester.runTests();
}
private final ToolBox tb = new ToolBox();
@Test
public void testPlain(Path base) throws Exception {
Path src = base.resolve("src");
tb.writeJavaFiles(src,
"""
package p;
import p2.Other;
/**
* Description.
* @see Other multi-word phrase
* @see Other <em>Other</em>
* @see Other Other() with trailing text
* @see Other simpleNameMismatch
*
* @see Other#Other() multi-word phrase
* @see Other#Other() Other#Other() with trailing text
* @see Other#Other() simpleNameMismatch
*
* @see Other#m() <code>Other.m</code> with formatting and trailing text
*/
public class C { }
""",
"""
package p2;
/** Lorem ipsum. */
public class Other {
/** Lorem ipsum. */
public void m() { }
}
""");
javadoc("-d", base.resolve("api").toString(),
"-Xdoclint:none",
"-sourcepath", src.toString(),
"p", "p2");
checkExit(Exit.OK);
// none of the following should contain <code>...</code>
checkOutput("p/C.html", true,
"""
<ul class="tag-list-long">
<li><a href="../p2/Other.html" title="class in p2">multi-word phrase</a></li>
<li><a href="../p2/Other.html" title="class in p2"><em>Other</em></a></li>
<li><a href="../p2/Other.html" title="class in p2">Other() with trailing text</a></li>
<li><a href="../p2/Other.html" title="class in p2">simpleNameMismatch</a></li>
<li><a href="../p2/Other.html#%3Cinit%3E()">multi-word phrase</a></li>
<li><a href="../p2/Other.html#%3Cinit%3E()">Other#Other() with trailing text</a></li>
<li><a href="../p2/Other.html#%3Cinit%3E()">simpleNameMismatch</a></li>
<li><a href="../p2/Other.html#m()"><code>Other.m</code> with formatting and trailing text</a></li>
</ul>
""");
}
@Test
public void testCode(Path base) throws Exception {
Path src = base.resolve("src");
tb.writeJavaFiles(src,
"""
package p;
import p2.Other;
/**
* Description.
* @see Other
* @see p2.Other Other
*
* @see Other#Other() Other
* @see Other#m() m
* @see Other#m() Other.m
*/
public class C { }
""",
"""
package p2;
/** Lorem ipsum. */
public class Other {
/** Lorem ipsum. */
public void m() { }
}
""");
javadoc("-d", base.resolve("api").toString(),
"-Xdoclint:none",
"-sourcepath", src.toString(),
"p", "p2");
checkExit(Exit.OK);
// all of the following should contain <code>...</code>
checkOutput("p/C.html", true,
"""
<ul class="tag-list">
<li><a href="../p2/Other.html" title="class in p2"><code>Other</code></a></li>
<li><a href="../p2/Other.html" title="class in p2"><code>Other</code></a></li>
<li><a href="../p2/Other.html#%3Cinit%3E()"><code>Other</code></a></li>
<li><a href="../p2/Other.html#m()"><code>m</code></a></li>
<li><a href="../p2/Other.html#m()"><code>Other.m</code></a></li>
</ul>
""");
}
}