1 /* 2 * Copyright (c) 2024, 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 import java.io.File; 25 import java.io.IOException; 26 import java.io.UncheckedIOException; 27 import java.lang.Runtime.Version; 28 import java.net.URI; 29 import java.nio.file.*; 30 import java.util.*; 31 import java.util.regex.Matcher; 32 import java.util.regex.Pattern; 33 import java.util.stream.Collectors; 34 import javax.lang.model.element.*; 35 import javax.lang.model.util.ElementFilter; 36 import javax.lang.model.util.Elements; 37 import javax.lang.model.util.Types; 38 import javax.tools.*; 39 import javax.tools.JavaFileManager.Location; 40 import com.sun.source.tree.*; 41 import com.sun.source.util.JavacTask; 42 import com.sun.source.util.TreePathScanner; 43 import com.sun.source.util.Trees; 44 import com.sun.tools.javac.api.JavacTaskImpl; 45 import com.sun.tools.javac.code.Flags; 46 import com.sun.tools.javac.code.Symbol; 47 import com.sun.tools.javac.util.Pair; 48 import jtreg.SkippedException; 49 50 /* 51 This checker checks the values of the `@since` tag found in the documentation comment for an element against 52 the release in which the element first appeared. 53 The source code containing the documentation comments is read from `src.zip` in the release of JDK used to run the test. 54 The releases used to determine the expected value of `@since` tags are taken from the historical data built into `javac`. 55 56 The `@since` checker works as a two-step process: 57 In the first step, we process JDKs 9-current, only classfiles, 58 producing a map `<unique-Element-ID`> => `<version(s)-where-it-was-introduced>`. 59 - "version(s)", because we handle versioning of Preview API, so there may be two versions 60 (we use a class with two fields for preview and stable), 61 one when it was introduced as a preview, and one when it went out of preview. More on that below. 62 - For each Element, we compute the unique ID, look into the map, and if there's nothing, 63 record the current version as the originating version. 64 - At the end of this step we have a map of the Real since values 65 66 In the second step, we look at "effective" `@since` tags in the mainline sources, from `src.zip` 67 (if the test run doesn't have it, we throw a `jtreg.SkippedException`) 68 - We only check the specific MODULE whose name was passed as an argument in the test. 69 In that module, we look for unqualified exports and test those packages. 70 - The `@since` checker verifies that for every API element, the real since value and 71 the effective since value are the same, and reports an error if they are not. 72 73 Important note : We only check code written since JDK 9 as the releases used to determine the expected value 74 of @since tags are taken from the historical data built into javac which only goes back that far 75 76 note on rules for Real and effective `@since: 77 78 Real since value of an API element is computed as the oldest release in which the given API element was introduced. 79 That is: 80 - for modules, packages, classes and interfaces, the release in which the element with the given qualified name was introduced 81 - for constructors, the release in which the constructor with the given VM descriptor was introduced 82 - for methods and fields, the release in which the given method or field with the given VM descriptor became a member 83 of its enclosing class or interface, whether direct or inherited 84 85 Effective since value of an API element is computed as follows: 86 - if the given element has a @since tag in its javadoc, it is used 87 - in all other cases, return the effective since value of the enclosing element 88 89 90 Special Handling for preview method, as per JEP 12: 91 - When an element is still marked as preview, the `@since` should be the first JDK release where the element was added. 92 - If the element is no longer marked as preview, the `@since` should be the first JDK release where it was no longer preview. 93 94 note on legacy preview: Until JDK 14, the preview APIs were not marked in any machine-understandable way. 95 It was deprecated, and had a comment in the javadoc. 96 and the use of `@PreviewFeature` only became standard in JDK 17. 97 So the checker has an explicit knowledge of these preview elements. 98 99 note: The `<unique-Element-ID>` for methods looks like 100 `method: <erased-return-descriptor> <binary-name-of-enclosing-class>.<method-name>(<ParameterDescriptor>)`. 101 it is somewhat inspired from the VM Method Descriptors. But we use the erased return so that methods 102 that were later generified remain the same. 103 104 To help projects still in development, unsure of actual `@since` tag value, one may want to use token name instead of continuely 105 updating the current version since tags. For example, `@since LongRunningProjectName`. The option `--ignoreSince` maybe used to 106 ignore these tags (`--ignoreSince LongRunningProjectName`). Maybe be specified multiple times. 107 108 usage: the checker is run from a module specific test 109 `@run main SinceChecker <moduleName> [--ignoreSince <string>] [--exclude package1,package2 | --exclude package1 package2]` 110 */ 111 112 public class SinceChecker { 113 private final Map<String, Set<String>> LEGACY_PREVIEW_METHODS = new HashMap<>(); 114 private final Map<String, IntroducedIn> classDictionary = new HashMap<>(); 115 private final JavaCompiler tool; 116 private int errorCount = 0; 117 118 // Ignored since tags 119 private static final Set<String> IGNORE_SINCE = new HashSet<>(); 120 // Simply replace ignored since tags with the latest version 121 private static final Version IGNORE_VERSION = Version.parse(Integer.toString(Runtime.version().major())); 122 123 // packages to skip during the test 124 private static final Set<String> EXCLUDE_LIST = new HashSet<>(); 125 126 public static class IntroducedIn { 127 public String introducedPreview; 128 public String introducedStable; 129 } 130 131 public static void main(String[] args) throws Exception { 132 if (args.length == 0) { 133 throw new IllegalArgumentException("Test module not specified"); 134 } 135 String moduleName = args[0]; 136 boolean excludeFlag = false; 137 138 for (int i = 1; i < args.length; i++) { 139 if ("--ignoreSince".equals(args[i])) { 140 i++; 141 IGNORE_SINCE.add("@since " + args[i]); 142 } 143 else if ("--exclude".equals(args[i])) { 144 excludeFlag = true; 145 continue; 146 } 147 148 if (excludeFlag) { 149 if (args[i].contains(",")) { 150 EXCLUDE_LIST.addAll(Arrays.asList(args[i].split(","))); 151 } else { 152 EXCLUDE_LIST.add(args[i]); 153 } 154 } 155 } 156 157 SinceChecker sinceCheckerTestHelper = new SinceChecker(moduleName); 158 sinceCheckerTestHelper.checkModule(moduleName); 159 } 160 161 private void error(String message) { 162 System.err.println(message); 163 errorCount++; 164 } 165 166 private SinceChecker(String moduleName) throws IOException { 167 tool = ToolProvider.getSystemJavaCompiler(); 168 for (int i = 9; i <= Runtime.version().feature(); i++) { 169 DiagnosticListener<? super JavaFileObject> noErrors = d -> { 170 if (!d.getCode().equals("compiler.err.module.not.found")) { 171 error(d.getMessage(null)); 172 } 173 }; 174 JavacTask ct = (JavacTask) tool.getTask(null, 175 null, 176 noErrors, 177 List.of("--add-modules", moduleName, "--release", String.valueOf(i)), 178 null, 179 Collections.singletonList(SimpleJavaFileObject.forSource(URI.create("myfo:/Test.java"), ""))); 180 ct.analyze(); 181 182 String version = String.valueOf(i); 183 Elements elements = ct.getElements(); 184 elements.getModuleElement("java.base"); // forces module graph to be instantiated 185 elements.getAllModuleElements().forEach(me -> 186 processModuleElement(me, version, ct)); 187 } 188 } 189 190 private void processModuleElement(ModuleElement moduleElement, String releaseVersion, JavacTask ct) { 191 processElement(moduleElement, moduleElement, ct.getTypes(), releaseVersion); 192 for (ModuleElement.ExportsDirective ed : ElementFilter.exportsIn(moduleElement.getDirectives())) { 193 if (ed.getTargetModules() == null) { 194 processPackageElement(ed.getPackage(), releaseVersion, ct); 195 } 196 } 197 } 198 199 private void processPackageElement(PackageElement pe, String releaseVersion, JavacTask ct) { 200 processElement(pe, pe, ct.getTypes(), releaseVersion); 201 List<TypeElement> typeElements = ElementFilter.typesIn(pe.getEnclosedElements()); 202 for (TypeElement te : typeElements) { 203 processClassElement(te, releaseVersion, ct.getTypes(), ct.getElements()); 204 } 205 } 206 207 /// JDK documentation only contains public and protected declarations 208 private boolean isDocumented(Element te) { 209 Set<Modifier> mod = te.getModifiers(); 210 return mod.contains(Modifier.PUBLIC) || mod.contains(Modifier.PROTECTED); 211 } 212 213 private boolean isMember(Element e) { 214 var kind = e.getKind(); 215 return kind.isField() || switch (kind) { 216 case METHOD, CONSTRUCTOR -> true; 217 default -> false; 218 }; 219 } 220 221 private void processClassElement(TypeElement te, String version, Types types, Elements elements) { 222 if (!isDocumented(te)) { 223 return; 224 } 225 processElement(te.getEnclosingElement(), te, types, version); 226 elements.getAllMembers(te).stream() 227 .filter(this::isDocumented) 228 .filter(this::isMember) 229 .forEach(element -> processElement(te, element, types, version)); 230 te.getEnclosedElements().stream() 231 .filter(element -> element.getKind().isDeclaredType()) 232 .map(TypeElement.class::cast) 233 .forEach(nestedClass -> processClassElement(nestedClass, version, types, elements)); 234 } 235 236 private void processElement(Element explicitOwner, Element element, Types types, String version) { 237 String uniqueId = getElementName(explicitOwner, element, types); 238 IntroducedIn introduced = classDictionary.computeIfAbsent(uniqueId, _ -> new IntroducedIn()); 239 if (isPreview(element, uniqueId, version)) { 240 if (introduced.introducedPreview == null) { 241 introduced.introducedPreview = version; 242 } 243 } else { 244 if (introduced.introducedStable == null) { 245 introduced.introducedStable = version; 246 } 247 } 248 } 249 250 private boolean isPreview(Element el, String uniqueId, String currentVersion) { 251 while (el != null) { 252 Symbol s = (Symbol) el; 253 if ((s.flags() & Flags.PREVIEW_API) != 0) { 254 return true; 255 } 256 el = el.getEnclosingElement(); 257 } 258 259 return LEGACY_PREVIEW_METHODS.getOrDefault(currentVersion, Set.of()) 260 .contains(uniqueId); 261 } 262 263 private void checkModule(String moduleName) throws Exception { 264 Path home = Paths.get(System.getProperty("java.home")); 265 Path srcZip = home.resolve("lib").resolve("src.zip"); 266 if (Files.notExists(srcZip)) { 267 //possibly running over an exploded JDK build, attempt to find a 268 //co-located full JDK image with src.zip: 269 Path testJdk = Paths.get(System.getProperty("test.jdk")); 270 srcZip = testJdk.getParent().resolve("images").resolve("jdk").resolve("lib").resolve("src.zip"); 271 } 272 if (!Files.isReadable(srcZip)) { 273 throw new SkippedException("Skipping Test because src.zip wasn't found or couldn't be read"); 274 } 275 URI uri = URI.create("jar:" + srcZip.toUri()); 276 try (FileSystem zipFO = FileSystems.newFileSystem(uri, Collections.emptyMap())) { 277 Path root = zipFO.getRootDirectories().iterator().next(); 278 Path moduleDirectory = root.resolve(moduleName); 279 try (StandardJavaFileManager fm = 280 tool.getStandardFileManager(null, null, null)) { 281 JavacTask ct = (JavacTask) tool.getTask(null, 282 fm, 283 null, 284 List.of("--add-modules", moduleName, "-d", "."), 285 null, 286 Collections.singletonList(SimpleJavaFileObject.forSource(URI.create("myfo:/Test.java"), ""))); 287 ct.analyze(); 288 Elements elements = ct.getElements(); 289 elements.getModuleElement("java.base"); 290 try (EffectiveSourceSinceHelper javadocHelper = EffectiveSourceSinceHelper.create(ct, List.of(root), this)) { 291 processModuleCheck(elements.getModuleElement(moduleName), ct, moduleDirectory, javadocHelper); 292 } catch (Exception e) { 293 e.printStackTrace(); 294 error("Initiating javadocHelper Failed " + e); 295 } 296 if (errorCount > 0) { 297 throw new Exception("The `@since` checker found " + errorCount + " problems"); 298 } 299 } 300 } 301 } 302 303 private boolean isExcluded(ModuleElement.ExportsDirective ed ){ 304 return EXCLUDE_LIST.stream().anyMatch(excludePackage -> 305 ed.getPackage().toString().equals(excludePackage) || 306 ed.getPackage().toString().startsWith(excludePackage + ".")); 307 } 308 309 private void processModuleCheck(ModuleElement moduleElement, JavacTask ct, Path moduleDirectory, EffectiveSourceSinceHelper javadocHelper) { 310 if (moduleElement == null) { 311 error("Module element: was null because `elements.getModuleElement(moduleName)` returns null." + 312 "fixes are needed for this Module"); 313 } 314 String moduleVersion = getModuleVersionFromFile(moduleDirectory); 315 checkModuleOrPackage(javadocHelper, moduleVersion, moduleElement, ct, "Module: "); 316 for (ModuleElement.ExportsDirective ed : ElementFilter.exportsIn(moduleElement.getDirectives())) { 317 if (ed.getTargetModules() == null) { 318 String packageVersion = getPackageVersionFromFile(moduleDirectory, ed); 319 if (packageVersion != null && !isExcluded(ed)) { 320 checkModuleOrPackage(javadocHelper, packageVersion, ed.getPackage(), ct, "Package: "); 321 analyzePackageCheck(ed.getPackage(), ct, javadocHelper); 322 } // Skip the package if packageVersion is null 323 } 324 } 325 } 326 327 private void checkModuleOrPackage(EffectiveSourceSinceHelper javadocHelper, String moduleVersion, Element moduleElement, JavacTask ct, String elementCategory) { 328 String id = getElementName(moduleElement, moduleElement, ct.getTypes()); 329 var elementInfo = classDictionary.get(id); 330 if (elementInfo == null) { 331 error("Element :" + id + " was not mapped"); 332 return; 333 } 334 String version = elementInfo.introducedStable; 335 if (moduleVersion == null) { 336 error("Unable to retrieve `@since` for " + elementCategory + id); 337 } else { 338 String position = javadocHelper.getElementPosition(id); 339 checkEquals(position, moduleVersion, version, id); 340 } 341 } 342 343 private String getModuleVersionFromFile(Path moduleDirectory) { 344 Path moduleInfoFile = moduleDirectory.resolve("module-info.java"); 345 String version = null; 346 if (Files.exists(moduleInfoFile)) { 347 try { 348 String moduleInfoContent = Files.readString(moduleInfoFile); 349 var extractedVersion = extractSinceVersionFromText(moduleInfoContent); 350 if (extractedVersion != null) { 351 version = extractedVersion.toString(); 352 } 353 } catch (IOException e) { 354 error("module-info.java not found or couldn't be opened AND this module has no unqualified exports"); 355 } 356 } 357 return version; 358 } 359 360 private String getPackageVersionFromFile(Path moduleDirectory, ModuleElement.ExportsDirective ed) { 361 Path pkgInfo = moduleDirectory.resolve(ed.getPackage() 362 .getQualifiedName() 363 .toString() 364 .replace(".", File.separator) 365 ) 366 .resolve("package-info.java"); 367 368 if (!Files.exists(pkgInfo)) { 369 return null; // Skip if the file does not exist 370 } 371 372 String packageTopVersion = null; 373 try { 374 String packageContent = Files.readString(pkgInfo); 375 var extractedVersion = extractSinceVersionFromText(packageContent); 376 if (extractedVersion != null) { 377 packageTopVersion = extractedVersion.toString(); 378 } else { 379 error(ed.getPackage().getQualifiedName() + ": package-info.java exists but doesn't contain @since"); 380 } 381 } catch (IOException e) { 382 error(ed.getPackage().getQualifiedName() + ": package-info.java couldn't be opened"); 383 } 384 return packageTopVersion; 385 } 386 387 private void analyzePackageCheck(PackageElement pe, JavacTask ct, EffectiveSourceSinceHelper javadocHelper) { 388 List<TypeElement> typeElements = ElementFilter.typesIn(pe.getEnclosedElements()); 389 for (TypeElement te : typeElements) { 390 analyzeClassCheck(te, null, javadocHelper, ct.getTypes(), ct.getElements()); 391 } 392 } 393 394 private boolean isNotCommonRecordMethod(TypeElement te, Element element, Types types) { 395 var isRecord = te.getKind() == ElementKind.RECORD; 396 if (!isRecord) { 397 return true; 398 } 399 String uniqueId = getElementName(te, element, types); 400 boolean isCommonMethod = uniqueId.endsWith(".toString()") || 401 uniqueId.endsWith(".hashCode()") || 402 uniqueId.endsWith(".equals(java.lang.Object)"); 403 if (isCommonMethod) { 404 return false; 405 } 406 for (var parameter : te.getEnclosedElements()) { 407 if (parameter.getKind() == ElementKind.RECORD_COMPONENT) { 408 if (uniqueId.endsWith(String.format("%s.%s()", te.getSimpleName(), parameter.getSimpleName().toString()))) { 409 return false; 410 } 411 } 412 } 413 return true; 414 } 415 416 private void analyzeClassCheck(TypeElement te, String version, EffectiveSourceSinceHelper javadocHelper, 417 Types types, Elements elementUtils) { 418 String currentjdkVersion = String.valueOf(Runtime.version().feature()); 419 if (!isDocumented(te)) { 420 return; 421 } 422 checkElement(te.getEnclosingElement(), te, types, javadocHelper, version, elementUtils); 423 te.getEnclosedElements().stream().filter(this::isDocumented) 424 .filter(this::isMember) 425 .filter(element -> isNotCommonRecordMethod(te, element, types)) 426 .forEach(element -> checkElement(te, element, types, javadocHelper, version, elementUtils)); 427 te.getEnclosedElements().stream() 428 .filter(element -> element.getKind().isDeclaredType()) 429 .map(TypeElement.class::cast) 430 .forEach(nestedClass -> analyzeClassCheck(nestedClass, currentjdkVersion, javadocHelper, types, elementUtils)); 431 } 432 433 private void checkElement(Element explicitOwner, Element element, Types types, 434 EffectiveSourceSinceHelper javadocHelper, String currentVersion, Elements elementUtils) { 435 String uniqueId = getElementName(explicitOwner, element, types); 436 437 if (element.getKind() == ElementKind.METHOD && 438 element.getEnclosingElement().getKind() == ElementKind.ENUM && 439 (uniqueId.contains(".values()") || uniqueId.contains(".valueOf(java.lang.String)"))) { 440 //mandated enum type methods 441 return; 442 } 443 String sinceVersion = null; 444 var effectiveSince = javadocHelper.effectiveSinceVersion(explicitOwner, element, types, elementUtils); 445 if (effectiveSince == null) { 446 // Skip the element if the java file doesn't exist in src.zip 447 return; 448 } 449 sinceVersion = effectiveSince.toString(); 450 IntroducedIn mappedVersion = classDictionary.get(uniqueId); 451 if (mappedVersion == null) { 452 error("Element: " + uniqueId + " was not mapped"); 453 return; 454 } 455 String realMappedVersion = null; 456 try { 457 realMappedVersion = isPreview(element, uniqueId, currentVersion) ? 458 mappedVersion.introducedPreview : 459 mappedVersion.introducedStable; 460 } catch (Exception e) { 461 error("For element " + element + "mappedVersion" + mappedVersion + " is null " + e); 462 } 463 String position = javadocHelper.getElementPosition(uniqueId); 464 checkEquals(position, sinceVersion, realMappedVersion, uniqueId); 465 } 466 467 private Version extractSinceVersionFromText(String documentation) { 468 for (String ignoreSince : IGNORE_SINCE) { 469 if (documentation.contains(ignoreSince)) { 470 return IGNORE_VERSION; 471 } 472 } 473 Pattern pattern = Pattern.compile("@since\\s+(\\d+(?:\\.\\d+)?)"); 474 Matcher matcher = pattern.matcher(documentation); 475 if (matcher.find()) { 476 String versionString = matcher.group(1); 477 try { 478 if (versionString.equals("1.0")) { 479 versionString = "1"; //ended up being necessary 480 } else if (versionString.startsWith("1.")) { 481 versionString = versionString.substring(2); 482 } 483 return Version.parse(versionString); 484 } catch (NumberFormatException ex) { 485 error("`@since` value that cannot be parsed: " + versionString); 486 return null; 487 } 488 } else { 489 return null; 490 } 491 } 492 493 private void checkEquals(String prefix, String sinceVersion, String mappedVersion, String name) { 494 if (sinceVersion == null || mappedVersion == null) { 495 error(name + ": NULL value for either real or effective `@since` . real/mapped version is=" 496 + mappedVersion + " while the `@since` in the source code is= " + sinceVersion); 497 return; 498 } 499 if (Integer.parseInt(sinceVersion) < 9) { 500 sinceVersion = "9"; 501 } 502 if (!sinceVersion.equals(mappedVersion)) { 503 String message = getWrongSinceMessage(prefix, sinceVersion, mappedVersion, name); 504 error(message); 505 } 506 } 507 private static String getWrongSinceMessage(String prefix, String sinceVersion, String mappedVersion, String elementSimpleName) { 508 String message; 509 if (mappedVersion.equals("9")) { 510 message = elementSimpleName + ": `@since` version is " + sinceVersion + " but the element exists before JDK 10"; 511 } else { 512 message = elementSimpleName + ": `@since` version: " + sinceVersion + "; should be: " + mappedVersion; 513 } 514 return prefix + message; 515 } 516 517 private static String getElementName(Element owner, Element element, Types types) { 518 String prefix = ""; 519 String suffix = ""; 520 ElementKind kind = element.getKind(); 521 if (kind.isField()) { 522 TypeElement te = (TypeElement) owner; 523 prefix = "field"; 524 suffix = ": " + te.getQualifiedName() + ":" + element.getSimpleName(); 525 } else if (kind == ElementKind.METHOD || kind == ElementKind.CONSTRUCTOR) { 526 prefix = "method"; 527 TypeElement te = (TypeElement) owner; 528 ExecutableElement executableElement = (ExecutableElement) element; 529 String returnType = types.erasure(executableElement.getReturnType()).toString(); 530 String methodName = executableElement.getSimpleName().toString(); 531 String descriptor = executableElement.getParameters().stream() 532 .map(p -> types.erasure(p.asType()).toString()) 533 .collect(Collectors.joining(",", "(", ")")); 534 suffix = ": " + returnType + " " + te.getQualifiedName() + "." + methodName + descriptor; 535 } else if (kind.isDeclaredType()) { 536 if (kind.isClass()) { 537 prefix = "class"; 538 } else if (kind.isInterface()) { 539 prefix = "interface"; 540 } 541 suffix = ": " + ((TypeElement) element).getQualifiedName(); 542 } else if (kind == ElementKind.PACKAGE) { 543 prefix = "package"; 544 suffix = ": " + ((PackageElement) element).getQualifiedName(); 545 } else if (kind == ElementKind.MODULE) { 546 prefix = "module"; 547 suffix = ": " + ((ModuleElement) element).getQualifiedName(); 548 } 549 return prefix + suffix; 550 } 551 552 //these were preview in before the introduction of the @PreviewFeature 553 { 554 LEGACY_PREVIEW_METHODS.put("9", Set.of( 555 "module: jdk.nio.mapmode", 556 "module: java.transaction.xa", 557 "module: jdk.unsupported.desktop", 558 "module: jdk.jpackage", 559 "module: java.net.http" 560 )); 561 LEGACY_PREVIEW_METHODS.put("10", Set.of( 562 "module: jdk.nio.mapmode", 563 "module: java.transaction.xa", 564 "module: java.net.http", 565 "module: jdk.unsupported.desktop", 566 "module: jdk.jpackage" 567 )); 568 LEGACY_PREVIEW_METHODS.put("11", Set.of( 569 "module: jdk.nio.mapmode", 570 "module: jdk.jpackage" 571 )); 572 LEGACY_PREVIEW_METHODS.put("12", Set.of( 573 "module: jdk.nio.mapmode", 574 "module: jdk.jpackage", 575 "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.BreakTree.getValue()", 576 "method: java.util.List com.sun.source.tree.CaseTree.getExpressions()", 577 "method: com.sun.source.tree.Tree com.sun.source.tree.CaseTree.getBody()", 578 "method: com.sun.source.tree.CaseTree.CaseKind com.sun.source.tree.CaseTree.getCaseKind()", 579 "class: com.sun.source.tree.CaseTree.CaseKind", 580 "field: com.sun.source.tree.CaseTree.CaseKind:STATEMENT", 581 "field: com.sun.source.tree.CaseTree.CaseKind:RULE", 582 "field: com.sun.source.tree.Tree.Kind:SWITCH_EXPRESSION", 583 "interface: com.sun.source.tree.SwitchExpressionTree", 584 "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.SwitchExpressionTree.getExpression()", 585 "method: java.util.List com.sun.source.tree.SwitchExpressionTree.getCases()", 586 "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)", 587 "method: java.lang.Object com.sun.source.util.TreeScanner.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)", 588 "method: java.lang.Object com.sun.source.util.SimpleTreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)" 589 )); 590 591 LEGACY_PREVIEW_METHODS.put("13", Set.of( 592 "module: jdk.nio.mapmode", 593 "module: jdk.jpackage", 594 "method: java.util.List com.sun.source.tree.CaseTree.getExpressions()", 595 "method: com.sun.source.tree.Tree com.sun.source.tree.CaseTree.getBody()", 596 "method: com.sun.source.tree.CaseTree.CaseKind com.sun.source.tree.CaseTree.getCaseKind()", 597 "class: com.sun.source.tree.CaseTree.CaseKind", 598 "field: com.sun.source.tree.CaseTree.CaseKind:STATEMENT", 599 "field: com.sun.source.tree.CaseTree.CaseKind:RULE", 600 "field: com.sun.source.tree.Tree.Kind:SWITCH_EXPRESSION", 601 "interface: com.sun.source.tree.SwitchExpressionTree", 602 "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.SwitchExpressionTree.getExpression()", 603 "method: java.util.List com.sun.source.tree.SwitchExpressionTree.getCases()", 604 "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)", 605 "method: java.lang.Object com.sun.source.util.TreeScanner.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)", 606 "method: java.lang.Object com.sun.source.util.SimpleTreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)", 607 "method: java.lang.String java.lang.String.stripIndent()", 608 "method: java.lang.String java.lang.String.translateEscapes()", 609 "method: java.lang.String java.lang.String.formatted(java.lang.Object[])", 610 "class: javax.swing.plaf.basic.motif.MotifLookAndFeel", 611 "field: com.sun.source.tree.Tree.Kind:YIELD", 612 "interface: com.sun.source.tree.YieldTree", 613 "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.YieldTree.getValue()", 614 "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitYield(com.sun.source.tree.YieldTree,java.lang.Object)", 615 "method: java.lang.Object com.sun.source.util.SimpleTreeVisitor.visitYield(com.sun.source.tree.YieldTree,java.lang.Object)", 616 "method: java.lang.Object com.sun.source.util.TreeScanner.visitYield(com.sun.source.tree.YieldTree,java.lang.Object)" 617 )); 618 619 LEGACY_PREVIEW_METHODS.put("14", Set.of( 620 "module: jdk.jpackage", 621 "class: javax.swing.plaf.basic.motif.MotifLookAndFeel", 622 "field: jdk.jshell.Snippet.SubKind:RECORD_SUBKIND", 623 "class: javax.lang.model.element.RecordComponentElement", 624 "method: javax.lang.model.type.TypeMirror javax.lang.model.element.RecordComponentElement.asType()", 625 "method: java.lang.Object javax.lang.model.element.ElementVisitor.visitRecordComponent(javax.lang.model.element.RecordComponentElement,java.lang.Object)", 626 "class: javax.lang.model.util.ElementScanner14", 627 "class: javax.lang.model.util.AbstractElementVisitor14", 628 "class: javax.lang.model.util.SimpleElementVisitor14", 629 "method: java.lang.Object javax.lang.model.util.ElementKindVisitor6.visitTypeAsRecord(javax.lang.model.element.TypeElement,java.lang.Object)", 630 "class: javax.lang.model.util.ElementKindVisitor14", 631 "method: javax.lang.model.element.RecordComponentElement javax.lang.model.util.Elements.recordComponentFor(javax.lang.model.element.ExecutableElement)", 632 "method: java.util.List javax.lang.model.util.ElementFilter.recordComponentsIn(java.lang.Iterable)", 633 "method: java.util.Set javax.lang.model.util.ElementFilter.recordComponentsIn(java.util.Set)", 634 "method: java.util.List javax.lang.model.element.TypeElement.getRecordComponents()", 635 "field: javax.lang.model.element.ElementKind:RECORD", 636 "field: javax.lang.model.element.ElementKind:RECORD_COMPONENT", 637 "field: javax.lang.model.element.ElementKind:BINDING_VARIABLE", 638 "field: com.sun.source.tree.Tree.Kind:RECORD", 639 "field: sun.reflect.annotation.TypeAnnotation.TypeAnnotationTarget:RECORD_COMPONENT", 640 "class: java.lang.reflect.RecordComponent", 641 "class: java.lang.runtime.ObjectMethods", 642 "field: java.lang.annotation.ElementType:RECORD_COMPONENT", 643 "method: boolean java.lang.Class.isRecord()", 644 "method: java.lang.reflect.RecordComponent[] java.lang.Class.getRecordComponents()", 645 "class: java.lang.Record", 646 "interface: com.sun.source.tree.PatternTree", 647 "field: com.sun.source.tree.Tree.Kind:BINDING_PATTERN", 648 "method: com.sun.source.tree.PatternTree com.sun.source.tree.InstanceOfTree.getPattern()", 649 "interface: com.sun.source.tree.BindingPatternTree", 650 "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitBindingPattern(com.sun.source.tree.BindingPatternTree,java.lang.Object)" 651 )); 652 653 LEGACY_PREVIEW_METHODS.put("15", Set.of( 654 "module: jdk.jpackage", 655 "field: jdk.jshell.Snippet.SubKind:RECORD_SUBKIND", 656 "class: javax.lang.model.element.RecordComponentElement", 657 "method: javax.lang.model.type.TypeMirror javax.lang.model.element.RecordComponentElement.asType()", 658 "method: java.lang.Object javax.lang.model.element.ElementVisitor.visitRecordComponent(javax.lang.model.element.RecordComponentElement,java.lang.Object)", 659 "class: javax.lang.model.util.ElementScanner14", 660 "class: javax.lang.model.util.AbstractElementVisitor14", 661 "class: javax.lang.model.util.SimpleElementVisitor14", 662 "method: java.lang.Object javax.lang.model.util.ElementKindVisitor6.visitTypeAsRecord(javax.lang.model.element.TypeElement,java.lang.Object)", 663 "class: javax.lang.model.util.ElementKindVisitor14", 664 "method: javax.lang.model.element.RecordComponentElement javax.lang.model.util.Elements.recordComponentFor(javax.lang.model.element.ExecutableElement)", 665 "method: java.util.List javax.lang.model.util.ElementFilter.recordComponentsIn(java.lang.Iterable)", 666 "method: java.util.Set javax.lang.model.util.ElementFilter.recordComponentsIn(java.util.Set)", 667 "method: java.util.List javax.lang.model.element.TypeElement.getRecordComponents()", 668 "field: javax.lang.model.element.ElementKind:RECORD", 669 "field: javax.lang.model.element.ElementKind:RECORD_COMPONENT", 670 "field: javax.lang.model.element.ElementKind:BINDING_VARIABLE", 671 "field: com.sun.source.tree.Tree.Kind:RECORD", 672 "field: sun.reflect.annotation.TypeAnnotation.TypeAnnotationTarget:RECORD_COMPONENT", 673 "class: java.lang.reflect.RecordComponent", 674 "class: java.lang.runtime.ObjectMethods", 675 "field: java.lang.annotation.ElementType:RECORD_COMPONENT", 676 "class: java.lang.Record", 677 "method: boolean java.lang.Class.isRecord()", 678 "method: java.lang.reflect.RecordComponent[] java.lang.Class.getRecordComponents()", 679 "field: javax.lang.model.element.Modifier:SEALED", 680 "field: javax.lang.model.element.Modifier:NON_SEALED", 681 "method: javax.lang.model.element.TypeElement:getPermittedSubclasses:()", 682 "method: java.util.List com.sun.source.tree.ClassTree.getPermitsClause()", 683 "method: boolean java.lang.Class.isSealed()", 684 "method: java.lang.constant.ClassDesc[] java.lang.Class.permittedSubclasses()", 685 "interface: com.sun.source.tree.PatternTree", 686 "field: com.sun.source.tree.Tree.Kind:BINDING_PATTERN", 687 "method: com.sun.source.tree.PatternTree com.sun.source.tree.InstanceOfTree.getPattern()", 688 "interface: com.sun.source.tree.BindingPatternTree", 689 "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitBindingPattern(com.sun.source.tree.BindingPatternTree,java.lang.Object)" 690 )); 691 692 LEGACY_PREVIEW_METHODS.put("16", Set.of( 693 "field: jdk.jshell.Snippet.SubKind:RECORD_SUBKIND", 694 "field: javax.lang.model.element.Modifier:SEALED", 695 "field: javax.lang.model.element.Modifier:NON_SEALED", 696 "method: javax.lang.model.element.TypeElement:getPermittedSubclasses:()", 697 "method: java.util.List com.sun.source.tree.ClassTree.getPermitsClause()", 698 "method: boolean java.lang.Class.isSealed()", 699 "method: java.lang.constant.ClassDesc[] java.lang.Class.permittedSubclasses()" 700 )); 701 702 // java.lang.foreign existed since JDK 19 and wasn't annotated - went out of preview in JDK 22 703 LEGACY_PREVIEW_METHODS.put("19", Set.of( 704 "package: java.lang.foreign" 705 )); 706 LEGACY_PREVIEW_METHODS.put("20", Set.of( 707 "package: java.lang.foreign" 708 )); 709 LEGACY_PREVIEW_METHODS.put("21", Set.of( 710 "package: java.lang.foreign" 711 )); 712 } 713 714 /** 715 * Helper to find javadoc and resolve @inheritDoc and the effective since version. 716 */ 717 718 private final class EffectiveSourceSinceHelper implements AutoCloseable { 719 private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 720 private final JavaFileManager baseFileManager; 721 private final StandardJavaFileManager fm; 722 private final Set<String> seenLookupElements = new HashSet<>(); 723 private final Map<String, Version> signature2Source = new HashMap<>(); 724 private final Map<String, String> signature2Location = new HashMap<>(); 725 726 /** 727 * Create the helper. 728 * 729 * @param mainTask JavacTask from which the further Elements originate 730 * @param sourceLocations paths where source files should be searched 731 * @param validator enclosing class of the helper, typically the object invoking this method 732 * @return a EffectiveSourceSinceHelper 733 */ 734 735 public static EffectiveSourceSinceHelper create(JavacTask mainTask, Collection<? extends Path> sourceLocations, SinceChecker validator) { 736 StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null); 737 try { 738 fm.setLocationFromPaths(StandardLocation.MODULE_SOURCE_PATH, sourceLocations); 739 return validator.new EffectiveSourceSinceHelper(mainTask, fm); 740 } catch (IOException ex) { 741 try { 742 fm.close(); 743 } catch (IOException closeEx) { 744 ex.addSuppressed(closeEx); 745 } 746 throw new UncheckedIOException(ex); 747 } 748 } 749 750 private EffectiveSourceSinceHelper(JavacTask mainTask, StandardJavaFileManager fm) { 751 this.baseFileManager = ((JavacTaskImpl) mainTask).getContext().get(JavaFileManager.class); 752 this.fm = fm; 753 } 754 755 public Version effectiveSinceVersion(Element owner, Element element, Types typeUtils, Elements elementUtils) { 756 String handle = getElementName(owner, element, typeUtils); 757 Version since = signature2Source.get(handle); 758 759 if (since == null) { 760 try { 761 Element lookupElement = switch (element.getKind()) { 762 case MODULE, PACKAGE -> element; 763 default -> elementUtils.getOutermostTypeElement(element); 764 }; 765 766 if (lookupElement == null) 767 return null; 768 769 String lookupHandle = getElementName(owner, element, typeUtils); 770 771 if (!seenLookupElements.add(lookupHandle)) { 772 //we've already processed this top-level, don't try to compute 773 //the values again: 774 return null; 775 } 776 777 Pair<JavacTask, CompilationUnitTree> source = findSource(lookupElement, elementUtils); 778 779 if (source == null) 780 return null; 781 782 fillElementCache(source.fst, source.snd, source.fst.getTypes(), source.fst.getElements()); 783 since = signature2Source.get(handle); 784 785 } catch (IOException ex) { 786 error("JavadocHelper failed for " + element); 787 } 788 } 789 790 return since; 791 } 792 793 private String getElementPosition(String signature) { 794 return signature2Location.getOrDefault(signature, ""); 795 } 796 797 //where: 798 private void fillElementCache(JavacTask task, CompilationUnitTree cut, Types typeUtils, Elements elementUtils) { 799 Trees trees = Trees.instance(task); 800 String fileName = cut.getSourceFile().getName(); 801 802 new TreePathScanner<Void, Void>() { 803 @Override 804 public Void visitMethod(MethodTree node, Void p) { 805 handleDeclaration(node, fileName); 806 return null; 807 } 808 809 @Override 810 public Void visitClass(ClassTree node, Void p) { 811 handleDeclaration(node, fileName); 812 return super.visitClass(node, p); 813 } 814 815 @Override 816 public Void visitVariable(VariableTree node, Void p) { 817 handleDeclaration(node, fileName); 818 return null; 819 } 820 821 @Override 822 public Void visitModule(ModuleTree node, Void p) { 823 handleDeclaration(node, fileName); 824 return null; 825 } 826 827 @Override 828 public Void visitBlock(BlockTree node, Void p) { 829 return null; 830 } 831 832 @Override 833 public Void visitPackage(PackageTree node, Void p) { 834 if (cut.getSourceFile().isNameCompatible("package-info", JavaFileObject.Kind.SOURCE)) { 835 handleDeclaration(node, fileName); 836 } 837 return super.visitPackage(node, p); 838 } 839 840 private void handleDeclaration(Tree node, String fileName) { 841 Element currentElement = trees.getElement(getCurrentPath()); 842 843 if (currentElement != null) { 844 long startPosition = trees.getSourcePositions().getStartPosition(cut, node); 845 long lineNumber = cut.getLineMap().getLineNumber(startPosition); 846 String filePathWithLineNumber = String.format("src%s:%s ", fileName, lineNumber); 847 848 signature2Source.put(getElementName(currentElement.getEnclosingElement(), currentElement, typeUtils), computeSinceVersion(currentElement, typeUtils, elementUtils)); 849 signature2Location.put(getElementName(currentElement.getEnclosingElement(), currentElement, typeUtils), filePathWithLineNumber); 850 } 851 } 852 }.scan(cut, null); 853 } 854 855 private Version computeSinceVersion(Element element, Types types, 856 Elements elementUtils) { 857 String docComment = elementUtils.getDocComment(element); 858 Version version = null; 859 if (docComment != null) { 860 version = extractSinceVersionFromText(docComment); 861 } 862 863 if (version != null) { 864 return version; //explicit @since has an absolute priority 865 } 866 867 if (element.getKind() != ElementKind.MODULE) { 868 version = effectiveSinceVersion(element.getEnclosingElement().getEnclosingElement(), element.getEnclosingElement(), types, elementUtils); 869 } 870 871 return version; 872 } 873 874 private Pair<JavacTask, CompilationUnitTree> findSource(Element forElement, Elements elementUtils) throws IOException { 875 String moduleName = elementUtils.getModuleOf(forElement).getQualifiedName().toString(); 876 String binaryName = switch (forElement.getKind()) { 877 case MODULE -> "module-info"; 878 case PACKAGE -> ((QualifiedNameable) forElement).getQualifiedName() + ".package-info"; 879 default -> elementUtils.getBinaryName((TypeElement) forElement).toString(); 880 }; 881 Location packageLocationForModule = fm.getLocationForModule(StandardLocation.MODULE_SOURCE_PATH, moduleName); 882 JavaFileObject jfo = fm.getJavaFileForInput(packageLocationForModule, 883 binaryName, 884 JavaFileObject.Kind.SOURCE); 885 886 if (jfo == null) 887 return null; 888 889 List<JavaFileObject> jfos = Arrays.asList(jfo); 890 JavaFileManager patchFM = moduleName != null 891 ? new PatchModuleFileManager(baseFileManager, jfo, moduleName) 892 : baseFileManager; 893 JavacTaskImpl task = (JavacTaskImpl) compiler.getTask(null, patchFM, d -> { 894 }, null, null, jfos); 895 Iterable<? extends CompilationUnitTree> cuts = task.parse(); 896 897 task.enter(); 898 899 return Pair.of(task, cuts.iterator().next()); 900 } 901 902 @Override 903 public void close() throws IOException { 904 fm.close(); 905 } 906 907 /** 908 * Manages files within a patch module. 909 * Provides custom behavior for handling file locations within a patch module. 910 * Includes methods to specify module locations, infer module names and determine 911 * if a location belongs to the patch module path. 912 */ 913 private static final class PatchModuleFileManager 914 extends ForwardingJavaFileManager<JavaFileManager> { 915 916 private final JavaFileObject file; 917 private final String moduleName; 918 919 public PatchModuleFileManager(JavaFileManager fileManager, 920 JavaFileObject file, 921 String moduleName) { 922 super(fileManager); 923 this.file = file; 924 this.moduleName = moduleName; 925 } 926 927 @Override 928 public Location getLocationForModule(Location location, 929 JavaFileObject fo) throws IOException { 930 return fo == file 931 ? PATCH_LOCATION 932 : super.getLocationForModule(location, fo); 933 } 934 935 @Override 936 public String inferModuleName(Location location) throws IOException { 937 return location == PATCH_LOCATION 938 ? moduleName 939 : super.inferModuleName(location); 940 } 941 942 @Override 943 public boolean hasLocation(Location location) { 944 return location == StandardLocation.PATCH_MODULE_PATH || 945 super.hasLocation(location); 946 } 947 948 private static final Location PATCH_LOCATION = new Location() { 949 @Override 950 public String getName() { 951 return "PATCH_LOCATION"; 952 } 953 954 @Override 955 public boolean isOutputLocation() { 956 return false; 957 } 958 959 @Override 960 public boolean isModuleOrientedLocation() { 961 return false; 962 } 963 }; 964 } 965 } 966 }