KT-45777: Don't compute snapshots for inaccessible classes

Also visit a class file with ASM once to extract all information we need
in advance, instead of visiting the class file each time some piece of
info is needed.
This commit is contained in:
Hung Nguyen
2021-11-17 11:48:12 +00:00
committed by teamcityserver
parent f52be5f471
commit 6bee7948e7
13 changed files with 395 additions and 540 deletions
@@ -25,7 +25,6 @@ import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.build.GeneratedJvmClass import org.jetbrains.kotlin.build.GeneratedJvmClass
import org.jetbrains.kotlin.incremental.storage.* import org.jetbrains.kotlin.incremental.storage.*
import org.jetbrains.kotlin.inline.inlineFunctionsJvmNames import org.jetbrains.kotlin.inline.inlineFunctionsJvmNames
import org.jetbrains.kotlin.load.kotlin.FileBasedKotlinClass
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCache import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCache
import org.jetbrains.kotlin.load.kotlin.incremental.components.JvmPackagePartProto import org.jetbrains.kotlin.load.kotlin.incremental.components.JvmPackagePartProto
@@ -630,30 +629,19 @@ class KotlinClassInfo constructor(
companion object { companion object {
fun createFrom(kotlinClass: LocalFileKotlinClass): KotlinClassInfo { fun createFrom(kotlinClass: LocalFileKotlinClass): KotlinClassInfo {
return KotlinClassInfo( return createFrom(kotlinClass.classId, kotlinClass.classHeader, kotlinClass.fileContents)
kotlinClass.classId,
kotlinClass.classHeader.kind,
kotlinClass.classHeader.data ?: emptyArray(),
kotlinClass.classHeader.strings ?: emptyArray(),
kotlinClass.classHeader.multifileClassName,
getConstantsMap(kotlinClass.fileContents),
getInlineFunctionsMap(kotlinClass.classHeader, kotlinClass.fileContents)
)
} }
/** Creates [KotlinClassInfo] from the given classContents, or returns `null` if the class is not a Kotlin class. */ fun createFrom(classId: ClassId, classHeader: KotlinClassHeader, classContents: ByteArray): KotlinClassInfo {
fun tryCreateFrom(classContents: ByteArray): KotlinClassInfo? { return KotlinClassInfo(
return FileBasedKotlinClass.create(classContents) { classId, _, classHeader, _ -> classId,
KotlinClassInfo( classHeader.kind,
classId, classHeader.data ?: emptyArray(),
classHeader.kind, classHeader.strings ?: emptyArray(),
classHeader.data ?: emptyArray(), classHeader.multifileClassName,
classHeader.strings ?: emptyArray(), getConstantsMap(classContents),
classHeader.multifileClassName, getInlineFunctionsMap(classHeader, classContents)
getConstantsMap(classContents), )
getInlineFunctionsMap(classHeader, classContents)
)
}
} }
} }
} }
@@ -1,214 +0,0 @@
/*
* Copyright 2010-2021 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.incremental
import com.intellij.openapi.util.Ref
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.Opcodes
/**
* Information about the name of a compiled Java class regarding its nesting level.
*
* A [JavaClassName] can be:
* - [TopLevelClass]
* - [NestedClass] (https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html)
*
* A [NestedClass] can be:
* - [NestedNonLocalClass]
* - [LocalClass] (https://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html)
*/
sealed class JavaClassName {
/** The full name of this class (e.g., "com/example/Foo$Bar"). */
abstract val name: String
/**
* Whether this class is an anonymous class.
*
* Note: Even though an anonymous class has no name in the source code, it always has a (not-null, not-empty) name in the compiled
* class (e.g., "com/example/Foo$1").
*/
abstract val isAnonymous: Boolean
/** Whether this class is a synthetic class. */
abstract val isSynthetic: Boolean
/** The package name of this class (e.g., "com/example"). */
val packageName: String
get() = name.substringBeforeLast('/', missingDelimiterValue = "")
/** The part of the full name of this class after [packageName] (e.g., "Foo$Bar"). */
val relativeClassName: String
get() = name.substringAfterLast('/', missingDelimiterValue = name)
companion object {
/** Computes the [JavaClassName] of a compiled Java class given its contents. */
fun compute(classContents: ByteArray): JavaClassName {
val nameRef = Ref.create<String>()
val isSyntheticRef = Ref.create<Boolean>()
val isTopLevelRef = Ref.create<Boolean>()
val outerNameRef = Ref.create<String>()
val isAnonymousRef = Ref.create<Boolean>()
ClassReader(classContents).accept(object : ClassVisitor(Opcodes.API_VERSION) {
override fun visit(
version: Int, access: Int, name: String,
signature: String?, superName: String?, interfaces: Array<String?>?
) {
nameRef.set(name)
isSyntheticRef.set((access and Opcodes.ACC_SYNTHETIC) != 0)
}
override fun visitInnerClass(name: String, outerName: String?, innerName: String?, access: Int) {
if (name == nameRef.get()!!) {
isTopLevelRef.set(false)
outerNameRef.set(outerName)
isAnonymousRef.set(innerName == null)
}
}
}, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES)
val name = nameRef.get()!!
val isSynthetic = isSyntheticRef.get()!!
val isTopLevel = isTopLevelRef.get() ?: true
val outerName = outerNameRef.get()
val isAnonymous = isAnonymousRef.get()
return when {
isTopLevel -> TopLevelClass(name, isSynthetic)
outerName != null -> NestedNonLocalClass(name, outerName, isAnonymous!!, isSynthetic)
else -> LocalClass(name, isAnonymous!!, isSynthetic)
}
}
}
}
/** See [JavaClassName]. */
class TopLevelClass(
override val name: String,
override val isSynthetic: Boolean
) : JavaClassName() {
override val isAnonymous: Boolean = false // A top-level class is never anonymous
}
/** See [JavaClassName]. */
sealed class NestedClass : JavaClassName()
/** See [JavaClassName]. */
class NestedNonLocalClass(
override val name: String,
/**
* The full name of the outer class of this class (e.g., the outer name of "com/example/OuterClass$NestedClass" is
* "com/example/OuterClass", the outer name of "com/example/OuterClassWith$Sign$NestedClassWith$Sign" is
* "com/example/OuterClassWith$Sign").
*
* The outer class can be of any type ([TopLevelClass], [NestedNonLocalClass], or [LocalClass]).
*/
val outerName: String,
override val isAnonymous: Boolean,
override val isSynthetic: Boolean
) : NestedClass() {
/**
* The simple name of this class (e.g., the simple name of "com/example/OuterClass$NestedClass" is "NestedClass", the simple name of
* class "com/example/OuterClassWith$Sign$NestedClassWith$Sign" is "NestedClassWith$Sign").
*
* Note: [simpleName] is not `null` and not empty even for anonymous classes (see [JavaClassName.isAnonymous]).
*/
val simpleName: String
get() = run {
check(name.startsWith("$outerName\$"))
name.substring("$outerName\$".length).also { check(it.isNotEmpty()) }
}
}
/** See [JavaClassName]. */
class LocalClass(
override val name: String,
override val isAnonymous: Boolean,
override val isSynthetic: Boolean
) : NestedClass()
/**
* Computes [ClassId]s of the given Java classes.
*
* Note that creating a [ClassId] for a nested class will require accessing the outer class for 2 reasons:
* - To disambiguate any '$' characters in the (outer) class name (e.g., "com/example/OuterClassWith$Sign$NestedClassWith$Sign").
* - To determine whether a class is a local class (a nested class of a local class is also considered local, see [ClassId]'s kdoc).
*
* Therefore, outer classes and nested classes must be passed together in one invocation of this method.
*/
fun computeJavaClassIds(classNames: List<JavaClassName>): List<ClassId> {
val classNameToClassId: MutableMap<JavaClassName, ClassId> = HashMap(classNames.size)
val nameToClassName: Map<String, JavaClassName> = classNames.associateBy { it.name }
fun JavaClassName.getClassId(): ClassId {
classNameToClassId[this]?.let { return it }
val packageName = FqName(packageName.replace('/', '.'))
val classId = when (this) {
is TopLevelClass -> {
ClassId(packageName, FqName(relativeClassName), /* local */ false)
}
is NestedNonLocalClass -> {
// JavaClassName.relativeClassName can contain '$' but not '.', whereas ClassId.relativeClassName can contain both '$' and
// '.' (e.g., "com/example/OuterClassWith$Sign$NestedClassWith$Sign" has JavaClassName.relativeClassName
// "OuterClassWith$Sign$NestedClassWith$Sign", but its ClassId.relativeClassName will be
// "OuterClassWith$Sign.NestedClassWith$Sign". To disambiguate '$' in the (outer) class name, we need to get the ClassId of
// the outer class first.
val outerClassId = nameToClassName[outerName]?.getClassId() ?: error("Can't find outer class '$outerName' of class '$name'")
val relativeClassName = FqName(outerClassId.relativeClassName.asString() + "." + simpleName)
// For ClassId, a nested non-local class of a local class is also considered local (see ClassId's kdoc).
val isLocal = outerClassId.isLocal
ClassId(packageName, relativeClassName, /* local */ isLocal)
}
is LocalClass -> {
// Note: The following computation for the relative class name of a local class does not exactly match the description given
// in ClassId's kdoc, which currently says "In the case of a local class, relativeClassName consists of a single name
// including all callables' and class' names all the way up to the package, separated by dollar signs."
//
// For example, consider this Java source:
// package com.example;
// class Foo {
// void someMethod() {
// class Bar {
// }
// }
// }
// The above source will compile into class "com/example/Foo" and "com/example/Foo$1Bar" (or
// "com/example/Foo$SomeOtherArbitraryUniqueName which need not contain the string "Bar").
//
// Given that class, the difference between the computed ClassId and the expected ClassId is as follows:
// Value computed below Expected value as defined in ClassId's kdoc
// relativeClassName Foo$1Bar Foo$someMethod$Bar
// classId com.example.Foo$1Bar com.example.Foo$someMethod$Bar
//
// Note: While they don't match, the ClassId computed below is still unique, so it can still be used as an identifier.
//
// TODO: Compute ClassID to match the expected value. It will require collecting information about the enclosing class,
// enclosing method, and simple class name (as written in source code). There will still be a challenge if the class is an
// anonymous class: The simple class name is not available, and it's not clear what the expected ClassID is.
//
// Alternatively, check if we can safely adjust the definition of ClassId for a local class and update any related code
// accordingly.
ClassId(packageName, FqName(relativeClassName), /* local */ true)
}
}
return classId.also {
classNameToClassId[this] = it
}
}
return classNames.map { it.getClassId() }
}
@@ -1,140 +0,0 @@
/*
* Copyright 2010-2021 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.incremental
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import kotlin.test.assertEquals
class JavaClassNameTest {
@get:Rule
val tmpDir = TemporaryFolder()
@Test
fun `test compute JavaClassName`() {
val compiledClasses = compileJava(className, sourceCode)
val classNames = compiledClasses.map { JavaClassName.compute(it) }
assertEquals(
listOf(
"TopLevelClass: com/example/JavaClassWithNestedClasses",
"LocalClass: com/example/JavaClassWithNestedClasses\$1",
"LocalClass: com/example/JavaClassWithNestedClasses\$1LocalClass",
"NestedNonLocalClass: com/example/JavaClassWithNestedClasses\$1LocalClass\$InnerClassWithinLocalClass",
"LocalClass: com/example/JavaClassWithNestedClasses\$2",
"NestedNonLocalClass: com/example/JavaClassWithNestedClasses\$InnerClass",
"LocalClass: com/example/JavaClassWithNestedClasses\$InnerClass\$1LocalClassWithinInnerClass",
"NestedNonLocalClass: com/example/JavaClassWithNestedClasses\$InnerClass\$InnerClassWithinInnerClass",
"NestedNonLocalClass: com/example/JavaClassWithNestedClasses\$InnerClassWith\$Sign",
"NestedNonLocalClass: com/example/JavaClassWithNestedClasses\$StaticNestedClass"
),
classNames.map { "${it.javaClass.simpleName}: ${it.name}" }
)
}
@Test
fun `test computeJavaClassIds`() {
val compiledClasses = compileJava(className, sourceCode)
val classNames = compiledClasses.map { JavaClassName.compute(it) }
val classIds = computeJavaClassIds(classNames)
assertEquals(
listOf(
classId("com.example", "JavaClassWithNestedClasses", local = false),
classId("com.example", "JavaClassWithNestedClasses$1", local = true),
classId("com.example", "JavaClassWithNestedClasses$1LocalClass", local = true),
classId("com.example", "JavaClassWithNestedClasses$1LocalClass.InnerClassWithinLocalClass", local = true),
classId("com.example", "JavaClassWithNestedClasses$2", local = true),
classId("com.example", "JavaClassWithNestedClasses.InnerClass", local = false),
classId("com.example", "JavaClassWithNestedClasses\$InnerClass\$1LocalClassWithinInnerClass", local = true),
classId("com.example", "JavaClassWithNestedClasses.InnerClass.InnerClassWithinInnerClass", local = false),
classId("com.example", "JavaClassWithNestedClasses.InnerClassWith\$Sign", local = false),
classId("com.example", "JavaClassWithNestedClasses.StaticNestedClass", local = false)
),
classIds
)
}
@Suppress("SameParameterValue")
private fun compileJava(className: String, sourceCode: String): List<ByteArray> {
val sourceFile = File(tmpDir.newFolder(), "$className.java").apply {
parentFile.mkdirs()
writeText(sourceCode)
}
val classesDir = tmpDir.newFolder()
KotlinTestUtils.compileJavaFiles(listOf(sourceFile), listOf("-d", classesDir.path))
return classesDir.walk().filter { it.isFile }
.sortedBy { it.path.substringBefore(".class") }
.map { it.readBytes() }
.toList()
}
private fun classId(@Suppress("SameParameterValue") packageFqName: String, relativeClassName: String, local: Boolean) =
ClassId(FqName(packageFqName), FqName(relativeClassName), local)
}
private const val className = "com/example/JavaClassWithNestedClasses"
private val sourceCode = """
package com.example;
public class JavaClassWithNestedClasses {
public class InnerClass {
public void publicMethod() {
System.out.println("I'm in a public method");
}
private void privateMethod() {
System.out.println("I'm in a private method");
}
public class InnerClassWithinInnerClass {
}
public void someMethod() {
class LocalClassWithinInnerClass {
}
}
}
public static class StaticNestedClass {
}
public void someMethod() {
class LocalClass {
class InnerClassWithinLocalClass {
}
}
Runnable objectOfAnonymousLocalClass = new Runnable() {
@Override
public void run() {
}
};
}
private Runnable objectOfAnonymousNonLocalClass = new Runnable() {
@Override
public void run() {
}
};
public class InnerClassWith${'$'}Sign {
}
}
""".trimIndent()
@@ -320,7 +320,7 @@ public abstract class FileBasedKotlinClass implements KotlinJvmBinaryClass {
} }
@NotNull @NotNull
private static ClassId resolveNameByInternalName(@NotNull String name, @NotNull InnerClassesInfo innerClasses) { public static ClassId resolveNameByInternalName(@NotNull String name, @NotNull InnerClassesInfo innerClasses) {
if (!name.contains("$")) { if (!name.contains("$")) {
return ClassId.topLevel(new FqName(name.replace('/', '.'))); return ClassId.topLevel(new FqName(name.replace('/', '.')));
} }
@@ -0,0 +1,115 @@
/*
* Copyright 2010-2021 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.incremental.classpathDiff
import org.jetbrains.kotlin.load.kotlin.FileBasedKotlinClass.*
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.load.kotlin.header.ReadKotlinClassHeaderAnnotationVisitor
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.org.objectweb.asm.AnnotationVisitor
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.Opcodes
/** Basic information about a class (e.g., [classId], [kotlinClassHeader] and [supertypes]). */
class BasicClassInfo(
val classId: ClassId,
val kotlinClassHeader: KotlinClassHeader?, // null if this is not a Kotlin class
val supertypes: List<JvmClassName>,
private val accessFlags: Int,
val isAnonymous: Boolean
) {
val isKotlinClass = kotlinClassHeader != null
val isPrivate = flagEnabled(accessFlags, Opcodes.ACC_PRIVATE)
val isLocal = classId.isLocal
/** Whether this is a synthetic Java class. */
val isJavaSynthetic = !isKotlinClass && flagEnabled(accessFlags, Opcodes.ACC_SYNTHETIC)
/**
* Whether this is a synthetic Kotlin class.
*
* Note that we use [KotlinClassHeader.Kind], not [accessFlags], to make this determination. For example,
* 'kotlin/Metadata.DefaultImpls' is synthetic according to its [KotlinClassHeader.Kind], but not synthetic according to its
* [accessFlags].
*/
@Suppress("unused")
val isKotlinSynthetic = isKotlinClass && kotlinClassHeader!!.kind == KotlinClassHeader.Kind.SYNTHETIC_CLASS
private fun flagEnabled(accessFlags: Int, flagToCheck: Int) = (accessFlags and flagToCheck) != 0
companion object {
fun compute(classContents: ByteArray): BasicClassInfo {
val kotlinClassHeaderClassVisitor = KotlinClassHeaderClassVisitor()
val innerClassesClassVisitor = InnerClassesClassVisitor(kotlinClassHeaderClassVisitor)
val basicClassInfoVisitor = BasicClassInfoClassVisitor(innerClassesClassVisitor)
ClassReader(classContents).accept(
basicClassInfoVisitor,
ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES
)
val className = basicClassInfoVisitor.getClassName()
val innerClassesInfo = innerClassesClassVisitor.getInnerClassesInfo()
return BasicClassInfo(
classId = resolveNameByInternalName(className, innerClassesInfo),
kotlinClassHeader = kotlinClassHeaderClassVisitor.getKotlinClassHeader(),
supertypes = basicClassInfoVisitor.getSupertypes(),
accessFlags = basicClassInfoVisitor.getAccessFlags(),
isAnonymous = innerClassesInfo[className]?.let { it.innerSimpleName == null } ?: false
)
}
}
}
private class BasicClassInfoClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.API_VERSION, cv) {
private var className: String? = null
private var classAccess: Int? = null
private val supertypeNames = mutableListOf<String>()
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<String>?) {
className = name
classAccess = access
superName?.let { supertypeNames.add(it) }
interfaces?.let { supertypeNames.addAll(it) }
super.visit(version, access, name, signature, superName, interfaces)
}
fun getClassName(): String = className!!
fun getAccessFlags(): Int = classAccess!!
fun getSupertypes(): List<JvmClassName> = supertypeNames.map { JvmClassName.byInternalName(it) }
}
private class InnerClassesClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.API_VERSION, cv) {
private val innerClassesInfo = InnerClassesInfo()
override fun visitInnerClass(name: String, outerName: String?, innerName: String?, access: Int) {
innerClassesInfo.add(name, outerName, innerName)
super.visitInnerClass(name, outerName, innerName, access)
}
fun getInnerClassesInfo(): InnerClassesInfo = innerClassesInfo
}
private class KotlinClassHeaderClassVisitor : ClassVisitor(Opcodes.API_VERSION) {
private val kotlinClassHeaderAnnotationVisitor = ReadKotlinClassHeaderAnnotationVisitor()
override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? {
return convertAnnotationVisitor(
kotlinClassHeaderAnnotationVisitor,
descriptor,
InnerClassesInfo() // This info is not needed to resolve KotlinClassHeader
)
}
fun getKotlinClassHeader(): KotlinClassHeader? = kotlinClassHeaderAnnotationVisitor.createHeader()
}
@@ -153,7 +153,7 @@ object ClasspathChangesComputer {
) )
incrementalJvmCache.markDirty(JvmClassName.byClassId(previousSnapshot.serializedJavaClass.classId)) incrementalJvmCache.markDirty(JvmClassName.byClassId(previousSnapshot.serializedJavaClass.classId))
} }
is RegularJavaClassSnapshot, is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> { is RegularJavaClassSnapshot, is InaccessibleClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${previousSnapshot.javaClass.name}") error("Unexpected type (it should have been handled earlier): ${previousSnapshot.javaClass.name}")
} }
} }
@@ -183,7 +183,7 @@ object ClasspathChangesComputer {
collector = changesCollector collector = changesCollector
) )
} }
is RegularJavaClassSnapshot, is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> { is RegularJavaClassSnapshot, is InaccessibleClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${currentSnapshot.javaClass.name}") error("Unexpected type (it should have been handled earlier): ${currentSnapshot.javaClass.name}")
} }
} }
@@ -331,7 +331,7 @@ internal fun ClassSnapshot.getClassId(): ClassId {
is KotlinClassSnapshot -> classInfo.classId is KotlinClassSnapshot -> classInfo.classId
is RegularJavaClassSnapshot -> classId is RegularJavaClassSnapshot -> classId
is ProtoBasedJavaClassSnapshot -> serializedJavaClass.classId is ProtoBasedJavaClassSnapshot -> serializedJavaClass.classId
is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> { is InaccessibleClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${javaClass.name}") error("Unexpected type (it should have been handled earlier): ${javaClass.name}")
} }
} }
@@ -351,7 +351,7 @@ internal fun ClassSnapshot.getSupertypes(classIdResolver: (JvmClassName) -> Clas
val (proto, nameResolver) = serializedJavaClass.toProtoData() val (proto, nameResolver) = serializedJavaClass.toProtoData()
proto.supertypes(TypeTable(proto.typeTable)).map { nameResolver.getClassId(it.className) } proto.supertypes(TypeTable(proto.typeTable)).map { nameResolver.getClassId(it.className) }
} }
is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> { is InaccessibleClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${javaClass.name}") error("Unexpected type (it should have been handled earlier): ${javaClass.name}")
} }
} }
@@ -107,12 +107,12 @@ class AbiSnapshotForTests(
class ProtoBasedJavaClassSnapshot(val serializedJavaClass: SerializedJavaClass) : JavaClassSnapshot() class ProtoBasedJavaClassSnapshot(val serializedJavaClass: SerializedJavaClass) : JavaClassSnapshot()
/** /**
* [JavaClassSnapshot] of a Java class where there is nothing to capture. * [ClassSnapshot] of an inaccessible class.
* *
* For example, the snapshot of a local class is empty as a local class can't be referenced from other source files and therefore any * For example, a local class is inaccessible as it can't be referenced from other source files and therefore any changes in a local class
* changes in a local class will not cause recompilation of other source files. * will not require recompilation of other source files.
*/ */
object EmptyJavaClassSnapshot : JavaClassSnapshot() object InaccessibleClassSnapshot : ClassSnapshot()
/** /**
* [JavaClassSnapshot] of a Java class where a proper snapshot can't be created for some reason, so we use the hash of the class contents as * [JavaClassSnapshot] of a Java class where a proper snapshot can't be created for some reason, so we use the hash of the class contents as
@@ -65,19 +65,28 @@ object ClasspathEntrySnapshotExternalizer : DataExternalizer<ClasspathEntrySnaps
object ClassSnapshotExternalizer : DataExternalizer<ClassSnapshot> { object ClassSnapshotExternalizer : DataExternalizer<ClassSnapshot> {
override fun save(output: DataOutput, snapshot: ClassSnapshot) { override fun save(output: DataOutput, snapshot: ClassSnapshot) {
output.writeBoolean(snapshot is KotlinClassSnapshot)
when (snapshot) { when (snapshot) {
is KotlinClassSnapshot -> KotlinClassSnapshotExternalizer.save(output, snapshot) is KotlinClassSnapshot -> {
is JavaClassSnapshot -> JavaClassSnapshotExternalizer.save(output, snapshot) output.writeString(KotlinClassSnapshot::class.java.name)
KotlinClassSnapshotExternalizer.save(output, snapshot)
}
is JavaClassSnapshot -> {
output.writeString(JavaClassSnapshot::class.java.name)
JavaClassSnapshotExternalizer.save(output, snapshot)
}
is InaccessibleClassSnapshot -> {
output.writeString(InaccessibleClassSnapshot::class.java.name)
InaccessibleClassSnapshotExternalizer.save(output, snapshot)
}
} }
} }
override fun read(input: DataInput): ClassSnapshot { override fun read(input: DataInput): ClassSnapshot {
val isKotlinClassSnapshot = input.readBoolean() return when (val className = input.readString()) {
return if (isKotlinClassSnapshot) { KotlinClassSnapshot::class.java.name -> KotlinClassSnapshotExternalizer.read(input)
KotlinClassSnapshotExternalizer.read(input) JavaClassSnapshot::class.java.name -> JavaClassSnapshotExternalizer.read(input)
} else { InaccessibleClassSnapshot::class.java.name -> InaccessibleClassSnapshotExternalizer.read(input)
JavaClassSnapshotExternalizer.read(input) else -> error("Unrecognized class name: $className")
} }
} }
} }
@@ -144,7 +153,6 @@ object JavaClassSnapshotExternalizer : DataExternalizer<JavaClassSnapshot> {
when (snapshot) { when (snapshot) {
is RegularJavaClassSnapshot -> RegularJavaClassSnapshotExternalizer.save(output, snapshot) is RegularJavaClassSnapshot -> RegularJavaClassSnapshotExternalizer.save(output, snapshot)
is ProtoBasedJavaClassSnapshot -> ProtoBasedJavaClassSnapshotExternalizer.save(output, snapshot) is ProtoBasedJavaClassSnapshot -> ProtoBasedJavaClassSnapshotExternalizer.save(output, snapshot)
is EmptyJavaClassSnapshot -> EmptyJavaClassSnapshotExternalizer.save(output, snapshot)
is ContentHashJavaClassSnapshot -> ContentHashJavaClassSnapshotExternalizer.save(output, snapshot) is ContentHashJavaClassSnapshot -> ContentHashJavaClassSnapshotExternalizer.save(output, snapshot)
} }
} }
@@ -153,7 +161,6 @@ object JavaClassSnapshotExternalizer : DataExternalizer<JavaClassSnapshot> {
return when (val className = input.readString()) { return when (val className = input.readString()) {
RegularJavaClassSnapshot::class.java.name -> RegularJavaClassSnapshotExternalizer.read(input) RegularJavaClassSnapshot::class.java.name -> RegularJavaClassSnapshotExternalizer.read(input)
ProtoBasedJavaClassSnapshot::class.java.name -> ProtoBasedJavaClassSnapshotExternalizer.read(input) ProtoBasedJavaClassSnapshot::class.java.name -> ProtoBasedJavaClassSnapshotExternalizer.read(input)
EmptyJavaClassSnapshot::class.java.name -> EmptyJavaClassSnapshotExternalizer.read(input)
ContentHashJavaClassSnapshot::class.java.name -> ContentHashJavaClassSnapshotExternalizer.read(input) ContentHashJavaClassSnapshot::class.java.name -> ContentHashJavaClassSnapshotExternalizer.read(input)
else -> error("Unrecognized class name: $className") else -> error("Unrecognized class name: $className")
} }
@@ -204,14 +211,14 @@ object ProtoBasedJavaClassSnapshotExternalizer : DataExternalizer<ProtoBasedJava
} }
} }
object EmptyJavaClassSnapshotExternalizer : DataExternalizer<EmptyJavaClassSnapshot> { object InaccessibleClassSnapshotExternalizer : DataExternalizer<InaccessibleClassSnapshot> {
override fun save(output: DataOutput, snapshot: EmptyJavaClassSnapshot) { override fun save(output: DataOutput, snapshot: InaccessibleClassSnapshot) {
// Nothing to save // Nothing to save
} }
override fun read(input: DataInput): EmptyJavaClassSnapshot { override fun read(input: DataInput): InaccessibleClassSnapshot {
return EmptyJavaClassSnapshot return InaccessibleClassSnapshot
} }
} }
@@ -20,7 +20,7 @@ object ClasspathSnapshotShrinker {
/** /**
* Shrinks the given [ClasspathSnapshot] by retaining only classes that are referenced. Referencing info is stored in [LookupStorage]. * Shrinks the given [ClasspathSnapshot] by retaining only classes that are referenced. Referencing info is stored in [LookupStorage].
* *
* This method also removes duplicate classes and [EmptyJavaClassSnapshot]s first. * This method also removes duplicate classes and [InaccessibleClassSnapshot]s first.
*/ */
fun shrink( fun shrink(
classpathSnapshot: ClasspathSnapshot, classpathSnapshot: ClasspathSnapshot,
@@ -28,10 +28,10 @@ object ClasspathSnapshotShrinker {
metrics: BuildMetricsReporter metrics: BuildMetricsReporter
): List<ClassSnapshotWithHash> { ): List<ClassSnapshotWithHash> {
val allClasses = metrics.measure(BuildTime.GET_NON_DUPLICATE_CLASSES) { val allClasses = metrics.measure(BuildTime.GET_NON_DUPLICATE_CLASSES) {
// It's important to remove duplicate classes first before removing `EmptyJavaClassSnapshot`s. // It's important to remove duplicate classes first before removing `InaccessibleClassSnapshot`s.
// For example, if jar1!/com/example/A.class is empty and jar2!/com/example/A.class is non-empty, incorrect order of the actions // For example, if jar1!/com/example/A.class is empty and jar2!/com/example/A.class is non-empty, incorrect order of the actions
// will lead to incorrect results. // will lead to incorrect results.
classpathSnapshot.getNonDuplicateClassSnapshots().filter { it.classSnapshot !is EmptyJavaClassSnapshot } classpathSnapshot.getNonDuplicateClassSnapshots().filter { it.classSnapshot !is InaccessibleClassSnapshot }
} }
val lookupSymbols = metrics.measure(BuildTime.GET_LOOKUP_SYMBOLS) { val lookupSymbols = metrics.measure(BuildTime.GET_LOOKUP_SYMBOLS) {
lookupStorage.lookupMap.keys lookupStorage.lookupMap.keys
@@ -5,18 +5,14 @@
package org.jetbrains.kotlin.incremental.classpathDiff package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.* import org.jetbrains.kotlin.incremental.JavaClassDescriptorCreator
import org.jetbrains.kotlin.incremental.KotlinClassInfo
import org.jetbrains.kotlin.incremental.md5
import org.jetbrains.kotlin.incremental.toSerializedJavaClass
import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor
import org.jetbrains.kotlin.metadata.deserialization.TypeTable import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.metadata.deserialization.supertypes
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.kotlin.serialization.deserialization.getClassId
import org.jetbrains.kotlin.utils.DFS import org.jetbrains.kotlin.utils.DFS
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.Opcodes
import java.io.File import java.io.File
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@@ -40,7 +36,7 @@ object ClasspathEntrySnapshotter {
val snapshots = try { val snapshots = try {
ClassSnapshotter.snapshot(classes, protoBased).map { it.addHash() } ClassSnapshotter.snapshot(classes, protoBased).map { it.addHash() }
} catch (e: Throwable) { } catch (e: Throwable) {
if (isKnownProblematicClasspathEntry(classpathEntry)) { if ((protoBased ?: protoBasedDefaultValue) && isKnownProblematicClasspathEntry(classpathEntry)) {
classes.map { ContentHashJavaClassSnapshot(it.contents.md5()).addHash() } classes.map { ContentHashJavaClassSnapshot(it.contents.md5()).addHash() }
} else throw e } else throw e
} }
@@ -74,112 +70,83 @@ object ClasspathEntrySnapshotter {
/** Creates [ClassSnapshot]s of classes. */ /** Creates [ClassSnapshot]s of classes. */
object ClassSnapshotter { object ClassSnapshotter {
/** /** Creates [ClassSnapshot]s of the given classes. */
* Creates [ClassSnapshot]s of the given classes.
*
* Note that for Java (non-Kotlin) classes, creating a [ClassSnapshot] for a nested class will require accessing the outer class (and
* possibly vice versa). Therefore, outer classes and nested classes must be passed together in one invocation of this method.
*/
fun snapshot( fun snapshot(
classes: List<ClassFileWithContents>, classes: List<ClassFileWithContents>,
protoBased: Boolean? = null, protoBased: Boolean? = null,
includeDebugInfoInSnapshot: Boolean? = null includeDebugInfoInSnapshot: Boolean? = null
): List<ClassSnapshot> { ): List<ClassSnapshot> {
// Snapshot Kotlin classes first val classesInfo: List<BasicClassInfo> = classes.map { BasicClassInfo.compute(it.contents) }
val kotlinClassSnapshots: Map<ClassFileWithContents, KotlinClassSnapshot?> = classes.associateWith {
trySnapshotKotlinClass(it)
}
// Snapshot the remaining Java classes in one invocation // Find inaccessible classes first, their snapshots will be `InaccessibleClassSnapshot`s.
val inaccessibleClasses: Set<BasicClassInfo> = getInaccessibleClasses(classesInfo).toSet()
// Snapshot the remaining accessible classes
val accessibleClasses: List<ClassFileWithContents> = classes.mapIndexedNotNull { index, clazz ->
if (classesInfo[index] in inaccessibleClasses) null else clazz
}
val accessibleClassesInfo: List<BasicClassInfo> = classesInfo.filterNot { it in inaccessibleClasses }
val accessibleSnapshots: List<ClassSnapshot> =
doSnapshot(accessibleClasses, accessibleClassesInfo, protoBased, includeDebugInfoInSnapshot)
val accessibleClassSnapshots: Map<ClassFileWithContents, ClassSnapshot> = accessibleClasses.zipToMap(accessibleSnapshots)
return classes.map { accessibleClassSnapshots[it] ?: InaccessibleClassSnapshot }
}
private fun doSnapshot(
classes: List<ClassFileWithContents>,
classesInfo: List<BasicClassInfo>,
protoBased: Boolean? = null,
includeDebugInfoInSnapshot: Boolean? = null
): List<ClassSnapshot> {
// Snapshot Kotlin classes first
val kotlinSnapshots: List<KotlinClassSnapshot?> = classes.mapIndexed { index, clazz ->
trySnapshotKotlinClass(clazz, classesInfo[index])
}
val kotlinClassSnapshots: Map<ClassFileWithContents, KotlinClassSnapshot?> = classes.zipToMap(kotlinSnapshots)
// Snapshot the remaining Java classes
val javaClasses: List<ClassFileWithContents> = classes.filter { kotlinClassSnapshots[it] == null } val javaClasses: List<ClassFileWithContents> = classes.filter { kotlinClassSnapshots[it] == null }
val snapshots: List<JavaClassSnapshot> = snapshotJavaClasses(javaClasses, protoBased, includeDebugInfoInSnapshot) val javaClassesInfo: List<BasicClassInfo> = classesInfo.mapIndexedNotNull { index, classInfo ->
val javaClassSnapshots: Map<ClassFileWithContents, JavaClassSnapshot> = javaClasses.zipToMap(snapshots) val javaClass = classes[index]
if (kotlinClassSnapshots[javaClass] == null) classInfo else null
}
val javaSnapshots: List<JavaClassSnapshot> =
snapshotJavaClasses(javaClasses, javaClassesInfo, protoBased, includeDebugInfoInSnapshot)
val javaClassSnapshots: Map<ClassFileWithContents, JavaClassSnapshot> = javaClasses.zipToMap(javaSnapshots)
return classes.map { kotlinClassSnapshots[it] ?: javaClassSnapshots[it]!! } return classes.map { kotlinClassSnapshots[it] ?: javaClassSnapshots[it]!! }
} }
/** Creates [KotlinClassSnapshot] of the given class, or returns `null` if the class is not a Kotlin class. */ /** Creates [KotlinClassSnapshot] of the given class, or returns `null` if the class is not a Kotlin class. */
private fun trySnapshotKotlinClass(clazz: ClassFileWithContents): KotlinClassSnapshot? { private fun trySnapshotKotlinClass(classFile: ClassFileWithContents, classInfo: BasicClassInfo): KotlinClassSnapshot? {
return KotlinClassInfo.tryCreateFrom(clazz.contents)?.let { return if (classInfo.isKotlinClass) {
KotlinClassSnapshot(it, computeSupertypes(it, clazz.contents)) val kotlinClassInfo = KotlinClassInfo.createFrom(classInfo.classId, classInfo.kotlinClassHeader!!, classFile.contents)
} KotlinClassSnapshot(kotlinClassInfo, classInfo.supertypes)
} else null
} }
// TODO: Find a faster way to get supertypes without loading protos (e.g., attach to an existing ASM visitor) /** Creates [JavaClassSnapshot]s of the given Java classes. */
private fun computeSupertypes(classInfo: KotlinClassInfo, classContents: ByteArray): List<JvmClassName> {
return try {
val (nameResolver, proto) = JvmProtoBufUtil.readClassDataFrom(classInfo.classHeaderData, classInfo.classHeaderStrings)
val supertypeClassIds = proto.supertypes(TypeTable(proto.typeTable)).map { nameResolver.getClassId(it.className) }
supertypeClassIds.map { JvmClassName.byClassId(it) }
} catch (e: Exception) {
// The above method call currently fails on a few classes for some reason:
// - org.jetbrains.kotlin.protobuf.InvalidProtocolBufferException: Message was missing required fields.
// (Lite runtime could not determine which fields were missing) for SomeClassKt.class
// - java.lang.NullPointerException: parseDelimitedFrom(this, EXTENSION_REGISTRY) must not be null for
// kotlin-stdlib-1.6.255-SNAPSHOT.jar
// Fall back to ASM visitor to get the supertypes.
val supertypeClassNames = mutableListOf<String>()
ClassReader(classContents).accept(object : ClassVisitor(Opcodes.API_VERSION) {
override fun visit(
version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<String>?
) {
superName?.let { supertypeClassNames.add(it) }
interfaces?.let { supertypeClassNames.addAll(it) }
}
}, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG)
supertypeClassNames.map { JvmClassName.byInternalName(it) }
}
}
/**
* Creates [JavaClassSnapshot]s of the given Java classes.
*
* Note that creating a [JavaClassSnapshot] for a nested class will require accessing the outer class (and possibly vice versa).
* Therefore, outer classes and nested classes must be passed together in one invocation of this method.
*/
private fun snapshotJavaClasses( private fun snapshotJavaClasses(
classes: List<ClassFileWithContents>, classes: List<ClassFileWithContents>,
classesInfo: List<BasicClassInfo>,
protoBased: Boolean? = null, protoBased: Boolean? = null,
includeDebugInfoInSnapshot: Boolean? = null includeDebugInfoInSnapshot: Boolean? = null
): List<JavaClassSnapshot> { ): List<JavaClassSnapshot> {
val classNames: List<JavaClassName> = classes.map { JavaClassName.compute(it.contents) } return if (protoBased ?: protoBasedDefaultValue) {
val classNameToClassFile: Map<JavaClassName, ClassFileWithContents> = classNames.zipToMap(classes) snapshotJavaClassesProtoBased(classes, classesInfo)
// We divide classes into 2 categories:
// - Special classes, which includes local, anonymous, or synthetic classes, and their nested classes. These classes can't be
// referenced from other source files, so any changes in these classes will not cause recompilation of other source files.
// Therefore, the snapshots of these classes are empty.
// - Regular classes: Any classes that do not belong to the above category.
val specialClasses = getSpecialClasses(classNames).toSet()
// Snapshot special classes first
val specialClassSnapshots: Map<JavaClassName, JavaClassSnapshot?> = classNames.associateWith {
if (it in specialClasses) {
EmptyJavaClassSnapshot
} else null
}
// Snapshot the remaining regular classes
val regularClasses: List<JavaClassName> = classNames.filter { specialClassSnapshots[it] == null }
val regularClassIds = computeJavaClassIds(regularClasses)
val regularClassFiles: List<ClassFileWithContents> = regularClasses.map { classNameToClassFile[it]!! }
val snapshots: List<JavaClassSnapshot> = if (protoBased ?: protoBasedDefaultValue) {
snapshotJavaClassesProtoBased(regularClassIds, regularClassFiles)
} else { } else {
regularClassIds.mapIndexed { index, classId -> classes.mapIndexed { index, clazz ->
JavaClassSnapshotter.snapshot(classId, regularClassFiles[index].contents, includeDebugInfoInSnapshot) JavaClassSnapshotter.snapshot(clazz.contents, classesInfo[index], includeDebugInfoInSnapshot)
} }
} }
val regularClassSnapshots: Map<JavaClassName, JavaClassSnapshot> = regularClasses.zipToMap(snapshots)
return classNames.map { specialClassSnapshots[it] ?: regularClassSnapshots[it]!! }
} }
private fun snapshotJavaClassesProtoBased( private fun snapshotJavaClassesProtoBased(
classIds: List<ClassId>, classFilesWithContents: List<ClassFileWithContents>,
classFilesWithContents: List<ClassFileWithContents> classesInfo: List<BasicClassInfo>
): List<JavaClassSnapshot> { ): List<JavaClassSnapshot> {
val classIds = classesInfo.map { it.classId }
val classesContents = classFilesWithContents.map { it.contents } val classesContents = classFilesWithContents.map { it.contents }
val descriptors: List<JavaClassDescriptor?> = JavaClassDescriptorCreator.create(classIds, classesContents) val descriptors: List<JavaClassDescriptor?> = JavaClassDescriptorCreator.create(classIds, classesContents)
val snapshots: List<JavaClassSnapshot> = descriptors.mapIndexed { index, descriptor -> val snapshots: List<JavaClassSnapshot> = descriptors.mapIndexed { index, descriptor ->
@@ -206,28 +173,52 @@ object ClassSnapshotter {
return snapshots return snapshots
} }
/** Returns local, anonymous, or synthetic classes, and their nested classes. */ /**
private fun getSpecialClasses(classNames: List<JavaClassName>): List<JavaClassName> { * Returns inaccessible classes, i.e. classes that can't be referenced from other source files (and therefore any changes in these
val specialClasses: MutableMap<JavaClassName, Boolean> = HashMap(classNames.size) * classes will not require recompilation of other source files).
val nameToClassName: Map<String, JavaClassName> = classNames.associateBy { it.name } *
* Examples include private, local, anonymous, and synthetic classes.
fun JavaClassName.isSpecial(): Boolean { *
specialClasses[this]?.let { return it } * If a class is inaccessible, its nested classes (if any) are also inaccessible.
*
return if (isAnonymous || isSynthetic) { * NOTE: If we do not have enough info to determine whether a class is inaccessible, we will assume that the class is accessible to be
true * safe.
} else when (this) { */
is TopLevelClass -> false private fun getInaccessibleClasses(classesInfo: List<BasicClassInfo>): List<BasicClassInfo> {
is NestedNonLocalClass -> { fun BasicClassInfo.isInaccessible(): Boolean {
nameToClassName[outerName]?.isSpecial() ?: error("Can't find outer class '$outerName' of class '$name'") if (this.isKotlinClass && this.kotlinClassHeader!!.kind != KotlinClassHeader.Kind.CLASS) {
} // We're not sure about these kinds of Kotlin classes, so we assume it's accessible (see this method's kdoc)
is LocalClass -> true return false
}.also { }
specialClasses[this] = it return if (isKotlinClass) {
// TODO: Is it safe to add isKotlinSynthetic to this lists?
isPrivate || isLocal || isAnonymous
} else {
isPrivate || isLocal || isAnonymous || isJavaSynthetic
} }
} }
return classNames.filter { it.isSpecial() } val classIsInaccessible: MutableMap<BasicClassInfo, Boolean> = HashMap(classesInfo.size)
val classIdToClassInfo: Map<ClassId, BasicClassInfo> = classesInfo.associateBy { it.classId }
fun BasicClassInfo.isTransitivelyInaccessible(): Boolean {
classIsInaccessible[this]?.let { return it }
val inaccessible = if (isInaccessible()) {
true
} else classId.outerClassId?.let { outerClassId ->
classIdToClassInfo[outerClassId]?.isTransitivelyInaccessible()
// If we can't find the outer class from the given list of classes (which could happen with faulty jars), we assume that
// the class is accessible (see this method's kdoc).
?: false
} ?: false
return inaccessible.also {
classIsInaccessible[this] = inaccessible
}
}
return classesInfo.filter { it.isTransitivelyInaccessible() }
} }
/** Returns `true` if it is known that the given exception can be thrown when calling [JavaClassDescriptor.toSerializedJavaClass]. */ /** Returns `true` if it is known that the given exception can be thrown when calling [JavaClassDescriptor.toSerializedJavaClass]. */
@@ -7,8 +7,6 @@ package org.jetbrains.kotlin.incremental.classpathDiff
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import org.jetbrains.kotlin.incremental.md5 import org.jetbrains.kotlin.incremental.md5
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import org.jetbrains.org.objectweb.asm.ClassReader import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.Opcodes import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.ClassNode import org.jetbrains.org.objectweb.asm.tree.ClassNode
@@ -16,7 +14,7 @@ import org.jetbrains.org.objectweb.asm.tree.ClassNode
/** Computes a [JavaClassSnapshot] of a Java class. */ /** Computes a [JavaClassSnapshot] of a Java class. */
object JavaClassSnapshotter { object JavaClassSnapshotter {
fun snapshot(classId: ClassId, classContents: ByteArray, includeDebugInfoInSnapshot: Boolean? = null): JavaClassSnapshot { fun snapshot(classContents: ByteArray, classInfo: BasicClassInfo, includeDebugInfoInSnapshot: Boolean? = null): JavaClassSnapshot {
// We will extract ABI information from the given class and store it into the `abiClass` variable. // We will extract ABI information from the given class and store it into the `abiClass` variable.
// It is acceptable to collect more info than required, but it is incorrect to collect less info than required. // It is acceptable to collect more info than required, but it is incorrect to collect less info than required.
// There are 2 approaches: // There are 2 approaches:
@@ -28,28 +26,25 @@ object JavaClassSnapshotter {
// First, collect all info. // First, collect all info.
// Note the parsing options passed to ClassReader: // Note the parsing options passed to ClassReader:
// - SKIP_CODE is set as method bodies will not be part of the ABI of the class. // - SKIP_CODE and SKIP_FRAMES are set as method bodies will not be part of the ABI of the class.
// - SKIP_DEBUG is not set as it would skip method parameters, which may be used by annotation processors like Room. // - SKIP_DEBUG is not set as it would skip method parameters, which may be used by annotation processors like Room.
// - SKIP_FRAMES and EXPAND_FRAMES are not relevant when SKIP_CODE is set. // - EXPAND_FRAMES is not needed (and not relevant when SKIP_CODE is set).
val classReader = ClassReader(classContents) val classReader = ClassReader(classContents)
classReader.accept(abiClass, ClassReader.SKIP_CODE) classReader.accept(abiClass, ClassReader.SKIP_CODE or ClassReader.SKIP_FRAMES)
// Then, remove non-ABI info: // Then, remove non-ABI info, which includes:
// - Method bodies have already been removed (see SKIP_CODE above). // - Method bodies: Should have already been removed (see SKIP_CODE above)
// - If the class is private, its snapshot will be empty. Otherwise, remove its private fields and methods. // - Private fields and methods
if (abiClass.access.isPrivate()) { fun Int.isPrivate() = (this and Opcodes.ACC_PRIVATE) != 0
return EmptyJavaClassSnapshot
}
abiClass.fields.removeIf { it.access.isPrivate() } abiClass.fields.removeIf { it.access.isPrivate() }
abiClass.methods.removeIf { it.access.isPrivate() } abiClass.methods.removeIf { it.access.isPrivate() }
// Sort fields and methods as their order is not important (we still use List instead of Set as we want the serialized snapshot to // Normalize the class: Sort fields and methods as their order is not important (we still use List instead of Set as we want the
// be deterministic). // serialized snapshot to be deterministic).
abiClass.fields.sortWith(compareBy({ it.name }, { it.desc })) abiClass.fields.sortWith(compareBy({ it.name }, { it.desc }))
abiClass.methods.sortWith(compareBy({ it.name }, { it.desc })) abiClass.methods.sortWith(compareBy({ it.name }, { it.desc }))
val supertypes = (listOf(abiClass.superName) + abiClass.interfaces.toList()).map { JvmClassName.byInternalName(it) } // Snapshot the class
val fieldsAbi = abiClass.fields.map { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) } val fieldsAbi = abiClass.fields.map { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) }
val methodsAbi = abiClass.methods.map { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) } val methodsAbi = abiClass.methods.map { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) }
@@ -57,30 +52,32 @@ object JavaClassSnapshotter {
abiClass.methods.clear() abiClass.methods.clear()
val classAbiExcludingMembers = abiClass.let { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) } val classAbiExcludingMembers = abiClass.let { snapshotJavaElement(it, it.name, includeDebugInfoInSnapshot) }
return RegularJavaClassSnapshot(classId, supertypes, classAbiExcludingMembers, fieldsAbi, methodsAbi) return RegularJavaClassSnapshot(classInfo.classId, classInfo.supertypes, classAbiExcludingMembers, fieldsAbi, methodsAbi)
} }
private fun Int.isPrivate() = (this and Opcodes.ACC_PRIVATE) != 0
private val gson by lazy { private val gson by lazy {
// Use serializeSpecialFloatingPointValues() to avoid // Use serializeSpecialFloatingPointValues() to avoid this error
// "java.lang.IllegalArgumentException: NaN is not a valid double value as per JSON specification. To override this behavior, use // "java.lang.IllegalArgumentException: NaN is not a valid double value as per JSON specification. To override this behavior, use
// GsonBuilder.serializeSpecialFloatingPointValues() method." // GsonBuilder.serializeSpecialFloatingPointValues() method."
// on jars such as ~/.gradle/kotlin-build-dependencies/repo/kotlin.build/ideaIC/203.8084.24/artifacts/lib/rhino-1.7.12.jar. // on jars such as ~/.gradle/kotlin-build-dependencies/repo/kotlin.build/ideaIC/203.8084.24/artifacts/lib/rhino-1.7.12.jar.
GsonBuilder() GsonBuilder().serializeSpecialFloatingPointValues().create()
.serializeSpecialFloatingPointValues() }
// Same as above but with `setPrettyPrinting()`
private val gsonForDebug by lazy {
GsonBuilder().serializeSpecialFloatingPointValues()
.setPrettyPrinting() .setPrettyPrinting()
.create() .create()
} }
private fun snapshotJavaElement(javaElement: Any, javaElementName: String, includeDebugInfoInSnapshot: Boolean? = null): AbiSnapshot { private fun snapshotJavaElement(javaElement: Any, javaElementName: String, includeDebugInfoInSnapshot: Boolean? = null): AbiSnapshot {
// TODO: Optimize this method later if necessary. Currently we focus on correctness first.
val abiValue = gson.toJson(javaElement)
val abiHash = abiValue.toByteArray().md5()
return if (includeDebugInfoInSnapshot == true) { return if (includeDebugInfoInSnapshot == true) {
val abiValue = gsonForDebug.toJson(javaElement)
val abiHash = abiValue.toByteArray().md5()
AbiSnapshotForTests(javaElementName, abiHash, abiValue) AbiSnapshotForTests(javaElementName, abiHash, abiValue)
} else { } else {
val abiValue = gson.toJson(javaElement)
val abiHash = abiValue.toByteArray().md5()
AbiSnapshot(javaElementName, abiHash) AbiSnapshot(javaElementName, abiHash)
} }
} }
@@ -0,0 +1,111 @@
/*
* Copyright 2010-2021 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.incremental.classpathDiff
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import kotlin.test.assertEquals
class BasicClassInfoTest {
@get:Rule
val tmpDir = TemporaryFolder()
@Test
fun `compute BasicClassInfo`() {
val compiledClasses = compileJava(className, sourceCode)
val classIds = compiledClasses.map { BasicClassInfo.compute(it).classId }
assertEquals(
listOf(
classId("com.example", "TopLevelClass", local = false),
classId("com.example", "TopLevelClass$1", local = true),
classId("com.example", "TopLevelClass$1LocalClass", local = true),
classId("com.example", "TopLevelClass$1LocalClass$1LocalClassWithinLocalClass", local = true),
classId("com.example", "TopLevelClass$1LocalClass.InnerClassWithinLocalClass", local = true),
classId("com.example", "TopLevelClass$2", local = true),
classId("com.example", "TopLevelClass.InnerClass", local = false),
classId("com.example", "TopLevelClass\$InnerClass\$1LocalClassWithinInnerClass", local = true),
classId("com.example", "TopLevelClass.InnerClass.InnerClassWithinInnerClass", local = false),
classId("com.example", "TopLevelClass.InnerClassWith\$Sign", local = false),
classId("com.example", "TopLevelClass.InnerClassWith\$Sign.InnerClassWith\$SignLevel2", local = false),
classId("com.example", "TopLevelClass.StaticNestedClass", local = false)
),
classIds
)
}
@Suppress("SameParameterValue")
private fun compileJava(className: String, sourceCode: String): List<ByteArray> {
val sourceFile = File(tmpDir.newFolder(), "$className.java").apply {
parentFile.mkdirs()
writeText(sourceCode)
}
val classesDir = tmpDir.newFolder()
KotlinTestUtils.compileJavaFiles(listOf(sourceFile), listOf("-d", classesDir.path))
return classesDir.walk().toList()
.filter { it.isFile }
.sortedBy { it.path.substringBefore(".class") }
.map { it.readBytes() }
}
private fun classId(@Suppress("SameParameterValue") packageFqName: String, relativeClassName: String, local: Boolean) =
ClassId(FqName(packageFqName), FqName(relativeClassName), local)
}
private const val className = "com/example/TopLevelClass"
private val sourceCode = """
package com.example;
public class TopLevelClass {
public class InnerClass {
public class InnerClassWithinInnerClass {
}
public void someMethod() {
class LocalClassWithinInnerClass {
}
}
}
public static class StaticNestedClass {
}
public void methodWithinTopLevelClass() {
class LocalClass {
class InnerClassWithinLocalClass {
}
public void methodWithinLocalClass() {
class LocalClassWithinLocalClass {
}
}
}
Runnable anonymousLocalClassInstance = new Runnable() {
@Override
public void run() {
}
};
}
private Runnable anonymousNonLocalClassInstance = new Runnable() {
@Override
public void run() {
}
};
public class InnerClassWith${'$'}Sign {
public class InnerClassWith${'$'}SignLevel2 {
}
}
}
""".trimIndent()
@@ -39,7 +39,7 @@
}, },
"supertypes": [ "supertypes": [
{ {
"internalName": "kotlin/Any" "internalName": "java/lang/Object"
} }
] ]
} }