1 /*
   2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @bug      8225055
  27  * @summary  Record types
  28  * @library  /tools/lib ../../lib
  29  * @modules jdk.javadoc/jdk.javadoc.internal.tool
  30  * @build    toolbox.ToolBox javadoc.tester.*
  31  * @compile --enable-preview --source ${jdk.version} TestRecordTypes.java
  32  * @run main/othervm --enable-preview TestRecordTypes
  33  */
  34 
  35 
  36 import java.io.IOException;
  37 import java.lang.annotation.ElementType;
  38 import java.nio.file.Path;
  39 import java.util.EnumSet;
  40 import java.util.Locale;
  41 import java.util.Set;
  42 import java.util.stream.Collectors;
  43 
  44 import javadoc.tester.JavadocTester;
  45 import toolbox.ToolBox;
  46 
  47 public class TestRecordTypes extends JavadocTester {
  48     public static void main(String... args) throws Exception {
  49         TestRecordTypes tester = new TestRecordTypes();
  50         tester.runTests(m -> new Object[] { Path.of(m.getName()) });
  51     }
  52 
  53     private final ToolBox tb = new ToolBox();
  54 
  55     // The following constants are set up for use with -linkoffline
  56     // (but note: JDK 11 does not include java.lang.Record, so expect
  57     // some 404 broken links until we can update this to a stable version.)
  58     private static final String externalDocs = 
  59         "https://docs.oracle.com/en/java/javase/11/docs/api";
  60     private static final String localDocs =
  61         Path.of(testSrc).resolve("jdk11").toUri().toString();
  62 
  63     @Test
  64     public void testRecordKeywordUnnamedPackage(Path base) throws IOException {
  65         Path src = base.resolve("src");
  66         tb.writeJavaFiles(src,
  67                 "public record R(int r1) { }");
  68 
  69         javadoc("-d", base.resolve("out").toString(),
  70                 "-quiet", "-noindex",
  71                 "-sourcepath", src.toString(),
  72                 "--enable-preview", "--source", thisRelease,
  73                 src.resolve("R.java").toString());
  74         checkExit(Exit.OK);
  75 
  76         checkOutput("R.html", true,
  77                 "<h1 title=\"Record R\" class=\"title\">Record R</h1>",
  78                 "public record <span class=\"typeNameLabel\">R</span>",
  79                 "<code><span class=\"memberNameLink\"><a href=\"#%3Cinit%3E(int)\">R</a></span>​(int&nbsp;r1)</code>");
  80     }
  81 
  82     @Test
  83     public void testRecordKeywordNamedPackage(Path base) throws IOException {
  84         Path src = base.resolve("src");
  85         tb.writeJavaFiles(src,
  86                 "package p; public record R(int r1) { }");
  87 
  88         javadoc("-d", base.resolve("out").toString(),
  89                 "-quiet", "-noindex",
  90                 "-sourcepath", src.toString(),
  91                 "--enable-preview", "--source", thisRelease,
  92                 "p");
  93         checkExit(Exit.OK);
  94 
  95         checkOutput("p/R.html", true,
  96                 "<h1 title=\"Record R\" class=\"title\">Record R</h1>",
  97                 "public record <span class=\"typeNameLabel\">R</span>",
  98                 "<code><span class=\"memberNameLink\"><a href=\"#%3Cinit%3E(int)\">R</a></span>​(int&nbsp;r1)</code>");
  99     }
 100 
 101     @Test
 102     public void testEmptyRecord(Path base) throws IOException {
 103         Path src = base.resolve("src");
 104         tb.writeJavaFiles(src,
 105                 "package p; public record R() { }");
 106 
 107         javadoc("-d", base.resolve("out").toString(),
 108                 "-quiet", "-noindex",
 109                 "-sourcepath", src.toString(),
 110                 "--enable-preview", "--source", thisRelease,
 111                 "p");
 112         checkExit(Exit.OK);
 113 
 114         checkOutput("p/R.html", true,
 115                 "<h1 title=\"Record R\" class=\"title\">Record R</h1>",
 116                 "public record <span class=\"typeNameLabel\">R</span>",
 117                 "<code><span class=\"memberNameLink\"><a href=\"#%3Cinit%3E()\">R</a></span>()</code>");
 118     }
 119 
 120     @Test
 121     public void testAtParam(Path base) throws IOException {
 122         Path src = base.resolve("src");
 123         tb.writeJavaFiles(src,
 124                 "package p; /** This is record R. \n"
 125                 + " * @param r1 This is a component.\n"
 126                 + " */\n"
 127                 + "public record R(int r1) { }");
 128 
 129         javadoc("-d", base.resolve("out").toString(),
 130                 "-quiet", "-noindex",
 131                 "-sourcepath", src.toString(),
 132                 "--enable-preview", "--source", thisRelease,
 133                 "p");
 134         checkExit(Exit.OK);
 135 
 136         checkOutput("p/R.html", true,
 137                 "<h1 title=\"Record R\" class=\"title\">Record R</h1>",
 138                 "public record <span class=\"typeNameLabel\">R</span>",
 139                 "<dl>\n"
 140                 + "<dt><span class=\"paramLabel\">Record Components:</span></dt>\n"
 141                 + "<dd><code><a id=\"param-r1\">r1</a></code> - This is a component.</dd>\n"
 142                 + "</dl>",
 143                 "<code><span class=\"memberNameLink\"><a href=\"#%3Cinit%3E(int)\">R</a></span>​(int&nbsp;r1)</code>");
 144     }
 145 
 146     @Test
 147     public void testAtParamTyParam(Path base) throws IOException {
 148         Path src = base.resolve("src");
 149         tb.writeJavaFiles(src,
 150                 "package p; /** This is record R. \n"
 151                 + " * @param r1  This is a component.\n"
 152                 + " * @param <T> This is a type parameter.\n"
 153                 + " */\n"
 154                 + "public record R<T>(int r1) { }");
 155 
 156         javadoc("-d", base.resolve("out").toString(),
 157                 "-quiet", "-noindex",
 158                 "-sourcepath", src.toString(),
 159                 "--enable-preview", "--source", thisRelease,
 160                 "p");
 161         checkExit(Exit.OK);
 162 
 163         checkOutput("p/R.html", true,
 164                 "<h1 title=\"Record R\" class=\"title\">Record R&lt;T&gt;</h1>",
 165                 "public record <span class=\"typeNameLabel\">R&lt;T&gt;</span>",
 166                 "<dl>\n"
 167                 + "<dt><span class=\"paramLabel\">Type Parameters:</span></dt>\n"
 168                 + "<dd><code>T</code> - This is a type parameter.</dd>\n"
 169                 + "<dt><span class=\"paramLabel\">Record Components:</span></dt>\n"
 170                 + "<dd><code><a id=\"param-r1\">r1</a></code> - This is a component.</dd>\n"
 171                 + "</dl>",
 172                 "<code><span class=\"memberNameLink\"><a href=\"#%3Cinit%3E(int)\">R</a></span>​(int&nbsp;r1)</code>");
 173     }
 174 
 175     @Test
 176     public void testGeneratedComments(Path base) throws IOException {
 177         Path src = base.resolve("src");
 178         tb.writeJavaFiles(src,
 179                 "package p; /** This is record R. \n"
 180                         + " * @param r1  This is a component.\n"
 181                         + " */\n"
 182                         + "public record R(int r1) { }");
 183 
 184         javadoc("-d", base.resolve("out").toString(),
 185                 "-quiet", "-noindex",
 186                 "-sourcepath", src.toString(),
 187                 "--enable-preview", "--source", thisRelease,
 188                 "p");
 189         checkExit(Exit.OK);
 190 
 191         // While we don't normally test values that just come from resource files,
 192         // in these cases, we want to verify that something non-empty was put into
 193         // the documentation for the generated members.
 194         checkOrder("p/R.html",
 195                 "<section class=\"constructorSummary\">",
 196                 "<a href=\"#%3Cinit%3E(int)\">R</a>",
 197                 "Creates an instance of a <code>R</code> record.",
 198                 "<section class=\"methodSummary\">",
 199                 "<a href=\"#equals(java.lang.Object)\">equals</a>",
 200                 "Indicates whether some other object is \"equal to\" this one.",
 201                 "<a href=\"#hashCode()\">hashCode</a>",
 202                 "Returns a hash code value for this object.",
 203                 "<a href=\"#r1()\">r1</a>",
 204                 "Returns the value of the <a href=\"#param-r1\"><code>r1</code></a> record component.",
 205                 "<a href=\"#toString()\">toString</a>",
 206                 "Returns a string representation of this record.",
 207                 "Method Details",
 208                 "<span class=\"memberName\">toString</span>",
 209                 "Returns a string representation of this record. The representation "
 210                 + "contains the name of the type, followed by the name and value of "
 211                 + "each of the record components.",
 212                 "<span class=\"memberName\">hashCode</span>",
 213                 "Returns a hash code value for this object. The value is derived "
 214                 + "from the hash code of each of the record components.",
 215                 "<span class=\"memberName\">equals</span>",
 216                 "Indicates whether some other object is \"equal to\" this one. "
 217                 + "The objects are equal if the other object is of the same class "
 218                 + "and if all the record components are equal. All components "
 219                 + "in this record are compared with '=='.",
 220                 "<span class=\"memberName\">r1</span>",
 221                 "Returns the value of the <a href=\"#param-r1\"><code>r1</code></a> "
 222                 + "record component."
 223         );
 224     }
 225 
 226     @Test
 227     public void testGeneratedCommentsWithLinkOffline(Path base) throws IOException {
 228         Path src = base.resolve("src");
 229         tb.writeJavaFiles(src,
 230                 "package p; /** This is record R. \n"
 231                         + " * @param r1  This is a component.\n"
 232                         + " */\n"
 233                         + "public record R(int r1) { }");
 234 
 235         javadoc("-d", base.resolve("out").toString(),
 236                 "-quiet", "-noindex",
 237                 "-sourcepath", src.toString(),
 238                 "-linkoffline", externalDocs, localDocs,
 239                 "--enable-preview", "--source", thisRelease,
 240                 "p");
 241         checkExit(Exit.OK);
 242 
 243         // While we don't normally test values that just come from resource files,
 244         // in these cases, we want to verify that something non-empty was put into
 245         // the documentation for the generated members.
 246         checkOrder("p/R.html",
 247                 "<section class=\"constructorSummary\">",
 248                 "<a href=\"#%3Cinit%3E(int)\">R</a>",
 249                 "Creates an instance of a <code>R</code> record.",
 250                 "<section class=\"methodSummary\">",
 251                 "<a href=\"#equals(java.lang.Object)\">equals</a>",
 252                 "Indicates whether some other object is \"equal to\" this one.",
 253                 "<a href=\"#hashCode()\">hashCode</a>",
 254                 "Returns a hash code value for this object.",
 255                 "<a href=\"#r1()\">r1</a>",
 256                 "Returns the value of the <a href=\"#param-r1\"><code>r1</code></a> record component.",
 257                 "<a href=\"#toString()\">toString</a>",
 258                 "Returns a string representation of this record.",
 259                 "Method Details",
 260                 "<span class=\"memberName\">toString</span>",
 261                 "Returns a string representation of this record. The representation "
 262                 + "contains the name of the type, followed by the name and value of "
 263                 + "each of the record components.",
 264                 "<span class=\"memberName\">hashCode</span>",
 265                 "Returns a hash code value for this object. The value is derived "
 266                 + "from the hash code of each of the record components.",
 267                 "<span class=\"memberName\">equals</span>",
 268                 "Indicates whether some other object is \"equal to\" this one. "
 269                 + "The objects are equal if the other object is of the same class "
 270                 + "and if all the record components are equal. All components "
 271                 + "in this record are compared with '=='.",
 272                 "<span class=\"memberName\">r1</span>",
 273                 "Returns the value of the <a href=\"#param-r1\"><code>r1</code></a> "
 274                 + "record component."
 275         );
 276     }
 277 
 278     @Test
 279     public void testGeneratedEqualsPrimitive(Path base) throws IOException {
 280         testGeneratedEquals(base, "int a, int b", 
 281              "All components in this record are compared with '=='.");
 282     }
 283 
 284     @Test
 285     public void testGeneratedEqualsReference(Path base) throws IOException {
 286         testGeneratedEquals(base, "Object a, Object b", 
 287              "All components in this record are compared with <code>Objects::equals(Object,Object)</code>");
 288     }
 289 
 290     @Test
 291     public void testGeneratedEqualsMixed(Path base) throws IOException {
 292         testGeneratedEquals(base, "int a, Object b", 
 293              "Reference components are compared with <code>Objects::equals(Object,Object)</code>; "
 294              + "primitive components are compared with '=='.");
 295     }
 296 
 297     private void testGeneratedEquals(Path base, String comps, String expect) throws IOException {
 298         Path src = base.resolve("src");
 299         tb.writeJavaFiles(src,
 300                 "package p; /** This is record R. \n"
 301                         + " */\n"
 302                         + "public record R(" + comps + ") { }");
 303 
 304         javadoc("-d", base.resolve("out").toString(),
 305                 "-quiet", "-noindex",
 306                 "-sourcepath", src.toString(),
 307                 "--enable-preview", "--source", thisRelease,
 308                 "p");
 309         checkExit(Exit.OK);
 310 
 311         checkOrder("p/R.html", expect);
 312     }
 313 
 314     @Test
 315     public void testUserComments(Path base) throws IOException {
 316         Path src = base.resolve("src");
 317         tb.writeJavaFiles(src,
 318                 "package p; /** This is record R. \n"
 319                 + " * @param r1  This is a component.\n"
 320                 + " */\n"
 321                 + "public record R(int r1) {\n"
 322                 + "/** User constructor. */ public R { }\n"
 323                 + "/** User equals. */ public boolean equals(Object other) { return false; }\n"
 324                 + "/** User hashCode. */ public int hashCode() { return 0; }\n"
 325                 + "/** User toString. */ public String toString() { return \"\"; }\n"
 326                 + "/** User accessor. */ public int r1() { return r1; }\n"
 327                 + "}");
 328 
 329         javadoc("-d", base.resolve("out").toString(),
 330                 "-quiet", "-noindex",
 331                 "-sourcepath", src.toString(),
 332                 "--enable-preview", "--source", thisRelease,
 333                 "p");
 334         checkExit(Exit.OK);
 335 
 336         checkOrder("p/R.html",
 337                 "<section class=\"constructorSummary\">",
 338                 "<a href=\"#%3Cinit%3E(int)\">R</a>",
 339                 "User constructor.",
 340                 "<section class=\"methodSummary\">",
 341                 "<a href=\"#equals(java.lang.Object)\">equals</a>",
 342                 "User equals.",
 343                 "<a href=\"#hashCode()\">hashCode</a>",
 344                 "User hashCode.",
 345                 "<a href=\"#r1()\">r1</a>",
 346                 "User accessor.",
 347                 "<a href=\"#toString()\">toString</a>",
 348                 "User toString."
 349         );
 350     }
 351 
 352     @Test
 353     public void testExamples(Path base) throws IOException {
 354         javadoc("-d", base.resolve("out-no-link").toString(),
 355                 "-quiet", "-noindex",
 356                 "-sourcepath", testSrc.toString(),
 357                 "-linksource",
 358                 "--enable-preview", "--source", thisRelease,
 359                 "examples");
 360 
 361         checkExit(Exit.OK);
 362         javadoc("-d", base.resolve("out-with-link").toString(),
 363                 "-quiet", "-noindex",
 364                 "-sourcepath", testSrc.toString(),
 365                 "-linksource",
 366                 "-linkoffline", externalDocs, localDocs,
 367                 "--enable-preview", "--source", thisRelease,
 368                 "examples");
 369         checkExit(Exit.OK);
 370     }
 371 
 372     @Test
 373     @SuppressWarnings("preview")
 374     public void testAnnotations(Path base) throws IOException {
 375         ElementType[] types = {
 376                 ElementType.FIELD,
 377                 ElementType.METHOD,
 378                 ElementType.PARAMETER,
 379                 ElementType.RECORD_COMPONENT
 380         };
 381         for (int i = 0; i < (1 << types.length); i++) {
 382             Set<ElementType> set = EnumSet.noneOf(ElementType.class);
 383             for (int b = 0; b < types.length; b++) {
 384                 if ((i & (1 << b)) != 0) {
 385                     set.add(types[b]);
 386                 }
 387             }
 388             testAnnotations(base, set);
 389         }
 390     }
 391 
 392     void testAnnotations(Path base, Set<ElementType> types) throws IOException {
 393         out.println("test " + types);
 394         String name = types.isEmpty() ? "none" : types.stream()
 395                 .map(k -> k.name().toLowerCase(Locale.US))
 396                 .collect(Collectors.joining("-"));
 397         Path dir = base.resolve(name);
 398         Path src = dir.resolve("src");
 399         String target = types.isEmpty() ? "" : types.stream()
 400                 .map(s -> "ElementType." + s)
 401                 .collect(Collectors.joining(", ", "@Target({", "})"));
 402         tb.writeJavaFiles(src,
 403                 "package p;\n"
 404                     + "import java.lang.annotation.*;\n"
 405                     + "@Documented\n"
 406                     + target + "\n"
 407                     + " public @interface Anno { }\n",
 408                 "package p; public @interface UndocAnno { }",
 409                 "package p; public record R(@Anno int i) { }\n");
 410 
 411         javadoc("-d", dir.resolve("out").toString(),
 412                 "-quiet", "-noindex",
 413                 "-sourcepath", src.toString(),
 414                 "-private",
 415                 "--enable-preview", "--source", thisRelease,
 416                 "p");
 417         checkExit(Exit.OK);
 418 
 419         checkOutput("p/R.html", false,
 420                 "UndocAnno");
 421 
 422         Set<ElementType> t = types.isEmpty() ? EnumSet.allOf(ElementType.class) : types;
 423         String anno = "<a href=\"Anno.html\" title=\"annotation in p\">@Anno</a>";
 424         String rcAnno = t.contains(ElementType.RECORD_COMPONENT) ? anno + " " : "";
 425         String fAnno = t.contains(ElementType.FIELD) ? "<span class=\"annotations\">" + anno + "\n</span>" : "";
 426         String pAnno = t.contains(ElementType.PARAMETER) ? anno + "\n" : "";
 427         String mAnno= t.contains(ElementType.METHOD) ? "<span class=\"annotations\">" + anno + "\n</span>" : "";
 428 
 429         checkOutput("p/R.html", true,
 430                 "<pre>public record <span class=\"typeNameLabel\">R</span>("
 431                         + rcAnno
 432                         + "int&nbsp;i)\n" +
 433                         "extends java.lang.Record</pre>",
 434                 "<div class=\"memberSignature\">"
 435                         + fAnno
 436                         + "<span class=\"modifiers\">private final</span>&nbsp;<span class=\"returnType\">int</span>"
 437                         + "&nbsp;<span class=\"memberName\">i</span></div>",
 438                 "<div class=\"memberSignature\"><span class=\"modifiers\">public</span>&nbsp;<span class=\"memberName\">R</span>"
 439                         + "​(<span class=\"arguments\">"
 440                         + pAnno
 441                         + "int&nbsp;i)</span></div>",
 442                 "<div class=\"memberSignature\">"
 443                         + mAnno
 444                         + "<span class=\"modifiers\">public</span>&nbsp;<span class=\"returnType\">int</span>"
 445                         + "&nbsp;<span class=\"memberName\">i</span>()</div>");
 446 
 447     }
 448 }