diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/AnnotationTypeMemberWriter.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/AnnotationTypeMemberWriter.java index 6c0337513d0..8f8585ecc62 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/AnnotationTypeMemberWriter.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/AnnotationTypeMemberWriter.java @@ -100,7 +100,7 @@ public class AnnotationTypeMemberWriter extends AbstractMemberWriter { buildAnnotationTypeMemberChildren(div); annotationContent.add(div); memberList.add(writer.getMemberListItem(annotationContent)); - writer.tableOfContents.addLink(htmlIds.forMember(typeElement, (ExecutableElement) member), + writer.tableOfContents.addLink(htmlIds.forMember((ExecutableElement) member).getFirst(), Text.of(name(member))); } Content annotationDetails = getAnnotationDetails(annotationDetailsHeader, memberList); @@ -210,7 +210,7 @@ public class AnnotationTypeMemberWriter extends AbstractMemberWriter { Text.of(name(member))); content.add(heading); return HtmlTree.SECTION(HtmlStyle.detail, content) - .setId(htmlIds.forMember(typeElement, (ExecutableElement) member)); + .setId(htmlIds.forMember((ExecutableElement) member).getFirst()); } protected Content getSignature(Element member) { diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/ConstructorWriter.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/ConstructorWriter.java index 5782dcd06a4..1c4af2faf97 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/ConstructorWriter.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/ConstructorWriter.java @@ -116,7 +116,7 @@ public class ConstructorWriter extends AbstractExecutableMemberWriter { buildTagInfo(div); constructorContent.add(div); memberList.add(getMemberListItem(constructorContent)); - writer.tableOfContents.addLink(htmlIds.forMember(currentConstructor), + writer.tableOfContents.addLink(htmlIds.forMember(currentConstructor).getFirst(), Text.of(utils.getSimpleName(constructor) + utils.makeSignature(currentConstructor, typeElement, false, true))); } @@ -189,13 +189,14 @@ public class ConstructorWriter extends AbstractExecutableMemberWriter { Content content = new ContentBuilder(); var heading = HtmlTree.HEADING(Headings.TypeDeclaration.MEMBER_HEADING, Text.of(name(constructor))); - HtmlId erasureAnchor = htmlIds.forErasure(constructor); - if (erasureAnchor != null) { - heading.setId(erasureAnchor); + + var anchors = htmlIds.forMember(constructor); + if (anchors.size() > 1) { + heading.setId(anchors.getLast()); } content.add(heading); return HtmlTree.SECTION(HtmlStyle.detail, content) - .setId(htmlIds.forMember(constructor)); + .setId(anchors.getFirst()); } protected Content getSignature(ExecutableElement constructor) { diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDocletWriter.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDocletWriter.java index 7295ba6d513..25ec603a064 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDocletWriter.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDocletWriter.java @@ -1037,7 +1037,7 @@ public abstract class HtmlDocletWriter { if (utils.isExecutableElement(element)) { ExecutableElement ee = (ExecutableElement)element; - HtmlId id = isProperty ? htmlIds.forProperty(ee) : htmlIds.forMember(ee); + HtmlId id = isProperty ? htmlIds.forProperty(ee) : htmlIds.forMember(ee).getFirst(); return getLink(new HtmlLinkInfo(configuration, context, typeElement) .label(label) .fragment(id.name()) diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlIds.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlIds.java index 10e91cc1204..fc772107274 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlIds.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlIds.java @@ -25,11 +25,16 @@ package jdk.javadoc.internal.doclets.formats.html; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +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.element.PackageElement; import javax.lang.model.element.TypeElement; @@ -159,34 +164,89 @@ public class HtmlIds { } /** - * Returns an id for an executable element, suitable for use when the - * simple name and argument list will be unique within the page, such as - * in the page for the declaration of the enclosing class or interface. + * {@return a non-empty list of ids to a constructor or a method} + * The ids from the returned list are alternative: the given constructor + * or method can be equally referred to by any of those ids. * - * @param element the element - * - * @return the id + * @param executable a constructor or method */ - HtmlId forMember(ExecutableElement element) { + List forMember(ExecutableElement executable) { + var htmlId = ids.get(executable); + if (htmlId != null) + return htmlId; + if (executable.getKind() != ElementKind.CONSTRUCTOR + && executable.getKind() != ElementKind.METHOD) + throw new IllegalArgumentException(String.valueOf(executable.getKind())); + var vmt = configuration.getVisibleMemberTable((TypeElement) executable.getEnclosingElement()); + var ctors = vmt.getVisibleMembers(VisibleMemberTable.Kind.CONSTRUCTORS); + var methods = vmt.getVisibleMembers(VisibleMemberTable.Kind.METHODS); + record Erased(ExecutableElement element, HtmlId id) { } + // split elements into two buckets: + // - elements whose erased id is present + // - elements whose erased id is absent (i.e. is null) + enum ErasedId { PRESENT, ABSENT } + var buckets = Stream.concat(ctors.stream(), methods.stream()) + .map(e -> (ExecutableElement) e) + .map(e -> new Erased(e, forErasure(e))) + .collect(Collectors.groupingBy(erased -> erased.id == null ? + ErasedId.ABSENT : ErasedId.PRESENT)); + var dups = new HashSet(); + // the order of elements in each bucket is important for reproducibility + // of ids: the same executable element must have the same id in any + // javadoc run + // Use simple id, unless we have to use erased id; for that, do the + // following _in order_: + // 1. Map all elements that can _only_ be addressed by the simple id + for (var e : buckets.getOrDefault(ErasedId.ABSENT, List.of())) { + var simpleId = forMember0(e.element); + ids.put(e.element, List.of(simpleId)); + boolean added = dups.add(simpleId.name()); + // we assume that the simple id for an executable member that + // does not use type parameters is unique + assert added; + } + // 2. Map all elements that can be addressed by simple id or erased id; + // if the simple id is not yet used, use it, otherwise use the erased id + for (var e : buckets.getOrDefault(ErasedId.PRESENT, List.of())) { + var simpleId = forMember0(e.element); + if (dups.add(simpleId.name())) { + ids.put(e.element, List.of(simpleId, e.id)); + } else { + ids.put(e.element, List.of(e.id)); + boolean added = dups.add(e.id.name()); + // Not only must an erased id not clash with any simple id, + // but it must also not clash with any other erased id. + // The latter is because JLS 8.4.2. Method Signature: + // it is a compile-time error to declare two methods + // with override-equivalent signatures in a class + assert added; + } + } + // Safety net: if for whatever reason we cannot find the element + // among those we just expanded, return the simple id. It might + // not be always right, but at least it won't fail. + // + // - one example where it might happen is linking to an inherited + // undocumented method (see test case T5093723) + // TODO the above will need to be revisited if and when we redesign + // VisibleMemberTable, which currently cannot correctly return the + // owner of such a method + // + // - another example is annotation interface methods: they are not + // included in VisibleMemberTable.Kind.METHODS and so cannot be + // found among them + return ids.computeIfAbsent(executable, e -> List.of(forMember0(e))); + } + + private final Map> ids = new HashMap<>(); + + private HtmlId forMember0(ExecutableElement element) { String a = element.getSimpleName() + utils.makeSignature(element, null, true, true); // utils.makeSignature includes spaces return HtmlId.of(a.replaceAll("\\s", "")); } - /** - * Returns an id for an executable element, including the context - * of its documented enclosing class or interface. - * - * @param typeElement the enclosing class or interface - * @param member the element - * - * @return the id - */ - HtmlId forMember(TypeElement typeElement, ExecutableElement member) { - return HtmlId.of(utils.getSimpleName(member) + utils.signature(member, typeElement)); - } - /** * Returns an id for a field, suitable for use when the simple name * will be unique within the page, such as in the page for the @@ -229,7 +289,7 @@ public class HtmlIds { * @param executableElement the element to anchor to * @return the 1.4.x style anchor for the executable element */ - protected HtmlId forErasure(ExecutableElement executableElement) { + private HtmlId forErasure(ExecutableElement executableElement) { final StringBuilder buf = new StringBuilder(executableElement.getSimpleName().toString()); buf.append("("); List parameters = executableElement.getParameters(); @@ -483,7 +543,7 @@ public class HtmlIds { */ public HtmlId forPreviewSection(Element el) { return HtmlId.of("preview-" + switch (el.getKind()) { - case CONSTRUCTOR, METHOD -> forMember((ExecutableElement) el).name(); + case CONSTRUCTOR, METHOD -> forMember((ExecutableElement) el).getFirst().name(); case PACKAGE -> forPackage((PackageElement) el).name(); default -> utils.getFullyQualifiedName(el, false); }); @@ -497,7 +557,7 @@ public class HtmlIds { * @return the id */ public HtmlId forRestrictedSection(ExecutableElement el) { - return HtmlId.of("restricted-" + forMember(el).name()); + return HtmlId.of("restricted-" + forMember(el).getFirst().name()); } /** diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlIndexBuilder.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlIndexBuilder.java index d17b795d9c0..51831b23336 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlIndexBuilder.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlIndexBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2024, 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 @@ -128,7 +128,7 @@ public class HtmlIndexBuilder extends IndexBuilder { item.setContainingModule(utils.getFullyQualifiedName(utils.containingModule(element))); } if (utils.isExecutableElement(element)) { - String url = HtmlTree.encodeURL(htmlIds.forMember((ExecutableElement) element).name()); + String url = HtmlTree.encodeURL(htmlIds.forMember((ExecutableElement) element).getFirst().name()); if (!url.equals(item.getLabel())) { item.setUrl(url); } diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/MethodWriter.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/MethodWriter.java index 5f20f360eb6..521616f982b 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/MethodWriter.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/MethodWriter.java @@ -116,7 +116,7 @@ public class MethodWriter extends AbstractExecutableMemberWriter { buildTagInfo(div); methodContent.add(div); memberList.add(writer.getMemberListItem(methodContent)); - writer.tableOfContents.addLink(htmlIds.forMember(currentMethod), + writer.tableOfContents.addLink(htmlIds.forMember(currentMethod).getFirst(), Text.of(utils.getSimpleName(method) + utils.makeSignature(currentMethod, typeElement, false, true))); } @@ -204,13 +204,13 @@ public class MethodWriter extends AbstractExecutableMemberWriter { Content content = new ContentBuilder(); var heading = HtmlTree.HEADING(Headings.TypeDeclaration.MEMBER_HEADING, Text.of(name(method))); - HtmlId erasureAnchor; - if ((erasureAnchor = htmlIds.forErasure(method)) != null) { - heading.setId(erasureAnchor); + var anchors = htmlIds.forMember(method); + if (anchors.size() > 1) { + heading.setId(anchors.getLast()); } content.add(heading); return HtmlTree.SECTION(HtmlStyle.detail, content) - .setId(htmlIds.forMember(method)); + .setId(anchors.getFirst()); } protected Content getSignature(ExecutableElement method) { @@ -375,7 +375,7 @@ public class MethodWriter extends AbstractExecutableMemberWriter { var codeOverriddenTypeLink = HtmlTree.CODE(overriddenTypeLink); Content methlink = writer.getLink( new HtmlLinkInfo(writer.configuration, HtmlLinkInfo.Kind.PLAIN, holder) - .fragment(writer.htmlIds.forMember(method).name()) + .fragment(writer.htmlIds.forMember(method).getFirst().name()) .label(method.getSimpleName())); var codeMethLink = HtmlTree.CODE(methlink); var dd = HtmlTree.DD(codeMethLink); diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Comparators.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Comparators.java index f1429922742..92c4cd9228a 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Comparators.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Comparators.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2024, 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 @@ -32,12 +32,14 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.ModuleElement; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.SimpleElementVisitor14; import javax.lang.model.util.SimpleTypeVisitor9; +import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -143,7 +145,11 @@ public class Comparators { if (result != 0) { return result; } - return compareModuleNames(e1, e2); + result = compareModuleNames(e1, e2); + if (result != 0) { + return result; + } + return compareTypeParameters(e1, e2); } }; } @@ -264,7 +270,10 @@ public class Comparators { result = compareFullyQualifiedNames(e1, e2); if (result != 0) return result; - return compareModuleNames(e1, e2); + result = compareModuleNames(e1, e2); + if (result != 0) + return result; + return compareTypeParameters(e1, e2); } }; } @@ -364,7 +373,14 @@ public class Comparators { if (result != 0) { return result; } - return compareModuleNames(e1, e2); + result = compareModuleNames(e1, e2); + if (result != 0) { + return result; + } + // this might not be needed: if e1 != e2 and both are + // executables they must differ in FQN and thus we + // shouldn't reach here + return compareTypeParameters(e1, e2); } }; } @@ -423,6 +439,21 @@ public class Comparators { }.visit(t); } + protected final int compareTypeParameters(Element e1, Element e2) { + if (!e1.getKind().isExecutable() || !e2.getKind().isExecutable()) + return 0; + var typeParameters1 = ((ExecutableElement) e1).getTypeParameters(); + var typeParameters2 = ((ExecutableElement) e2).getTypeParameters(); + var parameters1 = typeParameters1.toArray(new TypeParameterElement[0]); + var parameters2 = typeParameters2.toArray(new TypeParameterElement[0]); + return Arrays.compare(parameters1, parameters2, (p1, p2) -> { + var bounds1 = p1.getBounds().toArray(new TypeMirror[0]); + var bounds2 = p2.getBounds().toArray(new TypeMirror[0]); + return Arrays.compare(bounds1, bounds2, (b1, b2) -> + utils.compareStrings(true, b1.toString(), b2.toString())); + }); + } + /** * Compares two Elements, typically the name of a method, * field or constructor. diff --git a/test/langtools/jdk/javadoc/doclet/testErasure/TestErasure.java b/test/langtools/jdk/javadoc/doclet/testErasure/TestErasure.java new file mode 100644 index 00000000000..3c9f9b6fd9d --- /dev/null +++ b/test/langtools/jdk/javadoc/doclet/testErasure/TestErasure.java @@ -0,0 +1,381 @@ +/* + * Copyright (c) 2024, 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 8297879 + * @library /tools/lib ../../lib + * @modules jdk.javadoc/jdk.javadoc.internal.tool + * @build toolbox.ToolBox javadoc.tester.* + * @run main TestErasure + */ + +import java.io.IOException; +import java.nio.file.Path; + +import javadoc.tester.JavadocTester; +import toolbox.ToolBox; + +public class TestErasure extends JavadocTester { + + private final ToolBox tb = new ToolBox(); + + public static void main(String... args) throws Exception { + new TestErasure().runTests(); + } + + /* + * Create confusion between: + * - a constructor/method type parameter and a like-named class + * - similarly named but differently bounded constructor type parameters + * - similarly named but differently bounded type parameter in methods + */ + @Test + public void test1(Path base) throws IOException { + Path src = base.resolve("src"); + // - put public class first so that writeJavaFiles is not confused + // on the name of the file it should create + // + // - an _abstract_ class is used only for convenience: like an interface, + // it allows to keep the test minimal, but unlike an interface, it + // allows to test constructors + tb.writeJavaFiles(src, """ + public abstract class Foo { + public Foo(T arg) { } + public Foo(T arg) { } + public Foo(T arg) { } + public abstract T m(T arg); + public abstract T m(T arg); + public abstract T m(T arg); + } + class T { } + class X { } + class Y { } + """); + + javadoc("-d", base.resolve("out").toString(), + src.resolve("Foo.java").toString()); + + checkExit(Exit.OK); + // constructors + checkOutput("Foo.html", true, """ +
+

Constructor Summary

+
Constructors
+
+
Constructor
+
Description
+
\ + Foo(T arg)
+
 
+
\ + Foo(T arg)
+
 
+
\ + Foo(T arg)
+
 
+
+
"""); + checkOutput("Foo.html", true, """ +
  • Constructor Details +
      +
    1. Foo(T)
    2. +
    3. Foo(T)
    4. +
    5. Foo(T)
    6. +
    +
  • """); + checkOutput("index-all.html", true, """ +
    Foo(T)\ + - Constructor for class Foo
    +
     
    +
    Foo(T)\ + - Constructor for class Foo
    +
     
    +
    Foo(T)\ + - Constructor for class Foo
    +
     
    """); + checkOutput("member-search-index.js", true, """ + {"p":"","c":"Foo","l":"Foo(T)","u":"%3Cinit%3E(T)"},\ + {"p":"","c":"Foo","l":"Foo(T)","u":"%3Cinit%3E(X)"},\ + {"p":"","c":"Foo","l":"Foo(T)","u":"%3Cinit%3E(Y)"}"""); + // methods + checkOutput("Foo.html", true, """ +
    abstract T
    +
    m(T arg)
    +
     
    +
    abstract <T extends X>
    T
    +
    m(T arg)
    +
     
    +
    abstract <T extends Y>
    T
    +
    m(T arg)
    +
     
    """); + checkOutput("Foo.html", true, """ +
  • Method Details +
      +
    1. m(T)
    2. +
    3. m(T)
    4. +
    5. m(T)
    6. +
    +
  • """); + checkOutput("index-all.html", true, """ +
    m(T)\ + - Method in class Foo
    +
     
    +
    m(T)\ + - Method in class Foo
    +
     
    +
    m(T)\ + - Method in class Foo
    +
     
    """); + checkOutput("member-search-index.js", true, """ + {"p":"","c":"Foo","l":"m(T)"},\ + {"p":"","c":"Foo","l":"m(T)","u":"m(X)"},\ + {"p":"","c":"Foo","l":"m(T)","u":"m(Y)"}"""); + } + + /* + * Create confusion between the class type parameter + * and a like-named constructor/method type parameter. + */ + @Test + public void test2(Path base) throws IOException { + Path src = base.resolve("src"); + tb.writeJavaFiles(src, """ + public abstract class Foo { + public Foo(T arg) { } + public Foo(T arg) { } + public abstract T m(T arg); + public abstract T m(T arg); + } + class X { } + """); + + javadoc("-d", base.resolve("out").toString(), + src.resolve("Foo.java").toString()); + + checkExit(Exit.OK); + // constructors + checkOutput("Foo.html", true, """ +
    +

    Constructor Summary

    +
    Constructors
    +
    +
    Constructor
    +
    Description
    +
    \ + Foo\ + (T arg)
    +
     
    +
    \ + Foo(T arg)
    +
     
    +
    +
    """); + checkOutput("Foo.html", true, """ +
  • Constructor Details +
      +
    1. Foo(T)
    2. +
    3. Foo(T)
    4. +
    +
  • """); + checkOutput("index-all.html", true, """ +
    Foo(T)\ + - Constructor for class Foo
    +
     
    +
    Foo(T)\ + - Constructor for class Foo
    +
     
    """); + checkOutput("member-search-index.js", true, """ + {"p":"","c":"Foo","l":"Foo(T)","u":"%3Cinit%3E(T)"},\ + {"p":"","c":"Foo","l":"Foo(T)","u":"%3Cinit%3E(X)"}"""); + // methods + checkOutput("Foo.html", true, """ +
    abstract T
    +
    m\ + (T arg)
    +
     
    +
    abstract <T extends X>
    T
    +
    m(T arg)
    +
     
    """); + checkOutput("Foo.html", true, """ +
  • Method Details +
      +
    1. m(T)
    2. +
    3. m(T)
    4. +
    +
  • """); + checkOutput("index-all.html", true, """ +
    m(T)\ + - Method in class Foo
    +
     
    +
    m(T)\ + - Method in class Foo
    +
     
    """); + checkOutput("member-search-index.js", true, """ + {"p":"","c":"Foo","l":"m(T)"},\ + {"p":"","c":"Foo","l":"m(T)","u":"m(X)"}"""); + } + + @Test + public void testNewAndDeprecated(Path base) throws IOException { + Path src = base.resolve("src"); + tb.writeJavaFiles(src, """ + public abstract class Foo { + /** @since today */ + @Deprecated(since="tomorrow") + public Foo(T arg) { } + /** @since today */ + @Deprecated(since="tomorrow") + public Foo(T arg) { } + /** @since today */ + @Deprecated(since="tomorrow") + public Foo(T arg) { } + /** @since today */ + @Deprecated(since="tomorrow") + public abstract T m(T arg); + /** @since today */ + @Deprecated(since="tomorrow") + public abstract T m(T arg); + /** @since today */ + @Deprecated(since="tomorrow") + public abstract T m(T arg); + } + class T { } + class X { } + class Y { } + """); + + javadoc("-d", base.resolve("out").toString(), + "--since", "today", + src.resolve("Foo.java").toString()); + + checkExit(Exit.OK); + checkOutput("new-list.html", true, """ + +
    today
    +
     
    + +
    today
    +
     
    + +
    today
    +
     
    """); + checkOutput("new-list.html", true, """ + +
    today
    +
     
    + +
    today
    +
     
    + +
    today
    +
     
    """); + checkOutput("deprecated-list.html", true, """ + +
    tomorrow
    +
    + +
    tomorrow
    +
    + +
    tomorrow
    +
    """); + checkOutput("deprecated-list.html", true, """ + +
    tomorrow
    +
    + +
    tomorrow
    +
    + +
    tomorrow
    +
    """); + } + + @Test + public void testPreview(Path base) throws IOException { + // unlike that for other tests, here we cannot simulate ambiguity between + // a type parameter and a like-named class, because for that the class + // needs to be in the unnamed package, otherwise its FQN won't be T + Path src = base.resolve("src"); + tb.writeJavaFiles(src, """ + package p; + import jdk.internal.javac.PreviewFeature; + public abstract class Foo { + @PreviewFeature(feature=PreviewFeature.Feature.TEST) + public Foo(T arg) { } + @PreviewFeature(feature=PreviewFeature.Feature.TEST) + public Foo(T arg) { } + @PreviewFeature(feature=PreviewFeature.Feature.TEST) + public abstract T m(T arg); + @PreviewFeature(feature=PreviewFeature.Feature.TEST) + public abstract T m(T arg); + } + class X { } + class Y { } + """); + + javadoc("-d", base.resolve("out").toString(), + "--patch-module", "java.base=" + src.toAbsolutePath().toString(), + src.resolve("p").resolve("Foo.java").toString()); + + checkExit(Exit.OK); + checkOutput("preview-list.html", true, """ + +
    Test Feature
    +
    + +
    Test Feature
    +
    """); + checkOutput("preview-list.html", true, """ + +
    Test Feature
    +
    + +
    Test Feature
    +
    """); + } +} \ No newline at end of file