bcv: cleanup the old way of reading kotlin visibilities

This commit is contained in:
Ilya Gorbunov
2018-05-20 19:05:24 +03:00
parent 84d96d9537
commit 66a9a90455
8 changed files with 124 additions and 174 deletions
@@ -10,7 +10,6 @@ dependencies {
compile project(':kotlinx-metadata-jvm')
compile 'org.ow2.asm:asm:6.0'
compile 'org.ow2.asm:asm-tree:6.0'
compile 'com.google.code.gson:gson:2.6.2'
testCompile project(':kotlin-test:kotlin-test-junit')
testArtifacts project(':kotlin-stdlib')
@@ -1,17 +1,20 @@
/*
* 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 org.objectweb.asm.*
import org.objectweb.asm.tree.*
import java.io.File
import java.io.InputStream
import java.util.jar.JarFile
fun main(args: Array<String>) {
var src = args[0]
val src = args[0]
println(src)
println("------------------\n");
val visibilities = readKotlinVisibilities(File("""stdlib/target/stdlib-declarations.json"""))
getBinaryAPI(JarFile(src), visibilities).filterOutNonPublic().dump()
getBinaryAPI(JarFile(src)).filterOutNonPublic().dump()
}
@@ -19,11 +22,10 @@ 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>, visibilityFilter: (String) -> Boolean = { true }): List<ClassBinarySignature> =
getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityMap, visibilityFilter)
fun getBinaryAPI(jar: JarFile, visibilityFilter: (String) -> Boolean = { true }): List<ClassBinarySignature> =
getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityFilter)
fun getBinaryAPI(classStreams: Sequence<InputStream>, visibilityMap: Map<String, ClassVisibility>, visibilityFilter: (String) -> Boolean = { true }): List<ClassBinarySignature>
{
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)
@@ -34,7 +36,6 @@ fun getBinaryAPI(classStreams: Sequence<InputStream>, visibilityMap: Map<String,
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())
@@ -63,14 +64,14 @@ fun List<ClassBinarySignature>.filterOutNonPublic(nonPublicPackages: List<String
val classByName = associateBy { it.name }
fun ClassBinarySignature.isInNonPublicPackage() =
nonPublicPaths.any { name.startsWith(it) }
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)
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] })
@@ -87,15 +88,15 @@ fun List<ClassBinarySignature>.filterOutNonPublic(nonPublicPackages: List<String
}
return filter { !it.isInNonPublicPackage() && it.isPublicAndAccessible() }
.map { it.flattenNonPublicBases() }
.filterNot { it.isNotUsedWhenEmpty && it.memberSignatures.isEmpty()}
.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")
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,3 +1,8 @@
/*
* 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.jvm.KotlinClassMetadata
@@ -6,33 +11,31 @@ import org.objectweb.asm.tree.*
import kotlin.comparisons.*
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")
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 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()}"
}
fun ClassVisibility.findMember(signature: MemberSignature): MemberVisibility? =
members[signature] ?: partVisibilities.mapNotNull { it.members[signature] }.firstOrNull()
interface MemberBinarySignature {
val name: String
@@ -40,9 +43,9 @@ interface MemberBinarySignature {
val access: AccessFlags
val isPublishedApi: Boolean
fun isEffectivelyPublic(classAccess: AccessFlags, classVisibility: ClassVisibility?)
= access.isPublic && !(access.isProtected && classAccess.isFinal)
&& (findMemberVisibility(classVisibility)?.isPublic(isPublishedApi) ?: true)
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(MemberSignature(name, desc))
@@ -52,10 +55,11 @@ interface MemberBinarySignature {
}
data class MethodBinarySignature(
override val name: String,
override val desc: String,
override val isPublishedApi: Boolean,
override val access: AccessFlags) : MemberBinarySignature {
override val name: String,
override val desc: String,
override val isPublishedApi: Boolean,
override val access: AccessFlags
) : MemberBinarySignature {
override val signature: String
get() = "${access.getModifierString()} fun $name $desc"
@@ -92,35 +96,31 @@ data class MethodBinarySignature(
}
data class FieldBinarySignature(
override val name: String,
override val desc: String,
override val isPublishedApi: Boolean,
override val access: AccessFlags) : MemberBinarySignature {
override val name: String,
override val desc: String,
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? {
val fieldVisibility = super.findMemberVisibility(classVisibility)
return super.findMemberVisibility(classVisibility)
?: takeIf { access.isStatic }?.let { super.findMemberVisibility(classVisibility?.companionVisibilities) }
?: return null
if (fieldVisibility.isLateInit()) {
classVisibility?.findSetterForProperty(fieldVisibility)?.let { return it }
}
return fieldVisibility
}
}
val MemberBinarySignature.kind: Int get() = when (this) {
is FieldBinarySignature -> 1
is MethodBinarySignature -> 2
else -> error("Unsupported $this")
}
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 }
{ it.kind },
{ it.name },
{ it.desc }
)
@@ -143,10 +143,10 @@ fun isSynthetic(access: Int) = access and Opcodes.ACC_SYNTHETIC != 0
fun ClassNode.isEffectivelyPublic(classVisibility: ClassVisibility?) =
isPublic(access)
&& !isLocal()
&& !isWhenMappings()
&& (classVisibility?.isPublic(isPublishedApi()) ?: true)
isPublic(access)
&& !isLocal()
&& !isWhenMappings()
&& (classVisibility?.isPublic(isPublishedApi()) ?: true)
val ClassNode.innerClassNode: InnerClassNode? get() = innerClasses.singleOrNull { it.name == name }
@@ -164,21 +164,9 @@ fun MethodNode.isPublishedApi() = findAnnotation(publishedApiAnnotationName, inc
fun FieldNode.isPublishedApi() = findAnnotation(publishedApiAnnotationName, includeInvisible = true) != null
private object KotlinClassKind {
const val FILE = 2
const val SYNTHETIC_CLASS = 3
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 }
fun ClassNode.isDefaultImpls(metadata: KotlinClassMetadata?) = isInner() && name.endsWith("\$DefaultImpls") && metadata.isSyntheticClass()
val ClassNode.kotlinClassKind: Int?
get() = findAnnotation("kotlin/Metadata", false)?.get("k") as Int?
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)
@@ -186,15 +174,15 @@ fun FieldNode.findAnnotation(annotationName: String, includeInvisible: Boolean =
operator fun AnnotationNode.get(key: String): Any? = values.annotationValue(key)
private fun List<Any>.annotationValue(key: String): Any? {
for (index in (0 .. size / 2 - 1)) {
if (this[index*2] == key)
return this[index*2 + 1]
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
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)
@@ -12,6 +12,7 @@ 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?,
@@ -1,13 +1,19 @@
package org.jetbrains.kotlin.tools
/*
* 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.
*/
import com.google.gson.internal.Streams
import com.google.gson.stream.JsonReader
import java.io.File
package org.jetbrains.kotlin.tools
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>()
}
fun ClassVisibility.findMember(signature: MemberSignature): MemberVisibility? =
members[signature] ?: partVisibilities.mapNotNull { it.members[signature] }.firstOrNull()
data class MemberVisibility(val member: MemberSignature, val declaration: String?, val visibility: String?)
data class MemberSignature(val name: String, val desc: String)
@@ -15,44 +21,6 @@ private fun isPublic(visibility: String?, isPublishedApi: Boolean) = visibility
fun ClassVisibility.isPublic(isPublishedApi: Boolean) = isPublic(visibility, isPublishedApi)
fun MemberVisibility.isPublic(isPublishedApi: Boolean) = isPublic(visibility, isPublishedApi)
fun MemberVisibility.isLateInit() = declaration != null && "lateinit var " in declaration
private val varValPrefix = Regex("va[lr]\\s+")
fun ClassVisibility.findSetterForProperty(property: MemberVisibility): MemberVisibility? {
// ad-hoc solution:
val declaration = property.declaration ?: return null
val match = varValPrefix.find(declaration) ?: return null
val name = declaration.substring(match.range.endInclusive + 1).substringBefore(':')
val setterName = "<set-$name>"
return members.values.find { it.declaration?.contains(setterName) ?: false }
}
fun readKotlinVisibilities(declarationFile: File): Map<String, ClassVisibility> {
val result = mutableListOf<ClassVisibility>()
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 declaration = getAsJsonPrimitive("declaration")?.asString
val visibility = getAsJsonPrimitive("visibility")?.asString
MemberVisibility(MemberSignature(name, desc), declaration, visibility)
}
}
ClassVisibility(name, visibility, members.associateByTo(hashMapOf()) { it.member })
}
}
jsonReader.endArray()
}
return result.associateByTo(hashMapOf()) { it.name }
}
@@ -1,3 +1,8 @@
/*
* 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.tests
import org.jetbrains.kotlin.tools.*
@@ -8,12 +13,11 @@ import java.io.File
class CasesPublicAPITest {
companion object {
val visibilities by lazy { readKotlinVisibilities(File(System.getProperty("testCasesDeclarations")!!)) }
val baseClassPaths: List<File> =
System.getProperty("testCasesClassesDirs")
.let { requireNotNull(it) { "Specify testCasesClassesDirs with a system property"} }
.split(File.pathSeparator)
.map { File(it, "cases").canonicalFile }
System.getProperty("testCasesClassesDirs")
.let { requireNotNull(it) { "Specify testCasesClassesDirs with a system property"} }
.split(File.pathSeparator)
.map { File(it, "cases").canonicalFile }
val baseOutputPath = File("src/test/kotlin/cases")
}
@@ -54,7 +58,7 @@ class CasesPublicAPITest {
val testClassStreams = testClasses.asSequence().filter { it.name.endsWith(".class") }.map { it.inputStream() }
val api = getBinaryAPI(testClassStreams, visibilities).filterOutNonPublic()
val api = getBinaryAPI(testClassStreams).filterOutNonPublic()
val target = baseOutputPath.resolve(testClassRelativePath).resolve(testName.methodName + ".txt")
@@ -1,17 +1,6 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* 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.tests
@@ -29,56 +18,51 @@ class RuntimePublicAPITest {
val testName = TestName()
@Test fun kotlinRuntime() {
snapshotAPIAndCompare("../../stdlib/runtime/build/libs", "kotlin-runtime", listOf("../runtime-declarations.json"), listOf("kotlin.jvm.internal"))
snapshotAPIAndCompare("../../stdlib/runtime/build/libs", "kotlin-runtime", listOf("kotlin.jvm.internal"))
}
@Test fun kotlinStdlibRuntimeMerged() {
snapshotAPIAndCompare(
"../../stdlib/jvm/build/libs", "kotlin-stdlib",
listOf("../stdlib-declarations.json"/*, "../stdlib-experimental-declarations.json"*/),
listOf("kotlin.jvm.internal")
)
snapshotAPIAndCompare("../../stdlib/jvm/build/libs", "kotlin-stdlib", listOf("kotlin.jvm.internal"))
}
@Test fun kotlinStdlibJdk7() {
snapshotAPIAndCompare("../../stdlib/jdk7/build/libs", "kotlin-stdlib-jdk7", listOf("../stdlib-jdk7-declarations.json"))
snapshotAPIAndCompare("../../stdlib/jdk7/build/libs", "kotlin-stdlib-jdk7")
}
@Test fun kotlinStdlibJdk8() {
snapshotAPIAndCompare("../../stdlib/jdk8/build/libs", "kotlin-stdlib-jdk8", listOf("../stdlib-jdk8-declarations.json"))
snapshotAPIAndCompare("../../stdlib/jdk8/build/libs", "kotlin-stdlib-jdk8")
}
@Test fun kotlinStdlibJre7() {
snapshotAPIAndCompare("../../stdlib/jre7/build/libs", "kotlin-stdlib-jre7", listOf("../stdlib-jre7-declarations.json"))
snapshotAPIAndCompare("../../stdlib/jre7/build/libs", "kotlin-stdlib-jre7")
}
@Test fun kotlinStdlibJre8() {
snapshotAPIAndCompare("../../stdlib/jre8/build/libs", "kotlin-stdlib-jre8", listOf("../stdlib-jre8-declarations.json"))
snapshotAPIAndCompare("../../stdlib/jre8/build/libs", "kotlin-stdlib-jre8")
}
@Test fun kotlinReflect() {
snapshotAPIAndCompare("../../reflect/api/build/libs", "kotlin-reflect-api(?!-[-a-z]+)", listOf("../reflect-declarations.json"), nonPublicPackages = listOf("kotlin.reflect.jvm.internal"))
snapshotAPIAndCompare("../../reflect/api/build/libs", "kotlin-reflect-api(?!-[-a-z]+)", nonPublicPackages = listOf("kotlin.reflect.jvm.internal"))
}
private fun snapshotAPIAndCompare(basePath: String, jarPattern: String, kotlinJvmMappingsPath: List<String>, publicPackages: List<String> = emptyList(), nonPublicPackages: List<String> = emptyList()) {
private fun snapshotAPIAndCompare(
basePath: String,
jarPattern: String,
publicPackages: List<String> = emptyList(),
nonPublicPackages: List<String> = emptyList()
) {
val base = File(basePath).absoluteFile.normalize()
val jarFile = getJarPath(base, jarPattern, System.getProperty("kotlinVersion"))
val kotlinJvmMappingsFiles = kotlinJvmMappingsPath.map(base::resolve)
println("Reading kotlin visibilities from $kotlinJvmMappingsFiles")
val publicPackagePrefixes = publicPackages.map { it.replace('.', '/') + '/' }
val publicPackageFilter = { className: String -> publicPackagePrefixes.none { className.startsWith(it) } }
val visibilities =
kotlinJvmMappingsFiles
.map { readKotlinVisibilities(it).filterKeys(publicPackageFilter) }
.reduce { m1, m2 -> m1 + m2 }
println("Reading binary API from $jarFile")
val api = getBinaryAPI(JarFile(jarFile), visibilities, publicPackageFilter).filterOutNonPublic(nonPublicPackages)
val api = getBinaryAPI(JarFile(jarFile), publicPackageFilter).filterOutNonPublic(nonPublicPackages)
val target = File("reference-public-api")
.resolve(testName.methodName.replaceCamelCaseWithDashedLowerCase() + ".txt")
.resolve(testName.methodName.replaceCamelCaseWithDashedLowerCase() + ".txt")
api.dumpAndCompareWith(target)
}
@@ -89,8 +73,8 @@ class RuntimePublicAPITest {
val files = (base.listFiles() ?: throw Exception("Cannot list files in $base"))
.filter { it.name.let {
it matches regex
&& !it.endsWith("-sources.jar")
&& !it.endsWith("-javadoc.jar") } }
&& !it.endsWith("-sources.jar")
&& !it.endsWith("-javadoc.jar") } }
return files.singleOrNull() ?: throw Exception("No single file matching $regex in $base:\n${files.joinToString("\n")}")
}
@@ -1,3 +1,8 @@
/*
* 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.tests
import org.jetbrains.kotlin.tools.*