diff --git a/CHANGELOG.md b/CHANGELOG.md index 55e85a6d..90166cc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ **Added** - Add `--summary-only` flag. - Support diffing bytecode versions for classes. +- Support diffing Kotlin metadata versions for classes. **Changed** - Replace `com.jakewharton.diffuse.io.Size` with `me.saket.bytesize.ByteSize` in the APIs. diff --git a/formats/api/formats.api b/formats/api/formats.api index 3aee5f1e..02e95be6 100644 --- a/formats/api/formats.api +++ b/formats/api/formats.api @@ -221,6 +221,7 @@ public final class com/jakewharton/diffuse/format/Class { public final fun getBytecodeVersion ()I public final fun getDeclaredMembers ()Ljava/util/List; public final fun getDescriptor-BeHrSHk ()Ljava/lang/String; + public final fun getKotlinMetadataVersion ()[I public final fun getReferencedMembers ()Ljava/util/List; public fun hashCode ()I public static final fun parse (Lcom/jakewharton/diffuse/io/Input;)Lcom/jakewharton/diffuse/format/Class; 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 c45ff9b1..13368762 100644 --- a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt +++ b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -2,6 +2,7 @@ package com.jakewharton.diffuse.format import com.jakewharton.diffuse.io.Input import java.util.Objects +import org.objectweb.asm.AnnotationVisitor import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.FieldVisitor @@ -13,18 +14,26 @@ class Class private constructor( val descriptor: TypeDescriptor, val bytecodeVersion: Int, + val kotlinMetadataVersion: IntArray, val declaredMembers: List, val referencedMembers: List, ) { override fun toString() = descriptor.toString() override fun hashCode() = - Objects.hash(descriptor, bytecodeVersion, declaredMembers, referencedMembers) + Objects.hash( + descriptor, + bytecodeVersion, + kotlinMetadataVersion.contentHashCode(), + declaredMembers, + referencedMembers, + ) override fun equals(other: Any?) = other is Class && descriptor == other.descriptor && bytecodeVersion == other.bytecodeVersion && + kotlinMetadataVersion.contentEquals(other.kotlinMetadataVersion) && declaredMembers == other.declaredMembers && referencedMembers == other.referencedMembers @@ -42,6 +51,7 @@ private constructor( return Class( type, declaredVisitor.version, + declaredVisitor.kotlinMetadataVersion, declaredVisitor.members.sorted(), referencedVisitor.members.sorted(), ) @@ -52,6 +62,7 @@ private constructor( private class DeclaredMembersVisitor(val type: TypeDescriptor, val methodVisitor: MethodVisitor) : ClassVisitor(Opcodes.ASM9) { var version: Int = 0 + var kotlinMetadataVersion = intArrayOf() val members = mutableListOf() override fun visit( @@ -87,6 +98,28 @@ private class DeclaredMembersVisitor(val type: TypeDescriptor, val methodVisitor members += Field(type, name, TypeDescriptor(descriptor)) return null } + + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + if (descriptor == "Lkotlin/Metadata;") { + return KotlinMetadataAnnotationVisitor { kotlinMetadataVersion = it } + } + return super.visitAnnotation(descriptor, visible) + } +} + +/** + * Reads [Metadata.bytecodeVersion] directly via ASM's [AnnotationVisitor] instead of + * `kotlin.metadata.jvm.KotlinClassMetadata` to avoid overheads for parsing [Metadata.data1] and + * [Metadata.data2]. + */ +private class KotlinMetadataAnnotationVisitor( + private val onMetadataVersionFound: (IntArray) -> Unit +) : AnnotationVisitor(Opcodes.ASM9) { + override fun visit(name: String?, value: Any?) { + if (name == "mv" && value is IntArray) { + onMetadataVersionFound(value) + } + } } private class ReferencedMembersVisitor : MethodVisitor(Opcodes.ASM9) { diff --git a/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt b/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt index e80b1069..35d0927c 100644 --- a/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt +++ b/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt @@ -1,7 +1,10 @@ package com.jakewharton.diffuse.format +import assertk.all import assertk.assertThat import assertk.assertions.containsOnly +import assertk.assertions.hasSize +import assertk.assertions.index import assertk.assertions.isEqualTo import com.jakewharton.diffuse.format.Class.Companion.toClass import java.util.function.Function @@ -17,6 +20,11 @@ class ClassTest { assertThat(clazz.descriptor).isEqualTo(type) assertThat(clazz.bytecodeVersion).isEqualTo(55) // Reflects the JVM target 11. + assertThat(clazz.kotlinMetadataVersion).all { + hasSize(3) // Like [2,4,0]. + index(0).isEqualTo(2) + index(2).isEqualTo(0) + } val initMethod = Method(type, "", emptyList(), TypeDescriptor("V")) val stringArrayDescriptor = TypeDescriptor("[Ljava/lang/String;") diff --git a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt index 39ceb4cf..026a2b28 100644 --- a/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt +++ b/reports/src/main/kotlin/com/jakewharton/diffuse/diff/JarsDiff.kt @@ -22,6 +22,10 @@ internal class JarsDiff( componentDiff(oldJars, newJars) { jar -> jar.classes.map { "${it.descriptor}: ${it.bytecodeVersion}" } } + val kotlinMetadataVersions = + componentDiff(oldJars, newJars) { jar -> + jar.classes.map { "${it.descriptor}: ${it.kotlinMetadataVersion.joinToString(".")}" } + } val methods = componentDiff(oldJars, newJars) { it.members.filterIsInstance() } val declaredMethods = componentDiff(oldJars, newJars) { it.declaredMembers.filterIsInstance() } @@ -33,7 +37,8 @@ internal class JarsDiff( val referencedFields = componentDiff(oldJars, newJars) { it.referencedMembers.filterIsInstance() } - val changed = bytecodeVersions.changed || methods.changed || fields.changed + val changed = + bytecodeVersions.changed || kotlinMetadataVersions.changed || methods.changed || fields.changed } internal fun JarsDiff.toSummaryTable(name: String) = diffuseTable { @@ -76,6 +81,7 @@ internal fun JarsDiff.toDetailReport() = buildString { // TODO appendComponentDiff("STRINGS", strings)? appendComponentDiff("CLASSES", classes) appendComponentDiff("BYTECODE VERSIONS", bytecodeVersions) + appendComponentDiff("KOTLIN METADATA VERSIONS", kotlinMetadataVersions) appendComponentDiff("METHODS", methods) appendComponentDiff("FIELDS", fields) }