From c81aa7551c37cc025c9054db08472b8abb2cbcb5 Mon Sep 17 00:00:00 2001 From: Nizar Benalla Date: Wed, 16 Oct 2024 10:17:47 +0000 Subject: [PATCH] 8331051: Add an `@since` checker test for `java.base` module Reviewed-by: jlahoda, jjg --- test/jdk/TEST.groups | 9 +- test/jdk/tools/sincechecker/SinceChecker.java | 948 ++++++++++++++++++ .../java_base/CheckSince_javaBase.java | 34 + 3 files changed, 989 insertions(+), 2 deletions(-) create mode 100644 test/jdk/tools/sincechecker/SinceChecker.java create mode 100644 test/jdk/tools/sincechecker/modules/java_base/CheckSince_javaBase.java diff --git a/test/jdk/TEST.groups b/test/jdk/TEST.groups index d51fcec733b..e7ee8990f94 100644 --- a/test/jdk/TEST.groups +++ b/test/jdk/TEST.groups @@ -1,4 +1,4 @@ -# Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2013, 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 @@ -91,7 +91,8 @@ tier3 = \ :jdk_svc \ -:jdk_svc_sanity \ -:svc_tools \ - :jdk_jpackage + :jdk_jpackage \ + :jdk_since_checks # Everything not in other tiers tier4 = \ @@ -666,3 +667,7 @@ jdk_containers_extended = \ jdk_core_no_security = \ :jdk_core \ -:jdk_security + +# Set of tests for `@since` checks in source code documentation +jdk_since_checks = \ + tools/sincechecker/modules/java_base/CheckSince_javaBase.java diff --git a/test/jdk/tools/sincechecker/SinceChecker.java b/test/jdk/tools/sincechecker/SinceChecker.java new file mode 100644 index 00000000000..860db6a2798 --- /dev/null +++ b/test/jdk/tools/sincechecker/SinceChecker.java @@ -0,0 +1,948 @@ +/* + * 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; + } + }; + } + } +} diff --git a/test/jdk/tools/sincechecker/modules/java_base/CheckSince_javaBase.java b/test/jdk/tools/sincechecker/modules/java_base/CheckSince_javaBase.java new file mode 100644 index 00000000000..6d0b9d0e932 --- /dev/null +++ b/test/jdk/tools/sincechecker/modules/java_base/CheckSince_javaBase.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8331051 + * @summary Test for `@since` for java.base module + * @library /test/lib + * /test/jdk/tools/sincechecker + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.util + * jdk.compiler/com.sun.tools.javac.code + * @run main SinceChecker java.base --exclude java.lang.classfile + */