3e0bbd290c
6376959: Algorithm for Inheriting Method Comments seems to go not as documented 6934301: Support directed inheriting of class comments with @inheritDoc Reviewed-by: jjg, rriggs, aivanov, smarks, martin
531 lines
21 KiB
Java
531 lines
21 KiB
Java
/*
|
|
* Copyright (c) 2022, 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 8285368
|
|
* @library /tools/lib ../../lib /test/lib
|
|
* @modules jdk.javadoc/jdk.javadoc.internal.tool
|
|
* @build toolbox.ToolBox javadoc.tester.*
|
|
* @build jtreg.SkippedException
|
|
* @run main TestMethodCommentsAlgorithm
|
|
*/
|
|
|
|
import java.io.IOException;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.StandardOpenOption;
|
|
import java.util.Arrays;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import javax.lang.model.element.Modifier;
|
|
import javax.lang.model.type.TypeKind;
|
|
import javax.tools.ToolProvider;
|
|
|
|
import com.sun.source.doctree.DocCommentTree;
|
|
import com.sun.source.tree.CompilationUnitTree;
|
|
import com.sun.source.tree.IdentifierTree;
|
|
import com.sun.source.tree.MethodTree;
|
|
import com.sun.source.tree.PrimitiveTypeTree;
|
|
import com.sun.source.tree.Tree;
|
|
import com.sun.source.tree.VariableTree;
|
|
import com.sun.source.util.DocTrees;
|
|
import com.sun.source.util.JavacTask;
|
|
import com.sun.source.util.TreePath;
|
|
import com.sun.source.util.TreePathScanner;
|
|
import javadoc.tester.JavadocTester;
|
|
import jtreg.SkippedException;
|
|
import toolbox.ToolBox;
|
|
|
|
import static javadoc.tester.JavadocTester.Exit.OK;
|
|
|
|
/*
|
|
* These tests assert search order for _undirected_ documentation inheritance by
|
|
* following a series of javadoc runs on a progressively undocumented hierarchy
|
|
* of supertypes.
|
|
*
|
|
* Design
|
|
* ======
|
|
*
|
|
* Each test creates a hierarchy consisting of N types (T1, T2, ..., Tn) for
|
|
* which the search order is to be asserted. N-1 types are created (T1, T2,
|
|
* ..., T(n-1)) and one type, Tn, is implicitly present java.lang.Object.
|
|
* T1 is a type under test; T2, T3, ..., T(n-1) are direct or indirect
|
|
* supertypes of T1.
|
|
*
|
|
* By design, the index of a type is evocative of the order in which that type
|
|
* should be considered for documentation inheritance. If T1 lacks a doc
|
|
* comment, T2 should be considered next. If in turn T2 lacks a doc comment,
|
|
* T3 should be considered after that, and so on. Finally, Tn, which is
|
|
* java.lang.Object, whose documentation is ever-present, is considered.
|
|
*
|
|
* The test then runs javadoc N-1 times. Each run one fewer type has a doc
|
|
* comment: for the i-th run (1 <= i < N), type Tj has a doc comment if and
|
|
* only if j > i. So, for the i-th run, i comments are missing and N-i are
|
|
* present. In particular, for the first run (i = 1) the only _missing_ doc
|
|
* comment is that of T1 and for the last run (i = N-1) the only _available_
|
|
* doc comment is that of java.lang.Object.
|
|
*
|
|
* The test challenges javadoc by asking the following question:
|
|
*
|
|
* Whose documentation will T1 inherit if Tj (1 <= j <= i)
|
|
* do not have doc comments, but Tk (i < k <= N) do?
|
|
*
|
|
* For the i-th run the test checks that T1 inherits documentation of T(i+1).
|
|
*
|
|
* Technicalities
|
|
* ==============
|
|
*
|
|
* 1. To follow search order up to and including java.lang.Object, these tests
|
|
* need to be able to inherit documentation for java.lang.Object. For that,
|
|
* the tests access doc comments of java.lang.Object. To get such access,
|
|
* the tests patch the java.base module.
|
|
*
|
|
* 2. The documentation for java.lang.Object is slightly amended for
|
|
* uniformity with test documentation and for additional test
|
|
* coverage.
|
|
*
|
|
* 3. While documentation for java.lang.Object is currently inaccessible outside
|
|
* of the JDK, these test mimic what happens when the JDK documentation is
|
|
* built.
|
|
*/
|
|
public class TestMethodCommentsAlgorithm extends JavadocTester {
|
|
|
|
private final ToolBox tb = new ToolBox();
|
|
|
|
public static void main(String... args) throws Exception {
|
|
new TestMethodCommentsAlgorithm().runTests();
|
|
}
|
|
|
|
/*
|
|
* Tests that the documentation search order is as shown:
|
|
*
|
|
* (5)
|
|
* ^
|
|
* * /
|
|
* [7] (3) (4)
|
|
* ^ ^ ^
|
|
* \ | /
|
|
* \ | /
|
|
* [2] (6)
|
|
* ^ ^
|
|
* | /
|
|
* | /
|
|
* [1]
|
|
*/
|
|
@Test
|
|
public void testMixedHierarchyEquals(Path base) throws Exception {
|
|
Path p = Path.of(System.getProperty("test.src", ".")).toAbsolutePath();
|
|
while (!Files.exists(p.resolve("TEST.ROOT"))) {
|
|
p = p.getParent();
|
|
if (p == null) {
|
|
throw new SkippedException("can't find TEST.ROOT");
|
|
}
|
|
}
|
|
out.println("Test suite root: " + p);
|
|
Path javaBase = p.resolve("../../src/java.base").normalize();
|
|
if (!Files.exists(javaBase)) {
|
|
throw new SkippedException("can't find java.base");
|
|
}
|
|
out.println("java.base: " + javaBase);
|
|
|
|
for (int i = 1; i < 7; i++) {
|
|
mixedHierarchyI(base, javaBase, i);
|
|
new OutputChecker("mymodule/x/T1.html").check("""
|
|
<div class="block">T%s: main description</div>
|
|
""".formatted(i + 1), """
|
|
<dt>Parameters:</dt>
|
|
<dd><code>obj</code> - T%1$s: parameter description</dd>
|
|
<dt>Returns:</dt>
|
|
<dd>T%1$s: return description</dd>""".formatted(i + 1));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Generates source for the i-th run such that types whose index is less
|
|
* than i provide no documentation and those whose index is greater or
|
|
* equal to i provide documentation.
|
|
*/
|
|
private void mixedHierarchyI(Path base, Path javaBase, int i) throws IOException {
|
|
Path src = base.resolve("src-" + i);
|
|
Path mod = base.resolve("src-" + i).resolve("mymodule");
|
|
tb.writeJavaFiles(mod, """
|
|
package x;
|
|
public class T1 extends T2 implements T6 {
|
|
%s
|
|
@Override public boolean equals(Object obj) { return super.equals(obj); }
|
|
}
|
|
""".formatted(generateDocComment(1, i)), """
|
|
package x;
|
|
public class T2 /* extends Object */ implements T3, T4 {
|
|
%s
|
|
@Override public boolean equals(Object obj) { return super.equals(obj); }
|
|
}
|
|
""".formatted(generateDocComment(2, i)), """
|
|
package x;
|
|
public interface T3 {
|
|
%s
|
|
@Override boolean equals(Object obj);
|
|
}
|
|
""".formatted(generateDocComment(3, i)), """
|
|
package x;
|
|
public interface T4 extends T5 {
|
|
%s
|
|
@Override boolean equals(Object obj);
|
|
}
|
|
""".formatted(generateDocComment(4, i)), """
|
|
package x;
|
|
public interface T5 {
|
|
%s
|
|
@Override boolean equals(Object obj);
|
|
}
|
|
""".formatted(generateDocComment(5, i)), """
|
|
package x;
|
|
public interface T6 {
|
|
%s
|
|
@Override boolean equals(Object obj);
|
|
}
|
|
""".formatted(generateDocComment(6, i)), """
|
|
module mymodule { }
|
|
""");
|
|
|
|
createPatchedJavaLangObject(javaBase.resolve("share").resolve("classes").toAbsolutePath(),
|
|
Files.createDirectories(src.resolve("java.base")).toAbsolutePath(),
|
|
generateDocComment(7, i, false));
|
|
|
|
javadoc("-d", base.resolve("out-" + i).toAbsolutePath().toString(),
|
|
"-tag", "apiNote:a:API Note:",
|
|
"-tag", "implSpec:a:Implementation Requirements:",
|
|
"-tag", "implNote:a:Implementation Note:",
|
|
"--patch-module", "java.base=" + src.resolve("java.base").toAbsolutePath().toString(),
|
|
"--module-source-path", src.toAbsolutePath().toString(),
|
|
"mymodule/x");
|
|
|
|
checkExit(OK);
|
|
}
|
|
|
|
private static String generateDocComment(int index, int run) {
|
|
return generateDocComment(index, run, true);
|
|
}
|
|
|
|
/*
|
|
* Provides a doc comment for an override of Object.equals in a type with
|
|
* the specified index for the specified run.
|
|
*/
|
|
private static String generateDocComment(int index, int run, boolean includeCommentMarkers) {
|
|
if (index > run) {
|
|
String s = """
|
|
T%s: main description
|
|
*
|
|
* @param obj T%1$s: parameter description
|
|
* @return T%1$s: return description""";
|
|
if (includeCommentMarkers)
|
|
s = "/**\n* " + s + "\n*/";
|
|
return s.formatted(index).indent(4);
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Tests that the documentation search order is as shown:
|
|
*
|
|
* (3) (4)
|
|
* ^ ^
|
|
* \ /
|
|
* (2) (5)
|
|
* ^ ^
|
|
* \ /
|
|
* (1)
|
|
* |
|
|
* v
|
|
* [6]
|
|
* *
|
|
*/
|
|
@Test
|
|
public void testInterfaceHierarchy(Path base) throws Exception {
|
|
Path p = Path.of(System.getProperty("test.src", ".")).toAbsolutePath();
|
|
while (!Files.exists(p.resolve("TEST.ROOT"))) {
|
|
p = p.getParent();
|
|
if (p == null) {
|
|
throw new SkippedException("can't find TEST.ROOT");
|
|
}
|
|
}
|
|
System.err.println("Test suite root: " + p);
|
|
Path javaBase = p.resolve("../../src/java.base").normalize();
|
|
if (!Files.exists(javaBase)) {
|
|
throw new SkippedException("can't find java.base");
|
|
}
|
|
System.err.println("java.base: " + javaBase);
|
|
|
|
for (int i = 1; i < 6; i++) {
|
|
interfaceHierarchyI(base, javaBase, i);
|
|
new OutputChecker("mymodule/x/T1.html").check("""
|
|
<div class="block">T%s: main description</div>
|
|
""".formatted(i + 1), """
|
|
<dt>Parameters:</dt>
|
|
<dd><code>obj</code> - T%1$s: parameter description</dd>
|
|
<dt>Returns:</dt>
|
|
<dd>T%1$s: return description</dd>""".formatted(i + 1));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Nested/recursive `{@inheritDoc}` are processed before the comments that
|
|
* refer to them. This test highlights that a lone `{@inheritDoc}` is
|
|
* different from a missing/empty comment part.
|
|
*
|
|
* Whenever doclet sees `{@inheritDoc}` or `{@inheritDoc <supertype>}`
|
|
* while searching for a comment to inherit from up the hierarchy, it
|
|
* considers the comment found. A separate and unrelated search is
|
|
* then performed for that found `{@inheritDoc}`.
|
|
*
|
|
* The test case is wrapped in a module in order to be able to patch
|
|
* java.base (otherwise it doesn't seem to work).
|
|
*/
|
|
@Test
|
|
public void testRecursiveInheritDocTagsAreProcessedFirst(Path base) throws Exception {
|
|
Path p = Path.of(System.getProperty("test.src", ".")).toAbsolutePath();
|
|
while (!Files.exists(p.resolve("TEST.ROOT"))) {
|
|
p = p.getParent();
|
|
if (p == null) {
|
|
throw new SkippedException("can't find TEST.ROOT");
|
|
}
|
|
}
|
|
System.err.println("Test suite root: " + p);
|
|
Path javaBase = p.resolve("../../src/java.base").normalize();
|
|
if (!Files.exists(javaBase)) {
|
|
throw new SkippedException("can't find java.base");
|
|
}
|
|
System.err.println("java.base: " + javaBase);
|
|
|
|
Path src = base.resolve("src");
|
|
tb.writeJavaFiles(src.resolve("mymodule"), """
|
|
package x;
|
|
public class S {
|
|
/** {@inheritDoc} */
|
|
public boolean equals(Object obj) { return super.equals(obj); }
|
|
}
|
|
""", """
|
|
package x;
|
|
public interface I {
|
|
/** I::equals */
|
|
boolean equals(Object obj);
|
|
}
|
|
""", """
|
|
package x;
|
|
public class T extends S implements I {
|
|
public boolean equals(Object obj) { return super.equals(obj); }
|
|
}
|
|
""", """
|
|
module mymodule {}
|
|
""");
|
|
|
|
createPatchedJavaLangObject(javaBase.resolve("share").resolve("classes").toAbsolutePath(),
|
|
Files.createDirectories(src.resolve("java.base")).toAbsolutePath(),
|
|
"Object::equals");
|
|
|
|
javadoc("-d", base.resolve("out").toString(),
|
|
"-tag", "apiNote:a:API Note:",
|
|
"-tag", "implSpec:a:Implementation Requirements:",
|
|
"-tag", "implNote:a:Implementation Note:",
|
|
"--patch-module", "java.base=" + src.resolve("java.base").toAbsolutePath().toString(),
|
|
"--module-source-path", src.toAbsolutePath().toString(),
|
|
"mymodule/x");
|
|
|
|
checkExit(Exit.OK);
|
|
|
|
new OutputChecker("mymodule/x/T.html").check("""
|
|
<div class="block">Object::equals</div>""");
|
|
}
|
|
|
|
/*
|
|
* Generates source for the i-th run such that types whose index is less
|
|
* than i provide no documentation and those whose index is greater or
|
|
* equal to i provide documentation.
|
|
*/
|
|
private void interfaceHierarchyI(Path base, Path javaBase, int i) throws IOException {
|
|
Path src = base.resolve("src-" + i);
|
|
Path mod = base.resolve("src-" + i).resolve("mymodule");
|
|
tb.writeJavaFiles(mod, """
|
|
package x;
|
|
public interface T1 extends T2, T5 {
|
|
%s
|
|
@Override boolean equals(Object obj);
|
|
}
|
|
""".formatted(generateDocComment(1, i)), """
|
|
package x;
|
|
public interface T2 extends T3, T4 {
|
|
%s
|
|
@Override boolean equals(Object obj);
|
|
}
|
|
""".formatted(generateDocComment(2, i)), """
|
|
package x;
|
|
public interface T3 {
|
|
%s
|
|
@Override boolean equals(Object obj);
|
|
}
|
|
""".formatted(generateDocComment(3, i)), """
|
|
package x;
|
|
public interface T4 {
|
|
%s
|
|
@Override boolean equals(Object obj);
|
|
}
|
|
""".formatted(generateDocComment(4, i)), """
|
|
package x;
|
|
public interface T5 {
|
|
%s
|
|
@Override boolean equals(Object obj);
|
|
}
|
|
""".formatted(generateDocComment(5, i)), """
|
|
module mymodule { }
|
|
""");
|
|
|
|
createPatchedJavaLangObject(javaBase.resolve("share").resolve("classes").toAbsolutePath(),
|
|
Files.createDirectories(src.resolve("java.base")).toAbsolutePath(),
|
|
generateDocComment(6, i, false));
|
|
|
|
javadoc("-d", base.resolve("out-" + i).toAbsolutePath().toString(),
|
|
"-tag", "apiNote:a:API Note:",
|
|
"-tag", "implSpec:a:Implementation Requirements:",
|
|
"-tag", "implNote:a:Implementation Note:",
|
|
"--patch-module", "java.base=" + src.resolve("java.base").toAbsolutePath().toString(),
|
|
"--module-source-path", src.toAbsolutePath().toString(),
|
|
"mymodule/x");
|
|
|
|
checkExit(OK);
|
|
}
|
|
|
|
/*
|
|
* Takes a path to the java.base module, finds the Object.java file in
|
|
* there, creates a copy of that file _with the modified doc comment_
|
|
* for Object.equals in the provided destination directory and returns
|
|
* the path to that created copy.
|
|
*/
|
|
private Path createPatchedJavaLangObject(Path src, Path dst, String newComment)
|
|
throws IOException {
|
|
if (!Files.isDirectory(src) || !Files.isDirectory(dst)) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
var obj = Path.of("java/lang/Object.java");
|
|
List<Path> files;
|
|
// ensure Object.java is found and unique
|
|
try (var s = Files.find(src, Integer.MAX_VALUE,
|
|
(p, attr) -> attr.isRegularFile() && p.endsWith(obj))) {
|
|
files = s.limit(2).toList(); // 2 is enough to deduce non-uniqueness
|
|
}
|
|
if (files.size() != 1) {
|
|
throw new IllegalStateException(Arrays.toString(files.toArray()));
|
|
}
|
|
var original = files.get(0);
|
|
out.println("found " + original.toAbsolutePath());
|
|
var source = Files.readString(original);
|
|
var region = findDocCommentRegion(original);
|
|
var newSource = source.substring(0, region.start)
|
|
+ newComment
|
|
+ source.substring(region.end);
|
|
// create intermediate directories in the destination first, otherwise
|
|
// writeString will throw java.nio.file.NoSuchFileException
|
|
var copy = dst.resolve(src.relativize(original));
|
|
out.println("to be copied to " + copy);
|
|
if (Files.notExists(copy.getParent())) {
|
|
Files.createDirectories(copy.getParent());
|
|
}
|
|
return Files.writeString(copy, newSource, StandardOpenOption.CREATE);
|
|
}
|
|
|
|
private static SourceRegion findDocCommentRegion(Path src) throws IOException {
|
|
// to _reliably_ find the doc comment, parse the file and find source
|
|
// position of the doc tree corresponding to that comment
|
|
var compiler = ToolProvider.getSystemJavaCompiler();
|
|
var fileManager = compiler.getStandardFileManager(null, null, null);
|
|
var fileObject = fileManager.getJavaFileObjects(src).iterator().next();
|
|
var task = (JavacTask) compiler.getTask(null, null, null, null, null, List.of(fileObject));
|
|
Iterator<? extends CompilationUnitTree> iterator = task.parse().iterator();
|
|
if (!iterator.hasNext()) {
|
|
throw new AssertionError();
|
|
}
|
|
var tree = iterator.next();
|
|
var pathToEqualsMethod = findMethod(tree);
|
|
var trees = DocTrees.instance(task);
|
|
DocCommentTree docCommentTree = trees.getDocCommentTree(pathToEqualsMethod);
|
|
if (docCommentTree == null)
|
|
throw new AssertionError("cannot find the doc comment for java.lang.Object#equals");
|
|
var positions = trees.getSourcePositions();
|
|
long start = positions.getStartPosition(null, docCommentTree, docCommentTree);
|
|
long end = positions.getEndPosition(null, docCommentTree, docCommentTree);
|
|
return new SourceRegion((int) start, (int) end);
|
|
}
|
|
|
|
private static TreePath findMethod(Tree src) {
|
|
|
|
class Result extends RuntimeException {
|
|
final TreePath p;
|
|
|
|
Result(TreePath p) {
|
|
super("", null, false, false); // lightweight exception to short-circuit scan
|
|
this.p = p;
|
|
}
|
|
}
|
|
|
|
var scanner = new TreePathScanner<Void, Void>() {
|
|
@Override
|
|
public Void visitMethod(MethodTree m, Void unused) {
|
|
boolean solelyPublic = m.getModifiers().getFlags().equals(Set.of(Modifier.PUBLIC));
|
|
if (!solelyPublic) {
|
|
return null;
|
|
}
|
|
var returnType = m.getReturnType();
|
|
boolean returnsBoolean = returnType != null
|
|
&& returnType.getKind() == Tree.Kind.PRIMITIVE_TYPE
|
|
&& ((PrimitiveTypeTree) returnType).getPrimitiveTypeKind() == TypeKind.BOOLEAN;
|
|
if (!returnsBoolean) {
|
|
return null;
|
|
}
|
|
boolean hasNameEquals = m.getName().toString().equals("equals");
|
|
if (!hasNameEquals) {
|
|
return null;
|
|
}
|
|
List<? extends VariableTree> params = m.getParameters();
|
|
if (params.size() != 1)
|
|
return null;
|
|
var parameterType = params.get(0).getType();
|
|
if (parameterType.getKind() == Tree.Kind.IDENTIFIER &&
|
|
((IdentifierTree) parameterType).getName().toString().equals("Object")) {
|
|
throw new Result(getCurrentPath());
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
try {
|
|
scanner.scan(src, null);
|
|
return null; // not found
|
|
} catch (Result e) {
|
|
return e.p; // found
|
|
}
|
|
}
|
|
|
|
record SourceRegion(int start, int end) { }
|
|
}
|