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