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 index 753e7a775cf..e2074d2f980 100644 --- 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 @@ -1,10 +1,9 @@ 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.io.InputStream import java.util.jar.JarFile import kotlin.comparisons.compareBy @@ -17,27 +16,20 @@ fun main(args: Array) { } +fun JarFile.classEntries() = entries().asSequence().filter { !it.isDirectory && it.name.endsWith(".class") } -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 outerName: String?, val modifiers: String, val supertypes: List, val memberSignatures: List, val isPublic: Boolean) -data class ClassBinarySignature(val name: String, val outerName: String?, val signature: String, val memberSignatures: List, val isPublic: Boolean) +val ClassBinarySignature.signature: String + get() = "$modifiers class $name" + if (supertypes.isEmpty()) "" else ": ${supertypes.joinToString()}" -fun getBinaryAPI(jar: JarFile, visibilityMap: Map): List = jar.classEntries() - .map { entry -> - jar.getInputStream(entry).use { stream -> + +fun getBinaryAPI(jar: JarFile, visibilityMap: Map): List = + getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityMap) + +fun getBinaryAPI(classStreams: Sequence, visibilityMap: Map): List = + classStreams.map { it.use { stream -> val classNode = ClassNode() ClassReader(stream).accept(classNode, ClassReader.SKIP_CODE) classNode @@ -51,8 +43,7 @@ fun getBinaryAPI(jar: JarFile, visibilityMap: Map): Lis val supertypes = listOf(superName) - "java/lang/Object" + interfaces.sorted() - val classSignature = "${getModifierString(access)} class $name" + - if (supertypes.isEmpty()) "" else ": ${supertypes.joinToString()}" + val modifiers = getModifierString(access) val memberSignatures = fields.filter { it.isPublic() } @@ -62,7 +53,7 @@ fun getBinaryAPI(jar: JarFile, visibilityMap: Map): Lis .sortedWith(compareBy({ it.name }, { it.desc })) .map { with(it) { "${getModifierString(access)} fun $name $desc" } } - ClassBinarySignature(name, outerClassName, classSignature, memberSignatures, isPublic) + ClassBinarySignature(name, outerClassName, modifiers, supertypes, memberSignatures, isPublic) }} @@ -73,109 +64,14 @@ fun List.filterOutNonPublic(): List fun ClassBinarySignature.isPublicAndAccessible(): Boolean = isPublic && (outerName == null || classByName[outerName]?.isPublicAndAccessible() ?: true) - return filter { it.isPublicAndAccessible() } + return filter { it -> it.isPublicAndAccessible() } } -fun List.dump() = forEach { - println(it.signature) - it.memberSignatures.forEach { println(it) } - println("------------------\n") -} +fun List.dump() = dump(to = System.out) +fun List.dump(to: T): T = to.apply { this@dump.forEach { + appendln(it.signature) + it.memberSignatures.forEach { appendln(it) } + appendln("------------------\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 -} - -val ClassNode.outerClassName: String? - get() = innerClasses.singleOrNull { it.name == name }?.outerName - - - -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" diff --git a/libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools/asmUtils.kt b/libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools/asmUtils.kt new file mode 100644 index 00000000000..1b7a5d4d3dc --- /dev/null +++ b/libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools/asmUtils.kt @@ -0,0 +1,85 @@ +package org.jetbrains.kotlin.tools + +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.* + + +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") + + +private 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 object KotlinClassKind { + const val FILE = 2 + const val MULTIPART_FACADE = 4 + + val FILE_OR_MULTIPART_FACADE_KINDS = listOf(FILE, MULTIPART_FACADE) +} + +fun ClassNode.isFileOrMultipartFacade() = kotlinClassKind.let { it != null && it in KotlinClassKind.FILE_OR_MULTIPART_FACADE_KINDS } + + +val ClassNode.kotlinClassKind: Int? + get() = visibleAnnotations + ?.filter { it.desc == "Lkotlin/Metadata;" } + ?.map { (it.values.annotationValue("k") as? Int) } + ?.firstOrNull() + +fun ClassNode.hasAnnotation(annotationName: String) + = "L$annotationName;".let { desc -> visibleAnnotations?.any { it.desc == desc } ?: false } + +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 +} + +val ClassNode.outerClassName: String? + get() = innerClasses.singleOrNull { it.name == name }?.outerName + diff --git a/libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools/kotlinVisibilities.kt b/libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools/kotlinVisibilities.kt new file mode 100644 index 00000000000..f2f7c58231c --- /dev/null +++ b/libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools/kotlinVisibilities.kt @@ -0,0 +1,43 @@ +package org.jetbrains.kotlin.tools + +import com.google.gson.internal.Streams +import com.google.gson.stream.JsonReader +import java.io.File + +data class ClassVisibility(val name: String, val visibility: String?, val members: Map) +data class MemberVisibility(val member: MemberSignature, val visibility: String?) +data class MemberSignature(val name: String, val desc: String) + +private fun isPublic(visibility: String?) = visibility == null || visibility == "public" || visibility == "protected" +fun ClassVisibility.isPublic() = isPublic(visibility) +fun MemberVisibility.isPublic() = isPublic(visibility) + + +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 } +} + +