Skip to content

Commit

Permalink
bcv: use kotlinx-metadata-jvm to read kotlin visibilities
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ilya-g committed Feb 19, 2020
1 parent ee31506 commit 502d73c
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 44 deletions.
53 changes: 31 additions & 22 deletions src/main/kotlin/org.jetbrains.kotlin.tools/PublicApiDump.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,46 @@ fun main(args: Array<String>) {
}


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<String, ClassVisibility>): List<ClassBinarySignature> =
getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityMap)
fun getBinaryAPI(jar: JarFile, visibilityMap: Map<String, ClassVisibility>, visibilityFilter: (String) -> Boolean = { true }): List<ClassBinarySignature> =
getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityMap, visibilityFilter)

fun getBinaryAPI(classStreams: Sequence<InputStream>, visibilityMap: Map<String, ClassVisibility>): List<ClassBinarySignature> =
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<InputStream>, visibilityMap: Map<String, ClassVisibility>, visibilityFilter: (String) -> Boolean = { true }): List<ClassBinarySignature>
{
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 }
}



Expand Down
46 changes: 38 additions & 8 deletions src/main/kotlin/org.jetbrains.kotlin.tools/asmUtils.kt
Original file line number Diff line number Diff line change
@@ -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.*
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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 == "<init>" && 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 == "<init>" && "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(
Expand All @@ -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 }
}
Expand Down Expand Up @@ -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?
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Int>?)?.toIntArray(),
bytecodeVersion = (get("bv") as List<Int>?)?.toIntArray(),
data1 = (get("d1") as List<String>?)?.toTypedArray(),
data2 = (get("d2") as List<String>?)?.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<MemberVisibility>()
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<ClassNode>.readKotlinVisibilities(): Map<String, ClassVisibility> =
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<MemberSignature, MemberVisibility>)
class ClassVisibility(val name: String, val visibility: String?, val members: Map<MemberSignature, MemberVisibility>, val isCompanion: Boolean = false, val facadeClassName: String? = null) {
var companionVisibilities: ClassVisibility? = null
val partVisibilities = mutableListOf<ClassVisibility>()
}
data class MemberVisibility(val member: MemberSignature, val declaration: String?, val visibility: String?)
data class MemberSignature(val name: String, val desc: String)

Expand Down
12 changes: 0 additions & 12 deletions src/test/kotlin/cases/default/default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Loading

0 comments on commit 502d73c

Please sign in to comment.