From a9fd1de263e5b11a801aa93171d50c916edc99c3 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Sat, 12 Mar 2016 15:35:36 +0300 Subject: [PATCH] Tool to dump binary API signatures from compiled JAR. Incorporate declaration mappings output from the compiler to take into account kotlin visibilities. --- libraries/pom.xml | 2 + .../binary-compatibility-validator/pom.xml | 71 +++++++ .../PublicApiDump.kt | 176 ++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 libraries/tools/binary-compatibility-validator/pom.xml create mode 100644 libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools/PublicApiDump.kt diff --git a/libraries/pom.xml b/libraries/pom.xml index d0337dec091..466fe998a09 100644 --- a/libraries/pom.xml +++ b/libraries/pom.xml @@ -82,6 +82,8 @@ tools/kotlin-reflect tools/kotlin-osgi-bundle + tools/binary-compatibility-validator + diff --git a/libraries/tools/binary-compatibility-validator/pom.xml b/libraries/tools/binary-compatibility-validator/pom.xml new file mode 100644 index 00000000000..1de0d0245cf --- /dev/null +++ b/libraries/tools/binary-compatibility-validator/pom.xml @@ -0,0 +1,71 @@ + + + + 4.0.0 + + + org.jetbrains.kotlin + kotlin-project + 0.1-SNAPSHOT + ../../pom.xml + + + binary-compatibility-validator + jar + + + + org.jetbrains.kotlin + kotlin-stdlib + ${project.version} + + + org.ow2.asm + asm-debug-all + 5.0.4 + + + com.google.code.gson + gson + 2.6.2 + + + + + src/main/kotlin + src/test/kotlin + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${project.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + maven-deploy-plugin + + true + + + + + + diff --git a/libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools/PublicApiDump.kt b/libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools/PublicApiDump.kt new file mode 100644 index 00000000000..cbd16acac89 --- /dev/null +++ b/libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools/PublicApiDump.kt @@ -0,0 +1,176 @@ +package org.jetbrains.kotlin.tools + +import com.google.gson.internal.Streams +import com.google.gson.stream.JsonReader +import org.objectweb.asm.* +import org.objectweb.asm.tree.* +import java.io.File +import java.util.jar.JarFile +import kotlin.comparisons.compareBy + +fun main(args: Array) { + var src = args[0] + println(src) + println("------------------\n"); + val visibilities = readKotlinVisibilities(File("""stdlib/target/stdlib-declarations.json""")) + val apiDumper = ApiDumper(visibilities) + dumpBinaryAPI(apiDumper.getBinaryAPI(JarFile(src))) +} + + + +val ACCESS_NAMES = mapOf( + Opcodes.ACC_PUBLIC to "public", + Opcodes.ACC_PROTECTED to "protected", + Opcodes.ACC_PRIVATE to "private", + Opcodes.ACC_STATIC to "static", + Opcodes.ACC_FINAL to "final", + Opcodes.ACC_ABSTRACT to "abstract", + Opcodes.ACC_SYNTHETIC to "synthetic", + Opcodes.ACC_INTERFACE to "interface", + Opcodes.ACC_ANNOTATION to "annotation") + +fun JarFile.classEntries() = entries().asSequence().filter { + !it.isDirectory && it.name.endsWith(".class") +} + +data class ClassBinarySignature(val name: String, val signature: String, val memberSignatures: List) + +class ApiDumper(val visibilityMap: Map) { + fun getBinaryAPI(jar: JarFile): List = jar.classEntries() + .map { entry -> + jar.getInputStream(entry).use { stream -> + val classNode = ClassNode() + ClassReader(stream).accept(classNode, ClassReader.SKIP_CODE) + classNode + } + } + .mapNotNull { with(it) { + val classVisibility = visibilityMap[name] + if (!it.isEffectivelyPublic(classVisibility)) + return@mapNotNull null + + // TODO: Inspect outer classes + + val supertypes = listOf(superName) - "java/lang/Object" + interfaces.sorted() + + val classSignature = "${getModifierString(access)} class $name" + + if (supertypes.isEmpty()) "" else ": ${supertypes.joinToString()}" + + val memberSignatures = + fields.filter { it.isPublic() } + .sortedBy { it.name } + .map { with(it) { "${getModifierString(access)} field $name $desc" } } + + methods.filter { it.isEffectivelyPublic(classVisibility) } + .sortedWith(compareBy({ it.name }, { it.desc })) + .map { with(it) { "${getModifierString(access)} fun $name $desc" } } + + ClassBinarySignature(name, classSignature, memberSignatures) + }} + .asIterable() + .sortedBy { it.name } + +} + +fun dumpBinaryAPI(signatures: List) { + signatures.forEach { + println(it.signature) + it.memberSignatures.forEach { println(it) } + println("------------------\n") + } +} + + + +fun isPublic(access: Int) = access and Opcodes.ACC_PUBLIC != 0 || access and Opcodes.ACC_PROTECTED != 0 +fun getModifiers(access: Int): List = ACCESS_NAMES.entries.mapNotNull { if (access and it.key != 0) it.value else null } +fun getModifierString(access: Int): String = getModifiers(access).joinToString(" ") + +fun ClassNode.isSynthetic() = access and Opcodes.ACC_SYNTHETIC != 0 +fun MethodNode.isSynthetic() = access and Opcodes.ACC_SYNTHETIC != 0 +fun ClassNode.isPublic() = isPublic(access) +fun MethodNode.isPublic() = isPublic(access) +fun FieldNode.isPublic() = isPublic(access) + + +fun ClassNode.isEffectivelyPublic(classVisibility: ClassVisibility?) = + isPublic() + && !isLocal() + && !isWhenMappings() + && (classVisibility?.isPublic() ?: true) + && !isNonPublicFileOrFacade(classVisibility) + +fun ClassNode.isNonPublicFileOrFacade(classVisibility: ClassVisibility?) = + isFileOrMultipartFacade() + && methods.none { it.isEffectivelyPublic(classVisibility) } + && fields.none { it.isPublic() } + + +fun MethodNode.isEffectivelyPublic(classVisibility: ClassVisibility?) = + isPublic() + && (classVisibility?.members?.get(MemberSignature(name, desc))?.isPublic() ?: true) + && !isAccessMethod() + + + +fun ClassNode.isLocal() = innerClasses.filter { it.name == name && it.innerName == null && it.outerName == null }.count() == 1 +fun ClassNode.isWhenMappings() = isSynthetic() && name.endsWith("\$WhenMappings") +fun MethodNode.isAccessMethod() = isSynthetic() && name.startsWith("access\$") + + + +private val FILE_OR_MULTIPART_FACADE_KINDS = listOf(2, 4) +fun ClassNode.isFileOrMultipartFacade() = kotlinClassKind.let { it != null && it in FILE_OR_MULTIPART_FACADE_KINDS } + + +val ClassNode.kotlinClassKind: Int? + get() = visibleAnnotations + ?.filter { it.desc == "Lkotlin/Metadata;" } + ?.map { (it.values.annotationValue("k") as? Int) } + ?.firstOrNull() + +private fun List.annotationValue(key: String): Any? { + for (index in (0 .. size / 2 - 1)) { + if (this[index*2] == key) + return this[index*2 + 1] + } + return null +} + + + + +fun readKotlinVisibilities(declarationFile: File): Map { + val result = mutableListOf() + declarationFile.bufferedReader().use { reader -> + val jsonReader = JsonReader(reader) + jsonReader.beginArray() + while (jsonReader.hasNext()) { + val classObject = Streams.parse(jsonReader).asJsonObject + result += with (classObject) { + val name = getAsJsonPrimitive("class").asString + val visibility = getAsJsonPrimitive("visibility")?.asString + val members = getAsJsonArray("members").map { it -> with(it.asJsonObject) { + val name = getAsJsonPrimitive("name").asString + val desc = getAsJsonPrimitive("desc").asString + val visibility = getAsJsonPrimitive("visibility")?.asString + MemberVisibility(MemberSignature(name, desc), visibility) + }} + ClassVisibility(name, visibility, members.associateByTo(hashMapOf()) { it.member }) + } + } + jsonReader.endArray() + } + + return result.associateByTo(hashMapOf()) { it.name } +} + + +data class ClassVisibility(val name: String, val visibility: String?, val members: Map) + +fun ClassVisibility.isPublic() = visibility == "public" + +data class MemberVisibility(val member: MemberSignature, val visibility: String?) +data class MemberSignature(val name: String, val desc: String) + +fun MemberVisibility.isPublic() = visibility == "public" || visibility == "protected"