5154b71637
Reviewed-by: hannesw
945 lines
46 KiB
Java
945 lines
46 KiB
Java
/*
|
|
* 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 `<unique-Element-ID`> => `<version(s)-where-it-was-introduced>`.
|
|
- "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 `<unique-Element-ID>` for methods looks like
|
|
`method: <erased-return-descriptor> <binary-name-of-enclosing-class>.<method-name>(<ParameterDescriptor>)`.
|
|
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 <moduleName> [--exclude package1,package2 | --exclude package1 package2]`
|
|
*/
|
|
|
|
public class SinceChecker {
|
|
private final Map<String, Set<String>> LEGACY_PREVIEW_METHODS = new HashMap<>();
|
|
private final Map<String, IntroducedIn> classDictionary = new HashMap<>();
|
|
private final JavaCompiler tool;
|
|
private int errorCount = 0;
|
|
|
|
// packages to skip during the test
|
|
private static final Set<String> 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<? super JavaFileObject> 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<TypeElement> 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<Modifier> 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<TypeElement> 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()) {
|
|
prefix = "class";
|
|
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",
|
|
"class: 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",
|
|
"class: 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",
|
|
"class: 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",
|
|
"class: 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()",
|
|
"class: 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()",
|
|
"class: 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()",
|
|
"class: 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<String> seenLookupElements = new HashSet<>();
|
|
private final Map<String, Version> signature2Source = new HashMap<>();
|
|
private final Map<String, String> 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<? extends Path> 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<JavacTask, CompilationUnitTree> 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<Void, Void>() {
|
|
@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<JavacTask, CompilationUnitTree> 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<JavaFileObject> 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<? extends CompilationUnitTree> 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<JavaFileManager> {
|
|
|
|
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;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|