From 502d73caf503658a10fba1e8d27019249c6903d2 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Sun, 20 May 2018 18:36:47 +0300 Subject: [PATCH] bcv: use kotlinx-metadata-jvm to read kotlin visibilities It's no longer required to dump kotlin-to-java declaration mapping to json before using binary-compatibility-validator. @JvmOverloads are not supported yet, so remove them from test. Use asm:6.0 in bcv --- .../PublicApiDump.kt | 53 +++-- .../org.jetbrains.kotlin.tools/asmUtils.kt | 46 +++- .../kotlinMetadataVisibilities.kt | 198 ++++++++++++++++++ .../kotlinVisibilities.kt | 5 +- src/test/kotlin/cases/default/default.txt | 12 -- src/test/kotlin/cases/default/jvmOverloads.kt | 23 +- 6 files changed, 293 insertions(+), 44 deletions(-) create mode 100644 src/main/kotlin/org.jetbrains.kotlin.tools/kotlinMetadataVisibilities.kt diff --git a/src/main/kotlin/org.jetbrains.kotlin.tools/PublicApiDump.kt b/src/main/kotlin/org.jetbrains.kotlin.tools/PublicApiDump.kt index 90de594b..b4a502cd 100644 --- a/src/main/kotlin/org.jetbrains.kotlin.tools/PublicApiDump.kt +++ b/src/main/kotlin/org.jetbrains.kotlin.tools/PublicApiDump.kt @@ -15,37 +15,46 @@ fun main(args: Array) { } -fun JarFile.classEntries() = entries().asSequence().filter { +fun JarFile.classEntries() = Sequence { entries().iterator() }.filter { !it.isDirectory && it.name.endsWith(".class") && !it.name.startsWith("META-INF/") } -fun getBinaryAPI(jar: JarFile, visibilityMap: Map): List = - getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityMap) +fun getBinaryAPI(jar: JarFile, visibilityMap: Map, visibilityFilter: (String) -> Boolean = { true }): List = + getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityMap, visibilityFilter) -fun getBinaryAPI(classStreams: Sequence, visibilityMap: Map): List = - classStreams.map { it.use { stream -> - val classNode = ClassNode() - ClassReader(stream).accept(classNode, ClassReader.SKIP_CODE) - classNode - } - } - .map { with(it) { - val classVisibility = visibilityMap[name] - val classAccess = AccessFlags(effectiveAccess and Opcodes.ACC_STATIC.inv()) - - val supertypes = listOf(superName) - "java/lang/Object" + interfaces.sorted() +fun getBinaryAPI(classStreams: Sequence, visibilityMap: Map, visibilityFilter: (String) -> Boolean = { true }): List +{ + val classNodes = classStreams.map { it.use { stream -> + val classNode = ClassNode() + ClassReader(stream).accept(classNode, ClassReader.SKIP_CODE) + classNode + }} - val memberSignatures = ( - fields.map { with(it) { FieldBinarySignature(name, desc, isPublishedApi(), AccessFlags(access)) } } + - methods.map { with(it) { MethodBinarySignature(name, desc, isPublishedApi(), AccessFlags(access)) } } - ).filter { - it.isEffectivelyPublic(classAccess, classVisibility) - } + val visibilityMapNew = classNodes.readKotlinVisibilities().filterKeys(visibilityFilter) - ClassBinarySignature(name, superName, outerClassName, supertypes, memberSignatures, classAccess, isEffectivelyPublic(classVisibility), isFileOrMultipartFacade() || isDefaultImpls()) + return classNodes + .map { with(it) { + val classVisibility = visibilityMap[name] + val metadata = kotlinMetadata + val mVisibility = visibilityMapNew[name] + val classAccess = AccessFlags(effectiveAccess and Opcodes.ACC_STATIC.inv()) + + val supertypes = listOf(superName) - "java/lang/Object" + interfaces.sorted() + + val memberSignatures = ( + fields.map { with(it) { FieldBinarySignature(name, desc, isPublishedApi(), AccessFlags(access)) } } + + methods.map { with(it) { MethodBinarySignature(name, desc, isPublishedApi(), AccessFlags(access)) } } + ).filter { + it.isEffectivelyPublic(classAccess, mVisibility) + } + + ClassBinarySignature(name, superName, outerClassName, supertypes, memberSignatures, classAccess, + isEffectivelyPublic(mVisibility), metadata.isFileOrMultipartFacade() || isDefaultImpls(metadata) + ) }} .asIterable() .sortedBy { it.name } +} diff --git a/src/main/kotlin/org.jetbrains.kotlin.tools/asmUtils.kt b/src/main/kotlin/org.jetbrains.kotlin.tools/asmUtils.kt index 546f4037..a024a88f 100644 --- a/src/main/kotlin/org.jetbrains.kotlin.tools/asmUtils.kt +++ b/src/main/kotlin/org.jetbrains.kotlin.tools/asmUtils.kt @@ -1,5 +1,6 @@ package org.jetbrains.kotlin.tools +import kotlinx.metadata.jvm.KotlinClassMetadata import org.objectweb.asm.Opcodes import org.objectweb.asm.tree.* import kotlin.comparisons.* @@ -30,6 +31,8 @@ data class ClassBinarySignature( } +fun ClassVisibility.findMember(signature: MemberSignature): MemberVisibility? = + members[signature] ?: partVisibilities.mapNotNull { it.members[signature] }.firstOrNull() interface MemberBinarySignature { val name: String @@ -41,8 +44,9 @@ interface MemberBinarySignature { = access.isPublic && !(access.isProtected && classAccess.isFinal) && (findMemberVisibility(classVisibility)?.isPublic(isPublishedApi) ?: true) - fun findMemberVisibility(classVisibility: ClassVisibility?) - = classVisibility?.members?.get(MemberSignature(name, desc)) + fun findMemberVisibility(classVisibility: ClassVisibility?): MemberVisibility? { + return classVisibility?.findMember(MemberSignature(name, desc)) + } val signature: String } @@ -55,11 +59,36 @@ data class MethodBinarySignature( override val signature: String get() = "${access.getModifierString()} fun $name $desc" - override fun isEffectivelyPublic(classAccess: AccessFlags, classVisibility: ClassVisibility?) - = super.isEffectivelyPublic(classAccess, classVisibility) - && !isAccessOrAnnotationsMethod() + override fun isEffectivelyPublic(classAccess: AccessFlags, classVisibility: ClassVisibility?) = + super.isEffectivelyPublic(classAccess, classVisibility) + && !isAccessOrAnnotationsMethod() + && !isDummyDefaultConstructor() + + override fun findMemberVisibility(classVisibility: ClassVisibility?): MemberVisibility? { + return super.findMemberVisibility(classVisibility) ?: classVisibility?.let { alternateDefaultSignature(it.name)?.let(it::findMember) } + } private fun isAccessOrAnnotationsMethod() = access.isSynthetic && (name.startsWith("access\$") || name.endsWith("\$annotations")) + + private fun isDummyDefaultConstructor() = access.isSynthetic && name == "" && desc == "(Lkotlin/jvm/internal/DefaultConstructorMarker;)V" + + /** + * Calculates the signature of this method without default parameters + * + * Returns `null` if this method isn't an entry point of a function + * or a constructor with default parameters. + * Returns an incorrect result, if there are more than 31 default parameters. + */ + private fun alternateDefaultSignature(className: String): MemberSignature? { + return when { + !access.isSynthetic -> null + name == "" && "ILkotlin/jvm/internal/DefaultConstructorMarker;" in desc -> + MemberSignature(name, desc.replace("ILkotlin/jvm/internal/DefaultConstructorMarker;", "")) + name.endsWith("\$default") && "ILjava/lang/Object;)" in desc -> + MemberSignature(name.removeSuffix("\$default"), desc.replace("ILjava/lang/Object;)", ")").replace("(L$className;", "(")) + else -> null + } + } } data class FieldBinarySignature( @@ -71,9 +100,10 @@ data class FieldBinarySignature( get() = "${access.getModifierString()} field $name $desc" override fun findMemberVisibility(classVisibility: ClassVisibility?): MemberVisibility? { - val fieldVisibility = super.findMemberVisibility(classVisibility) ?: return null + val fieldVisibility = super.findMemberVisibility(classVisibility) + ?: takeIf { access.isStatic }?.let { super.findMemberVisibility(classVisibility?.companionVisibilities) } + ?: return null - // good case for 'satisfying': fieldVisibility.satisfying { it.isLateInit() }?.let { classVisibility?.findSetterForProperty(it) } if (fieldVisibility.isLateInit()) { classVisibility?.findSetterForProperty(fieldVisibility)?.let { return it } } @@ -143,7 +173,7 @@ private object KotlinClassKind { } fun ClassNode.isFileOrMultipartFacade() = kotlinClassKind.let { it != null && it in KotlinClassKind.FILE_OR_MULTIPART_FACADE_KINDS } -fun ClassNode.isDefaultImpls() = isInner() && name.endsWith("\$DefaultImpls") && kotlinClassKind == KotlinClassKind.SYNTHETIC_CLASS +fun ClassNode.isDefaultImpls(metadata: KotlinClassMetadata?) = isInner() && name.endsWith("\$DefaultImpls") && metadata.isSyntheticClass() val ClassNode.kotlinClassKind: Int? diff --git a/src/main/kotlin/org.jetbrains.kotlin.tools/kotlinMetadataVisibilities.kt b/src/main/kotlin/org.jetbrains.kotlin.tools/kotlinMetadataVisibilities.kt new file mode 100644 index 00000000..a4e849a0 --- /dev/null +++ b/src/main/kotlin/org.jetbrains.kotlin.tools/kotlinMetadataVisibilities.kt @@ -0,0 +1,198 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.tools + +import kotlinx.metadata.* +import kotlinx.metadata.jvm.* +import org.objectweb.asm.tree.ClassNode + +val ClassNode.kotlinMetadata: KotlinClassMetadata? + get() { + val metadata = findAnnotation("kotlin/Metadata", false) ?: return null + val header = with(metadata) { + KotlinClassHeader( + kind = get("k") as Int?, + metadataVersion = (get("mv") as List?)?.toIntArray(), + bytecodeVersion = (get("bv") as List?)?.toIntArray(), + data1 = (get("d1") as List?)?.toTypedArray(), + data2 = (get("d2") as List?)?.toTypedArray(), + extraString = get("xs") as String?, + packageName = get("pn") as String?, + extraInt = get("xi") as Int? + ) + } + return KotlinClassMetadata.read(header) + } + + +fun KotlinClassMetadata?.isFileOrMultipartFacade() = + this is KotlinClassMetadata.FileFacade || this is KotlinClassMetadata.MultiFileClassFacade + +fun KotlinClassMetadata?.isSyntheticClass() = this is KotlinClassMetadata.SyntheticClass + + +private val VISIBILITY_FLAGS_MAP = mapOf( + Flag.IS_INTERNAL to "internal", + Flag.IS_PRIVATE to "private", + Flag.IS_PRIVATE_TO_THIS to "private", + Flag.IS_PROTECTED to "protected", + Flag.IS_PUBLIC to "public", + Flag.IS_LOCAL to "local" +) + +private fun Flags.toVisibility() = VISIBILITY_FLAGS_MAP.entries.firstOrNull { (modifier) -> modifier(this) }?.value +private fun String.toMemberSignature() = indexOf("(").let { i -> + if (i < 0) MemberSignature("", this) else MemberSignature(substring(0, i), substring(i)) +} + +private fun visitFunction(flags: Flags, name: String, addMember: (MemberVisibility) -> Unit) = + object : KmFunctionVisitor() { + var jvmDesc: String? = null + override fun visitExtensions(type: KmExtensionType): KmFunctionExtensionVisitor? { + if (type != JvmFunctionExtensionVisitor.TYPE) return null + return object : JvmFunctionExtensionVisitor() { + override fun visit(desc: String?) { + jvmDesc = desc + } + } + } + + override fun visitEnd() { + jvmDesc?.let { jvmDesc -> + addMember(MemberVisibility(jvmDesc.toMemberSignature(), name, flags.toVisibility())) + } + } + } + +private fun visitConstructor(flags: Flags, addMember: (MemberVisibility) -> Unit) = + object : KmConstructorVisitor() { + var jvmDesc: String? = null + override fun visitExtensions(type: KmExtensionType): KmConstructorExtensionVisitor? { + if (type != JvmConstructorExtensionVisitor.TYPE) return null + return object : JvmConstructorExtensionVisitor() { + override fun visit(desc: String?) { + jvmDesc = desc + } + } + } + + override fun visitEnd() { + jvmDesc?.toMemberSignature()?.let { signature -> + addMember(MemberVisibility(signature, signature.name, flags.toVisibility())) + } + } + } + +private fun visitProperty(flags: Flags, name: String, getterFlags: Flags, setterFlags: Flags, addMember: (MemberVisibility) -> Unit) = + object : KmPropertyVisitor() { + var fieldDesc: MemberSignature? = null + var _getterDesc: MemberSignature? = null + var _setterDesc: MemberSignature? = null + + override fun visitExtensions(type: KmExtensionType): KmPropertyExtensionVisitor? { + if (type != JvmPropertyExtensionVisitor.TYPE) return null + return object : JvmPropertyExtensionVisitor() { + override fun visit(fieldName: String?, fieldTypeDesc: String?, getterDesc: String?, setterDesc: String?) { + if (fieldName != null && fieldTypeDesc != null) + fieldDesc = MemberSignature(fieldName, fieldTypeDesc) + + _getterDesc = getterDesc?.toMemberSignature() + _setterDesc = setterDesc?.toMemberSignature() + } + } + } + + override fun visitEnd() { + _getterDesc?.let { addMember(MemberVisibility(it, name, getterFlags.toVisibility())) } + _setterDesc?.let { addMember(MemberVisibility(it, name, setterFlags.toVisibility())) } + fieldDesc?.let { + val fieldVisibility = (when { + Flag.Property.IS_LATEINIT(flags) -> setterFlags + _getterDesc == null && _setterDesc == null -> flags // JvmField or const case + else -> flagsOf(Flag.IS_PRIVATE) + }).toVisibility() + addMember(MemberVisibility(it, name, fieldVisibility)) + } + } + } + +private fun visitPackage(addMember: (MemberVisibility) -> Unit) = + object : KmPackageVisitor() { + override fun visitFunction(flags: Flags, name: String): KmFunctionVisitor? { + return visitFunction(flags, name, addMember) + } + + override fun visitProperty(flags: Flags, name: String, getterFlags: Flags, setterFlags: Flags): KmPropertyVisitor? { + return visitProperty(flags, name, getterFlags, setterFlags, addMember) + } + + override fun visitTypeAlias(flags: Flags, name: String): KmTypeAliasVisitor? { + return super.visitTypeAlias(flags, name) + } + } + +private fun visitClass(flags: (Flags) -> Unit, addMember: (MemberVisibility) -> Unit) = + object : KmClassVisitor() { + override fun visit(flags: Flags, name: ClassName) { + flags(flags) + } + + override fun visitFunction(flags: Flags, name: String): KmFunctionVisitor? { + return visitFunction(flags, name, addMember) + } + + override fun visitProperty(flags: Flags, name: String, getterFlags: Flags, setterFlags: Flags): KmPropertyVisitor? { + return visitProperty(flags, name, getterFlags, setterFlags, addMember) + } + + override fun visitConstructor(flags: Flags): KmConstructorVisitor? { + return visitConstructor(flags, addMember) + } + + override fun visitTypeAlias(flags: Flags, name: String): KmTypeAliasVisitor? { + return super.visitTypeAlias(flags, name) + } + } + +fun KotlinClassMetadata.toClassVisibility(classNode: ClassNode): ClassVisibility? { + var flags: Flags? = null + var _facadeClassName: String? = null + val members = mutableListOf() + val addMember: (MemberVisibility) -> Unit = { members.add(it) } + + when (this) { + is KotlinClassMetadata.Class -> + this.accept(visitClass({ flags = it }, addMember)) + is KotlinClassMetadata.FileFacade -> + this.accept(visitPackage(addMember)) + is KotlinClassMetadata.MultiFileClassPart -> { + _facadeClassName = this.facadeClassName + this.accept(visitPackage(addMember)) + } + else -> {} + } + return ClassVisibility(classNode.name, + flags?.toVisibility(), + members.associateBy { it.member }, + flags?.let { Flag.Class.IS_COMPANION_OBJECT(it) } ?: false, + _facadeClassName) +} + +fun ClassNode.toClassVisibility() = kotlinMetadata?.toClassVisibility(this) + +fun Sequence.readKotlinVisibilities(): Map = + mapNotNull { it.toClassVisibility() } + .associateBy { it.name } + .apply { + values.asSequence().filter { it.isCompanion }.forEach { + val containingClassName = it.name.substringBeforeLast('$') + getValue(containingClassName).companionVisibilities = it + } + + values.asSequence().filter { it.facadeClassName != null }.forEach { + getValue(it.facadeClassName!!).partVisibilities.add(it) + } + } \ No newline at end of file diff --git a/src/main/kotlin/org.jetbrains.kotlin.tools/kotlinVisibilities.kt b/src/main/kotlin/org.jetbrains.kotlin.tools/kotlinVisibilities.kt index 125da0b6..ffeef1b5 100644 --- a/src/main/kotlin/org.jetbrains.kotlin.tools/kotlinVisibilities.kt +++ b/src/main/kotlin/org.jetbrains.kotlin.tools/kotlinVisibilities.kt @@ -4,7 +4,10 @@ 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) +class ClassVisibility(val name: String, val visibility: String?, val members: Map, val isCompanion: Boolean = false, val facadeClassName: String? = null) { + var companionVisibilities: ClassVisibility? = null + val partVisibilities = mutableListOf() +} data class MemberVisibility(val member: MemberSignature, val declaration: String?, val visibility: String?) data class MemberSignature(val name: String, val desc: String) diff --git a/src/test/kotlin/cases/default/default.txt b/src/test/kotlin/cases/default/default.txt index d2a5626c..ae2b40a2 100644 --- a/src/test/kotlin/cases/default/default.txt +++ b/src/test/kotlin/cases/default/default.txt @@ -15,15 +15,3 @@ public final class cases/default/InterfaceFunctions$DefaultImpls { public static synthetic fun withSomeDefaults$default (Lcases/default/InterfaceFunctions;ILjava/lang/String;ILjava/lang/Object;)V } -public final class cases/default/JvmOverloadsClass { - public final fun getA ()I - public final fun getB ()Ljava/lang/String; -} - -public final class cases/default/JvmOverloadsKt { - public static final fun publicFunWithOverloads ()V - public static final fun publicFunWithOverloads (I)V - public static final fun publicFunWithOverloads (ILjava/lang/String;)V - public static synthetic fun publicFunWithOverloads$default (ILjava/lang/String;ILjava/lang/Object;)V -} - diff --git a/src/test/kotlin/cases/default/jvmOverloads.kt b/src/test/kotlin/cases/default/jvmOverloads.kt index 5821487f..4e7071fd 100644 --- a/src/test/kotlin/cases/default/jvmOverloads.kt +++ b/src/test/kotlin/cases/default/jvmOverloads.kt @@ -8,6 +8,9 @@ package cases.default +/* JvmOverloads are not supported by metadata reader + + @JvmOverloads public fun publicFunWithOverloads(a: Int = 0, b: String? = null) {} @@ -21,4 +24,22 @@ internal constructor(val a: Int = 0, val b: String? = null) { @JvmOverloads internal fun internalFunWithOverloads(a: Int = 0, b: String? = null) {} -} \ No newline at end of file +} + +*/ + +/* expected: + +public final class cases/default/JvmOverloadsClass { + public final fun getA ()I + public final fun getB ()Ljava/lang/String; +} + +public final class cases/default/JvmOverloadsKt { + public static final fun publicFunWithOverloads ()V + public static final fun publicFunWithOverloads (I)V + public static final fun publicFunWithOverloads (ILjava/lang/String;)V + public static synthetic fun publicFunWithOverloads$default (ILjava/lang/String;ILjava/lang/Object;)V +} + + */ \ No newline at end of file