Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion libs/entitlement/tools/jdk-api-extractor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,23 @@ That is:
The output of this tool is meant to be diffed against the output for another JDK
version to identify changes that need to be reviewed for entitlements.

The following `TAB`-separated columns are written:
1. module name
2. fully qualified class name (ASM style, with `/` separators)
3. method name
4. method descriptor (ASM signature)
5. visibility (`PUBLIC` / `PROTECTED`)
6. `STATIC` modifier or empty
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's tab delimited, what is "empty"? does it mean a pair of quotes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just an empty string, the fields are not quoted...

7. `FINAL` modifier or empty

Usage example:
```bash
./gradlew :libs:entitlement:tools:jdk-api-extractor:run -Druntime.java=24 --args="api-jdk24.tsv"
./gradlew :libs:entitlement:tools:jdk-api-extractor:run -Druntime.java=25 --args="api-jdk25.tsv"
diff libs/entitlement/tools/jdk-api-extractor/api-jdk24.tsv libs/entitlement/tools/jdk-api-extractor/api-jdk25.tsv
# diff the public apis
diff -u libs/entitlement/tools/jdk-api-extractor/api-jdk24.tsv libs/entitlement/tools/jdk-api-extractor/api-jdk25.tsv > libs/entitlement/tools/jdk-api-extractor/api.diff
# extract additions in the new JDK, these require the most careful review
cat libs/entitlement/tools/jdk-api-extractor/api.diff | grep '^+[^+]' | sed 's/^+//' > api-jdk25-additions.tsv
```

### Optional arguments:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Collections.emptySet;
import static org.objectweb.asm.Opcodes.ACC_DEPRECATED;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
Expand Down Expand Up @@ -64,9 +65,9 @@ public static void main(String[] args) throws IOException {
boolean deprecationsOnly = optionalArgs(args).anyMatch(DEPRECATIONS_ONLY::equals);

final Map<String, String> moduleNameByClass = new HashMap<>();
final Map<String, Set<AccessibleMethod>> accessibleImplementationsByClass = new TreeMap<>();
final Map<String, Set<AccessibleMethod>> accessibleForOverridesByClass = new TreeMap<>();
final Map<String, Set<AccessibleMethod>> deprecationsByClass = new TreeMap<>();
final Map<ModuleClass, Set<AccessibleMethod>> accessibleImplementationsByClass = new TreeMap<>(ModuleClass.COMPARATOR);
final Map<ModuleClass, Set<AccessibleMethod>> accessibleForOverridesByClass = new TreeMap<>(ModuleClass.COMPARATOR);
final Map<ModuleClass, Set<AccessibleMethod>> deprecationsByClass = new TreeMap<>(ModuleClass.COMPARATOR);

final Map<String, Set<String>> exportsByModule = Utils.findModuleExports();
// 1st: map class names to module names (including later excluded modules) for lookup in 2nd step
Expand All @@ -93,7 +94,8 @@ public static void main(String[] args) throws IOException {
Utils.walkJdkModules(modulePredicate, exportsByModule, (moduleName, moduleClasses, moduleExports) -> {
for (var classFile : moduleClasses) {
// skip if class was already visited earlier due to a dependency on it
if (accessibleImplementationsByClass.containsKey(internalClassName(classFile, moduleName))) {
String className = internalClassName(classFile, moduleName);
if (accessibleImplementationsByClass.containsKey(new ModuleClass(moduleName, className))) {
continue;
}
try {
Expand All @@ -106,15 +108,14 @@ public static void main(String[] args) throws IOException {
});

// finally, skip some implementations we're not interested in
Predicate<Map.Entry<String, Set<AccessibleMethod>>> predicate = entry -> {
if (entry.getKey().startsWith("com/sun/") && entry.getKey().contains("/internal/")) {
Predicate<Map.Entry<ModuleClass, Set<AccessibleMethod>>> predicate = entry -> {
if (entry.getKey().clazz.startsWith("com/sun/") && entry.getKey().clazz.contains("/internal/")) {
// skip com.sun.*.internal classes as they are not part of the supported JDK API
// even if methods override some publicly visible API
return false;
}
// skip classes that are not part of included modules, but checked due to dependencies
String moduleName = moduleNameByClass.get(entry.getKey());
return modulePredicate.test(moduleName);
return modulePredicate.test(entry.getKey().module);
};
writeFile(Path.of(args[0]), deprecationsOnly ? deprecationsByClass : accessibleImplementationsByClass, predicate);
}
Expand Down Expand Up @@ -159,8 +160,8 @@ private static boolean isWritableOutputPath(String pathStr) {
@SuppressForbidden(reason = "cli tool printing to standard err/out")
private static void writeFile(
Path path,
Map<String, Set<AccessibleMethod>> methods,
Predicate<Map.Entry<String, Set<AccessibleMethod>>> predicate
Map<ModuleClass, Set<AccessibleMethod>> methods,
Predicate<Map.Entry<ModuleClass, Set<AccessibleMethod>>> predicate
) throws IOException {
System.out.println("Writing result for " + Runtime.version() + " to " + path.toAbsolutePath());
Files.write(
Expand All @@ -170,6 +171,11 @@ private static void writeFile(
);
}

record ModuleClass(String module, String clazz) {
private static final Comparator<ModuleClass> COMPARATOR = Comparator.comparing(ModuleClass::module)
.thenComparing(ModuleClass::clazz);
}

record AccessibleMethod(String method, String descriptor, boolean isPublic, boolean isFinal, boolean isStatic) {

private static final String SEPARATOR = "\t";
Expand All @@ -178,10 +184,11 @@ record AccessibleMethod(String method, String descriptor, boolean isPublic, bool
.thenComparing(AccessibleMethod::descriptor)
.thenComparing(AccessibleMethod::isStatic);

CharSequence toLine(String clazz) {
CharSequence toLine(ModuleClass moduleClass) {
return String.join(
SEPARATOR,
clazz,
moduleClass.module,
moduleClass.clazz,
method,
descriptor,
isPublic ? "PUBLIC" : "PROTECTED",
Expand All @@ -190,23 +197,23 @@ CharSequence toLine(String clazz) {
);
}

static Stream<CharSequence> toLines(Map.Entry<String, Set<AccessibleMethod>> entry) {
static Stream<CharSequence> toLines(Map.Entry<ModuleClass, Set<AccessibleMethod>> entry) {
return entry.getValue().stream().map(m -> m.toLine(entry.getKey()));
}
}

static class AccessibleClassVisitor extends ClassVisitor {
private final Map<String, String> moduleNameByClass;
private final Map<String, Set<String>> exportsByModule;
private final Map<String, Set<AccessibleMethod>> accessibleImplementationsByClass;
private final Map<String, Set<AccessibleMethod>> accessibleForOverridesByClass;
private final Map<String, Set<AccessibleMethod>> deprecationsByClass;
private final Map<ModuleClass, Set<AccessibleMethod>> accessibleImplementationsByClass;
private final Map<ModuleClass, Set<AccessibleMethod>> accessibleForOverridesByClass;
private final Map<ModuleClass, Set<AccessibleMethod>> deprecationsByClass;

private Set<AccessibleMethod> accessibleImplementations;
private Set<AccessibleMethod> accessibleForOverrides;
private Set<AccessibleMethod> deprecations;

private String className;
private ModuleClass moduleClass;
private boolean isPublicClass;
private boolean isFinalClass;
private boolean isDeprecatedClass;
Expand All @@ -215,9 +222,9 @@ static class AccessibleClassVisitor extends ClassVisitor {
AccessibleClassVisitor(
Map<String, String> moduleNameByClass,
Map<String, Set<String>> exportsByModule,
Map<String, Set<AccessibleMethod>> accessibleImplementationsByClass,
Map<String, Set<AccessibleMethod>> accessibleForOverridesByClass,
Map<String, Set<AccessibleMethod>> deprecationsByClass
Map<ModuleClass, Set<AccessibleMethod>> accessibleImplementationsByClass,
Map<ModuleClass, Set<AccessibleMethod>> accessibleForOverridesByClass,
Map<ModuleClass, Set<AccessibleMethod>> deprecationsByClass
) {
super(ASM9);
this.moduleNameByClass = moduleNameByClass;
Expand All @@ -235,23 +242,25 @@ private static Set<AccessibleMethod> newSortedSet() {
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
final Set<AccessibleMethod> currentAccessibleForOverrides = newSortedSet();
if (superName != null) {
if (accessibleImplementationsByClass.containsKey(superName) == false) {
var superModuleClass = getModuleClass(superName);
if (accessibleImplementationsByClass.containsKey(superModuleClass) == false) {
visitSuperClass(superName);
}
currentAccessibleForOverrides.addAll(accessibleForOverridesByClass.getOrDefault(superName, Collections.emptySet()));
currentAccessibleForOverrides.addAll(accessibleForOverridesByClass.getOrDefault(superModuleClass, emptySet()));
}
if (interfaces != null && interfaces.length > 0) {
for (var interfaceName : interfaces) {
if (accessibleImplementationsByClass.containsKey(interfaceName) == false) {
var interfaceModuleClass = getModuleClass(interfaceName);
if (accessibleImplementationsByClass.containsKey(interfaceModuleClass) == false) {
visitInterface(interfaceName);
}
currentAccessibleForOverrides.addAll(accessibleForOverridesByClass.getOrDefault(interfaceName, Collections.emptySet()));
currentAccessibleForOverrides.addAll(accessibleForOverridesByClass.getOrDefault(interfaceModuleClass, emptySet()));
}
}
// only initialize local state AFTER visiting all dependencies above!
super.visit(version, access, name, signature, superName, interfaces);
this.isExported = getModuleExports(getModuleName(name)).contains(getPackageName(name));
this.className = name;
this.moduleClass = getModuleClass(name);
this.isExported = getModuleExports(moduleClass.module).contains(getPackageName(name));
this.isPublicClass = (access & ACC_PUBLIC) != 0;
this.isFinalClass = (access & ACC_FINAL) != 0;
this.isDeprecatedClass = (access & ACC_DEPRECATED) != 0;
Expand All @@ -260,12 +269,12 @@ public void visit(int version, int access, String name, String signature, String
this.deprecations = newSortedSet();
}

private String getModuleName(String name) {
private ModuleClass getModuleClass(String name) {
String module = moduleNameByClass.get(name);
if (module == null) {
throw new IllegalStateException("Unknown module for class: " + name);
}
return module;
return new ModuleClass(module, name);
}

private Set<String> getModuleExports(String module) {
Expand All @@ -279,15 +288,15 @@ private Set<String> getModuleExports(String module) {
@Override
public void visitEnd() {
super.visitEnd();
if (accessibleImplementationsByClass.put(className, unmodifiableSet(accessibleImplementations)) != null
|| accessibleForOverridesByClass.put(className, unmodifiableSet(accessibleForOverrides)) != null
|| deprecationsByClass.put(className, unmodifiableSet(deprecations)) != null) {
throw new IllegalStateException("Class " + className + " was already visited!");
if (accessibleImplementationsByClass.put(moduleClass, unmodifiableSet(accessibleImplementations)) != null
|| accessibleForOverridesByClass.put(moduleClass, unmodifiableSet(accessibleForOverrides)) != null
|| deprecationsByClass.put(moduleClass, unmodifiableSet(deprecations)) != null) {
throw new IllegalStateException("Class " + moduleClass.clazz + " was already visited!");
}
}

private static Set<AccessibleMethod> unmodifiableSet(Set<AccessibleMethod> set) {
return set.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(set);
return set.isEmpty() ? emptySet() : Collections.unmodifiableSet(set);
}

@SuppressForbidden(reason = "cli tool printing to standard err/out")
Expand Down