From f75de254adb7c3d7912d7f0a05965e46540bfe50 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 8 Mar 2026 11:15:52 +0800 Subject: [PATCH 1/7] Migrate from ASM to Class-File API --- CHANGELOG.md | 4 + build.gradle | 6 +- formats/build.gradle | 2 - .../com/jakewharton/diffuse/format/Class.kt | 182 ++++++++---------- gradle/libs.versions.toml | 2 +- 5 files changed, 89 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b260f267..69e26b2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ **Added** - Add `--summary-only` flag. +**Changed** +- Bump minimum Java requirement to 24 for Class-File API support. +- Migrate from ASM to Class-File API. + **Fixed** - Significantly improve `.jar` diff performance. diff --git a/build.gradle b/build.gradle index e9799b9f..71e00dc4 100644 --- a/build.gradle +++ b/build.gradle @@ -34,18 +34,18 @@ subprojects { } } compilerOptions { - jvmTarget = JvmTarget.JVM_11 + jvmTarget = JvmTarget.fromTarget(libs.versions.jdkRelease.get()) freeCompilerArgs = [ "-progressive", '-opt-in=kotlin.contracts.ExperimentalContracts', - '-Xjdk-release=11', + "-Xjdk-release=${libs.versions.jdkRelease.get()}", ] } } } tasks.withType(JavaCompile).configureEach { - options.release = 11 + options.release = libs.versions.jdkRelease.get().toInteger() } configurations.configureEach { diff --git a/formats/build.gradle b/formats/build.gradle index 4e351213..23b0aaee 100644 --- a/formats/build.gradle +++ b/formats/build.gradle @@ -5,8 +5,6 @@ apply plugin: 'org.jetbrains.dokka' dependencies { api projects.io - implementation libs.asm - implementation libs.apkSigner implementation libs.binaryResources implementation libs.dalvikDx diff --git a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt index df6a67d1..03fc0954 100644 --- a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt +++ b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -1,13 +1,14 @@ package com.jakewharton.diffuse.format import com.jakewharton.diffuse.io.Input +import java.lang.classfile.ClassFile +import java.lang.classfile.ClassModel +import java.lang.classfile.constantpool.MethodHandleEntry +import java.lang.classfile.instruction.FieldInstruction +import java.lang.classfile.instruction.InvokeDynamicInstruction +import java.lang.classfile.instruction.InvokeInstruction import java.util.Objects -import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassVisitor -import org.objectweb.asm.FieldVisitor -import org.objectweb.asm.Handle -import org.objectweb.asm.MethodVisitor -import org.objectweb.asm.Opcodes +import kotlin.jvm.optionals.getOrNull class Class private constructor( @@ -29,104 +30,92 @@ private constructor( @JvmStatic @JvmName("parse") fun Input.toClass(): Class { - val reader = ClassReader(toByteArray()) - val type = TypeDescriptor("L${reader.className};") - - val referencedVisitor = ReferencedMembersVisitor() - val declaredVisitor = DeclaredMembersVisitor(type, referencedVisitor) - reader.accept(declaredVisitor, 0) - - return Class(type, declaredVisitor.members.sorted(), referencedVisitor.members.sorted()) + val classModel = ClassFile.of().parse(toByteArray()) + val type = TypeDescriptor("L${classModel.thisClass().asInternalName()};") + val (declaredMembers, referencedMembers) = classModel.parseMembers(type) + return Class(type, declaredMembers.sorted(), referencedMembers.sorted()) } } } -private class DeclaredMembersVisitor(val type: TypeDescriptor, val methodVisitor: MethodVisitor) : - ClassVisitor(Opcodes.ASM9) { - val members = mutableListOf() - - override fun visitMethod( - access: Int, - name: String, - descriptor: String, - signature: String?, - exceptions: Array?, - ): MethodVisitor { - members += parseMethod(type, name, descriptor) - return methodVisitor - } - - override fun visitField( - access: Int, - name: String, - descriptor: String, - signature: String?, - value: Any?, - ): FieldVisitor? { - members += Field(type, name, TypeDescriptor(descriptor)) - return null - } -} - -private class ReferencedMembersVisitor : MethodVisitor(Opcodes.ASM9) { - val members = mutableSetOf() - - override fun visitMethodInsn( - opcode: Int, - owner: String, - name: String, - descriptor: String, - isInterface: Boolean, - ) { - val ownerType = parseOwner(owner) - val referencedMethod = parseMethod(ownerType, name, descriptor) - members += referencedMethod +private fun ClassModel.parseMembers(type: TypeDescriptor): Pair, Set> { + val declaredMembers = mutableListOf() + val referencedMembers = mutableSetOf() + + for (field in fields()) { + declaredMembers += + Field( + type, + field.fieldName().stringValue(), + TypeDescriptor(field.fieldTypeSymbol().descriptorString()), + ) } - override fun visitInvokeDynamicInsn( - name: String?, - descriptor: String?, - bootstrapMethodHandle: Handle, - vararg bootstrapMethodArguments: Any?, - ) { - members += parseHandle(bootstrapMethodHandle) - - if (bootstrapMethodHandle == lambdaMetaFactory) { - // LambdaMetaFactory.metafactory accepts 6 arguments. The first 3 are - // provided automatically and the latter 3 are supplied as the arguments to - // this method. The second of those is a MethodHandle to the lambda - // implementation which needs to be counted as a method reference. - val implementationHandle = bootstrapMethodArguments[1] as Handle - members += parseHandle(implementationHandle) + for (method in methods()) { + declaredMembers += + parseMethod( + type, + method.methodName().stringValue(), + method.methodTypeSymbol().descriptorString(), + ) + + method.code().getOrNull()?.let { codeModel -> + for (instruction in codeModel) { + when (instruction) { + is FieldInstruction -> { + val ownerType = parseOwner(instruction.owner().name().stringValue()) + val name = instruction.name().stringValue() + val descriptor = instruction.type().stringValue() + referencedMembers += Field(ownerType, name, TypeDescriptor(descriptor)) + } + is InvokeInstruction -> { + val ownerType = parseOwner(instruction.owner().name().stringValue()) + val name = instruction.name().stringValue() + val descriptor = instruction.type().stringValue() + referencedMembers += parseMethod(ownerType, name, descriptor) + } + is InvokeDynamicInstruction -> { + val bootstrapMethodEntry = instruction.invokedynamic().bootstrap() + referencedMembers += parseHandle(bootstrapMethodEntry.bootstrapMethod()) + + if ( + bootstrapMethodEntry.bootstrapMethod().reference().owner().name().stringValue() == + "java/lang/invoke/LambdaMetafactory" && + bootstrapMethodEntry.bootstrapMethod().reference().name().stringValue() == + "metafactory" + ) { + // LambdaMetaFactory.metafactory accepts 6 arguments. The first 3 are + // provided automatically and the latter 3 are supplied as the arguments to + // this method. The second of those is a MethodHandle to the lambda + // implementation which needs to be counted as a method reference. + val implementationHandle = bootstrapMethodEntry.arguments()[1] as MethodHandleEntry + referencedMembers += parseHandle(implementationHandle) + } + } + else -> Unit + } + } } } - private fun parseHandle(handle: Handle): Member { - val handlerOwner = parseOwner(handle.owner) - val handlerName = handle.name - val handlerDescriptor = handle.desc - return if (handlerDescriptor.startsWith('(')) { - parseMethod(handlerOwner, handlerName, handlerDescriptor) - } else { - Field(handlerOwner, handlerName, TypeDescriptor(handlerDescriptor)) - } - } + return declaredMembers to referencedMembers +} - override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) { - val ownerType = parseOwner(owner) - val referencedField = Field(ownerType, name, TypeDescriptor(descriptor)) - members += referencedField +private fun parseHandle(handle: MethodHandleEntry): Member { + val ref = handle.reference() + val handlerOwner = parseOwner(ref.owner().name().stringValue()) + val handlerName = ref.name().stringValue() + val handlerDescriptor = ref.type().stringValue() + return if (handlerDescriptor.startsWith('(')) { + parseMethod(handlerOwner, handlerName, handlerDescriptor) + } else { + Field(handlerOwner, handlerName, TypeDescriptor(handlerDescriptor)) } +} - private fun parseOwner(owner: String): TypeDescriptor { - val ownerDescriptor = - if (owner.startsWith('[')) { - owner - } else { - "L$owner;" - } - return TypeDescriptor(ownerDescriptor) - } +private fun parseOwner(owner: String): TypeDescriptor { + val ownerDescriptor = if (owner.startsWith('[')) owner else "L$owner;" + return TypeDescriptor(ownerDescriptor) } private fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { @@ -153,12 +142,3 @@ private fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String) val returnType = TypeDescriptor(descriptor.substring(i + 1)) return Method(owner, name, parameterTypes, returnType) } - -private val lambdaMetaFactory = - Handle( - Opcodes.H_INVOKESTATIC, - "java/lang/invoke/LambdaMetafactory", - "metafactory", - "(Ljava/lang/invoke/MethodHandles\$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", - false, - ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a588809..842e5f5d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ aapt2Proto = "9.1.0-14792394" protobufJava = "4.34.0" guava = "30.1-jre" +jdkRelease = "24" [libraries] dalvikDx = "com.jakewharton.android.repackaged:dalvik-dx:16.0.1" @@ -16,7 +17,6 @@ junit = "junit:junit:4.13.2" assertk = "com.willowtreeapps.assertk:assertk:0.28.1" okio = "com.squareup.okio:okio:3.16.4" byteunits = "com.jakewharton.byteunits:byteunits:0.9.1" -asm = "org.ow2.asm:asm:9.9.1" diffUtils = "io.github.java-diff-utils:java-diff-utils:4.16" picnic = "com.jakewharton.picnic:picnic:0.7.0" mordant = "com.github.ajalt.mordant:mordant:3.0.2" From 27b16a8bec613c233f9326cd1f55e7bcd2721aae Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 11 Mar 2026 10:24:05 +0800 Subject: [PATCH 2/7] Multi-Release for Class-File API --- CHANGELOG.md | 3 +- formats/api/formats.api | 1 - formats/build.gradle | 58 ++++++ .../com/jakewharton/diffuse/format/Class.kt | 98 +++++++++ .../com/jakewharton/diffuse/format/Class.kt | 194 ++++++++++-------- gradle/libs.versions.toml | 3 +- settings.gradle | 5 + 7 files changed, 271 insertions(+), 91 deletions(-) create mode 100644 formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e26b2b..61379804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,7 @@ - Add `--summary-only` flag. **Changed** -- Bump minimum Java requirement to 24 for Class-File API support. -- Migrate from ASM to Class-File API. +- Prefer Class-File API on Java 24 or above. **Fixed** - Significantly improve `.jar` diff performance. diff --git a/formats/api/formats.api b/formats/api/formats.api index 9d248958..2708ed73 100644 --- a/formats/api/formats.api +++ b/formats/api/formats.api @@ -234,7 +234,6 @@ public abstract interface class com/jakewharton/diffuse/format/BinaryFormat { public final class com/jakewharton/diffuse/format/Class { public static final field Companion Lcom/jakewharton/diffuse/format/Class$Companion; - public synthetic fun (Ljava/lang/String;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getDeclaredMembers ()Ljava/util/List; public final fun getDescriptor-BeHrSHk ()Ljava/lang/String; diff --git a/formats/build.gradle b/formats/build.gradle index 23b0aaee..08f6f7ed 100644 --- a/formats/build.gradle +++ b/formats/build.gradle @@ -1,10 +1,23 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile + apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'com.vanniktech.maven.publish' apply plugin: 'org.jetbrains.dokka' +// Keep associated Kotlin compilations from depending on archive tasks (e.g., jar), which can create circular task +// graphs in multi-release setups. +// https://kotlinlang.org/docs/gradle-configure-project.html//disable-use-of-artifact-in-compilation-task +// https://kotlinlang.org/docs/whatsnew2020.html#added-task-dependency-for-rare-cases-when-the-compile-task-lacks-one-on-an-artifact +ext['kotlin.build.archivesTaskOutputAsFriendModule'] = false + +addMultiReleaseSourceSet(24) + dependencies { api projects.io + implementation libs.asm + implementation libs.apkSigner implementation libs.binaryResources implementation libs.dalvikDx @@ -19,3 +32,48 @@ dependencies { testImplementation libs.assertk testImplementation projects.testHelpers } + +tasks.named('jar', Jar) { + manifest { + attributes 'Multi-Release': 'true' + } +} + +def addMultiReleaseSourceSet(int version) { + kotlin.target.compilations.create("java${version}") { + // Import main and its classpath as dependencies and establish internal visibility. + associateWith(kotlin.target.compilations.main) + + compileJavaTaskProvider.configure { JavaCompile task -> + task.options.release = version + } + compileTaskProvider.configure { KotlinJvmCompile task -> + task.compilerOptions { + jvmTarget = JvmTarget.fromTarget(version.toString()) + freeCompilerArgs = [ + "-Xjdk-release=$version", + ] + } + } + + tasks.named('jar', Jar) { jar -> + jar.from(output.allOutputs) { + into("META-INF/versions/$version") + } + } + } + + def versionedTest = tasks.register("testJava${version}", Test) { task -> + task.group = LifecycleBasePlugin.VERIFICATION_GROUP + task.description = "Runs test suite using Java ${version} toolchain." + task.testClassesDirs = sourceSets.test.output.classesDirs + task.classpath = sourceSets.test.runtimeClasspath + task.javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(version) + vendor = JvmVendorSpec.AZUL + } + } + tasks.named('check') { + dependsOn(versionedTest) + } +} diff --git a/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt b/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt new file mode 100644 index 00000000..21b28593 --- /dev/null +++ b/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -0,0 +1,98 @@ +package com.jakewharton.diffuse.format + +import com.jakewharton.diffuse.io.Input +import java.lang.classfile.ClassFile +import java.lang.classfile.ClassModel +import java.lang.classfile.constantpool.MethodHandleEntry +import java.lang.classfile.instruction.FieldInstruction +import java.lang.classfile.instruction.InvokeDynamicInstruction +import java.lang.classfile.instruction.InvokeInstruction +import kotlin.jvm.optionals.getOrNull + +@Suppress("unused") // Used by Multi-Release JARs for Java 24+. +internal fun Input.toClassImpl(): Class { + val classModel = ClassFile.of().parse(toByteArray()) + val type = TypeDescriptor("L${classModel.thisClass().asInternalName()};") + val (declaredMembers, referencedMembers) = classModel.parseMembers(type) + return Class(type, declaredMembers.sorted(), referencedMembers.sorted()) +} + +private fun ClassModel.parseMembers(type: TypeDescriptor): Pair, Set> { + val declaredMembers = mutableListOf() + val referencedMembers = mutableSetOf() + + for (field in fields()) { + declaredMembers += + Field( + type, + field.fieldName().stringValue(), + TypeDescriptor(field.fieldTypeSymbol().descriptorString()), + ) + } + + for (method in methods()) { + declaredMembers += + parseMethod( + type, + method.methodName().stringValue(), + method.methodTypeSymbol().descriptorString(), + ) + + method.code().getOrNull()?.let { codeModel -> + for (instruction in codeModel) { + when (instruction) { + is FieldInstruction -> { + val ownerType = parseOwner(instruction.owner().name().stringValue()) + val name = instruction.name().stringValue() + val descriptor = instruction.type().stringValue() + referencedMembers += Field(ownerType, name, TypeDescriptor(descriptor)) + } + is InvokeInstruction -> { + val ownerType = parseOwner(instruction.owner().name().stringValue()) + val name = instruction.name().stringValue() + val descriptor = instruction.type().stringValue() + referencedMembers += parseMethod(ownerType, name, descriptor) + } + is InvokeDynamicInstruction -> { + val bootstrapMethodEntry = instruction.invokedynamic().bootstrap() + referencedMembers += parseHandle(bootstrapMethodEntry.bootstrapMethod()) + + if ( + bootstrapMethodEntry.bootstrapMethod().reference().owner().name().stringValue() == + "java/lang/invoke/LambdaMetafactory" && + bootstrapMethodEntry.bootstrapMethod().reference().name().stringValue() == + "metafactory" + ) { + // LambdaMetaFactory.metafactory accepts 6 arguments. The first 3 are + // provided automatically and the latter 3 are supplied as the arguments to + // this method. The second of those is a MethodHandle to the lambda + // implementation which needs to be counted as a method reference. + val implementationHandle = bootstrapMethodEntry.arguments()[1] as MethodHandleEntry + referencedMembers += parseHandle(implementationHandle) + } + } + else -> Unit + } + } + } + } + + return declaredMembers to referencedMembers +} + +private fun parseHandle(handle: MethodHandleEntry): Member { + val ref = handle.reference() + val handlerOwner = parseOwner(ref.owner().name().stringValue()) + val handlerName = ref.name().stringValue() + val handlerDescriptor = ref.type().stringValue() + return if (handlerDescriptor.startsWith('(')) { + parseMethod(handlerOwner, handlerName, handlerDescriptor) + } else { + Field(handlerOwner, handlerName, TypeDescriptor(handlerDescriptor)) + } +} + +private fun parseOwner(owner: String): TypeDescriptor { + val ownerDescriptor = if (owner.startsWith('[')) owner else "L$owner;" + return TypeDescriptor(ownerDescriptor) +} diff --git a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt index 03fc0954..90fc9226 100644 --- a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt +++ b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -1,17 +1,16 @@ package com.jakewharton.diffuse.format import com.jakewharton.diffuse.io.Input -import java.lang.classfile.ClassFile -import java.lang.classfile.ClassModel -import java.lang.classfile.constantpool.MethodHandleEntry -import java.lang.classfile.instruction.FieldInstruction -import java.lang.classfile.instruction.InvokeDynamicInstruction -import java.lang.classfile.instruction.InvokeInstruction import java.util.Objects -import kotlin.jvm.optionals.getOrNull +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.FieldVisitor +import org.objectweb.asm.Handle +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes class Class -private constructor( +internal constructor( val descriptor: TypeDescriptor, val declaredMembers: List, val referencedMembers: List, @@ -27,98 +26,110 @@ private constructor( referencedMembers == other.referencedMembers companion object { - @JvmStatic - @JvmName("parse") - fun Input.toClass(): Class { - val classModel = ClassFile.of().parse(toByteArray()) - val type = TypeDescriptor("L${classModel.thisClass().asInternalName()};") - val (declaredMembers, referencedMembers) = classModel.parseMembers(type) - return Class(type, declaredMembers.sorted(), referencedMembers.sorted()) - } + @JvmStatic @JvmName("parse") fun Input.toClass(): Class = toClassImpl() } } -private fun ClassModel.parseMembers(type: TypeDescriptor): Pair, Set> { - val declaredMembers = mutableListOf() - val referencedMembers = mutableSetOf() - - for (field in fields()) { - declaredMembers += - Field( - type, - field.fieldName().stringValue(), - TypeDescriptor(field.fieldTypeSymbol().descriptorString()), - ) +internal fun Input.toClassImpl(): Class { + val reader = ClassReader(toByteArray()) + val type = TypeDescriptor("L${reader.className};") + + val referencedVisitor = ReferencedMembersVisitor() + val declaredVisitor = DeclaredMembersVisitor(type, referencedVisitor) + reader.accept(declaredVisitor, 0) + + return Class(type, declaredVisitor.members.sorted(), referencedVisitor.members.sorted()) +} + +private class DeclaredMembersVisitor(val type: TypeDescriptor, val methodVisitor: MethodVisitor) : + ClassVisitor(Opcodes.ASM9) { + val members = mutableListOf() + + override fun visitMethod( + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array?, + ): MethodVisitor { + members += parseMethod(type, name, descriptor) + return methodVisitor } - for (method in methods()) { - declaredMembers += - parseMethod( - type, - method.methodName().stringValue(), - method.methodTypeSymbol().descriptorString(), - ) - - method.code().getOrNull()?.let { codeModel -> - for (instruction in codeModel) { - when (instruction) { - is FieldInstruction -> { - val ownerType = parseOwner(instruction.owner().name().stringValue()) - val name = instruction.name().stringValue() - val descriptor = instruction.type().stringValue() - referencedMembers += Field(ownerType, name, TypeDescriptor(descriptor)) - } - is InvokeInstruction -> { - val ownerType = parseOwner(instruction.owner().name().stringValue()) - val name = instruction.name().stringValue() - val descriptor = instruction.type().stringValue() - referencedMembers += parseMethod(ownerType, name, descriptor) - } - is InvokeDynamicInstruction -> { - val bootstrapMethodEntry = instruction.invokedynamic().bootstrap() - referencedMembers += parseHandle(bootstrapMethodEntry.bootstrapMethod()) - - if ( - bootstrapMethodEntry.bootstrapMethod().reference().owner().name().stringValue() == - "java/lang/invoke/LambdaMetafactory" && - bootstrapMethodEntry.bootstrapMethod().reference().name().stringValue() == - "metafactory" - ) { - // LambdaMetaFactory.metafactory accepts 6 arguments. The first 3 are - // provided automatically and the latter 3 are supplied as the arguments to - // this method. The second of those is a MethodHandle to the lambda - // implementation which needs to be counted as a method reference. - val implementationHandle = bootstrapMethodEntry.arguments()[1] as MethodHandleEntry - referencedMembers += parseHandle(implementationHandle) - } - } - else -> Unit - } - } + override fun visitField( + access: Int, + name: String, + descriptor: String, + signature: String?, + value: Any?, + ): FieldVisitor? { + members += Field(type, name, TypeDescriptor(descriptor)) + return null + } +} + +private class ReferencedMembersVisitor : MethodVisitor(Opcodes.ASM9) { + val members = mutableSetOf() + + override fun visitMethodInsn( + opcode: Int, + owner: String, + name: String, + descriptor: String, + isInterface: Boolean, + ) { + val ownerType = parseOwner(owner) + val referencedMethod = parseMethod(ownerType, name, descriptor) + members += referencedMethod + } + + override fun visitInvokeDynamicInsn( + name: String?, + descriptor: String?, + bootstrapMethodHandle: Handle, + vararg bootstrapMethodArguments: Any?, + ) { + members += parseHandle(bootstrapMethodHandle) + + if (bootstrapMethodHandle == lambdaMetaFactory) { + // LambdaMetaFactory.metafactory accepts 6 arguments. The first 3 are + // provided automatically and the latter 3 are supplied as the arguments to + // this method. The second of those is a MethodHandle to the lambda + // implementation which needs to be counted as a method reference. + val implementationHandle = bootstrapMethodArguments[1] as Handle + members += parseHandle(implementationHandle) } } - return declaredMembers to referencedMembers -} + private fun parseHandle(handle: Handle): Member { + val handlerOwner = parseOwner(handle.owner) + val handlerName = handle.name + val handlerDescriptor = handle.desc + return if (handlerDescriptor.startsWith('(')) { + parseMethod(handlerOwner, handlerName, handlerDescriptor) + } else { + Field(handlerOwner, handlerName, TypeDescriptor(handlerDescriptor)) + } + } -private fun parseHandle(handle: MethodHandleEntry): Member { - val ref = handle.reference() - val handlerOwner = parseOwner(ref.owner().name().stringValue()) - val handlerName = ref.name().stringValue() - val handlerDescriptor = ref.type().stringValue() - return if (handlerDescriptor.startsWith('(')) { - parseMethod(handlerOwner, handlerName, handlerDescriptor) - } else { - Field(handlerOwner, handlerName, TypeDescriptor(handlerDescriptor)) + override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) { + val ownerType = parseOwner(owner) + val referencedField = Field(ownerType, name, TypeDescriptor(descriptor)) + members += referencedField } -} -private fun parseOwner(owner: String): TypeDescriptor { - val ownerDescriptor = if (owner.startsWith('[')) owner else "L$owner;" - return TypeDescriptor(ownerDescriptor) + private fun parseOwner(owner: String): TypeDescriptor { + val ownerDescriptor = + if (owner.startsWith('[')) { + owner + } else { + "L$owner;" + } + return TypeDescriptor(ownerDescriptor) + } } -private fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { +internal fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { val parameterTypes = mutableListOf() var i = 1 while (true) { @@ -142,3 +153,12 @@ private fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String) val returnType = TypeDescriptor(descriptor.substring(i + 1)) return Method(owner, name, parameterTypes, returnType) } + +private val lambdaMetaFactory = + Handle( + Opcodes.H_INVOKESTATIC, + "java/lang/invoke/LambdaMetafactory", + "metafactory", + "(Ljava/lang/invoke/MethodHandles\$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", + false, + ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 842e5f5d..688a1774 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ aapt2Proto = "9.1.0-14792394" protobufJava = "4.34.0" guava = "30.1-jre" -jdkRelease = "24" +jdkRelease = "11" [libraries] dalvikDx = "com.jakewharton.android.repackaged:dalvik-dx:16.0.1" @@ -17,6 +17,7 @@ junit = "junit:junit:4.13.2" assertk = "com.willowtreeapps.assertk:assertk:0.28.1" okio = "com.squareup.okio:okio:3.16.4" byteunits = "com.jakewharton.byteunits:byteunits:0.9.1" +asm = "org.ow2.asm:asm:9.9.1" diffUtils = "io.github.java-diff-utils:java-diff-utils:4.16" picnic = "com.jakewharton.picnic:picnic:0.7.0" mordant = "com.github.ajalt.mordant:mordant:3.0.2" diff --git a/settings.gradle b/settings.gradle index b69be549..04e5da7f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,11 @@ pluginManagement { } } } + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + dependencyResolutionManagement { repositories { mavenCentral() From 1643a8ae0e4eff5b2b9ecca9e3305a2abcd20870 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 11 Mar 2026 12:53:56 +0800 Subject: [PATCH 3/7] Pin ASM on 9.6 for manual tests --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 688a1774..88234767 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ junit = "junit:junit:4.13.2" assertk = "com.willowtreeapps.assertk:assertk:0.28.1" okio = "com.squareup.okio:okio:3.16.4" byteunits = "com.jakewharton.byteunits:byteunits:0.9.1" -asm = "org.ow2.asm:asm:9.9.1" +asm = "org.ow2.asm:asm:9.6" diffUtils = "io.github.java-diff-utils:java-diff-utils:4.16" picnic = "com.jakewharton.picnic:picnic:0.7.0" mordant = "com.github.ajalt.mordant:mordant:3.0.2" From 6ae22179a0d1ae84c8df985c888b80e57225d34b Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 11 Mar 2026 12:56:43 +0800 Subject: [PATCH 4/7] Don't internal parseMethod --- .../com/jakewharton/diffuse/format/Class.kt | 26 +++++++++++++++++++ .../com/jakewharton/diffuse/format/Class.kt | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt b/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt index 21b28593..c1a5933f 100644 --- a/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt +++ b/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -96,3 +96,29 @@ private fun parseOwner(owner: String): TypeDescriptor { val ownerDescriptor = if (owner.startsWith('[')) owner else "L$owner;" return TypeDescriptor(ownerDescriptor) } + +@Suppress("DuplicatedCode") // Reuse this function by internal will cause NoSuchMethodError. +private fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { + val parameterTypes = mutableListOf() + var i = 1 + while (true) { + if (descriptor[i] == ')') { + break + } + var typeIndex = i + while (descriptor[typeIndex] == '[') { + typeIndex++ + } + val end = + if (descriptor[typeIndex] == 'L') { + descriptor.indexOf(';', startIndex = typeIndex) + } else { + typeIndex + } + val parameterDescriptor = descriptor.substring(i, end + 1) + parameterTypes += TypeDescriptor(parameterDescriptor) + i += parameterDescriptor.length + } + val returnType = TypeDescriptor(descriptor.substring(i + 1)) + return Method(owner, name, parameterTypes, returnType) +} diff --git a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt index 90fc9226..71dde6f2 100644 --- a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt +++ b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -129,7 +129,7 @@ private class ReferencedMembersVisitor : MethodVisitor(Opcodes.ASM9) { } } -internal fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { +private fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { val parameterTypes = mutableListOf() var i = 1 while (true) { From 619f85a2254a4de9bf33dcddde927ebb1d5c7e74 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 11 Mar 2026 12:57:34 +0800 Subject: [PATCH 5/7] Revert "Pin ASM on 9.6 for manual tests" This reverts commit 1643a8ae0e4eff5b2b9ecca9e3305a2abcd20870. --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 88234767..688a1774 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ junit = "junit:junit:4.13.2" assertk = "com.willowtreeapps.assertk:assertk:0.28.1" okio = "com.squareup.okio:okio:3.16.4" byteunits = "com.jakewharton.byteunits:byteunits:0.9.1" -asm = "org.ow2.asm:asm:9.6" +asm = "org.ow2.asm:asm:9.9.1" diffUtils = "io.github.java-diff-utils:java-diff-utils:4.16" picnic = "com.jakewharton.picnic:picnic:0.7.0" mordant = "com.github.ajalt.mordant:mordant:3.0.2" From 4200bc29a4376180a4d390fbd1d195f817fd6a34 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 17 Mar 2026 10:37:54 +0800 Subject: [PATCH 6/7] Prefer MR classes on the classpath --- formats/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/formats/build.gradle b/formats/build.gradle index f0a0210b..39476bef 100644 --- a/formats/build.gradle +++ b/formats/build.gradle @@ -68,7 +68,9 @@ def addMultiReleaseSourceSet(int version) { task.group = LifecycleBasePlugin.VERIFICATION_GROUP task.description = "Runs test suite using Java ${version} toolchain." task.testClassesDirs = sourceSets.test.output.classesDirs - task.classpath = sourceSets.test.runtimeClasspath + def testCpWithoutMainOutput = sourceSets.test.runtimeClasspath - sourceSets.main.output + // Prefer MR classes on the classpath, so remove main output from the test runtime classpath. + task.classpath = files(tasks.named('jar', Jar).flatMap { it.archiveFile }) + testCpWithoutMainOutput task.javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(version) vendor = JvmVendorSpec.AZUL From 076adbadad37766c21461a7dadad5a90cc324cef Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 17 Mar 2026 10:43:46 +0800 Subject: [PATCH 7/7] internal parseMethod --- .../com/jakewharton/diffuse/format/Class.kt | 26 ----------------- .../com/jakewharton/diffuse/format/Class.kt | 25 ---------------- .../com/jakewharton/diffuse/format/util.kt | 29 +++++++++++++++++++ 3 files changed, 29 insertions(+), 51 deletions(-) diff --git a/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt b/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt index c1a5933f..21b28593 100644 --- a/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt +++ b/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -96,29 +96,3 @@ private fun parseOwner(owner: String): TypeDescriptor { val ownerDescriptor = if (owner.startsWith('[')) owner else "L$owner;" return TypeDescriptor(ownerDescriptor) } - -@Suppress("DuplicatedCode") // Reuse this function by internal will cause NoSuchMethodError. -private fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { - val parameterTypes = mutableListOf() - var i = 1 - while (true) { - if (descriptor[i] == ')') { - break - } - var typeIndex = i - while (descriptor[typeIndex] == '[') { - typeIndex++ - } - val end = - if (descriptor[typeIndex] == 'L') { - descriptor.indexOf(';', startIndex = typeIndex) - } else { - typeIndex - } - val parameterDescriptor = descriptor.substring(i, end + 1) - parameterTypes += TypeDescriptor(parameterDescriptor) - i += parameterDescriptor.length - } - val returnType = TypeDescriptor(descriptor.substring(i + 1)) - return Method(owner, name, parameterTypes, returnType) -} diff --git a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt index 71dde6f2..6c3f3bc5 100644 --- a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt +++ b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -129,31 +129,6 @@ private class ReferencedMembersVisitor : MethodVisitor(Opcodes.ASM9) { } } -private fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { - val parameterTypes = mutableListOf() - var i = 1 - while (true) { - if (descriptor[i] == ')') { - break - } - var typeIndex = i - while (descriptor[typeIndex] == '[') { - typeIndex++ - } - val end = - if (descriptor[typeIndex] == 'L') { - descriptor.indexOf(';', startIndex = typeIndex) - } else { - typeIndex - } - val parameterDescriptor = descriptor.substring(i, end + 1) - parameterTypes += TypeDescriptor(parameterDescriptor) - i += parameterDescriptor.length - } - val returnType = TypeDescriptor(descriptor.substring(i + 1)) - return Method(owner, name, parameterTypes, returnType) -} - private val lambdaMetaFactory = Handle( Opcodes.H_INVOKESTATIC, diff --git a/formats/src/main/kotlin/com/jakewharton/diffuse/format/util.kt b/formats/src/main/kotlin/com/jakewharton/diffuse/format/util.kt index ca622f77..01cc808d 100644 --- a/formats/src/main/kotlin/com/jakewharton/diffuse/format/util.kt +++ b/formats/src/main/kotlin/com/jakewharton/diffuse/format/util.kt @@ -4,3 +4,32 @@ import com.google.devrel.gmscore.tools.apk.arsc.ResourceFile import com.jakewharton.diffuse.io.Input internal fun Input.toResourceFile() = ResourceFile(toByteArray()) + +/** + * TODO: private this into `Class.kt` once we bump to Java 24+ and can use the new class file + * parser. + */ +internal fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { + val parameterTypes = mutableListOf() + var i = 1 + while (true) { + if (descriptor[i] == ')') { + break + } + var typeIndex = i + while (descriptor[typeIndex] == '[') { + typeIndex++ + } + val end = + if (descriptor[typeIndex] == 'L') { + descriptor.indexOf(';', startIndex = typeIndex) + } else { + typeIndex + } + val parameterDescriptor = descriptor.substring(i, end + 1) + parameterTypes += TypeDescriptor(parameterDescriptor) + i += parameterDescriptor.length + } + val returnType = TypeDescriptor(descriptor.substring(i + 1)) + return Method(owner, name, parameterTypes, returnType) +}