Binary compatibility validator is now an external tool

Use its code from a dependency.
This commit is contained in:
Ilya Gorbunov
2020-02-20 21:22:02 +03:00
parent ceb03ce739
commit a95f205c1a
8 changed files with 6 additions and 450 deletions
@@ -6,13 +6,7 @@ configurations {
}
dependencies {
compile project(':kotlin-stdlib')
compileOnly project(':kotlinx-metadata')
compileOnly project(':kotlinx-metadata-jvm')
compile 'org.ow2.asm:asm:6.0'
compile 'org.ow2.asm:asm-tree:6.0'
runtime project(path: ':kotlinx-metadata-jvm', configuration: 'runtime')
compile("org.jetbrains.kotlinx:binary-compatibility-validator:0.2.3")
testCompile project(':kotlin-test:kotlin-test-junit')
@@ -1,104 +0,0 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.jvm.JvmFieldSignature
import kotlinx.metadata.jvm.JvmMethodSignature
import org.objectweb.asm.*
import org.objectweb.asm.tree.*
import java.io.InputStream
import java.util.jar.JarFile
fun main(args: Array<String>) {
val src = args[0]
println(src)
println("------------------\n");
getBinaryAPI(JarFile(src)).filterOutNonPublic().dump()
}
fun JarFile.classEntries() = Sequence { entries().iterator() }.filter {
!it.isDirectory && it.name.endsWith(".class") && !it.name.startsWith("META-INF/")
}
fun getBinaryAPI(jar: JarFile, visibilityFilter: (String) -> Boolean = { true }): List<ClassBinarySignature> =
getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityFilter)
fun getBinaryAPI(classStreams: Sequence<InputStream>, 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 visibilityMapNew = classNodes.readKotlinVisibilities().filterKeys(visibilityFilter)
return classNodes
.map { with(it) {
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(JvmFieldSignature(name, desc), isPublishedApi(), AccessFlags(access)) } } +
methods.map { with(it) { MethodBinarySignature(JvmMethodSignature(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 }
}
fun List<ClassBinarySignature>.filterOutNonPublic(nonPublicPackages: List<String> = emptyList()): List<ClassBinarySignature> {
val nonPublicPaths = nonPublicPackages.map { it.replace('.', '/') + '/' }
val classByName = associateBy { it.name }
fun ClassBinarySignature.isInNonPublicPackage() =
nonPublicPaths.any { name.startsWith(it) }
fun ClassBinarySignature.isPublicAndAccessible(): Boolean =
isEffectivelyPublic &&
(outerName == null || classByName[outerName]?.let { outerClass ->
!(this.access.isProtected && outerClass.access.isFinal)
&& outerClass.isPublicAndAccessible()
} ?: true)
fun supertypes(superName: String) = generateSequence({ classByName[superName] }, { classByName[it.superName] })
fun ClassBinarySignature.flattenNonPublicBases(): ClassBinarySignature {
val nonPublicSupertypes = supertypes(superName).takeWhile { !it.isPublicAndAccessible() }.toList()
if (nonPublicSupertypes.isEmpty())
return this
val inheritedStaticSignatures = nonPublicSupertypes.flatMap { it.memberSignatures.filter { it.access.isStatic }}
// not covered the case when there is public superclass after chain of private superclasses
return this.copy(memberSignatures = memberSignatures + inheritedStaticSignatures, supertypes = supertypes - superName)
}
return filter { !it.isInNonPublicPackage() && it.isPublicAndAccessible() }
.map { it.flattenNonPublicBases() }
.filterNot { it.isNotUsedWhenEmpty && it.memberSignatures.isEmpty()}
}
fun List<ClassBinarySignature>.dump() = dump(to = System.out)
fun <T : Appendable> List<ClassBinarySignature>.dump(to: T): T = to.apply { this@dump.forEach {
append(it.signature).appendln(" {")
it.memberSignatures.sortedWith(MEMBER_SORT_ORDER).forEach { append("\t").appendln(it.signature) }
appendln("}\n")
}}
@@ -1,189 +0,0 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.jvm.*
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"
)
data class ClassBinarySignature(
val name: String,
val superName: String,
val outerName: String?,
val supertypes: List<String>,
val memberSignatures: List<MemberBinarySignature>,
val access: AccessFlags,
val isEffectivelyPublic: Boolean,
val isNotUsedWhenEmpty: Boolean
) {
val signature: String
get() = "${access.getModifierString()} class $name" + if (supertypes.isEmpty()) "" else " : ${supertypes.joinToString()}"
}
interface MemberBinarySignature {
val jvmMember: JvmMemberSignature
val name: String get() = jvmMember.name
val desc: String get() = jvmMember.desc
val access: AccessFlags
val isPublishedApi: Boolean
fun isEffectivelyPublic(classAccess: AccessFlags, classVisibility: ClassVisibility?) =
access.isPublic && !(access.isProtected && classAccess.isFinal)
&& (findMemberVisibility(classVisibility)?.isPublic(isPublishedApi) ?: true)
fun findMemberVisibility(classVisibility: ClassVisibility?): MemberVisibility? {
return classVisibility?.findMember(jvmMember)
}
val signature: String
}
data class MethodBinarySignature(
override val jvmMember: JvmMethodSignature,
override val isPublishedApi: Boolean,
override val access: AccessFlags
) : MemberBinarySignature {
override val signature: String
get() = "${access.getModifierString()} fun $name $desc"
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): JvmMethodSignature? {
return when {
!access.isSynthetic -> null
name == "<init>" && "ILkotlin/jvm/internal/DefaultConstructorMarker;" in desc ->
JvmMethodSignature(name, desc.replace("ILkotlin/jvm/internal/DefaultConstructorMarker;", ""))
name.endsWith("\$default") && "ILjava/lang/Object;)" in desc ->
JvmMethodSignature(
name.removeSuffix("\$default"),
desc.replace("ILjava/lang/Object;)", ")").replace("(L$className;", "(")
)
else -> null
}
}
}
data class FieldBinarySignature(
override val jvmMember: JvmFieldSignature,
override val isPublishedApi: Boolean,
override val access: AccessFlags
) : MemberBinarySignature {
override val signature: String
get() = "${access.getModifierString()} field $name $desc"
override fun findMemberVisibility(classVisibility: ClassVisibility?): MemberVisibility? {
return super.findMemberVisibility(classVisibility)
?: takeIf { access.isStatic }?.let { super.findMemberVisibility(classVisibility?.companionVisibilities) }
}
}
private val MemberBinarySignature.kind: Int
get() = when (this) {
is FieldBinarySignature -> 1
is MethodBinarySignature -> 2
else -> error("Unsupported $this")
}
val MEMBER_SORT_ORDER = compareBy<MemberBinarySignature>(
{ it.kind },
{ it.name },
{ it.desc }
)
data class AccessFlags(val access: Int) {
val isPublic: Boolean get() = isPublic(access)
val isProtected: Boolean get() = isProtected(access)
val isStatic: Boolean get() = isStatic(access)
val isFinal: Boolean get() = isFinal(access)
val isSynthetic: Boolean get() = isSynthetic(access)
fun getModifiers(): List<String> = ACCESS_NAMES.entries.mapNotNull { if (access and it.key != 0) it.value else null }
fun getModifierString(): String = getModifiers().joinToString(" ")
}
fun isPublic(access: Int) = access and Opcodes.ACC_PUBLIC != 0 || access and Opcodes.ACC_PROTECTED != 0
fun isProtected(access: Int) = access and Opcodes.ACC_PROTECTED != 0
fun isStatic(access: Int) = access and Opcodes.ACC_STATIC != 0
fun isFinal(access: Int) = access and Opcodes.ACC_FINAL != 0
fun isSynthetic(access: Int) = access and Opcodes.ACC_SYNTHETIC != 0
fun ClassNode.isEffectivelyPublic(classVisibility: ClassVisibility?) =
isPublic(access)
&& !isLocal()
&& !isWhenMappings()
&& (classVisibility?.isPublic(isPublishedApi()) ?: true)
val ClassNode.innerClassNode: InnerClassNode? get() = innerClasses.singleOrNull { it.name == name }
fun ClassNode.isLocal() = innerClassNode?.run { innerName == null && outerName == null} ?: false
fun ClassNode.isInner() = innerClassNode != null
fun ClassNode.isWhenMappings() = isSynthetic(access) && name.endsWith("\$WhenMappings")
val ClassNode.effectiveAccess: Int get() = innerClassNode?.access ?: access
val ClassNode.outerClassName: String? get() = innerClassNode?.outerName
const val publishedApiAnnotationName = "kotlin/PublishedApi"
fun ClassNode.isPublishedApi() = findAnnotation(publishedApiAnnotationName, includeInvisible = true) != null
fun MethodNode.isPublishedApi() = findAnnotation(publishedApiAnnotationName, includeInvisible = true) != null
fun FieldNode.isPublishedApi() = findAnnotation(publishedApiAnnotationName, includeInvisible = true) != null
fun ClassNode.isDefaultImpls(metadata: KotlinClassMetadata?) = isInner() && name.endsWith("\$DefaultImpls") && metadata.isSyntheticClass()
fun ClassNode.findAnnotation(annotationName: String, includeInvisible: Boolean = false) = findAnnotation(annotationName, visibleAnnotations, invisibleAnnotations, includeInvisible)
fun MethodNode.findAnnotation(annotationName: String, includeInvisible: Boolean = false) = findAnnotation(annotationName, visibleAnnotations, invisibleAnnotations, includeInvisible)
fun FieldNode.findAnnotation(annotationName: String, includeInvisible: Boolean = false) = findAnnotation(annotationName, visibleAnnotations, invisibleAnnotations, includeInvisible)
operator fun AnnotationNode.get(key: String): Any? = values.annotationValue(key)
private fun List<Any>.annotationValue(key: String): Any? {
for (index in (0 until size / 2)) {
if (this[index * 2] == key)
return this[index * 2 + 1]
}
return null
}
private fun findAnnotation(annotationName: String, visibleAnnotations: List<AnnotationNode>?, invisibleAnnotations: List<AnnotationNode>?, includeInvisible: Boolean): AnnotationNode? =
visibleAnnotations?.firstOrNull { it.refersToName(annotationName) }
?: if (includeInvisible) invisibleAnnotations?.firstOrNull { it.refersToName(annotationName) } else null
fun AnnotationNode.refersToName(name: String) = desc.startsWith('L') && desc.endsWith(';') && desc.regionMatches(1, name, 0, name.length)
@@ -1,102 +0,0 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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
@Suppress("UNCHECKED_CAST")
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
fun KotlinClassMetadata.toClassVisibility(classNode: ClassNode): ClassVisibility? {
var flags: Flags? = null
var _facadeClassName: String? = null
val members = mutableListOf<MemberVisibility>()
fun addMember(signature: JvmMemberSignature?, flags: Flags, isReified: Boolean) {
if (signature != null) {
members.add(MemberVisibility(signature, flags, isReified))
}
}
val container: KmDeclarationContainer? = when (this) {
is KotlinClassMetadata.Class ->
toKmClass().also { klass ->
flags = klass.flags
for (constructor in klass.constructors) {
addMember(constructor.signature, constructor.flags, isReified = false)
}
}
is KotlinClassMetadata.FileFacade ->
toKmPackage()
is KotlinClassMetadata.MultiFileClassPart ->
toKmPackage().also { _facadeClassName = this.facadeClassName }
else -> null
}
if (container != null) {
fun List<KmTypeParameter>.containsReified() = any { Flag.TypeParameter.IS_REIFIED(it.flags) }
for (function in container.functions) {
addMember(function.signature, function.flags, function.typeParameters.containsReified())
}
for (property in container.properties) {
val isReified = property.typeParameters.containsReified()
addMember(property.getterSignature, property.getterFlags, isReified)
addMember(property.setterSignature, property.setterFlags, isReified)
val fieldVisibility = when {
Flag.Property.IS_LATEINIT(property.flags) -> property.setterFlags
property.getterSignature == null && property.setterSignature == null -> property.flags // JvmField or const case
else -> flagsOf(Flag.IS_PRIVATE)
}
addMember(property.fieldSignature, fieldVisibility, isReified = false)
}
}
return ClassVisibility(classNode.name, flags, members.associateBy { it.member }, _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)
}
}
@@ -1,43 +0,0 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.Flag
import kotlinx.metadata.Flags
import kotlinx.metadata.jvm.JvmMemberSignature
class ClassVisibility(
val name: String,
val flags: Flags?,
val members: Map<JvmMemberSignature, MemberVisibility>,
val facadeClassName: String? = null
) {
val visibility get() = flags
val isCompanion: Boolean get() = flags != null && Flag.Class.IS_COMPANION_OBJECT(flags)
var companionVisibilities: ClassVisibility? = null
val partVisibilities = mutableListOf<ClassVisibility>()
}
fun ClassVisibility.findMember(signature: JvmMemberSignature): MemberVisibility? =
members[signature] ?: partVisibilities.mapNotNull { it.members[signature] }.firstOrNull()
data class MemberVisibility(val member: JvmMemberSignature, val visibility: Flags?, val isReified: Boolean)
private fun isPublic(visibility: Flags?, isPublishedApi: Boolean) =
visibility == null
|| Flag.IS_PUBLIC(visibility)
|| Flag.IS_PROTECTED(visibility)
|| (isPublishedApi && Flag.IS_INTERNAL(visibility))
fun ClassVisibility.isPublic(isPublishedApi: Boolean) = isPublic(visibility, isPublishedApi)
fun MemberVisibility.isPublic(isPublishedApi: Boolean) =
// Assuming isReified implies inline
!isReified && isPublic(visibility, isPublishedApi)
@@ -5,7 +5,7 @@
package org.jetbrains.kotlin.tools.tests
import org.jetbrains.kotlin.tools.*
import kotlinx.validation.api.*
import org.junit.*
import org.junit.rules.TestName
import java.io.File
@@ -58,7 +58,7 @@ class CasesPublicAPITest {
val testClassStreams = testClasses.asSequence().filter { it.name.endsWith(".class") }.map { it.inputStream() }
val api = getBinaryAPI(testClassStreams).filterOutNonPublic()
val api = testClassStreams.loadApiFromJvmClasses().filterOutNonPublic()
val target = baseOutputPath.resolve(testClassRelativePath).resolve(testName.methodName + ".txt")
@@ -5,7 +5,7 @@
package org.jetbrains.kotlin.tools.tests
import org.jetbrains.kotlin.tools.*
import kotlinx.validation.api.*
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
@@ -51,7 +51,7 @@ class RuntimePublicAPITest {
val publicPackageFilter = { className: String -> publicPackagePrefixes.none { className.startsWith(it) } }
println("Reading binary API from $jarFile")
val api = getBinaryAPI(JarFile(jarFile), publicPackageFilter).filterOutNonPublic(nonPublicPackages)
val api = JarFile(jarFile).loadApiFromJvmClasses(publicPackageFilter).filterOutNonPublic(nonPublicPackages)
val target = File("reference-public-api")
.resolve(testName.methodName.replaceCamelCaseWithDashedLowerCase() + ".txt")
@@ -5,7 +5,7 @@
package org.jetbrains.kotlin.tools.tests
import org.jetbrains.kotlin.tools.*
import kotlinx.validation.api.*
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.fail