/* * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.lang.Runtime.Version; import java.net.URI; import java.nio.file.*; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.lang.model.element.*; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.*; import javax.tools.JavaFileManager.Location; import com.sun.source.tree.*; import com.sun.source.util.JavacTask; import com.sun.source.util.TreePathScanner; import com.sun.source.util.Trees; import com.sun.tools.javac.api.JavacTaskImpl; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.util.Pair; import jtreg.SkippedException; /* This checker checks the values of the `@since` tag found in the documentation comment for an element against the release in which the element first appeared. The source code containing the documentation comments is read from `src.zip` in the release of JDK used to run the test. The releases used to determine the expected value of `@since` tags are taken from the historical data built into `javac`. The `@since` checker works as a two-step process: In the first step, we process JDKs 9-current, only classfiles, producing a map ` => ``. - "version(s)", because we handle versioning of Preview API, so there may be two versions (we use a class with two fields for preview and stable), one when it was introduced as a preview, and one when it went out of preview. More on that below. - For each Element, we compute the unique ID, look into the map, and if there's nothing, record the current version as the originating version. - At the end of this step we have a map of the Real since values In the second step, we look at "effective" `@since` tags in the mainline sources, from `src.zip` (if the test run doesn't have it, we throw a `jtreg.SkippedException`) - We only check the specific MODULE whose name was passed as an argument in the test. In that module, we look for unqualified exports and test those packages. - The `@since` checker verifies that for every API element, the real since value and the effective since value are the same, and reports an error if they are not. Important note : We only check code written since JDK 9 as the releases used to determine the expected value of @since tags are taken from the historical data built into javac which only goes back that far note on rules for Real and effective `@since: Real since value of an API element is computed as the oldest release in which the given API element was introduced. That is: - for modules, packages, classes and interfaces, the release in which the element with the given qualified name was introduced - for constructors, the release in which the constructor with the given VM descriptor was introduced - for methods and fields, the release in which the given method or field with the given VM descriptor became a member of its enclosing class or interface, whether direct or inherited Effective since value of an API element is computed as follows: - if the given element has a @since tag in its javadoc, it is used - in all other cases, return the effective since value of the enclosing element Special Handling for preview method, as per JEP 12: - When an element is still marked as preview, the `@since` should be the first JDK release where the element was added. - If the element is no longer marked as preview, the `@since` should be the first JDK release where it was no longer preview. note on legacy preview: Until JDK 14, the preview APIs were not marked in any machine-understandable way. It was deprecated, and had a comment in the javadoc. and the use of `@PreviewFeature` only became standard in JDK 17. So the checker has an explicit knowledge of these preview elements. note: The `` for methods looks like `method: .()`. it is somewhat inspired from the VM Method Descriptors. But we use the erased return so that methods that were later generified remain the same. usage: the checker is run from a module specific test `@run main SinceChecker [--exclude package1,package2 | --exclude package1 package2]` */ public class SinceChecker { private final Map> LEGACY_PREVIEW_METHODS = new HashMap<>(); private final Map classDictionary = new HashMap<>(); private final JavaCompiler tool; private int errorCount = 0; // packages to skip during the test private static final Set EXCLUDE_LIST = new HashSet<>(); public static class IntroducedIn { public String introducedPreview; public String introducedStable; } public static void main(String[] args) throws Exception { if (args.length == 0) { throw new IllegalArgumentException("Test module not specified"); } String moduleName = args[0]; boolean excludeFlag = false; for (int i = 1; i < args.length; i++) { if ("--exclude".equals(args[i])) { excludeFlag = true; continue; } if (excludeFlag) { if (args[i].contains(",")) { EXCLUDE_LIST.addAll(Arrays.asList(args[i].split(","))); } else { EXCLUDE_LIST.add(args[i]); } } } SinceChecker sinceCheckerTestHelper = new SinceChecker(moduleName); sinceCheckerTestHelper.checkModule(moduleName); } private void error(String message) { System.err.println(message); errorCount++; } private SinceChecker(String moduleName) throws IOException { tool = ToolProvider.getSystemJavaCompiler(); for (int i = 9; i <= Runtime.version().feature(); i++) { DiagnosticListener noErrors = d -> { if (!d.getCode().equals("compiler.err.module.not.found")) { error(d.getMessage(null)); } }; JavacTask ct = (JavacTask) tool.getTask(null, null, noErrors, List.of("--add-modules", moduleName, "--release", String.valueOf(i)), null, Collections.singletonList(SimpleJavaFileObject.forSource(URI.create("myfo:/Test.java"), ""))); ct.analyze(); String version = String.valueOf(i); Elements elements = ct.getElements(); elements.getModuleElement("java.base"); // forces module graph to be instantiated elements.getAllModuleElements().forEach(me -> processModuleElement(me, version, ct)); } } private void processModuleElement(ModuleElement moduleElement, String releaseVersion, JavacTask ct) { processElement(moduleElement, moduleElement, ct.getTypes(), releaseVersion); for (ModuleElement.ExportsDirective ed : ElementFilter.exportsIn(moduleElement.getDirectives())) { if (ed.getTargetModules() == null) { processPackageElement(ed.getPackage(), releaseVersion, ct); } } } private void processPackageElement(PackageElement pe, String releaseVersion, JavacTask ct) { processElement(pe, pe, ct.getTypes(), releaseVersion); List typeElements = ElementFilter.typesIn(pe.getEnclosedElements()); for (TypeElement te : typeElements) { processClassElement(te, releaseVersion, ct.getTypes(), ct.getElements()); } } /// JDK documentation only contains public and protected declarations private boolean isDocumented(Element te) { Set mod = te.getModifiers(); return mod.contains(Modifier.PUBLIC) || mod.contains(Modifier.PROTECTED); } private boolean isMember(Element e) { var kind = e.getKind(); return kind.isField() || switch (kind) { case METHOD, CONSTRUCTOR -> true; default -> false; }; } private void processClassElement(TypeElement te, String version, Types types, Elements elements) { if (!isDocumented(te)) { return; } processElement(te.getEnclosingElement(), te, types, version); elements.getAllMembers(te).stream() .filter(this::isDocumented) .filter(this::isMember) .forEach(element -> processElement(te, element, types, version)); te.getEnclosedElements().stream() .filter(element -> element.getKind().isDeclaredType()) .map(TypeElement.class::cast) .forEach(nestedClass -> processClassElement(nestedClass, version, types, elements)); } private void processElement(Element explicitOwner, Element element, Types types, String version) { String uniqueId = getElementName(explicitOwner, element, types); IntroducedIn introduced = classDictionary.computeIfAbsent(uniqueId, _ -> new IntroducedIn()); if (isPreview(element, uniqueId, version)) { if (introduced.introducedPreview == null) { introduced.introducedPreview = version; } } else { if (introduced.introducedStable == null) { introduced.introducedStable = version; } } } private boolean isPreview(Element el, String uniqueId, String currentVersion) { while (el != null) { Symbol s = (Symbol) el; if ((s.flags() & Flags.PREVIEW_API) != 0) { return true; } el = el.getEnclosingElement(); } return LEGACY_PREVIEW_METHODS.getOrDefault(currentVersion, Set.of()) .contains(uniqueId); } private void checkModule(String moduleName) throws Exception { Path home = Paths.get(System.getProperty("java.home")); Path srcZip = home.resolve("lib").resolve("src.zip"); if (Files.notExists(srcZip)) { //possibly running over an exploded JDK build, attempt to find a //co-located full JDK image with src.zip: Path testJdk = Paths.get(System.getProperty("test.jdk")); srcZip = testJdk.getParent().resolve("images").resolve("jdk").resolve("lib").resolve("src.zip"); } if (!Files.isReadable(srcZip)) { throw new SkippedException("Skipping Test because src.zip wasn't found or couldn't be read"); } URI uri = URI.create("jar:" + srcZip.toUri()); try (FileSystem zipFO = FileSystems.newFileSystem(uri, Collections.emptyMap())) { Path root = zipFO.getRootDirectories().iterator().next(); Path moduleDirectory = root.resolve(moduleName); try (StandardJavaFileManager fm = tool.getStandardFileManager(null, null, null)) { JavacTask ct = (JavacTask) tool.getTask(null, fm, null, List.of("--add-modules", moduleName, "-d", "."), null, Collections.singletonList(SimpleJavaFileObject.forSource(URI.create("myfo:/Test.java"), ""))); ct.analyze(); Elements elements = ct.getElements(); elements.getModuleElement("java.base"); try (EffectiveSourceSinceHelper javadocHelper = EffectiveSourceSinceHelper.create(ct, List.of(root), this)) { processModuleCheck(elements.getModuleElement(moduleName), ct, moduleDirectory, javadocHelper); } catch (Exception e) { e.printStackTrace(); error("Initiating javadocHelper Failed " + e); } if (errorCount > 0) { throw new Exception("The `@since` checker found " + errorCount + " problems"); } } } } private boolean isExcluded(ModuleElement.ExportsDirective ed ){ return EXCLUDE_LIST.stream().anyMatch(excludePackage -> ed.getPackage().toString().equals(excludePackage) || ed.getPackage().toString().startsWith(excludePackage + ".")); } private void processModuleCheck(ModuleElement moduleElement, JavacTask ct, Path moduleDirectory, EffectiveSourceSinceHelper javadocHelper) { if (moduleElement == null) { error("Module element: was null because `elements.getModuleElement(moduleName)` returns null." + "fixes are needed for this Module"); } String moduleVersion = getModuleVersionFromFile(moduleDirectory); checkModuleOrPackage(javadocHelper, moduleVersion, moduleElement, ct, "Module: "); for (ModuleElement.ExportsDirective ed : ElementFilter.exportsIn(moduleElement.getDirectives())) { if (ed.getTargetModules() == null) { String packageVersion = getPackageVersionFromFile(moduleDirectory, ed); if (packageVersion != null && !isExcluded(ed)) { checkModuleOrPackage(javadocHelper, packageVersion, ed.getPackage(), ct, "Package: "); analyzePackageCheck(ed.getPackage(), ct, javadocHelper); } // Skip the package if packageVersion is null } } } private void checkModuleOrPackage(EffectiveSourceSinceHelper javadocHelper, String moduleVersion, Element moduleElement, JavacTask ct, String elementCategory) { String id = getElementName(moduleElement, moduleElement, ct.getTypes()); var elementInfo = classDictionary.get(id); if (elementInfo == null) { error("Element :" + id + " was not mapped"); return; } String version = elementInfo.introducedStable; if (moduleVersion == null) { error("Unable to retrieve `@since` for " + elementCategory + id); } else { String position = javadocHelper.getElementPosition(id); checkEquals(position, moduleVersion, version, id); } } private String getModuleVersionFromFile(Path moduleDirectory) { Path moduleInfoFile = moduleDirectory.resolve("module-info.java"); String version = null; if (Files.exists(moduleInfoFile)) { try { String moduleInfoContent = Files.readString(moduleInfoFile); var extractedVersion = extractSinceVersionFromText(moduleInfoContent); if (extractedVersion != null) { version = extractedVersion.toString(); } } catch (IOException e) { error("module-info.java not found or couldn't be opened AND this module has no unqualified exports"); } } return version; } private String getPackageVersionFromFile(Path moduleDirectory, ModuleElement.ExportsDirective ed) { Path pkgInfo = moduleDirectory.resolve(ed.getPackage() .getQualifiedName() .toString() .replace(".", File.separator) ) .resolve("package-info.java"); if (!Files.exists(pkgInfo)) { return null; // Skip if the file does not exist } String packageTopVersion = null; try { String packageContent = Files.readString(pkgInfo); var extractedVersion = extractSinceVersionFromText(packageContent); if (extractedVersion != null) { packageTopVersion = extractedVersion.toString(); } else { error(ed.getPackage().getQualifiedName() + ": package-info.java exists but doesn't contain @since"); } } catch (IOException e) { error(ed.getPackage().getQualifiedName() + ": package-info.java couldn't be opened"); } return packageTopVersion; } private void analyzePackageCheck(PackageElement pe, JavacTask ct, EffectiveSourceSinceHelper javadocHelper) { List typeElements = ElementFilter.typesIn(pe.getEnclosedElements()); for (TypeElement te : typeElements) { analyzeClassCheck(te, null, javadocHelper, ct.getTypes(), ct.getElements()); } } private boolean isNotCommonRecordMethod(TypeElement te, Element element, Types types) { var isRecord = te.getKind() == ElementKind.RECORD; if (!isRecord) { return true; } String uniqueId = getElementName(te, element, types); boolean isCommonMethod = uniqueId.endsWith(".toString()") || uniqueId.endsWith(".hashCode()") || uniqueId.endsWith(".equals(java.lang.Object)"); if (isCommonMethod) { return false; } for (var parameter : te.getEnclosedElements()) { if (parameter.getKind() == ElementKind.RECORD_COMPONENT) { if (uniqueId.endsWith(String.format("%s.%s()", te.getSimpleName(), parameter.getSimpleName().toString()))) { return false; } } } return true; } private void analyzeClassCheck(TypeElement te, String version, EffectiveSourceSinceHelper javadocHelper, Types types, Elements elementUtils) { String currentjdkVersion = String.valueOf(Runtime.version().feature()); if (!isDocumented(te)) { return; } checkElement(te.getEnclosingElement(), te, types, javadocHelper, version, elementUtils); te.getEnclosedElements().stream().filter(this::isDocumented) .filter(this::isMember) .filter(element -> isNotCommonRecordMethod(te, element, types)) .forEach(element -> checkElement(te, element, types, javadocHelper, version, elementUtils)); te.getEnclosedElements().stream() .filter(element -> element.getKind().isDeclaredType()) .map(TypeElement.class::cast) .forEach(nestedClass -> analyzeClassCheck(nestedClass, currentjdkVersion, javadocHelper, types, elementUtils)); } private void checkElement(Element explicitOwner, Element element, Types types, EffectiveSourceSinceHelper javadocHelper, String currentVersion, Elements elementUtils) { String uniqueId = getElementName(explicitOwner, element, types); if (element.getKind() == ElementKind.METHOD && element.getEnclosingElement().getKind() == ElementKind.ENUM && (uniqueId.contains(".values()") || uniqueId.contains(".valueOf(java.lang.String)"))) { //mandated enum type methods return; } String sinceVersion = null; var effectiveSince = javadocHelper.effectiveSinceVersion(explicitOwner, element, types, elementUtils); if (effectiveSince == null) { // Skip the element if the java file doesn't exist in src.zip return; } sinceVersion = effectiveSince.toString(); IntroducedIn mappedVersion = classDictionary.get(uniqueId); if (mappedVersion == null) { error("Element: " + uniqueId + " was not mapped"); return; } String realMappedVersion = null; try { realMappedVersion = isPreview(element, uniqueId, currentVersion) ? mappedVersion.introducedPreview : mappedVersion.introducedStable; } catch (Exception e) { error("For element " + element + "mappedVersion" + mappedVersion + " is null " + e); } String position = javadocHelper.getElementPosition(uniqueId); checkEquals(position, sinceVersion, realMappedVersion, uniqueId); } private Version extractSinceVersionFromText(String documentation) { Pattern pattern = Pattern.compile("@since\\s+(\\d+(?:\\.\\d+)?)"); Matcher matcher = pattern.matcher(documentation); if (matcher.find()) { String versionString = matcher.group(1); try { if (versionString.equals("1.0")) { versionString = "1"; //ended up being necessary } else if (versionString.startsWith("1.")) { versionString = versionString.substring(2); } return Version.parse(versionString); } catch (NumberFormatException ex) { error("`@since` value that cannot be parsed: " + versionString); return null; } } else { return null; } } private void checkEquals(String prefix, String sinceVersion, String mappedVersion, String name) { if (sinceVersion == null || mappedVersion == null) { error(name + ": NULL value for either real or effective `@since` . real/mapped version is=" + mappedVersion + " while the `@since` in the source code is= " + sinceVersion); return; } if (Integer.parseInt(sinceVersion) < 9) { sinceVersion = "9"; } if (!sinceVersion.equals(mappedVersion)) { String message = getWrongSinceMessage(prefix, sinceVersion, mappedVersion, name); error(message); } } private static String getWrongSinceMessage(String prefix, String sinceVersion, String mappedVersion, String elementSimpleName) { String message; if (mappedVersion.equals("9")) { message = elementSimpleName + ": `@since` version is " + sinceVersion + " but the element exists before JDK 10"; } else { message = elementSimpleName + ": `@since` version: " + sinceVersion + "; should be: " + mappedVersion; } return prefix + message; } private static String getElementName(Element owner, Element element, Types types) { String prefix = ""; String suffix = ""; ElementKind kind = element.getKind(); if (kind.isField()) { TypeElement te = (TypeElement) owner; prefix = "field"; suffix = ": " + te.getQualifiedName() + ":" + element.getSimpleName(); } else if (kind == ElementKind.METHOD || kind == ElementKind.CONSTRUCTOR) { prefix = "method"; TypeElement te = (TypeElement) owner; ExecutableElement executableElement = (ExecutableElement) element; String returnType = types.erasure(executableElement.getReturnType()).toString(); String methodName = executableElement.getSimpleName().toString(); String descriptor = executableElement.getParameters().stream() .map(p -> types.erasure(p.asType()).toString()) .collect(Collectors.joining(",", "(", ")")); suffix = ": " + returnType + " " + te.getQualifiedName() + "." + methodName + descriptor; } else if (kind.isDeclaredType()) { if (kind.isClass()) { prefix = "class"; } else if (kind.isInterface()) { prefix = "interface"; } suffix = ": " + ((TypeElement) element).getQualifiedName(); } else if (kind == ElementKind.PACKAGE) { prefix = "package"; suffix = ": " + ((PackageElement) element).getQualifiedName(); } else if (kind == ElementKind.MODULE) { prefix = "module"; suffix = ": " + ((ModuleElement) element).getQualifiedName(); } return prefix + suffix; } //these were preview in before the introduction of the @PreviewFeature { LEGACY_PREVIEW_METHODS.put("9", Set.of( "module: jdk.nio.mapmode", "module: java.transaction.xa", "module: jdk.unsupported.desktop", "module: jdk.jpackage", "module: java.net.http" )); LEGACY_PREVIEW_METHODS.put("10", Set.of( "module: jdk.nio.mapmode", "module: java.transaction.xa", "module: java.net.http", "module: jdk.unsupported.desktop", "module: jdk.jpackage" )); LEGACY_PREVIEW_METHODS.put("11", Set.of( "module: jdk.nio.mapmode", "module: jdk.jpackage" )); LEGACY_PREVIEW_METHODS.put("12", Set.of( "module: jdk.nio.mapmode", "module: jdk.jpackage", "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.BreakTree.getValue()", "method: java.util.List com.sun.source.tree.CaseTree.getExpressions()", "method: com.sun.source.tree.Tree com.sun.source.tree.CaseTree.getBody()", "method: com.sun.source.tree.CaseTree.CaseKind com.sun.source.tree.CaseTree.getCaseKind()", "class: com.sun.source.tree.CaseTree.CaseKind", "field: com.sun.source.tree.CaseTree.CaseKind:STATEMENT", "field: com.sun.source.tree.CaseTree.CaseKind:RULE", "field: com.sun.source.tree.Tree.Kind:SWITCH_EXPRESSION", "interface: com.sun.source.tree.SwitchExpressionTree", "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.SwitchExpressionTree.getExpression()", "method: java.util.List com.sun.source.tree.SwitchExpressionTree.getCases()", "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)", "method: java.lang.Object com.sun.source.util.TreeScanner.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)", "method: java.lang.Object com.sun.source.util.SimpleTreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)" )); LEGACY_PREVIEW_METHODS.put("13", Set.of( "module: jdk.nio.mapmode", "module: jdk.jpackage", "method: java.util.List com.sun.source.tree.CaseTree.getExpressions()", "method: com.sun.source.tree.Tree com.sun.source.tree.CaseTree.getBody()", "method: com.sun.source.tree.CaseTree.CaseKind com.sun.source.tree.CaseTree.getCaseKind()", "class: com.sun.source.tree.CaseTree.CaseKind", "field: com.sun.source.tree.CaseTree.CaseKind:STATEMENT", "field: com.sun.source.tree.CaseTree.CaseKind:RULE", "field: com.sun.source.tree.Tree.Kind:SWITCH_EXPRESSION", "interface: com.sun.source.tree.SwitchExpressionTree", "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.SwitchExpressionTree.getExpression()", "method: java.util.List com.sun.source.tree.SwitchExpressionTree.getCases()", "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)", "method: java.lang.Object com.sun.source.util.TreeScanner.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)", "method: java.lang.Object com.sun.source.util.SimpleTreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)", "method: java.lang.String java.lang.String.stripIndent()", "method: java.lang.String java.lang.String.translateEscapes()", "method: java.lang.String java.lang.String.formatted(java.lang.Object[])", "class: javax.swing.plaf.basic.motif.MotifLookAndFeel", "field: com.sun.source.tree.Tree.Kind:YIELD", "interface: com.sun.source.tree.YieldTree", "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.YieldTree.getValue()", "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitYield(com.sun.source.tree.YieldTree,java.lang.Object)", "method: java.lang.Object com.sun.source.util.SimpleTreeVisitor.visitYield(com.sun.source.tree.YieldTree,java.lang.Object)", "method: java.lang.Object com.sun.source.util.TreeScanner.visitYield(com.sun.source.tree.YieldTree,java.lang.Object)" )); LEGACY_PREVIEW_METHODS.put("14", Set.of( "module: jdk.jpackage", "class: javax.swing.plaf.basic.motif.MotifLookAndFeel", "field: jdk.jshell.Snippet.SubKind:RECORD_SUBKIND", "class: javax.lang.model.element.RecordComponentElement", "method: javax.lang.model.type.TypeMirror javax.lang.model.element.RecordComponentElement.asType()", "method: java.lang.Object javax.lang.model.element.ElementVisitor.visitRecordComponent(javax.lang.model.element.RecordComponentElement,java.lang.Object)", "class: javax.lang.model.util.ElementScanner14", "class: javax.lang.model.util.AbstractElementVisitor14", "class: javax.lang.model.util.SimpleElementVisitor14", "method: java.lang.Object javax.lang.model.util.ElementKindVisitor6.visitTypeAsRecord(javax.lang.model.element.TypeElement,java.lang.Object)", "class: javax.lang.model.util.ElementKindVisitor14", "method: javax.lang.model.element.RecordComponentElement javax.lang.model.util.Elements.recordComponentFor(javax.lang.model.element.ExecutableElement)", "method: java.util.List javax.lang.model.util.ElementFilter.recordComponentsIn(java.lang.Iterable)", "method: java.util.Set javax.lang.model.util.ElementFilter.recordComponentsIn(java.util.Set)", "method: java.util.List javax.lang.model.element.TypeElement.getRecordComponents()", "field: javax.lang.model.element.ElementKind:RECORD", "field: javax.lang.model.element.ElementKind:RECORD_COMPONENT", "field: javax.lang.model.element.ElementKind:BINDING_VARIABLE", "field: com.sun.source.tree.Tree.Kind:RECORD", "field: sun.reflect.annotation.TypeAnnotation.TypeAnnotationTarget:RECORD_COMPONENT", "class: java.lang.reflect.RecordComponent", "class: java.lang.runtime.ObjectMethods", "field: java.lang.annotation.ElementType:RECORD_COMPONENT", "method: boolean java.lang.Class.isRecord()", "method: java.lang.reflect.RecordComponent[] java.lang.Class.getRecordComponents()", "class: java.lang.Record", "interface: com.sun.source.tree.PatternTree", "field: com.sun.source.tree.Tree.Kind:BINDING_PATTERN", "method: com.sun.source.tree.PatternTree com.sun.source.tree.InstanceOfTree.getPattern()", "interface: com.sun.source.tree.BindingPatternTree", "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitBindingPattern(com.sun.source.tree.BindingPatternTree,java.lang.Object)" )); LEGACY_PREVIEW_METHODS.put("15", Set.of( "module: jdk.jpackage", "field: jdk.jshell.Snippet.SubKind:RECORD_SUBKIND", "class: javax.lang.model.element.RecordComponentElement", "method: javax.lang.model.type.TypeMirror javax.lang.model.element.RecordComponentElement.asType()", "method: java.lang.Object javax.lang.model.element.ElementVisitor.visitRecordComponent(javax.lang.model.element.RecordComponentElement,java.lang.Object)", "class: javax.lang.model.util.ElementScanner14", "class: javax.lang.model.util.AbstractElementVisitor14", "class: javax.lang.model.util.SimpleElementVisitor14", "method: java.lang.Object javax.lang.model.util.ElementKindVisitor6.visitTypeAsRecord(javax.lang.model.element.TypeElement,java.lang.Object)", "class: javax.lang.model.util.ElementKindVisitor14", "method: javax.lang.model.element.RecordComponentElement javax.lang.model.util.Elements.recordComponentFor(javax.lang.model.element.ExecutableElement)", "method: java.util.List javax.lang.model.util.ElementFilter.recordComponentsIn(java.lang.Iterable)", "method: java.util.Set javax.lang.model.util.ElementFilter.recordComponentsIn(java.util.Set)", "method: java.util.List javax.lang.model.element.TypeElement.getRecordComponents()", "field: javax.lang.model.element.ElementKind:RECORD", "field: javax.lang.model.element.ElementKind:RECORD_COMPONENT", "field: javax.lang.model.element.ElementKind:BINDING_VARIABLE", "field: com.sun.source.tree.Tree.Kind:RECORD", "field: sun.reflect.annotation.TypeAnnotation.TypeAnnotationTarget:RECORD_COMPONENT", "class: java.lang.reflect.RecordComponent", "class: java.lang.runtime.ObjectMethods", "field: java.lang.annotation.ElementType:RECORD_COMPONENT", "class: java.lang.Record", "method: boolean java.lang.Class.isRecord()", "method: java.lang.reflect.RecordComponent[] java.lang.Class.getRecordComponents()", "field: javax.lang.model.element.Modifier:SEALED", "field: javax.lang.model.element.Modifier:NON_SEALED", "method: javax.lang.model.element.TypeElement:getPermittedSubclasses:()", "method: java.util.List com.sun.source.tree.ClassTree.getPermitsClause()", "method: boolean java.lang.Class.isSealed()", "method: java.lang.constant.ClassDesc[] java.lang.Class.permittedSubclasses()", "interface: com.sun.source.tree.PatternTree", "field: com.sun.source.tree.Tree.Kind:BINDING_PATTERN", "method: com.sun.source.tree.PatternTree com.sun.source.tree.InstanceOfTree.getPattern()", "interface: com.sun.source.tree.BindingPatternTree", "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitBindingPattern(com.sun.source.tree.BindingPatternTree,java.lang.Object)" )); LEGACY_PREVIEW_METHODS.put("16", Set.of( "field: jdk.jshell.Snippet.SubKind:RECORD_SUBKIND", "field: javax.lang.model.element.Modifier:SEALED", "field: javax.lang.model.element.Modifier:NON_SEALED", "method: javax.lang.model.element.TypeElement:getPermittedSubclasses:()", "method: java.util.List com.sun.source.tree.ClassTree.getPermitsClause()", "method: boolean java.lang.Class.isSealed()", "method: java.lang.constant.ClassDesc[] java.lang.Class.permittedSubclasses()" )); // java.lang.foreign existed since JDK 19 and wasn't annotated - went out of preview in JDK 22 LEGACY_PREVIEW_METHODS.put("19", Set.of( "package: java.lang.foreign" )); LEGACY_PREVIEW_METHODS.put("20", Set.of( "package: java.lang.foreign" )); LEGACY_PREVIEW_METHODS.put("21", Set.of( "package: java.lang.foreign" )); } /** * Helper to find javadoc and resolve @inheritDoc and the effective since version. */ private final class EffectiveSourceSinceHelper implements AutoCloseable { private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); private final JavaFileManager baseFileManager; private final StandardJavaFileManager fm; private final Set seenLookupElements = new HashSet<>(); private final Map signature2Source = new HashMap<>(); private final Map signature2Location = new HashMap<>(); /** * Create the helper. * * @param mainTask JavacTask from which the further Elements originate * @param sourceLocations paths where source files should be searched * @param validator enclosing class of the helper, typically the object invoking this method * @return a EffectiveSourceSinceHelper */ public static EffectiveSourceSinceHelper create(JavacTask mainTask, Collection sourceLocations, SinceChecker validator) { StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null); try { fm.setLocationFromPaths(StandardLocation.MODULE_SOURCE_PATH, sourceLocations); return validator.new EffectiveSourceSinceHelper(mainTask, fm); } catch (IOException ex) { try { fm.close(); } catch (IOException closeEx) { ex.addSuppressed(closeEx); } throw new UncheckedIOException(ex); } } private EffectiveSourceSinceHelper(JavacTask mainTask, StandardJavaFileManager fm) { this.baseFileManager = ((JavacTaskImpl) mainTask).getContext().get(JavaFileManager.class); this.fm = fm; } public Version effectiveSinceVersion(Element owner, Element element, Types typeUtils, Elements elementUtils) { String handle = getElementName(owner, element, typeUtils); Version since = signature2Source.get(handle); if (since == null) { try { Element lookupElement = switch (element.getKind()) { case MODULE, PACKAGE -> element; default -> elementUtils.getOutermostTypeElement(element); }; if (lookupElement == null) return null; String lookupHandle = getElementName(owner, element, typeUtils); if (!seenLookupElements.add(lookupHandle)) { //we've already processed this top-level, don't try to compute //the values again: return null; } Pair source = findSource(lookupElement, elementUtils); if (source == null) return null; fillElementCache(source.fst, source.snd, source.fst.getTypes(), source.fst.getElements()); since = signature2Source.get(handle); } catch (IOException ex) { error("JavadocHelper failed for " + element); } } return since; } private String getElementPosition(String signature) { return signature2Location.getOrDefault(signature, ""); } //where: private void fillElementCache(JavacTask task, CompilationUnitTree cut, Types typeUtils, Elements elementUtils) { Trees trees = Trees.instance(task); String fileName = cut.getSourceFile().getName(); new TreePathScanner() { @Override public Void visitMethod(MethodTree node, Void p) { handleDeclaration(node, fileName); return null; } @Override public Void visitClass(ClassTree node, Void p) { handleDeclaration(node, fileName); return super.visitClass(node, p); } @Override public Void visitVariable(VariableTree node, Void p) { handleDeclaration(node, fileName); return null; } @Override public Void visitModule(ModuleTree node, Void p) { handleDeclaration(node, fileName); return null; } @Override public Void visitBlock(BlockTree node, Void p) { return null; } @Override public Void visitPackage(PackageTree node, Void p) { if (cut.getSourceFile().isNameCompatible("package-info", JavaFileObject.Kind.SOURCE)) { handleDeclaration(node, fileName); } return super.visitPackage(node, p); } private void handleDeclaration(Tree node, String fileName) { Element currentElement = trees.getElement(getCurrentPath()); if (currentElement != null) { long startPosition = trees.getSourcePositions().getStartPosition(cut, node); long lineNumber = cut.getLineMap().getLineNumber(startPosition); String filePathWithLineNumber = String.format("src%s:%s ", fileName, lineNumber); signature2Source.put(getElementName(currentElement.getEnclosingElement(), currentElement, typeUtils), computeSinceVersion(currentElement, typeUtils, elementUtils)); signature2Location.put(getElementName(currentElement.getEnclosingElement(), currentElement, typeUtils), filePathWithLineNumber); } } }.scan(cut, null); } private Version computeSinceVersion(Element element, Types types, Elements elementUtils) { String docComment = elementUtils.getDocComment(element); Version version = null; if (docComment != null) { version = extractSinceVersionFromText(docComment); } if (version != null) { return version; //explicit @since has an absolute priority } if (element.getKind() != ElementKind.MODULE) { version = effectiveSinceVersion(element.getEnclosingElement().getEnclosingElement(), element.getEnclosingElement(), types, elementUtils); } return version; } private Pair findSource(Element forElement, Elements elementUtils) throws IOException { String moduleName = elementUtils.getModuleOf(forElement).getQualifiedName().toString(); String binaryName = switch (forElement.getKind()) { case MODULE -> "module-info"; case PACKAGE -> ((QualifiedNameable) forElement).getQualifiedName() + ".package-info"; default -> elementUtils.getBinaryName((TypeElement) forElement).toString(); }; Location packageLocationForModule = fm.getLocationForModule(StandardLocation.MODULE_SOURCE_PATH, moduleName); JavaFileObject jfo = fm.getJavaFileForInput(packageLocationForModule, binaryName, JavaFileObject.Kind.SOURCE); if (jfo == null) return null; List jfos = Arrays.asList(jfo); JavaFileManager patchFM = moduleName != null ? new PatchModuleFileManager(baseFileManager, jfo, moduleName) : baseFileManager; JavacTaskImpl task = (JavacTaskImpl) compiler.getTask(null, patchFM, d -> { }, null, null, jfos); Iterable cuts = task.parse(); task.enter(); return Pair.of(task, cuts.iterator().next()); } @Override public void close() throws IOException { fm.close(); } /** * Manages files within a patch module. * Provides custom behavior for handling file locations within a patch module. * Includes methods to specify module locations, infer module names and determine * if a location belongs to the patch module path. */ private static final class PatchModuleFileManager extends ForwardingJavaFileManager { private final JavaFileObject file; private final String moduleName; public PatchModuleFileManager(JavaFileManager fileManager, JavaFileObject file, String moduleName) { super(fileManager); this.file = file; this.moduleName = moduleName; } @Override public Location getLocationForModule(Location location, JavaFileObject fo) throws IOException { return fo == file ? PATCH_LOCATION : super.getLocationForModule(location, fo); } @Override public String inferModuleName(Location location) throws IOException { return location == PATCH_LOCATION ? moduleName : super.inferModuleName(location); } @Override public boolean hasLocation(Location location) { return location == StandardLocation.PATCH_MODULE_PATH || super.hasLocation(location); } private static final Location PATCH_LOCATION = new Location() { @Override public String getName() { return "PATCH_LOCATION"; } @Override public boolean isOutputLocation() { return false; } @Override public boolean isModuleOrientedLocation() { return false; } }; } } }