diff --git a/libs/entitlement/tools/jdk-api-extractor/README.md b/libs/entitlement/tools/jdk-api-extractor/README.md index ea3627da07a60..f1a7225259baf 100644 --- a/libs/entitlement/tools/jdk-api-extractor/README.md +++ b/libs/entitlement/tools/jdk-api-extractor/README.md @@ -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 +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: diff --git a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java index c56095cc5c283..1ca6b66602188 100644 --- a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java +++ b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java @@ -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; @@ -64,9 +65,9 @@ public static void main(String[] args) throws IOException { boolean deprecationsOnly = optionalArgs(args).anyMatch(DEPRECATIONS_ONLY::equals); final Map moduleNameByClass = new HashMap<>(); - final Map> accessibleImplementationsByClass = new TreeMap<>(); - final Map> accessibleForOverridesByClass = new TreeMap<>(); - final Map> deprecationsByClass = new TreeMap<>(); + final Map> accessibleImplementationsByClass = new TreeMap<>(ModuleClass.COMPARATOR); + final Map> accessibleForOverridesByClass = new TreeMap<>(ModuleClass.COMPARATOR); + final Map> deprecationsByClass = new TreeMap<>(ModuleClass.COMPARATOR); final Map> exportsByModule = Utils.findModuleExports(); // 1st: map class names to module names (including later excluded modules) for lookup in 2nd step @@ -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 { @@ -106,15 +108,14 @@ public static void main(String[] args) throws IOException { }); // finally, skip some implementations we're not interested in - Predicate>> predicate = entry -> { - if (entry.getKey().startsWith("com/sun/") && entry.getKey().contains("/internal/")) { + Predicate>> 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); } @@ -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> methods, - Predicate>> predicate + Map> methods, + Predicate>> predicate ) throws IOException { System.out.println("Writing result for " + Runtime.version() + " to " + path.toAbsolutePath()); Files.write( @@ -170,6 +171,11 @@ private static void writeFile( ); } + record ModuleClass(String module, String clazz) { + private static final Comparator 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"; @@ -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", @@ -190,7 +197,7 @@ CharSequence toLine(String clazz) { ); } - static Stream toLines(Map.Entry> entry) { + static Stream toLines(Map.Entry> entry) { return entry.getValue().stream().map(m -> m.toLine(entry.getKey())); } } @@ -198,15 +205,15 @@ static Stream toLines(Map.Entry> ent static class AccessibleClassVisitor extends ClassVisitor { private final Map moduleNameByClass; private final Map> exportsByModule; - private final Map> accessibleImplementationsByClass; - private final Map> accessibleForOverridesByClass; - private final Map> deprecationsByClass; + private final Map> accessibleImplementationsByClass; + private final Map> accessibleForOverridesByClass; + private final Map> deprecationsByClass; private Set accessibleImplementations; private Set accessibleForOverrides; private Set deprecations; - private String className; + private ModuleClass moduleClass; private boolean isPublicClass; private boolean isFinalClass; private boolean isDeprecatedClass; @@ -215,9 +222,9 @@ static class AccessibleClassVisitor extends ClassVisitor { AccessibleClassVisitor( Map moduleNameByClass, Map> exportsByModule, - Map> accessibleImplementationsByClass, - Map> accessibleForOverridesByClass, - Map> deprecationsByClass + Map> accessibleImplementationsByClass, + Map> accessibleForOverridesByClass, + Map> deprecationsByClass ) { super(ASM9); this.moduleNameByClass = moduleNameByClass; @@ -235,23 +242,25 @@ private static Set newSortedSet() { public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { final Set 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; @@ -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 getModuleExports(String module) { @@ -279,15 +288,15 @@ private Set 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 unmodifiableSet(Set 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")